地图脚本编程-101-全-
地图脚本编程 101(全)
原文:Map Scripting 101
译者:飞龙
引言

互联网以许多方式改变了我们的生活。MapQuest 提供的第一个在线、按需驾驶方向几乎使传统的道路地图变得过时。今天,许多提供驾驶方向的网站也向开发者提供他们的地图。使用这些地图 API,您可以绘制自己的点或与其他网站的地理数据混合。
本书向您展示如何利用这些服务并在您的网站上包含它们的地图。而不是限制您使用一个提供商,我会向您展示如何通过一个名为Mapstraction的开源库来使用所有这些服务。编写一次代码,然后观察它在 Google Maps、Bing、MapQuest、Yahoo!、OpenStreetMap 等地图上运行。
除了教您如何使用这些提供商的地图外,我还会向您展示许多其他常见的地理项目。您将学习如何计算地点之间的距离,并在您自己的网站上嵌入驾驶方向。您还将学习如何通过添加自己的图标、添加大型图形覆盖层,甚至完全更改底层地图图像来自定义地图的外观。
通过嵌入地图将位置信息引入网络是现在大多数网站的一个重要组成部分,但同时也越来越需要将网络“带到”运行移动浏览器的智能手机上。您可以使用本书中展示的技术将地图添加到您网站的移动版本中。我还会向您展示如何使用方便的地理位置标准来定位用户的位置,无论他是在使用手机、平板电脑,还是普通电脑。
您只需几页就能在您的网站上添加一些“位置”。本书旨在帮助您快速开始您已经构思的应用程序,或者激发您下一个地图的灵感。为此,我将本书组织成项目。一旦您成为地图脚本大师,我希望这本书足够有用,可以作为参考书籍放在您的书架上。
本书简介
本书基于项目的方法从基本示例开始,然后迅速加快速度。如果您喜欢跳过,我建议您至少先阅读创建 Mapstraction 地图。本书中的几乎所有示例都是基于您在该部分创建的地图。
在第一章中,您将学习构建在线地图的基础知识。我将介绍 Mapstraction 并展示如何向您的地图添加控件,例如缩放界面。
在第二章中,您将开始向地图添加自己的点。您将创建自定义图标并添加信息框来描述位置。
在第三章中,您将学习许多将地址和城市名称转换为地图提供商可以理解的坐标的方法。这个过程称为地理编码,是使地图对人类友好的重要部分。
在第四章中,您将为您的地图添加更复杂的图层。您将学习如何绘制线条来描述路线和形状来勾勒边界。您甚至将看到如何将大型图形地理编码,然后将其作为地图叠加层添加。
在第五章中,您将使您的地图对事件做出响应,例如拖动、点击和缩放。这些钩子允许您为用户提供更互动的体验。
在第六章中,我们将探索邻近性。您将学习如何创建驾驶方向或围绕一个点进行搜索。您还将深入了解一些更高级的主题,例如确定一个位置是否在形状内(称为碰撞测试)。
在第七章中,您将学习几种简单的方法以不同精度访问用户的位置。我将介绍使用地理位置标准、回退到 IP 地址数据以及与位置共享服务集成。
在第八章中,您将专注于在网络上使用的常见定位数据格式。您将学习如何解析 GeoRSS、Google Earth 的 KML 以及大多数 GPS 设备的 XML 输出。
在第九章中,是时候转向服务器端了。您将获得 PHP 和 MySQL 的快速课程,这两种技术由许多网络托管商提供。然后我们将使用这些语言进行常见的定位任务,例如从您的数据库中查找最近的点。
在第十章中,您将通过五个有趣的项目将所有内容整合在一起。您将创建一个 Twitter 推文查找器、一个交互式天气地图,以及一种在两个地点之间查找咖啡馆的方法(这样您就可以在中间与朋友见面)。还有本地音乐会查找器和可视化全球地震的方法。
如何使用本书
本书向网络开发者介绍了制图学,并展示了制图师和其他地理爱好者如何将他们的地图上线。本书适合初学者和高级程序员阅读——您的技能水平和地图知识将影响您如何使用本书。第一章是每个人开始的好地方,因为大多数后续示例都是基于那里展示的基本地图构建的。
如果您之前没有使用过 JavaScript,或者需要复习,请务必阅读附录 A。这将为您介绍本书中使用的概念,并提供对 JavaScript 框架 jQuery 的快速介绍。
每一章都是基于前一章的内容构建的,因此你可以从开始读到结束,随着你对地图知识的扩展。这本书也很好地作为参考书使用——你可以跳过一些内容,找到你想要学习的概念,或者找到你当前应用所需的章节或项目。
书中另一个有用的部分是附录 B,它详细介绍了 Mapstraction 中的类和函数。这个参考可以作为快速检查语法的方式,并提供如何使用每个函数的示例。
关于网站
我鼓励你利用这本书的配套网站mapscripting101.com/ (图 1). 在其他方面,你将找到书中每个项目的实时示例——这样你就可以通过下载或复制代码来节省一些打字时间。

图 1. 配套网站
此外,由于地图脚本技术和网络都在迅速变化,你需要查看网站以了解最新动态,这样你就可以保持你的技能更新。我将发布更新和教程,帮助你将知识扩展到这本书的页面之外。
第一章. 地图基础

X标记了地点,对吧?这是老海盗的说法。你有没有想过是谁为海盗制作地图?海盗不得不自己来做。难怪他们那么烦躁!如果他们只有今天的技术,海盗就可以使用别人的地图,只需要自己标记X,把复杂的海岸线细节留给制图师。
幸运的是,你生活在现在,有各种各样的地图选项。你可以使用 Google Maps、Yahoo! Maps 以及许多其他地图。这些地图使地图制作变得简单;你只需要几行代码就可以在你的网页上包含地图。图 1-1 显示了来自 Yelp 的页面,这是一个餐厅评论网站,也是成千上万个使用地图标记位置的网站之一。

图 1-1. 本地搜索网站 Yelp 使用 Google Maps。
要嵌入地图,你需要使用一个 API。一个 API……什么?API 代表应用程序编程接口,它由一系列使创建地图更简单的函数组成。你仍然需要进行一些编程,但与如果你必须自己完成所有事情相比,编写代码将变得微不足道。这听起来熟悉吗,伙计?
地图 API:Google、Yahoo!和 Mapstraction
正如我提到的,你可以从许多地图 API 提供者中选择。地图的功能和风格各不相同,尽管 API 共享许多元素。这本书将涵盖 Google 和 Yahoo!的地图工具,但大多数代码示例将使用一个名为 Mapstraction 的 JavaScript 库,它也是一个 API,但与其他的不同。Mapstraction 本身不是一个地图服务;相反,它是其他 API 的包装器。你只需编写一次代码,它就可以在 Google Maps、Yahoo! Maps 和其他十个提供者上工作.^([1])
Mapstraction 并不总是支持每个提供者的所有功能,但它涵盖了这些服务共享的功能以及更多。对于大多数地图项目来说,使用 Mapstraction 是有意义的。偶尔,你会遇到只能与一个提供者一起工作的示例。在这些情况下,我会清楚地指出 Mapstraction 代码的结束和专有代码的开始。
使用 Mapstraction 是关于前瞻性的。例如,如果 Google 关闭其地图 API,你需要重写多少代码?如果这听起来不太可能,那么考虑一下,如果你的地图提供者开始显示令人讨厌的广告,或者另一个提供者出现了,其地图颜色更适合你的设计,会发生什么。Mapstraction 允许你在提供者之间无缝切换。所以你只需编写一次代码,它就可以在任何地方工作。
然而,在你开始在地图上标记位置之前,你需要了解地图的基础知识。最重要的概念之一是用来描述地球上某一点的坐标系。让我们看看这是如何完成的。
^([1]) CloudMade, FreeEarth, Map24, MapQuest, Microsoft, MultiMap, OpenLayers, OpenSpace, OpenStreetMap, and ViaMichelin
描述地球上的一个点
地理学家的工作很困难,他们需要将一个圆形的地球在平面上赋予意义。对于那些有技能的人来说,这项工作是一项接受不精确性的练习。因为,尽管哥伦布说过,地球不是圆的;它甚至不是一个球体。地球是一个椭球体,比它的高度略宽。我们非常感谢过去几百年来辛勤工作的天文学家和数学家,他们帮助我们尽可能精确地确定位置。
描述地球上某一点最常见的方式是使用纬度和经度坐标。这个系统被 GPS 设备、每个网络地图 API 提供商和这本书所使用。有了它,我们可以将复杂的椭球体转换成我们用于在代数课上创建图表的标准坐标系。一张带有网格覆盖的世界地图在图 1-2 中显示,轴被叠加。
我们所标记的点表示地球上的位置,误差仅为两厘米(0.8 英寸)。我们不像在学校里那样将轴称为x和y,而是称它们为纬度和经度。我们可以用几种方式表示坐标对:
| 45° 33′ 25″ N, 122° 31′ 55″ W |
|---|
| 45° 33.4′, −122° 31.9′ 或 45d33.4m, −122d31.9m |
| 45.55713, −122.53194 |
如你所猜想的,这些坐标对都是大致相同地表达同一点的方法。单位是度(°)、分(′)和秒(″)。每个度被分成 60 分,每个分再进一步分成 60 秒。第三种示例中的十进制表示法被地图提供商使用,也是你在这本书中最常看到的形式。
就像我们所有人都熟悉的坐标系一样,每个轴都有一个零点,数值在一个方向上增加,在另一个方向上减少。因此,纬度和经度都可以有正负数。
纬度衡量垂直轴,它描述了一个位置距离北或南有多远。纬度的零点是赤道。向北,数值增加,直到在极点达到 90 度。赤道以南,纬度减少,−90 度是另一个极点。

图 1-2. 带有网格覆盖的世界地图
水平轴的测量称为经度。经度描述了一个位置在西方或东方有多远。地球没有自然的垂直赤道,所以科学家和政治家必须决定一个零点。他们选择了天文学家乔治·比德尔·艾里的望远镜在伦敦格林威治(皇家天文台)的位置作为本初子午线。在此点以东,经度增加到地球另一侧的 180 度。同样,经度减少到−180 度,在西方与正经度相对(称为对跖子午线)。
为什么纬度在 90 度停止,而经度继续到 180 度?水平轴没有任何极点,所以选择一个停止点将像子午线一样任意。此外,纬度度数彼此平行,而经度线在极点处彼此更近。特定纬度上的点是对称的。如果你追踪假设的 100 度纬度在地球半圈,你会到达 80 度纬度,所以你不妨就称它为那样。
现在你对坐标的使用有了感觉,让我们看看它们的不同表达方式以及如何在它们之间切换。
在十进制度和度数格式之间转换
当我第一次介绍纬度和经度点时,我展示了几个例子。如果你处理足够的地理数据,你可能会看到表达相同点的这些方式。在本节中,我将向你展示如何在两种最常见格式之间进行转换。
地图 API 接受纬度和经度值作为一对小数。例如,旧金山的 No Starch Press 办公室位于 37.7740486, −122.4101883。这意味着什么?首先,找到主要度数很容易——这些是在小数点之前的数字,所以在这个例子中是 37 和−122。
剩余的小数描述了这个值接近下一个度数有多近。纬度为 37.7740486 位于第 37 度和第 38 度之间的一半以上。一度的一部分表示为分钟和秒。
将坐标的小数部分乘以 60 以得到分钟数:
| 0.7740486 × 60 = 46.442916 分钟 |
|---|
| 0.4101883 × 60 = 24.611298 分钟 |
现在我们有进展了。纬度是 37 度 46 分。经度是−122 度 24 分。然而,答案有小数部分。我们需要重复之前的步骤,将这些新的小数乘以 60,以确定秒数:
| 0.442916 × 60 = 26.57496 秒 |
|---|
| 0.611298 × 60 = 36.67788 秒 |
再次,我们只剩下一个小数部分。除非我们想要极其精确,否则我们只需取整数部分即可。26.57496 秒与 26 秒之间的差异是 1/100 英里。有些人选择在小数点后保留一位数字。然后我们的测量精度大约在五英尺以内,这几乎肯定小于网络地图上的一个像素。
将点 37.7740486, −122.4101883 从度、分、秒格式转换为度、分、秒格式后的最终答案是 37° 46′ 26″ N,122° 24′ 36″ W。
注意方向,这表明该位置位于北半球和西半球。此外,经度不再表示为负数,因为该位置位于本初子午线以西。
从度、分、秒转换为小数格式甚至更容易。与之前一样,转换度数很容易,因为它成为小数点左边的整数部分。现在我们只需记住使用负数来表示南半球和西半球。
接下来,将分钟部分转换为秒(乘以 60)并将该结果加到现有的秒数上:
| 46 × 60 + 26 = 2,786 秒纬度 |
|---|
| 24 × 60 + 36 = 1,476 秒经度 |
最后,将每个结果除以一度中的秒数,即 3,600(60 × 60):
| 2,786 / 3600 = 0.77389 |
|---|
| 1,476 / 3600 = 0.41000 |
将点 37° 46′ 26″ N,122° 24′ 36″ W 转换为小数格式后的答案是 37.77389,−122.41000。这个答案与该节开始时的小数版本大致相同。差异是舍入误差,对于这个目的来说并不非常显著。你将在下一个项目中发现更多关于精度的问题。
确定小数坐标的精度
最常用的经纬度格式将度数表示为小数。地图服务提供商以及大多数服务都将度数转换为小数以提供位置数据。
你可能还记得在数学课上学习实数时遇到的一个小难题:它们可以无限延伸。你必须决定使用多少位数字,就像计算器只能容纳一定数量的数字一样。在某个时候,小数需要被截断。
纬度和经度点的小数位数因服务而异。然而,大多数服务至少提供小数点后五位数字。正如你在表 1-1 中看到的那样,五位数字足以将位置精确到四英尺以内。换句话说,对于在网络上绘制点,五位数字已经足够了。
表 1-1. 小数位数纬度精度
| 小数点后位数 | 可能的误差 |
|---|---|
| 1 | 7 英里(11 公里) |
| 2 | 3/4 英里(1 公里) |
| 3 | 370 英尺(110 米) |
| 4 | 37 英尺(11 米) |
| 5 | 4 英尺(1 米) |
城市级别的坐标通常在小数点后只有一位数字。仅使用一位数字可能导致 7 英里的误差,但坐标仍然在大多数城市的范围内。例如,Yahoo!的天气 API(在第六十九部分:创建天气地图中使用,第六十九部分:创建天气地图)只提供两位精度的城市坐标,这使误差降低到不到一英里。
经度的精度不像纬度那样容易计算,因为经度线不是平行的。在赤道,纬度和经度图将是相同的。然而,当经度线接近两极时,它们会越来越靠近。好消息是纬度误差是经度可能存在的最大误差,因此,在大多数情况下,经度的误差更小。
注意
纬度误差在赤道和两极之间大约变化 1 公里,但这个误差与经度相比要小得多。
你将在第三十六部分:计算两点之间的距离中了解更多关于经度奇怪特性的内容,在那里我会展示如何根据纬度调整度之间的不同距离。
创建你的第一个地图
你即将开始学习新制图学,从普通的网络开发者转变为地理定位网络开发者。在本节中,我们将创建一个基本的地图,它将成为未来项目的基石。
首先,我们将使用 Google Maps API 创建一个以旧金山 No Starch Press 办公室为中心的地图。然后,我会向你展示如何使用 Yahoo!的服务创建相同的地图。最后,我会展示几乎相同的 Mapstraction 代码如何创建这两个地图。
创建一个 Google 地图
Google 在许多网络领域都是 500 磅的大猩猩,地图也不例外。大多数地图开发者选择 Google Maps API,仅因为其无处不在。除了无处不在,Google Maps 还快速稳定。
Google Maps 自 2005 年以来一直存在。这并不是说地图团队没有创新。相反,Google Maps 经常是第一个在其 API 中添加新功能的,例如驾驶方向和在美国许多城市以及全球选定地点的任何地址的 360 度视图。
让我们使用 Google 创建一个基本的地图。打开一个新的 HTML 文件,并输入以下内容:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html >
<head>
<title>Basic Google Map</title>
<script
src="*`http://maps.google.com/maps/api/js?sensor=false`*"
type="text/javascript"></script>
<style type="text/css">
div#mymap {
width: 400px;
height: 350px;
}
</style>
<script type="text/javascript">
function create_map() {
❶ var opt = {center: new google.maps.LatLng(37.7740486,-122.4101883),
zoom: 15, mapTypeId: google.maps.MapTypeId.ROADMAP};
❷ var map = new google.maps.Map(document.getElementById("mapdiv"), opt);
}
</script>
</head>
<body onload="create_map()">
❸ <div id="mymap"></div>
</body>
</html>
保存你的文件并在浏览器中打开它。结果应该看起来像图 1-3,你的 Google 地图将位于旧金山 No Starch Press 所在地区的中心。

图 1-3. 基本的 Google 地图
如你所见,HTML 钩子是最小的。只需要一个带有id属性的空div标签 ❸。JavaScript 函数create_map()接管并调用 API。这个函数可以取任何你想要的名称。在这本书的许多示例中,我会使用这个相同的名称。
在创建地图之前,我们需要设置一些选项 ❶。所需的最基本信息是一个中心点(使用经纬度对),缩放级别和地图类型。然后,我们传递这些选项并引用div标签的id来创建地图 ❷。
就这样,你已经创建了你的第一个 Google 地图。继续阅读,了解这个地图与 Yahoo!的不同之处以及如何使用 Mapstraction 编写一次即可与任何地图提供商一起工作的代码。
创建一个 Yahoo!地图
Yahoo!在 Google 发布其地图 API 的大约同一时间发布了它的地图 API。不幸的是,第一个版本是基于 Flash 的,使用起来很困难。Google 抢得了先机,而 Yahoo!一直处于追赶状态。现在 Yahoo!有一个具有与 Google 类似功能的 JavaScript API。
你需要从 Yahoo!获取一个 API 密钥和一个 Yahoo!账户才能使用其地图。要注册应用程序,以获取密钥,请访问此网页:developer.yahoo.com/wsregapp/。
选择无认证的选项,因为你将不会访问 Yahoo!用户数据。填写表单的其余部分,包括有关你的应用程序的信息,点击按钮,设置完成。
一旦你获得了 API 密钥,你就可以创建一个 Yahoo!地图了。要做到这一点,从上一节中的 Google 示例开始。将调用 Google 的 JavaScript 替换为包含 Yahoo!的代码(确保使用你的 API 密钥):
<script type="text/javascript"
src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=*`yourkeyhere`*"></script>
接下来,按照如下方式修改create_map函数的内容:
function create_map() {
❶ var map = new Ymap(document.getElementById('mymap'));
❷ map.drawZoomAndCenter(new YGeoPoint(37.7740486,-122.4101883), 3);
}
保存你的文件,并在浏览器中加载它。结果应该类似于图 1-4,你的 Yahoo!地图将位于旧金山 No Starch Press 的社区中心。

图 1-4. 基本的 Yahoo!地图
代码与 Google 地图没有太大区别。你通过引用div标签的id来创建一个新的地图 ❶。然后,你使用经纬度对给地图设置一个中心点 ❷。最大的区别在于语法和顺序。在 Yahoo 中,你创建地图,然后添加选项,如缩放级别和中心点。
两个地图背后的概念非常相似。但是,这些细微的差异累积起来,如果你需要从其中一个切换到另一个,就会变得非常痛苦。这就是为什么 Mapstraction 如此强大的原因,你将在下一节中看到。
创建一个 Mapstraction 地图
Mapstraction 与 Google Maps 和 Yahoo! Maps 略有不同。Mapstraction 是一个开源的 JavaScript 库,它可以与其他地图 API 集成。如果你使用 Mapstraction,你可以通过非常少的努力在一种类型的地图和另一种类型之间切换,而不是完全重写你的代码。
使用 Mapstraction 可以降低 API 更改的风险。例如,如果你的网站流量超过了所选提供者的限制,或者提供者开始在地图上放置广告,Mapstraction 让你能够快速且低成本地切换提供者。
要使用 Mapstraction,你必须首先选择一个提供者。在这个例子中,我正在使用 Mapstraction 来创建一个 Google 地图。
打开一个新的 HTML 文件并输入以下内容:
<html>
<head>
<title>Basic Mapstraction Map</title>
<script
❶ src="*`http://maps.google.com/maps/api/js?sensor=false`*"
type="text/javascript"></script>
<script type="text/javascript" src="mxn.js?(❷googlev3)"></script>
<style type="text/css">
div#mymap {
width: 400px;
height: 350px;
}
</style>
<script type="text/javascript">
function create_map() {
var mapstraction = new mxn.Mapstraction('mymap', '❸googlev3');
mapstraction.setCenterAndZoom(
new mxn.LatLonPoint(37.7740486,-122.4101883), 15);
}
</script>
</head>
<body onload="create_map()">
<div id="mymap"></div>
</body>
</html>
就像为正常的 Google 地图一样,我们包括 Google 的 JavaScript ❶。为了使这段代码正常工作,你还需要下载 Mapstraction 文件。访问mapstraction.com/,并按照说明将文件保存在与你的 HTML 文件相同的目录下。最佳实践会建议你将 JavaScript 文件保存在它们自己的目录中,与 HTML 文件分开,但为了这个例子,我在简化事情。
你至少应该拥有的 Mapstraction 文件是mxn.js、mxn.core.js和googlev3.core.js。你可能还会有其他提供者的文件,例如yahoo.core.js。在我们 HTML 代码中需要引用的只有一个mxn.js,它会加载它需要的其他文件,包括我们通过文件名传递给它的那些文件 ❷。然后,在create_map函数中,我们让它知道我们正在创建哪种类型的地图 ❸。
一旦你有了你的 Mapstraction 地图,保存你的 HTML 文件并在浏览器中加载它。结果应该看起来与图 1-4 中的 Google 地图完全一样。这个通过 Mapstraction 创建的 Google 地图应该位于旧金山 No Starch Press 的社区中心。
使用 Mapstraction 和 Yahoo!地图
为了了解 Mapstraction 库有多强大,让我们尝试使用 Yahoo!地图而不是 Google 地图。你只需要在代码中更改几个部分。最好的部分是,即使你有大量的 Mapstraction 代码,你仍然只需要更改这一行。
就像从标准 Google 地图切换到 Yahoo!地图一样,你需要包括 Yahoo!'s JavaScript。在继续之前,确保以下行在你的文件中:
<script
src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=*`yourkeyhere`*"
type="text/javascript"></script>
<script type="text/javascript" src="mxn.js?(❶yahoo)"></script>
注意,我们不是加载带有 Google 支持的 Mapstraction JavaScript,而是指定了 Yahoo!版本的 Mapstraction ❶。尽管 Mapstraction 的核心是提供者无关的,但你需要告诉它你想要使用哪个提供者。你还需要确保yahoo.core.js与mxn.js在同一个目录下。
现在让我们看看create_map函数内部的 Mapstraction 代码:
function create_map() {
var mapstraction = new mxn.Mapstraction('mymap', ❷'yahoo');
mapstraction.setCenterAndZoom(
new mxn.LatLonPoint(37.7740486,-122.4101883), 15);
}
在这里,使用谷歌制作的 Mapstraction 地图和这个地图之间的唯一区别是我们已经注明我们正在制作一个雅虎地图❷。仅此而已。无需更改setCenterAndZoom或LatLonPoint函数。语法相同,因为 Mapstraction 是围绕提供者包装的。
保存并重新加载你的文件,原本的谷歌地图应该会被雅虎地图所替代,就像图 1-5 所示。这是同一张雅虎地图,只是这一张是通过 Mapstraction 创建的。
Mapstraction 就像魔法一样,但更好。实际上,你甚至不需要选择谷歌或雅虎!你可以在同一张地图中同时使用两者,或者在一页上使用每个中的一个。
查找底层地图瓦片
网络地图的可拖动性可能是其最好的属性。我经常发现自己只是因为可以而拖动地图。这个特性也证明是一个非常棒的错觉。
看起来像是一张无缝地图,实际上是由许多并排放置的小瓦片组成的。你可能曾在网络延迟的一瞬间注意到这一点,当时你的地图的一部分未能加载。网络延迟最可能发生在你更改缩放级别或快速将地图从原始位置拖远之后。
提供者通常会尝试通过预加载所有接触你当前区域瓦片的瓦片来避免打破无缝的错觉。
每个瓦片的标准尺寸为 256 像素平方。在基本地图示例中,有六个瓦片至少部分可见。如果谷歌是提供者,它还会加载额外的周围瓦片。图 1-5 显示了地图的可见部分如何对应其瓦片。在原始视图中,我们只能看到顶部两个瓦片的边缘。

图 1-5. 显示周围瓦片的基本地图
当我们更改缩放级别时会发生什么?我们需要下载一组全新的瓦片。每个缩放级别显示的细节都不同,每个瓦片现在代表地球的不同部分。
由于版权问题,提供者往往会使其瓦片难以直接访问。然而,当你访问提供者的地图时,你可以查看下载到浏览器中的瓦片。这样做将更好地了解瓦片系统是如何工作的。
虽然你可以通过多种方式获取文件,但我将向你展示一个使用 Firefox 浏览器和 Firebug 开发者插件非常简单的方法。你可以在www.getfirefox.com/和www.getfirebug.com/找到它们,分别。
在 Firefox 中,加载一个嵌入的地图,例如基本地图示例中的地图。点击右下角的 Firebug 图标,或从工具菜单中选择Firebug ▸ 打开 Firebug。在 Firebug 面板中,点击检查按钮,这允许你看到高亮的页面元素。将鼠标悬停在地图中心,应该会在地图的一部分周围出现一个蓝色边框。你找到了一个瓦片!
当一个瓦片被高亮时,点击鼠标,你将被带到包含图像标签的 HTML 代码中,如图图 1-6 所示。一开始,这个标签可能看起来很令人困惑,因为你没有将此图像添加到你的代码中。与在浏览器中简单地查看源代码不同,Firebug 显示了添加了 JavaScript 元素的页面。为了创建地图,你的提供商必须将图像注入到地图div标签的子元素中。
在图片的src属性中,你会看到使用 Firebug 高亮的单个瓦片的 URL。你可以将这个 URL 复制到一个新的窗口或标签页中,只查看那个瓦片,而不包括其周围兄弟的上下文。此外,你还可以修改 URL 中的参数来查看其他瓦片。
地球由数千或数百万个瓦片组成,具体取决于缩放级别。地图提供商根据简单的网格系统来引用瓦片。你可以将其想象成类似于纸地图,它通过字母-数字组合来帮助你识别区域。例如,你可能查看(K, 18)以找到 Maple Street。
瓦片也通过其网格参考来命名;只是这些通常以千为单位编号。例如,我的旧金山示例可能包含一个位于(5241, 3718)的瓦片。每个缩放级别都有不同的网格,因此这个参考是调用特定瓦片所需的重要第三条信息。要调用示例瓦片右侧的瓦片,我会在同一缩放级别查找(5242, 3718)。只有第一个数字会改变,因为它代表网格的水平部分。垂直方向上,两个瓦片都在第 3718 行。
总结一下,你通常会在瓦片 URL 中找到三个数字:水平网格参考(通常称为X),垂直网格参考(Y)和缩放级别。
Mapstraction 提供了一种使用你想要的任何瓦片的方法,无论提供商如何。再次强调,大多数提供商不支持直接访问瓦片。很多时候,你可能发现他们使用方法来阻止你调用它们。你始终可以创建自己的瓦片。我在第二十六部分:使用自定义瓦片中向你展示了如何做到这一点以及如何将它们连接到 Mapstraction。第二十六部分:使用自定义瓦片。

图 1-6. 使用 Firebug 查找瓦片。
更改地图大小
初始地图大小由div标签的 CSS 样式确定。然而,你可以使用 Mapstraction 以编程方式更改地图的大小。
将以下行添加到create_map函数中,或者将其作为链接的点击事件包含(参见附录 A):
mapstraction.resizeTo(200, 300);
当调用resizeTo函数时,你需要传递宽度和高度。Mapstraction 随后立即将地图的大小设置为所需的像素。请注意,地图的中心不会被重置。如果你缩小地图,你可能需要重新定位地图以保持相同的中心点在视图中。
此外,关于缩小地图的一些建议:根据你使用的提供商,界面元素可能会开始碰撞。务必测试以查看你可以缩小到多小。
添加缩放和其他控件
在你的网站上使用地图的最好之处之一是用户可以使用它们进行探索。他们想看看附近有什么,更仔细地查看特定位置,或者弄清楚某个地点在城市或国家中的位置。我迄今为止展示的基本地图并没有给用户太多的控制权。
让我们给用户提供一个可以点击的界面,这样他们就可以在地图上缩放和平移。Mapstraction 提供了几种实现此功能的方法。
小控件
当你的地图不是很大时,小控件很有用,例如当你将其嵌入侧边栏时。此外,如果你不期望用户进行很多缩放操作,你可能更喜欢保持界面简洁。
要在你的地图中添加小控件,请在设置mapstraction变量后在create_map函数中添加此行:
mapstraction.addSmallControls();
保存并重新加载你的文件,你应该会在地图的左上角看到一组小按钮。控件的具体外观将取决于你指示 Mapstraction 使用的地图服务提供商。地图可能包含用于缩放的加号和减号按钮,以及一组箭头按钮用于平移。
大控件
如果你的地图是网站或页面的焦点,你可能想要大控件。虽然小控件只能让用户逐级缩放,但大控件可以跳到任何缩放级别。
将此行添加到你的create_map函数中:
mapstraction.addLargeControls();
保存并重新加载你的文件。当使用 Google Maps 时,Mapstraction 除了较大的缩放/平移工具外,还会添加几个其他控件。要查看小和大缩放/平移工具的外观,请参阅图 1-7。
要仅添加缩放/平移的大版本,尝试此函数:
mapstraction.addControls({zoom:'large'});
在这里,你需要传递一个对象(用花括号声明),并为其提供一个用于大缩放控制的选项。


图 1-7. Google 地图(顶部)和 Yahoo!地图(底部)中的小和大缩放控件
地图类型控件
地图服务提供商通常允许用户选择查看哪种类型的地图。除了普通地图外,卫星视图和普通与卫星视图的混合视图也很常见。并非每个地图服务提供商都提供所有这些视图。例如,一些使用航空摄影而不是卫星图像,或者根本不提供摄影图像。
使用此命令将地图类型控件添加到您的地图中,就像图 1-8 中所示,以便用户可以选择他们想要查看的地图:
mapstraction.addMapTypeControls();
保存并重新加载您的文件,以查看地图右上角(在大多数服务提供商中)的按钮。

图 1-8. Google 地图中的地图类型控件
设置缩放级别
现在您已经为地图添加了一些控件,用户可以更改缩放级别。在大多数项目中,您会希望赋予用户这种权力。无论如何,您需要设置一个初始缩放级别,这样地图服务提供商就知道要显示什么。
缩放级别决定了显示多少细节。整个世界的地图无法包含街道或公园。显示国界线与该地图的复杂程度相当——甚至大多数城市都比一个像素小。当缩放到城市级别的地图时,您将看到主要街道,但可能看不到街区特征。
在基本地图中,您同时设置中心和缩放级别。Mapstraction 还有一个仅设置缩放级别而保持中心不变的功能:
mapstraction.setZoom(10);
与setCenterAndZoom函数一样,缩放级别以整数形式传达。在这个例子中,我设置了 10 的缩放级别,而在基本地图中我使用了 15。这些数字代表什么?哪一个更接近缩放?
Mapstraction 使用缩放级别 0-16,较大的数字对应于更详细的缩放级别。使用卫星视图,您甚至可以进一步缩放。表 1-2 显示了缩放级别与通常显示的区域之间的近似对应关系。当然,确切级别取决于国家、州或城市的大小。
表 1-2. Mapstraction 缩放级别
| 地理描述 | 缩放级别 |
|---|---|
| 世界 | 0 |
| 国家 | 4 |
| 州 | 7 |
| 城市 | 13 |
| 街道 | 16 |
欣赏 Mapstraction 的另一个原因是它适应了地图服务提供商处理缩放级别的不同方式。例如,Yahoo!使用反向编号系统,其中较大的数字意味着地图缩放程度更大。另一方面,MapQuest 的计数方式与 Mapstraction 相同,但具有更少的级别和最少的细节。通过其单一接口,Mapstraction 处理这些差异。
就像您可以设置缩放级别一样,Mapstraction 有一个非常相似的命令来检索当前缩放级别:
var currentzoom = mapstraction.getZoom();
返回的整数位于 0-16 的 Mapstraction 范围内,如果需要,则从提供者的缩放系统中转换。你可以使用 setZoom 和 getZoom 的组合来创建自己的控件,而不是坚持使用地图提供者的默认设置。创建自己的缩放界面 在 创建自己的缩放界面 中展示了如何通过逐步说明来完成此操作。
设置地图类型
大多数地图提供者提供了三种确定地图外观的选项。你可以选择一个简单的地图,这是默认设置,卫星视图,或者两者的组合。正如我之前所展示的,你可以通过添加控件(通常位于地图的右上角)让用户决定要查看哪种地图类型。地图类型也可以通过编程方式设置,这就是本节我们将要做的。
你可以在任何时候设置地图类型,尽管最常见的情况是在地图加载时声明除默认类型之外的内容。例如,将此行添加到基本地图的 create_map 函数中,以显示带有街道信息的卫星视图:
mapstraction.setMapType(Mapstraction.HYBRID);
setMapType 函数接受的参数只是一个简单的整数,但正如你所看到的,我们传递了其他类型的变量。Mapstraction 有 常量——这些特殊变量是为了赋予比简单数字更多的意义而创建的,这使得选择地图类型变得容易。将地图类型设置为 HYBRID 比记住在需要同时显示街道和卫星视图时需要传递数字 3 要有意义得多。表 1-3 展示了所有地图类型选项及其对应的数值。
表 1-3. Mapstraction 地图类型
| 名称 | 值 |
|---|---|
Mapstraction.ROAD |
1 |
Mapstraction.SATELLITE |
2 |
Mapstraction.HYBRID |
3 |
当从 Mapstraction 获取当前地图类型时,地图类型常量也非常有用。正如你可以设置和获取缩放级别一样,你也可以访问地图类型。在你创建地图之后,添加此行:
var maptype = mapstraction.getMapType();
记住,setMapType 函数接受一个整数作为参数。getMapType 返回的值是 1、2 或 3。再次强调,通过 Mapstraction 常量来引用地图类型要容易得多。例如,你可以使用 switch...case 语句根据地图类型执行不同的操作:
switch (maptype) {
case Mapstraction.ROAD:
// Run this code only when it is a road map type
break;
case Mapstraction.SATELLITE:
// Run this code only when it is a satellite map type
break;
case Mapstraction.HYBRID:
// Run this code only when it is a hybrid map type
break;
}
将注释(以两个斜杠开始的行)替换为你想要在描述的情境中使用的代码。例如,你可能想要在只显示卫星地图的情况下从地图中删除路线,因为当没有街道可见时显示路线没有意义。
重新定位地图
如果您已经显示了一个地图,那么您已经使用setCenterAndZoom函数给它设置了一个中心点。您永远不知道用户何时会将地图拖离您的中心点。知道如何重置中心点——仅重置中心点——可能很有用。
将以下行添加到create_map函数中,或者将其作为链接中的点击事件包含在内:
mapstraction.setCenter(new mxn.LatLonPoint(37.7740486,-122.4101883));
当然,直接将地图移动到某个点可能会让人感到突然。幸运的是,用户可以拖动到新的中心点,就像他们拖动地图到那里一样:
mapstraction.setCenter(new mxn.LatLonPoint(37.7740486,-122.4101883), {pan:true});
在这里,我们向setCenter函数传递了一个选项对象。大括号在 JavaScript 中声明了一个对象,其值由key:value对设置。Mapstraction 会检查是否存在pan。如果pan设置为true,则地图将平滑地移动到新点。
检索地图中心
当您加载一个新的地图时,需要给它一个中心点。然后您放弃控制权,因为您的用户会根据他们的意愿拖动和缩放地图。这是好事,因为地图是交互式的。另一方面,您可能想知道地图的当前中心点。
Mapstraction 有一个简单的命令来检索地图的中心:
var centerpoint = mapstraction.getCenter();
创建的变量centerpoint保存了getCenter()调用返回的结果,作为一个LatLonPoint,这是 Mapstraction 存储坐标的方式。将坐标保存在变量中很方便,因为您可以转身将其传递给其他函数。或者您可以直接访问纬度和经度:
centerpoint.lat
centerpoint.lon
现在,您可以使用这些值在地图上添加标记,在地图上绘制本地结果,或用于其他目的。
查找用户点击的点
开箱即用的交互性使得地图 API 非常特别。使用内置控件,用户可以拖动和缩放地图,放大,并更改地图类型。用户也可以点击,但除非您帮助他们,否则不会发生任何事情。
要找到用户点击的点,您需要“监听”点击事件:
mapstraction.addEventListener('click', function(clickpoint) {
alert('latitude: ' + clickpoint.lat + '\nlongitude: ' + clickpoint.lon);
});
当用户点击地图上的任何位置时,会以clickpoint作为参数调用一个匿名函数。这与您检索地图中心时类似,变量是一个LatLonPoint。本例使用 JavaScript 弹窗来显示用户点击点的纬度和经度,如图图 1-9 所示。

图 1-9. JavaScript 弹窗显示点击点的纬度和经度
当然,您可能想要做一些比 JavaScript 弹窗更有用的事情。例如,您可以在该点在地图上添加标记。碰巧的是,下一章将向您展示如何做到这一点以及更多。
第二章. 绘制标记和消息框

创建简单的地图是查看某个位置周围区域的一种酷炫且实用的方式,但您会发现,当您在地图上绘制自己的点时,创建地图会更加有趣和实用。使用地图 API,您可以在地图上叠加小图形来引起对位置的注意(由纬度和经度坐标确定)。可选地,当点击标记时,您可以创建描述位置的文本消息。
您几乎在任何连锁店的网站上都能看到这些原则的应用,以及其他许多地方。如果您打算亲自购物,您可能已经使用了“查找商店”链接。从那里,您输入您的城市、ZIP 代码、地址或其他确定您位置的信息。然后出现一个地图,显示最近的商店,每个商店的位置都有标记,通常有一个与结果列表匹配的数字。如果数字是可点击的,您很可能会找到该商店的地址、电话号码或其他信息。
本章将指导您开始创建类似商店定位器这样的工具。您将学习如何添加标记、创建自定义图标、在悬停框中显示消息等。地图提供商实现了类似但略有不同的方式,在您的地图上绘制标记。Mapstraction 将这些差异整合成一组函数,无论底层地图类型如何,都可以添加标记和消息框。
#1: 在您的地图上添加标记
基本标记是网络地图的必备元素。标记将用户的注意力吸引到地图上的一个或多个点上。对于许多项目来说,您可能不需要比地图和一些基本标记更复杂的元素。
虽然我们将使用 Mapstraction 来生成我们的标记地图,但底层工作是由我们使用的任何地图服务完成的。就像地图的外观由提供商决定一样,基本标记的默认样式也将由提供商决定。图 2-1 显示了主要地图服务之间标记的差异。

图 2-1. 来自不同提供商的默认标记:Google、Yahoo! 和 Microsoft
要在您的地图上添加一个简单的标记,您只需要使用两个 Mapstraction 函数。首先,创建标记。然后,将其添加到地图上。为什么需要这两个不同的步骤将在后续项目中变得清晰,当我们开始使用高级选项,如自定义标记图标时。
让我们看看在代码中创建标记的样子。从你在创建 Mapstraction 地图中创建的基本 Mapstraction 地图开始,并在create_map()函数中添加以下行:
marker = new mxn.Marker(new mxn.LatLonPoint(37.7740486,-122.4101883));
// marker options will go here
mapstraction.addMarker(marker);
第一行创建了一个标记对象,传递了旧金山 No Starch Press 办公室的纬度/经度坐标。记住这是我们第一章中用作地图中心的同一个点。通过引起对图形标记的注意,我们实际上是在标记这个位置很重要。
第二行是用于稍后添加的任何标记选项的占位符。(任何以两个斜杠开头的 JavaScript 行都是注释,浏览器会忽略它们。)标记选项是我们告诉 Mapstraction 使用哪个图标或添加在点击标记时显示的消息的地方。
最后,第三行将标记添加到地图上。一旦发生这种情况,就无法再添加其他选项。原因是标记对象仅由 Mapstraction 使用。然而,一旦标记被添加到地图上,Mapstraction 就会调用映射提供者进行适当的调用。Mapstraction 根据事先设置的所有选项绘制标记。在这种情况下,我们没有要添加的选项,但我们将在这个地图的后续项目中添加。
如果你使用 Google 作为你的映射提供者,你的新地图将看起来像图 2-2。默认的 Google 图标位于地图中心。尽管标记是可点击的,但这个标记非常简单,如果你点击它实际上不会发生任何事情。继续阅读以了解你可以用标记做的其他酷事情。

图 2-2. 带有简单标记的 Google 地图
#2: 移除或隐藏标记
一旦你的地图上有标记,你可能希望选择性地从地图中移除它们。你可能想用新的结果替换当前的标记。或者,用户可能添加了一个不包括当前标记的过滤器。Mapstraction 提供了三个函数来使标记消失和再次出现。尽管移除和隐藏听起来像相似术语,但理解它们之间的区别很重要。从地图中移除标记意味着标记永远消失了。简单地隐藏标记允许你再次使其可见。
要使用移除、隐藏和显示标记的函数,你需要访问 Mapstraction 和标记对象。这些对象在你创建新地图以及每次创建新标记时生成。然而,它们是否可供脚本的其他部分使用,则取决于变量的作用域。
作用域指的是可以访问变量的代码部分。在create_map函数内部创建的任何变量只能在该函数内部使用。为了删除或隐藏标记,我们需要使我们的 Mapstraction 和标记对象全局化。为此,在create_map函数上方添加此声明:
var mapstraction, marker;
这两个变量现在具有全局作用域,这意味着我们可以在create_map函数外部使用这些变量。要删除标记,你需要在 Mapstraction 对象上调用removeMarker函数:
mapstraction.removeMarker(marker);
要简单地隐藏一个标记,你需要在标记对象上调用hide函数:
marker.hide();
要使隐藏的标记重新出现,你需要在标记对象上调用show函数:
marker.show();
你在哪里调用这些函数?任何需要的地方。为了测试目的,在<body>标签后的任何地方创建一个链接。例如,这里有一个将隐藏你的标记的链接:
<a href="javascript:marker.hide();">hide marker</a>
再次强调,这个位置只是为了测试。你想要不干扰的 JavaScript,它不会被链接的href调用。使用这三个函数的一个障碍是访问标记对象。
这个单个标记示例只需要变量在全局范围内。正如你所看到的,这很容易。当你开始使用许多标记时,你需要一种方法来组织它们,而不仅仅是声明几十个变量。
Mapstraction 内置的过滤特定标记的能力(参见第九部分:过滤特定标记)可能是最简单的解决方案。如果它没有提供你需要的所有功能,你总是可以访问 Mapstraction 添加的所有标记:
var allmarkers = mapstraction.markers;
Mapstraction 的markers对象为你提供了一个标记数组。从这里,你可以根据需要删除、隐藏或显示它们。
#3: 当你的标记被点击时显示消息框
标记本身很有用,因为它们可以标识地图上的位置。一旦你的地图有多个标记,观众就会开始想知道每个标记代表什么。当然,你可以使用自定义图标来区分标记,我们很快就会看到如何做到这一点。但你可以通过在用户点击标记时显示描述性文本来提供更多信息。
每个映射提供商都有一种显示消息框的方式。就像标记本身一样,根据提供商的不同,框的外观也不同。图 2-3 显示了主要地图服务消息框之间的差异。

图 2-3. 来自不同提供商的消息框
Mapstraction 提供了一个名为InfoBubble的接口,它与所有提供商一起工作。要为标记创建一个 InfoBubble,你可以添加一个标记选项,如下所示:
marker.setInfoBubble("Look ma, No Starch!");
setInfoBubble 函数接受一个文本字符串(HTML 也同样适用)并将其与标记关联保存。该行必须在创建标记对象之后但在将其添加到地图之前插入。如果您有创建基本标记的代码(在第一章:向地图添加标记中),您只需将 setInfoBubble 行添加到关于标记选项的注释处即可。
为了澄清,以下是创建全新标记、包含信息气泡并将其放置在地图上的必要命令:
marker = new mxn.Marker(new mxn.LatLonPoint(37.7740486,-122.4101883));
marker.setInfoBubble("Look ma, No Starch!");
mapstraction.addMarker(marker);
太好了!现在如果您加载此文件,您将在 No Starch Press 社区看到基本标记。信息气泡在哪里?点击标记,您将看到类似于图 2-4 的内容。Mapstraction 和地图服务提供商会完成捕获点击事件和显示信息气泡的所有工作。您需要做的只是提供内容。如果您希望自动打开信息气泡或从代码中打开,请继续阅读;我将在下一个项目中向您展示如何在不让用户点击的情况下显示消息框。

图 2-4. 显示消息的消息框
#4: 不点击标记显示和隐藏消息框
地图允许用户点击并交互位置。您已经看到了如何添加可点击标记,这些标记提供了关于您所绘制位置更多信息的功能。但有时您想要更多控制。有时您想在用户没有权限的情况下打开信息气泡。
例如,如果您的地图显示搜索结果,您可以在地图旁边以列表格式复制位置。然后用户可以从列表中选择位置,相应的标记将在地图上打开消息框。
从代码中显示消息框的基本设置与标准可点击标记相同。您只需将一些文本设置为标记选项:
marker.setInfoBubble("Look ma, No Starch!");
这样确保了点击的标记仍然会显示您的信息。然后,在您的代码的其他地方(例如当用户点击搜索结果之一时),您可以告诉标记打开信息气泡:
marker.openBubble();
您可以使用类似的命令关闭信息气泡:
marker.closeBubble();
openBubble 和 closeBubble 函数需要标记变量全局可访问。也就是说,标记对象需要在代码顶部声明,或者您需要找到另一种方法来访问它。在函数中,我描述了变量作用域以及如何声明变量以便在代码的任何地方访问。
#5: 创建自定义图标标记
要让地图感觉像是自己的,最快的方法就是更改用于标记的默认图标。Mapstraction 提供简单的标记选项,使得使用自定义图标的技术过程变得非常简单。可能更费力的部分是创建图标文件本身。为了避免这个问题,你可以在网上找到其他人免费制作的图标。我在mapscripting.com/download-custom-markers/列出了几个资源。
还想自己创建吗?继续阅读。
拿出图像编辑器
要创建自己的标记图标,你只需要有一个可以保存透明 .png 文件的图形程序。图标可以是任何大小,但保持每个维度在 20 到 50 像素之间可能是最好的。如果图标太小,点击它变得困难;太大,图标会遮挡你试图指出的位置。
如果你使用 Google 作为你的地图提供商,你还需要创建一个图像作为标记的阴影。如果你的标记形状与 Google 默认形状相似或如果你使用其他提供商,这一步是不必要的。
注意
不是很好的图像魔术师?你可以在www.cycloloco.com/shadowmaker/找到一个在线服务来创建阴影。
将您的图标添加到地图中
现在你有了图标,添加到标记选项的部分就简单了。只需设置几个值来告诉 Mapstraction 图标图像文件的位置。你最好的选择是将自定义标记图标保存在服务器上的一个特殊目录中。如果你在本地测试,你可以使用本地副本,通过它们相对于包含地图的页面的位置来访问。为了简单起见,我在这个例子中将 HTML 文件和图标文件放在同一个目录中。实际上,你可能更喜欢更有组织。
我决定使用一个微小的 No Starch Press 标志作为我的自定义图标。它宽 27 像素,高 31 像素。就像我说的,图标非常小。然后,我使用阴影制作服务创建了一个包括标记阴影的 43×31 像素的文件。
最后,是时候编码了。将这些行作为标记选项添加。这些行是在创建标记之后但在将标记添加到地图之前插入的:
marker.setIcon(❶'nostarch-logo.png', ❷[27,31]);
marker.setShadowIcon('nostarch-shadow.png', [43,31]);
你需要包含的唯一参数是图标和阴影的图像路径 ❶。注意,每个图形的尺寸都作为内联数组 ❷传递。此参数是可选的,但建议使用。如果你省略它,一些提供商将假设默认标记的尺寸,这可能导致图形缩放不良。
自定义标记代码的结果显示在图 2-5 中。No Starch Press 的办公室用公司的标志,一个小铁图标标记。注意,还有阴影,这使得图形从地图中突出出来。
在自己的风险下省略阴影图标。一些地图提供者将假设默认的阴影,这可能与你的图标看起来很傻。并非每个地图提供者都使用阴影,但计划一个阴影是好的。如果你真的不想有阴影,考虑使用完全透明的图形。我在#69: 创建天气地图中展示了无阴影图标的示例。

图 2-5. 自定义标记显示了 No Starch Press 的标志
#6: 创建编号标记
当你在网页上有一系列位置信息,并且希望将这些位置也绘制到地图上时,为用户提供带编号的标记。例如,在显示搜索结果时,你希望在地图上和地图外都显示匹配的标签,以便用户能够轻松识别。
编号标记与其他自定义标记没有区别。你需要为每个数字创建一个图形图标。网上有大量的图标集可供使用,或者你可以使用 Google Charts API 动态创建它们。
生成编号图标
Google Charts API 生成反向泪滴风格的图钉,看起来像默认的 Google 标记。使用这些由 Google 生成的图标并不意味着你必须使用 Google Maps。Mapstraction 将为任何提供者添加图标到你的地图上。
你可以控制标记的背景和边框颜色,以及标签的读取内容。你需要的标准通过图标本身的 URL 发送。例如,这是一个带有数字一的红色标记的 URL:
http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=❶1|❷FF3333|❸000000
URL 的最后一个参数包含了标记的所有重要信息:标签文本❶(在这种情况下,数字一),背景颜色❷以及边框❸颜色。颜色以十六进制值表示,类似于 CSS 中颜色的声明方式。
chld参数的各个部分由管道字符|分隔。从某种意义上说,最后的参数实际上是三个参数,它们各自以自己的方式分割值。
当使用 Google 作为地图提供者时,添加到地图上的自定义标记也需要一个阴影。因为这些动态标记的形状都是相同的,所以阴影可以是静态的。Google Charts API 提供了这个 URL:
http://chart.apis.google.com/chart?chst=d_map_pin_shadow
现在你已经可以生成图标了,你需要将它们放置到地图上。为此,我们将实时调用这些 Google Charts URL。
将图标添加到地图
带着由 Google Charts 动态生成的标记 URL,将这些编号标记添加到地图上的过程与添加任何自定义图标的过程非常相似。以下是在旧金山内部创建五个随机点的代码示例。每个标记都根据创建的顺序分配了一个从一到五的编号标签:
mapstraction = new mxn.Mapstraction('mymap', 'googlev3');
mapstraction.setCenterAndZoom(new mxn.LatLonPoint(37.7740486,-122.4101883), 11);
mapstraction.addLargeControls();
for (i=1; i<=5; i++) {
var rndlatlon = get_random_by_bounds(mapstraction.getBounds());
marker = new mxn.Marker(rndlatlon);
`marker.setIcon(`
`'http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=' + i +`
`'|FF3333|000000', [21,32]);`
`marker.setShadowIcon(`
`'http://chart.apis.google.com/chart?chst=d_map_pin_shadow');`
mapstraction.addMarker(marker);
}
mapstraction.autoCenterAndZoom();
加粗的行设置了生成的图标及其阴影。其余的行要么设置地图,要么创建随机点。为了使代码正常工作,您需要一个名为 get_random_by_bounds 的 JavaScript 函数,该函数在 第六章 中讨论,但我将其重新打印如下。将前面的代码放入迄今为止所有示例中使用的 create_map 函数中,然后确保以下函数包含在 JavaScript 中(但不在其他函数中):
function get_random_by_bounds(bounds) {
var lat = bounds.sw.lat + (Math.random() * (bounds.ne.lat − bounds.sw.lat));
var lon = bounds.sw.lon + (Math.random() * (bounds.ne.lon − bounds.sw.lon));
return new mxn.LatLonPoint(lat, lon);
}
保存您的文件。您将看到一个类似于 图 2-6(标记位置可能不同——请记住,它们是随机的)所示的地图。

图 2-6. 编号标记,随机绘制
当顺序很重要时,例如在显示附近位置时,请使用编号标记。当用户将搜索结果或其他列表与地图上的单个标记匹配时,编号也很有帮助。
#7:遍历所有标记
当您向地图添加了大量标记后,您可能希望有一种方法来访问它们。例如,您可能正在寻找异常值或确定哪个标记是最北边的。
Mapstraction 提供了一个属性,它包含地图上绘制的每个标记的数组。然后,您可以使用标准的 JavaScript 代码从该数组中引用单个标记,以在特定索引处提取值。对地图上的每个标记执行此操作可以让您遍历并执行所有标记上的操作。
将这些行添加到您的代码中,您需要为每个标记执行某些操作的地方:
❶ var allm = mapstraction.markers;
❷ for (var i=0; i<allm.length; i++) {
❸ var thism = allm[i];
// Any code for thism variable goes here
}
我们首先使用一个新的变量名 allm 引用 Mapstraction ❶ 中所有标记的数组。这节省了我们一些打字,因为我们还需要多次使用标记变量。接下来,我们使用 JavaScript 的 for 语句 ❷ 遍历数组。一个临时变量 i 跟踪索引,因为我们从零(数组中的第一个元素位于零)计数到标记总数。
随着每个标记变得可用,我们在临时变量 thism ❸ 中放置对该标记的引用,我选择这个名字是因为它描述了“这个标记”,即我们目前正在使用的标记。现在,for 循环的大括号 { 和 } 中的任何内容都可以访问这个新变量。
我们可以查找标记选项或在标记上调用函数(例如,showBubble 或 hide 等)。在大多数情况下,我们无法 添加 选项,因为选项需要在标记添加到地图之前添加。例如,我们不能更改标记的图标,除非移除并重新添加标记。
尽管有这些限制,但遍历标记是将有用的技巧添加到您的制图工具包中的好方法。Mapstraction 的许多函数,如过滤或自动居中,都使用循环作为内部操作。
#8:根据标记确定正确的缩放级别
当你的地图上有几个标记点时,确保所有标记点都可以被查看变成了一项繁琐的工作。这尤其在你从数据库中获取位置信息时(见第六十六部分:从数据库中绘制位置)更是如此。标记点开始超出你手动设置的中心和缩放级别。
你可能已经尝试通过更改缩放级别来解决这个问题。如果你缩小缩放级别,你的标记点可能会挤在一起,留有足够的空间进行放大。要为任何标记点达到一个良好的缩放级别,唯一的方法是在所有标记点都添加到地图后,通过编程方式确定它。
Mapstraction 将设置缩放级别简化为一个函数调用。在你添加了一些标记点后,将以下行添加到基本地图的create_map函数中:
mapstraction.autoCenterAndZoom();
你还可以使用一个类似的功能,它只适用于显示的标记点。也就是说,如果你隐藏或过滤掉了一些标记点,你可能希望放大到仍然在地图上的那些标记点。而不是使用之前的函数,使用以下函数:
mapstraction.visibleCenterAndZoom();
如果这些功能感觉像是魔法,那也没关系。Mapstraction 使这变得简单,但幕后有很多事情在进行。以下是 Mapstraction 如何实现自动缩放的概述:
-
遍历所有标记点(或仅遍历可见的标记点),并确定标记点的最大和最小纬度和经度。这个测量值被称为边界框,由四个数字组成,描述了框的每一边。
-
通过平均两个纬度和两个经度来找到边界框的中心。
-
检查缩放级别,直到找到可以显示整个边界框的一个。
实际上,Mapstraction 不需要为许多地图服务提供商执行最后两个步骤。大多数地图服务提供商的库中已经有一些可以自动缩放的功能。然而,情况并非总是如此,这也指向了 Mapstraction 的力量。Mapstraction 能够在所有地图 API 可用之前添加这些不可或缺的功能。
为了了解自动缩放是如何工作的,将以下代码插入基本地图的create_map函数中,以添加随机标记点:
❶ var num_markers = 5;
❷ var bigbounds = new mxn.BoundingBox(37.766, −122.400, 37.784, −122.418);
for (i=1; i<=num_markers; i++) {
var rndlatlon = ❸get_random_by_bounds(bigbounds);
marker = new mxn.Marker(rndlatlon);
mapstraction.addMarker(marker);
}
❹ mapstraction.autoCenterAndZoom();
此代码选择了五个随机标记点,但你可以将第一个变量❶更改为任何你想要的数字。我还创建了一个比基本地图可见区域更大的边界框❷(你可以在第六章中了解更多关于边界的信息)。这些边界用于为我的每个标记点生成一个新的随机点。实际上,你需要包含一个特殊函数❸来创建这个点。我们稍后会讨论这个问题。
首先,注意最后一行❹,自动缩放的行。尝试通过在行首放置//来注释掉它,看看没有自动缩放时标记器看起来如何。多次重新加载地图,带有和没有注释斜杠的情况。图 2-7 显示了在这些情况下每种情况的地图示例比较。

图 2-7. 无自动居中和缩放与有自动居中和缩放的标记器之间的差异
当然,你需要那个特殊函数,get_random_by_bounds。与之前的代码不同,将此函数添加到create_map函数外部,但仍在 JavaScript 部分内部:
function get_random_by_bounds(bounds) {
var lat = bounds.sw.lat + (Math.random() * (bounds.ne.lat − bounds.sw.lat));
var lon = bounds.sw.lon + (Math.random() * (bounds.ne.lon − bounds.sw.lon));
return new mxn.LatLonPoint(lat, lon);
}
这个函数在#44: 在边界框中获取随机点中有详细描述。
关于自动居中和缩放,现在你可以在一个函数调用中完成它,你可能会将其包含在除了最简单的地图之外的所有地图中——它真的非常有用。
#9: 过滤掉某些标记器
到现在为止,你肯定有一大堆标记器,对吧?这意味着你正在掌握这种映射技术。当有满屏的标记器时,用户可能会想要看到他们关心的内容。这就是 Mapstraction 的过滤选项变得方便的地方。
在没有过滤选项的情况下跟踪许多标记器是一件痛苦的事情。你需要维护全局数组或使用mapstraction.markers对象。在两种情况下,你都需要一种方法来区分标记器的类型或与之相关的某些数据。
过滤的第一步是创建一个新的属性并将其添加到你的新标记器中。你是通过添加一些标记器选项来做到这一点的,这必须在创建标记之后但在将其添加到地图之前完成。在这里,我将价格设置为 1000——也许这个标记代表一个公寓,而这个属性是它的租金:
marker.setAttribute('price', '1000');
如果你想要添加更多属性,比如卧室数量,你可以通过额外的setAttribute行来实现。一旦你为几个标记器添加了数据,你就可以继续进行过滤。
过滤是在标记器添加到地图之后应用的。实际上,过滤通常是在用户行为(如用户点击过滤器按钮或输入搜索词)之后发生的。
要显示具有大于或等于 1000 的价格属性的标记器,请使用此代码:
mapstraction.removeAllFilters();
mapstraction.addFilter(❶'price', ❷'ge', ❸1000);
❹ mapstraction.doFilter();
首先,使用removeAllFilters,除非你知道没有应用任何过滤器。原因是过滤器是累加的,这意味着第二个过滤器不会删除第一个。由于之前应用的过滤器,你可能会得到比你预期的更少的结果。
一旦移除了过滤器,你可以继续。要添加过滤器需要三个参数:属性名称 ❶、操作符(在这种情况下为 greater than 或 equal to) ❷,以及用作比较的数字 ❸。最后,除非你应用了过滤器 ❹,否则地图将不会发生任何变化。
表 2-1. 过滤运算符
| 操作符 | 描述 |
|---|---|
ge |
大于或等于——用于数字。 |
le |
小于或等于——用于数字。 |
eq |
等于——用于数字或与单词(如标签或类型)一起使用。 |
一旦应用了过滤器,不符合过滤器的标记将消失。在我们的例子中,任何价格属性小于 1000(或没有价格属性)的物品都将被移除。因此,过滤可以被视为过滤“内”而不是“外”。
Mapstraction 提供了三个操作符用于过滤标记,如 表 2-1 所示。你可以组合过滤器以实现更精细的结果。继续以公寓搜索为主题,你可能添加一个区域属性,这样用户就可以查看某个区域价格低于某个水平的公寓,例如。
Mapstraction 过滤器是一种快速显示基于简单标准的一组标记子集的方法。它们不需要与服务器进行任何额外的通信,因为 Mapstraction 将每个标记的信息存储在内存中。有关在真实项目中使用过滤器的示例,请参阅 #71: 通过位置搜索音乐活动 在 #71: 通过位置搜索音乐活动。
#10: 删除或隐藏所有标记
需要重新开始,或者在某些情况下想要显示一个清晰的地图?我们时不时都可以用一点春季大扫除。我已经在本章中展示了如何删除或隐藏单个标记。现在我们将摆脱它们所有。
再次确保你明白,当你删除一个标记时,它将永远消失。有时删除标记是希望的行为,例如当用户激活新的搜索时。Mapstraction 有一个功能可以做到从零开始。另一方面,隐藏的标记总是可以再次显示。我们将不得不编写自己的函数来隐藏所有标记并使画板看起来干净。
首先,让我们来点破坏性的,从我们的地图中删除所有标记。在任何你想砍掉所有标记的地方添加此行:
mapstraction.removeAllMarkers();
就这样——它们消失了。#71: 通过位置搜索音乐活动 在 #71: 通过位置搜索音乐活动 中展示了每次用户开始搜索时使用此函数的示例。通过删除标记,我们确保之前的搜索结果不会与新结果混淆。
如果我们只想隐藏所有标记,我们需要编写自己的函数。为此,我们需要遍历所有标记(在#7: 遍历所有标记中详细描述),并隐藏每一个。
function hideAllMarkers() {
❶ var allm = mapstraction.markers;
for (var i=0; i<allm.length; i++) {
❷ var thism = allm[i];
❸ thism.hide();
}
}
到目前为止,我们的大部分代码都是在create_map函数内部使用的。因为hideAllMarkers是一个新函数,所以我们需要在它的位置添加它,但不在其他函数之外,仍然在页面的 JavaScript 部分。
函数本身很简单。它首先从 Mapstraction 获取标记对象的引用❶,该对象包含添加到地图上的所有标记的数组。然后,使用这个标记数组,函数逐个遍历它们。每次循环中,函数都会取另一个标记并将其放入一个名为thism❷的临时变量中(代表“这个标记”)。最后,它在这个标记上调用hide函数❸。
到函数结束时,地图上不会显示任何标记。我们已经遍历了每个标记,并逐个隐藏了它们。
编写函数还不够。我们需要在代码的其他地方调用这个函数:
hideAllMarkers();
注意,这个调用看起来与移除所有标记的调用非常相似。一个主要区别是removeAllMarkers是在 Mapstraction 对象上被调用的。这个新函数仅仅是独立调用的。区别在于我们自己在代码中编写了hideAllMarkers,而 Mapstraction 的函数是其包的一部分。
编写实用函数,就像我们在这里隐藏每个标记一样,是编程的重要部分。现在我们已经编写了这个函数一次,我们可以在需要时随时调用它。
#11: 处理标记簇
本章已经介绍了几种理解带有许多标记的地图的方法。你可以对它们进行编号和筛选。你可以自动缩放以显示地图可见部分内的所有标记。这些工具都很有用,但你可能会发现有时你的标记仍然挤在一起并重叠。这是不可避免的,但你可以使这个问题不那么严重。
而不是显示每一个单独的标记,你可以使用一个特殊的图标来代表一组标记。然后,当用户放大查看时,该组标记将消失,并被实际的标记所取代。你可以在图 2-8 中看到一个带有和没有聚类的许多标记的例子。

图 2-8. 带和不带聚类的标记差异
标记聚类的代码背后非常复杂,但概念很简单。尽管存在许多方法,但通常地图被分成一个网格。如果一个地图的单元包含一个以上的标记(或者超过一定数量——你可能更喜欢每个单元五个标记的截止值),它们将被一个聚类替换。
我们不会自己编写这个算法,而是会使用一个已经编写好并可以直接与谷歌地图一起工作的实用工具,称为ClusterMarker。你可以从www.acme.com/javascript/下载代码,并将其保存为名为clusterer2.js的文件。
与本书中的大多数示例不同,你将直接与谷歌地图(Google Maps)工作,就像你在创建谷歌地图中做的那样,在创建你的第一张地图。除了在页眉中包含谷歌地图 API JavaScript 之外,你还需要引用新的聚类文件。将以下内容添加到 HTML 的页眉部分:
<script type="text/javascript" src="clusterer2.js"></script>
聚类代码与 Mapstraction 类似,因为它将自己包裹在谷歌地图周围。添加标记的代码将通过聚类函数进行,然后被路由到谷歌地图。用这个函数替换你的create_map函数,它将在地图上放置 100 个随机标记并在必要时进行聚类:
function create_map() {
if (GBrowserIsCompatible()) {
// Basic Google Map
var map = new Gmap2(document.getElementById("mymap"));
var center_point = new GLatLng(39.34, −98.26);
map.setCenter(center_point, 8);
map.addControl(new GsmallMapControl());
// Cluster settings
❶ var clustobj = new Clusterer(map);
❷ clustobj.SetMaxVisibleMarkers(50);
❸ clustobj.SetMinMarkersPerCluster(2);
// Add Markers
for (var i=1; i<=100; i++) {
var lat = center_point.lat() + Math.random() − 0.5;
var lon = center_point.lng() + Math.random() − 0.5;
var gmk = new GMarker(new GLatLng(lat, lon));
❹ clustobj.AddMarker(gmk, 'Marker #' + i);
}
}
我将这个地图大致定位在美国中部(嗨,堪萨斯州!)。一旦创建了基本地图,我们需要告诉聚类代码在哪里可以找到它❶,这将创建一个对象,并将其放入名为clustobj的变量中。
在我们添加任何标记之前,我们希望重置聚类器的一些属性。第一个❷设置当聚类代码开始聚类时的标记数量。默认值是 150,这意味着如果我们不更改此设置,我们所有的 100 个标记将不会进行聚类而全部显示。下一个设置❸声明在聚类接管之前需要多少个标记占据一个网格单元。默认值是 5,对于我们的示例来说似乎有点拥挤。尝试找出最适合你地图的设置。
现在我们已经准备好向地图添加标记。我编写了一个for循环,在地图中心附近随机生成 100 个标记。然后,我们不是直接将它们添加到地图上,而是将它们添加到聚类器❹中。
保存你的文件,并在浏览器中打开它以查看大型的聚类标记。你也可能看到一些散乱的普通标记——这些标记不需要聚类,因为其他标记附近没有标记。地图看起来整洁多了,不是吗?放大一些,一些聚类将消失,因为地图能够显示实际的标记而不使它们拥挤。
更改聚类图标
默认情况下,你的聚类代码使用一个大的蓝色图标作为聚类标记。如果你有其他你更愿意使用的图形,你可以包含它。
在你的其他集群设置之后,但在你开始添加标记之前,添加以下代码:
var cicon = new GIcon();
cicon.image = 'icon.png';
cicon.iconSize = new GSize(27,31);
cicon.shadow = 'shadow.png';
cicon.shadowSize = new Gsize(43,31);
clusterer.SetIcon(cicon);
聚类图标解决了标记过载问题,其中标记数量如此之多以至于变得没有意义。聚类也是一种快速的方法,仍然可以展示所有内容,而不会让你的用户感到不知所措。
第三章。地理编码

我在这本书的前两章中演示了一些有趣的地图示例,但房间里有一头大象。我在描述地球上的一个点中解释的描述地球上的一个点中的经纬度点显然对计算机很有用,但对人类来说并不如此。你有多少次被邀请到一个仅用地理坐标指定的地点参加派对?
在本章中,我将展示如何将对你有意义的事物——地址、城市名称,甚至邮政编码——转换成 Mapstraction 所需的纬度和经度点。这种转换称为地理编码,我们将探讨你可以在自己的地图项目中使用的几种方法。
地理编码器是如何工作的?
初看起来,地理编码器就像一个先知。你给它一个地址,它看看它的水晶球,然后给出一个回复。就像魔法一样,当这些坐标在地图上标出时,它们正好位于地址所在的位置。让我们揭开幕布,看看地理编码器是如何工作的。
就像大多数魔法一样,你可能会失望地发现,大多数地理编码实际上只是估计。地理编码并不优雅——它有点像蛮力。
首先,地理编码器会将地址分解成各个部分。例如,考虑格莱美庄园的地址:
3734 Elvis Presley Blvd, Memphis, TN
街道号码,街道名称,后缀,城市,州
城市很重要,因为信不信由你,其他城市也有 Elvis Presley Blvd。当你想到更常见的名称(比如,Main Street)时,你可以看到这一步是如何变得重要的。事实上,在城市内部,道路的后缀可能是一个问题,同一条街道可能被称为 Avenue、Street、Circle 等等。如果你认为这些变化在导航时很令人困惑,那么想想它们如何让地理编码器困惑。
街道名称匹配可能是修复位置中最困难的部分。处理拼写错误和其他名称格式化方式可能很困难。像许多其他大城市一样,我家乡俄勒冈州的波特兰市有一条以马丁·路德·金爵士命名的街道。这条街道在波特兰和其他城市都通常被称为MLK。是否应该让地理编码器识别这个缩写是有争议的,但这让你了解你必须考虑的一些问题。
现在你已经确定了正确的街道并知道它的城市,你需要找到街道上的实际地址。存储世界上所有的地址是不必要的,而且由于新地址不断产生,这会很困难。相反,大多数地理编码器使用街道段,如图图 3-1 所示。

图 3-1. 地理编码器使用街道段来估计位置。
以我们的格拉辛德(Graceland)为例,我们可能知道伊丽莎白·泰勒大道(Elvis Presley Blvd)的一段从 3700 号开始,到 3799 号结束。我们还知道该段两端的地纬度和经度点。使用这些信息,我们可以估计格拉辛德(3734 号)大约位于 3700 号和 3799 号之间三分之一的位置。使用这两个点,地理编码器可以计算出大致的位置。
JavaScript 与 HTTP 地理编码
在本章中,我将介绍两种从地址中检索纬度和经度结果的主要方法:通过 JavaScript 地理编码 API 或通过 HTTP 上的地理编码网络服务。表面上,这些方法非常相似,因为返回的数据是相同的。区别在于你可以用结果做什么。
JavaScript 地理编码器使用客户端代码调用外部服务器,这意味着代码在浏览器中运行。这种方法与 Mapstraction 和每个映射 API 所使用的方法相同。从这个意义上说,JavaScript 地理编码器对于网络地图开发者来说非常方便。
HTTP 地理编码器也调用外部服务器,但这是从你的服务器进行的,所以代码在浏览器外运行。这种方法与我在第九章(ch09.html "第九章。服务器端编程")中使用的网页编程语言 PHP 所使用的方法类似。事实上,你可能会使用 PHP 来解释 HTTP 地理编码器的结果。
为什么你会选择使用一种类型的地理编码器而不是另一种?假设地理编码器来自同一提供商(例如,谷歌),数据质量是相似的。JavaScript 地理编码器通常只是服务器端地理编码器的一个包装器。换句话说,两种类型最终都会调用相同的数据集。
关于使用哪种地理编码器的决定取决于你对输入和输出的自由度有多少。你只能以非常有限的方式使用 JavaScript 地理编码器。输入很可能来自用户通过表单。输出几乎肯定是要发送到网页或网页上的地图。
当然,你大部分的位置数据最终都会出现在地图上。然而,在发生这种情况之前,你可能想做很多事情。获取输出控制是你会使用 HTTP 地理编码器的主要原因之一。当结果在服务器端检索时,你有选项将其存储到数据库中,缓存数据,通过短信或电子邮件共享数据,或者将其发送到第三方服务。其中一些选项可能使用 JavaScript 地理编码器是可能的,但它们不容易实现,因为它们超出了 JavaScript 地理编码器的正常用途——在浏览器内部。
通过使用 HTTP 地理编码器,您控制着输入的来源。例如,您可能从数据库、第三方服务或地址列表中检索它。使用 JavaScript 地理编码器时,输入很可能直接来自用户在网站上的输入框中输入的数据。对于许多用途,这已经足够了,但有时您可能需要更多的自由度。
如果您花足够的时间制作网络地图,您可能会结合使用 JavaScript 和 HTTP 地理编码。本章的其余部分将重点介绍您在将可读地址转换为地理坐标(反之亦然)时可以使用的不同服务。
#12: 使用 JavaScript 进行地理编码
当您想要在网页浏览器内进行地理编码时,您会使用 JavaScript,这是 Mapstraction 和它所支持的每个地图服务提供商所使用的相同编程语言。您可以从用户那里获取城市名称或地址,并将其发送进行地理编码。响应将发送到您声明的 JavaScript 回调函数。
与地图 API 一样,您可以选择自己的 JavaScript 地理编码器。不出所料,我将展示 Mapstraction 的地理编码器,它将使用您指定的任何提供商,并为您提供使用 Mapstraction 创建地图时相同的灵活性:编写一次代码,轻松切换提供商。
由于并非每个地图都需要地理编码,因此地理编码器被保留在主 Mapstraction 代码之外。此外,并非每个提供商都有地理编码器,因此每个都单独存储。在这个例子中,我们将使用 Google。您可以从 mapstraction.com/ 下载 mxn.google.geocoder.js。
您可以在代码中像包含您的地图提供商和 Mapstraction 本身一样包含 Mapstraction 地理编码器。假设文件位于当前目录中,在基本地图的 <head> 部分添加此行:
<script src="mxn.google.geocoder.js"></script>
包含脚本后,使用以下代码初始化地图、将地址进行地理编码并在地图上标记结果:
var mapstraction, geocoder;
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'googlev3');
geocoder = new MapstractionGeocoder(❶add_point, ❷'googlev3');
// Create address object
❸ var address = {
street : "38 Ringold Street",
locality : "San Francisco",
region : "CA",
country : "US"
};
❹ geocoder.geocode(address);
}
function add_point(loc) {
var mk = new mxn.Marker(❺loc.point);
mk.setInfoBubble(❻loc.address);
mapstraction.addMarker(mk);
mk.openBubble();
mapstraction.autoCenterAndZoom();
}
与本书中的大多数地图示例一样,我使用 create_map 函数来初始化地图。您可以给这个函数取任何名字,只要在页面加载时调用这个函数即可。
要创建 Mapstraction 地理编码器对象,我们需要提供一个回调函数 ❶ 并告诉 Mapstraction 我们想使用哪个提供商 ❷(提供商的 JavaScript 仍然需要包含)。回调函数接收地理编码的结果。首先,我们需要给地理编码器提供一个地址或城市名称。
我们创建一个通用的 JavaScript 对象 ❸ 来存储文本位置信息。然后,我们为地址的各个部分添加属性。名称可能有些奇怪,因为 Mapstraction 正在尝试在全球范围内工作,而不仅仅是您的国家。
一旦地址的部分被填写完整,你可以将其发送到地理编码器 ❹。然后,你的回调函数将作为位置对象传递结果,该对象本身也有一些有趣的属性。最重要的是地址的LatLonPoint ❺。在这个例子中,我使用了这个点来创建一个新的标记。
标记需要一个描述,我会把它放在一个消息框里。我使用的是完整的地址 ❻,这是地理编码器清理过的。正如你在图 3-2 中看到的,地理编码器添加了邮政编码,将“Street”转换为“St”,并将国家称为“USA”而不是“US”。

图 3-2. No Starch Press 办公室的地理编码结果
当然,这个例子只具有这样的用途。地址是硬编码的。你很可能会从用户那里获取输入。
地理编码用户输入
JavaScript 地理编码器通常需要用户输入才能发挥作用。让我们调整上一节中的代码,从输入框中获取文本位置并进行地理编码。
首先,你需要一个地方让用户输入数据。让我们在我们的 HTML 中添加一个表单,要么在地图div上方,要么在下方:
<form id="addrform">
<input type="text" id="newpt" />
<input type="submit" id="butnew" value="Geocode" />
</form>
现在我们需要在用户输入地址或城市名称时做些事情。下面是create_map函数的新版本,其中做了一些修改:
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'googlev3');
mapstraction.setCenterAndZoom(new mxn.LatLonPoint(0, 0), 1);
mapstraction.addSmallControls();
geocoder = new MapstractionGeocoder(add_point,'google');
❶ document.getElementById('addrform').onsubmit = function() {
var address = {
❷ document.getElementById('newpt').value
}
geocoder.geocode(address);
return false; // avoids posting form to the server
};
}
我们在得到输入之前不能进行地理编码,而输入只有在用户提交表单时才会发生。所以我们等待提交 ❶,然后使用匿名函数采取行动。我们也可以使用命名函数,但只有几行代码,直接在代码中编写是最容易的。
在上一节中,我分别处理了地址的每一部分。在用户输入的情况下,这并不总是可能的。相反,我已经将输入框的整个值提供给地理编码器 ❷。
回调函数add_point可以保持不变。输入一个地址,它会在地理编码的点处添加一个标记。再次操作,现在你有了两个标记。这种地理编码可能会上瘾。
#13: 使用 HTTP 网络服务进行地理编码
当你希望在浏览器中进行地理编码时,JavaScript 非常方便。然而,通过使用网络服务,你可以获得更多对地理编码的控制。地址可以来自任何地方,包括地点列表。结果可以存储在任何地方,包括数据库,这样你就可以随时访问并在地图上绘制它们。
有几个地理编码器可供选择,包括来自 Google 和 Yahoo!的各一个。每个的输入略有不同,结果也是如此。在接下来的几节中,我将概述如何使用这两个地理编码器,并指向一些其他的地理编码器。
使用 Google 的地理编码网络服务
Google 是网络地图世界的王者,因此当然,Google 有一个地理编码网络服务。您可以使用它将地址或城市转换为经纬度点,并以几种数据格式之一获取结果。作为额外的好处,Google 为您美化了地址,并在查询模糊时提供了多个选项。
使用网络服务进行地理编码的额外自由之一是您可以在网页浏览器中查看结果,而无需编写任何实际代码。请在您的浏览器中查看以下 URL:
http://maps.google.com/maps/geo?`q`=38+Ringold+Street+San+Francisco+CA&`output`
=xml&`sensor`=false
您可以向 Google 发送的参数以粗体显示,以及所有在表 3-1 中显示的可能参数。整个地址作为q查询参数传递。您也可以简单地在这里包含一个城市名称或邮政编码。
表 3-1. Google 地理编码器参数
| 参数 | 描述 |
|---|---|
q |
必需。查询,例如地址或城市名称 |
sensor |
必需。是否来自移动设备:true或false |
output |
结果格式:json(默认),xml,kml或csv |
gl |
国家代码作为顶级域名。例如,us,ca,uk |
我们想要的output是 XML 格式,大多数编程语言都可以读取。除非您是从移动设备使用此地理编码器,否则将sensor设置为 false。
现在我们来看看从 Google 的 HTTP 地理编码器调用中得到的 XML 结果:
<?xml version="1.0" encoding="UTF-8" ?>
<kml >
<Response>
<name>37.7740486,-122.4101883</name>
<Status>
<code>200</code>
<request>geocode</request>
</Status>
❶ <Placemark id="p1">
❷ <address>38 Ringold St, San Francisco, CA 94103, USA</address>
<AddressDetails Accuracy="8"
>
<Country>
<CountryNameCode>US</CountryNameCode>
<CountryName>USA</CountryName>
<AdministrativeArea>
❸ <AdministrativeAreaName>CA</AdministrativeAreaName>
<Locality>
❹ <LocalityName>San Francisco</LocalityName>
<Thoroughfare>
❺ <ThoroughfareName>38 Ringold St</ThoroughfareName>
</Thoroughfare>
<PostalCode>
❻ <PostalCodeNumber>94103</PostalCodeNumber>
</PostalCode>
</Locality>
</AdministrativeArea>
</Country>
</AddressDetails>
<ExtendedData>
<LatLonBox north="37.7773156" south="37.7710204"
east="-122.4071324" west="-122.4134276" />
</ExtendedData>
<Point>
❼ <coordinates>-122.4102800,37.7741680,0</coordinates>
</Point>
</Placemark>
</Response>
</kml>
结果实际上是在Keyhole 标记语言(KML)中,这是一种 XML 的变体(参见#55: 使用 KML)。地理编码地点的坐标和其他信息存储为 Placemark。在我们的例子中,我们只有一个 Placemark❶,因为对于完整的地址只有一个可能的结果。在模糊的情况下(比如说我们搜索了简单的“Springfield”——许多地方都有这个名称),最佳结果将作为第一个 Placemark 列出,其他结果将获得递增的 id(即,p2,p3 等)。
事实上,如果您想让您的应用程序显示可能的结果,就像maps.google.com/所做的那样,请使用每个 Placemark 的完整格式化地址❷。您可以看到,谷歌甚至清理了我的具体地址,将“Street”转换为“St”并添加了邮政编码。
地址的各个部分也可以单独访问,但标签名称可能对您来说很奇怪。这是因为 Google 使它们通用,所以标签不会使不在美国的人感到困惑。例如,州缩写❸被称为行政区域名称。
单独访问值可以更轻松地显示城市❹或仅地址❺。此外,单独访问它们是快速确定一个地方的邮政编码❻(在美国称为 ZIP Code)的方法。
最后,地理编码最重要的部分是纬度和经度点。这些点存储在一个单独的标签 ❼ 中。你可以使用分割函数(下一节将展示 PHP 中的一个)来检索坐标的各个部分。
你注意到谷歌提供了三个数字而不是两个吗?第三个数字代表海拔,是 KML 格式的一个属性。地理编码器不会发送这个值,所以它总是为零。
如果你需要帮助将这些地理编码结果引入你的应用程序,第九章可以展示如何在 PHP 中这样做。或者,如果你只需要坐标,请继续阅读,了解谷歌对真正简单地理编码的方法。
其他数据格式
我喜欢 XML,但它并不总是首选的数据格式。谷歌的地理编码网络服务提供了多种格式选择,包括 JavaScript 对象表示法 (JSON) 和逗号分隔值 (CSV)。后者在你只想“只看事实”时非常棒。
在 URL 中放入一个 output 参数,并将其值设置为所需的格式,例如:
http://maps.google.com/maps/geo?q=38+Ringold+St+San+Francisco
+CA&output=`csv`&sensor=false
在这里,我们请求 CSV。与其他结果格式不同,我们不会得到重写的地址、邮编或其他任何好处。然而,我们确实得到了四个最重要的值,由逗号分隔:
200,8,37.7741680,-122.4102800
第一部分是来自服务器的代码。200 表示我们有一个好的结果。其他任何东西,我们可能有一个错误。
第二部分是一个数字,代表我们结果的粒度。它是街道级别(一个地址)、邮政级别还是城市级别?可能的结果大致相当于缩放级别,如表 3-2 所示。
表 3-2. 地理编码精度级别代码
| 代码 | 描述 |
|---|---|
| 0 | 未知 |
| 1 | 国家 |
| 2 | 州(或类似区域) |
| 3 | 县(或其他子区域) |
| 4 | 城市 |
| 5 | 邮编 |
| 6 | 街道 |
| 7 | 交叉点 |
| 8 | 地址 |
| 9 | 建筑物(如地标) |
CSV 结果的最后两个数字可能看起来很熟悉。它们是纬度和经度点(按此顺序)。这些结果可能是最重要的,因为地理编码的全部内容就是将城市名称或地址转换为可绘制的坐标。
这里有一些简单的 PHP 代码,它调用谷歌地理编码网络服务,解析 CSV 结果,并将坐标保存到变量中:
<?
$url = "http://maps.google.com/maps/geo?q=38+Ringold+St+San+Francisco+CA";
❶ $url += "&output=csv&sensor=false";
$csvtxt = ❷get_url($url);
$llarray = ❸explode(",", $csvtxt);
if (❹count($llarray) == 4 && ❺$llarray[0] == "200") {
$lat = $llarray[2];
$lon = $llarray[3];
// Now do something here with the $lat and $lon variables
}
// Additional PHP code/functions could go here
?>
这段代码只是一个片段,以给你一个如何分离简单 CSV 结果(如本例中所示)的思路。如果你运行这段代码,什么也不会发生,因为我所展示的只是将结果存储在变量中。
首先,我们创建一个变量来保存我们将用于调用谷歌的 URL。因为变量很长,而这本书的页面只有那么宽,所以我将其拆分为两行❶,但对于 PHP 来说,这个变量是一个完整的文本字符串。
然后将 URL 传递给 get_url 函数❷,这是一个我将在 #61: 获取网页 中向你展示如何编写的函数。#61: 获取网页。你需要包含一个包含该代码的文件或在你 PHP 文件的底部附近粘贴该函数的副本。
一旦我们从谷歌得到结果,我们就将文本 ❸ 分解成几个部分,所有这些部分都存储在一个单独的数组变量中。因为数据是用逗号分隔的,所以我们将使用逗号作为分隔符来分割文本。
将部分存储为数组的元素后,我们几乎准备好获取我们的纬度和经度了。我们需要确保数组变量有四个结果 ❹,正如预期的那样。此外,结果中的第一个数字必须是 200 ❺,这是良好结果的代码。
因为 PHP 中的数组从零开始计数,所以我们的纬度和经度被存储在 explode 结果变量的第二个和第三个索引中。用很少的 PHP 代码和更少的文本,你现在已经成功地将一个地址转换成了地理坐标。
使用 Yahoo! 地理编码网络服务
虽然谷歌可能获得了大部分的媒体报道,但 Yahoo 的地理开发者工具非常出色。这种情况适用于其易于使用、功能齐全的地理编码网络服务。你传递一个城市名称或完整的地址,Yahoo! 就会输出带有坐标和其他地理数据的简单 XML。
因为结果只是普通的 XML,你可以在你的网络浏览器中查看它,以了解该服务的工作方式。访问这个 URL:
http://local.yahooapis.com/MapsService/V1/geocode?`appid`=*`YOURKEY`*&
`street`=38+Ringold+St&`city`=San+Francisco&`state`=CA
参数以粗体显示。你需要将你的 API 密钥作为 appid。这个 ID 与 Yahoo! 地图 API 中的 ID 相同。我在 创建 Yahoo! 地图 中向你展示了如何注册 ID。创建 Yahoo! 地图。
在这个示例中,地址的部分被分割成 street、city 和 state。你也可以用一个参数表示地址,类似于 Google 的地理编码器:
http://local.yahooapis.com/MapsService/V1/geocode?appid=*`YOURKEY`*&
`location`=38+Ringold+St+San+Francisco+CA
location 参数包含上一个示例中的所有部分,但将它们放在一个地方。如果你从用户那里接收地址作为输入,你将更喜欢这个选项,除非你有方法将地址分解成各个部分(例如多个表单字段)。
无论你以何种方式调用 API,结果将以相同的方式格式化:
<?xml version="1.0"?>
<ResultSet ...>
<Result precision="address">
<Latitude>37.774155</Latitude>
<Longitude>-122.410230</Longitude>
<Address>38 Ringold St</Address>
<City>San Francisco</City>
<State>CA</State>
<Zip>94103-4403</Zip>
<Country>US</Country>
</Result>
</ResultSet>
与 Google 的 XML 结果相比,这些结果非常简单。纬度和经度分别显示,地址的各个部分也是如此(即使你发送的是一个文本字符串,Yahoo! 也会为你分开)。只要你在美国对地址或城市进行地理编码,每个字段都有意义。例如,在加拿大,你必须知道省份存储在 <state> 标签中。
如果你的搜索结果模糊,例如城市名称不唯一,Yahoo! 将将最佳结果放在首位。其他结果将跟随在其自己的 <Result> 标签内。
由于你在这里使用的是简单的 XML,你可以像解析任何其他 XML 一样解析它们。#52: 使用 XML 在 #52: 使用 XML 中展示了这个过程在 PHP 和 JavaScript 中的实现。如果你需要其他格式,Yahoo! 提供了 JSON 或序列化 PHP 格式的结果。第一种格式在 #53: 使用 JSON 中介绍,而第二种格式在 developer.yahoo.com/common/phpserial.html/ 中解释。
其他地理编码网络服务
之前的例子展示了地理编码器的两种最可能的选择,但你还有其他选择,尤其是如果你愿意为服务付费的话。为什么要在 Google 和 Yahoo! 免费提供地理编码的情况下花钱呢?你的选择实际上取决于服务条款和速率限制,这些限制可能会限制你在高流量、商业用途中对地理编码器的使用。
你不一定需要打破那个储蓄罐来使用付费地理编码器。例如,geocoder.us 只需收取四分之一美分来对地址进行地理编码。有关地理编码服务器的最新列表,请参阅 mapscripting.com/geocoders/。
#14: 反向地理编码:从点获取地址
到目前为止,我们使用的是人类可读的信息——城市名称或地址——来检索经纬度坐标点,这对于计算机来说更容易理解。有时,你可能想反过来操作。如果你只有一组坐标,你可以使用它们进行反向地理编码,以获取对人类有意义的地址和其他地理信息。
正常地理编码既复杂又不精确,但反向地理编码更是如此。首先,地理编码器找到离坐标最近的街道;然后,它确定哪个地址属于该点。说实话,结果往往是地址范围。
考虑到它并不完美,反向地理编码可能看起来有点荒谬。但随着位置信息在互联网上的普及,反向地理编码将会变得更加常见。例如,考虑 #48: 使用 JavaScript 获取位置 在 #48: 使用 JavaScript 获取位置。在许多情况下,GPS 或其他报告某人位置的设备只会提供经纬度坐标点。这些信息足以在地图上标出,但对于查看信息的人类来说,信息并不充分。
在以下部分,我将展示两个服务,这两个服务都来自 Google,它们将提供反向地理编码,帮助你从可读数据中创建地理信息。
使用 JavaScript 进行反向地理编码
如果你使用 Google 作为你的地图服务提供商,反向地理编码可以在你的 JavaScript 代码中发生,甚至不需要加载 Mapstraction 的地理编码器(它只支持 正向 地理编码)。在这个例子中,我们仍然使用 Mapstraction,因为反向地理编码只是地图代码的一个小部分。
让我们创建一个基本的 Mapstraction 地图,使用 Google 作为提供者。我们将地图的中心转换为 Google 点,并发送它进行反向地理编码。
假设你的 HTML 设置如 创建 Mapstraction 地图 中所述,以下是创建地图并调用 Google 地理编码器的 JavaScript 代码:
var mapstraction;
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'googlev3');
mapstraction.setCenterAndZoom(
new mxn.LatLonPoint(37.7740486,-122.4101883), 15);
// Google-specific calls
❶ var geocoder = new GclientGeocoder();
geocoder.getLocations(❷mapstraction.getCenter().toProprietary(mapstraction.api),
❸found_address);
}
function found_address(response) {
if (response && ❹response.Status.code == 200) {
❺ var pt = response.Placemark[0].Point;
var marker = new mxn.Marker(new mxn.LatLonPoint(pt.coordinates
[1], pt.coordinates[0]));
marker.setInfoBubble(❻response.Placemark[0].address);
mapstraction.addMarker(marker);
marker.openBubble();
}
}
如承诺的那样,大部分代码都是由 Mapstraction 函数组成的。特定于 Google 的调用被分离出来。例如,我们创建一个地理编码器对象 ❶,然后调用以获取位置。甚至这个调用也包含一些 Mapstraction,因为我们使用它来获取地图的中心 ❷,并将该点转换为 Google 可以理解的点。
在调用地理编码器时,我们需要提供一个回调函数 ❸。这个函数在结果从 Google 返回时使用。因为我们创建了一个命名函数,所以我们还需要用那个名字创建该函数。
found_address 函数接受一个参数,即 Google 地理编码器发送给我们的结果对象。一旦我们确定我们有一个良好的响应 ❹(状态码为 200),我们就可以获取点 ❺,它包含我们的坐标。
你可能会想知道为什么这个点甚至有必要,因为这是你开始的地方的数据。在许多情况下,Google 无法在您的 确切 点找到地址(例如,想象一下一个大公园的中心),因此它会选择一个附近的地址。在这种情况下,您将想要知道它使用的点,以便相应地绘制。
Google 实际上可能会发送多个结果,这些结果将存储在 response.Placemark 数组中。第一个是它的最佳猜测,并且可能是应该使用的那个,尽管在某些情况下,您可能允许用户选择最准确的结果。
found_address 中剩余的大部分代码看起来很熟悉,因为它来自 第二章 的标准 Mapstraction 函数。我们在 Google 返回的位置放置一个标记。然后,我们使用最重要的信息,即地址 ❻,作为标记框内的信息。
为了更好地了解反向地理编码,尝试通过改变地图的中心来更改传递给 Google 的坐标。或者,继续阅读以创建一个点击任何地方都会进行反向地理编码的地图。
点击进行反向地理编码
想要尝试反向地理编码吗?尝试精确点击你的地址,看看你能多接近你的确切位置,这可以很有趣。此外,快速访问反向地理编码也可以是一个好的开发者工具,帮助你更好地了解这个过程。
你可以使用大多数之前的示例。实际上,found_address可以保持完全相同。将create_map函数替换为这个略微修改的版本:
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'googlev3');
❶ mapstraction.addSmallControls();
mapstraction.setCenterAndZoom(
new mxn.LatLonPoint(37.7740486,-122.4101883), 15);
❷ mapstraction.addEventListener('click', function(clickpoint) {
// Google-specific calls
var geocoder = new GclientGeocoder();
geocoder.getLocations(❸clickpoint.toProprietary
(mapstraction.api), found_address);
});
}
我在地图上添加了一些缩放控件❶,这样你可以找到特定的点击位置(除非你更改地图的中心或滚动地图到另一个位置,否则是在旧金山)。然后我编写了一些代码,等待地图上的点击事件❷。当点击发生时,它调用一个匿名内联函数。这个函数可以被命名,但只有几行简单的代码,内联编写更容易。
在匿名函数中,我们调用谷歌地理编码器,与之前使用的非常相似。在这个例子中,我们传递用户点击的点❸而不是地图的中心。请注意,我们需要将点转换为专有的谷歌坐标类型,因为 Mapstraction 捕获点击点,然后需要将其传递给谷歌地理编码器。Mapstraction 说谷歌话,但谷歌不说 Mapstraction 话。
由于其他代码相同,点击地图会在打开的消息框中添加带有地址的标记。点击几次更多,地图上会出现额外的标记,包含反向地理编码器提供的地理信息。
你开始看到反向地理编码器的有用性了吗?在下一节中,你将能够使用谷歌的 HTTP 地理编码器在 JavaScript 之外访问这些数据。
使用谷歌网络服务进行反向地理编码
正如我在本章的其他地方提到的,力量和灵活性来自于能够控制地理编码器的输入和输出。你可以使用谷歌提供的反向地理编码服务获得相同的自由。
通过对 URL 进行微调,谷歌的地理编码器变成了反向地理编码器:
http://maps.google.com/maps/geo?q=`37.7740486,-122.4101883`&output=xml&sensor=false
我们仍然使用q查询参数,就像在第十三部分:使用 HTTP 网络服务进行地理编码中谷歌部分所做的那样。我们发送的是纬度和经度,按照这个顺序,用逗号分隔(在上面的 URL 中用粗体标出)。
结果几乎与使用正向地理编码器相同:
<?xml version="1.0" encoding="UTF-8" ?>
<kml >
<Response>
<name>37.7740486,-122.4101883</name>
<Status>
<code>200</code>
<request>geocode</request>
</Status>
<tPlacemark id="p1">
<address>38 Ringold St, San Francisco, CA 94103, USA</address>
<AddressDetails Accuracy="8"
>
...
</AddressDetails>
<Point>
<coordinates>-122.4102800,37.7741680,0</coordinates>
</Point>
</Placemark>
<Placemark id="p2">
...
</Placemark>
...
</Response>
</kml>
最大的不同之处在于你可能会拥有多个地标,因为反向地理编码比标准地理编码要粗糙得多。否则,每个地标内接收到的内容都是相同的,包括邮政编码——当然,地址(或范围)也是,这是这个过程最初的目的。
你再也不必让用户尝试解读为计算机理解而设计的奇怪数字了。无论你选择 JavaScript 还是服务器端网络服务,你都可以通过快速调用反向地理编码器从坐标转换为文本。
#15: 获取邮政编码坐标
你是否曾经访问过要求你输入 ZIP 代码以查找商店最近位置的网站?可能吧,我猜。本节将帮助你迈出创建类似功能的第一步。你需要一种将邮政编码转换为地理坐标的方法。
你可能认为大面积不能转换成一个地理点。你是对的,尽管对于任何地理编码结果倾向于街道附近点的地址来说,也可以这么说。那么你的后院呢?
记住,地理编码不是一门精确的科学。对于一个地址,选择一个有意义的点。对于邮政编码,最合理的点通常位于中心附近。然而,对于一些边界模糊的地方,确定中心点仍然很困难。因此,纬度和经度代表的是中心附近的一个点。
获取邮政编码坐标的最简单方法是通过地理编码服务进行搜索。例如,如果你想使用 Yahoo!地理编码器查找比弗利山最著名的 ZIP 代码,你会使用这个 URL:
http://local.yahooapis.com/MapsService/V1/geocode?appid=*`YOURKEY`*&location=90210
你的结果可能看起来像这样:
<?xml version="1.0"?>
<ResultSet ...>
<Result precision="zip">
<Latitude>34.092807</Latitude>
<Longitude>-118.411115</Longitude>
<Address />
<City>Beverly Hills</City>
<State>CA</State>
<Zip>90210</Zip>
<Country>US</Country>
</Result>
</ResultSet>
注意,精度是 ZIP 级别的,地址标签为空。否则,结果与搜索完整地址时返回的结果相似。
安装邮政编码数据库
如果你需要执行大量的查找,或者想要更快地访问结果,拥有一个无需使用其他服务的数据库表来地理编码邮政编码是有意义的。美国有不到 50,000 个 ZIP 代码,这是一个相当小的记录数量,便于存储和访问。其他国家有更多独特的邮政编码(例如,加拿大有近一百万,这仍然足够小,值得这样做)。
你需要一个数据库来存储你的邮政编码及其对应的坐标。在第九章中,我描述了如何安装 MySQL 并从 CSV 文件导入数据。本书的网站包含可以免费下载邮政编码数据库的链接。请参阅mapscripting.com/postal-code-database。
数据库中包含的字段可能会有所不同,但这里是一个美国 ZIP 代码数据库的示例结构:
| ZIP 代码 邮政编码 |
|---|
| 名称 该 ZIP 代码的文本描述,例如街区或城市名称 |
| 纬度 坐标的南/北部分 |
| 经度 坐标的东/西部分 |
一个非常基础的数据库甚至可能不包含名称字段,因为邮政代码数据库最重要的部分是将代码转换为点。
注意
你需要注意 ZIP 代码字段是存储为文本还是数字。有些人更喜欢文本,因为文本可以更好地表示以零开头的 ZIP 代码。然而,数据库能够更有效地搜索数字,所以你需要确保从用户输入中去除任何开头的零。
当你将完整的邮政编码数据库加载到zipcoord表(这是我取的名字)中时,查找比弗利山 90210 的坐标的 SQL 语句可能看起来像这样:
select latitude, longitude from zipcoord where zipcode='90210';
你应该只从数据库调用中获取一组坐标,因为只有一个 90210 的邮政编码。要了解更多关于使用 PHP 访问 SQL 结果的信息,请参阅第六十五部分:从 PHP 中使用 MySQL。
现在你已经可以获取邮政编码坐标了,你可以开始使用它们了。在这个项目的开始阶段,我提到了那些有搜索框以查找你 ZIP 代码附近位置的网站。将你的邮政编码结果与第四十六部分:从你的数据库中获取最近的位置结合,你将构建了一个商店定位器,就像你在那些网站上看到的那样。
第四章:层层叠加

地图绘制就像在地理参考画布上绘画。最常见的画笔是标记和信息框,这是我们迄今为止使用的大部分内容。为了达到不同的质感,您需要做出改变。在本章中,我们将通过一些专门的图层来扩展,这些图层将改善您地图的外观。
首先,我们将简单地绘制线条。通过连接地理坐标,可以简单地表示很多内容:路线、政治边界,甚至单个建筑物。本章甚至有一个项目可以在地图上着色州或国家,这可以用来制作像最近美国总统选举中变得流行的选举地图。
我们还将向地图添加图片。我们不是用自定义标记做过吗?是的,但我们将添加的图片将更大,并像您自己的地图图像一样使用。实际上,我们还将创建自定义瓦片,仍然利用地图 API,但绕过(或增强)其自己的地图。
在地图上创建图层是使您的地图脱颖而出成为杰作的一大步。拿出那些新画笔,让我们开始绘图。
#16:在地图上绘制线条
从点 A 到点 B 的最短距离是多少?这里有一个提示:阅读本节后,您将能够在地图上表示它。
绘制线条,Mapstraction 称之为折线,非常有用。有了它们,您可以勾勒出路线,例如驾驶方向或徒步旅行路线。就像标记一样,线条使用纬度和经度点。当然,您不能只有一个点的线条,所以您至少需要两对坐标来绘制线条。
Mapstraction 允许您拥有无限数量的点,但让我们从简单开始。这个例子将在乔治亚州(美国州)和乔治亚国(国家)的首都之间绘制线条。在初始化地图后,将以下代码添加到您的create_map函数中:
var georgias = ❶[new LatLonPoint(33.754487, −84.389663),
new LatLonPoint(41.709981, 44.792998)];
var poly = ❷new Polyline(georgias);
❸ mapstraction.addPolyline(poly);
mapstraction.autoCenterAndZoom();
我首先声明了两个点❶,将它们存储在一个数组变量中。注意创建数组的方括号——[和]——以及数组内部两个LatLonPoints之间用逗号分隔。Mapstraction 需要一个至少包含两个LatLonPoints的数组来创建折线。
接下来,我实际上使用数组创建了Polyline对象❷。就像标记对象一样,这个对象是 Mapstraction 表示数据的方式,以便在必要时可以与多个地图服务提供商一起重现。然而,创建折线并不足以绘制线条。我还需要将其添加到地图中❸。
最后,我使用在#8:根据标记确定正确的缩放级别和#7:遍历所有标记中描述的相同函数自动将地图居中。在这种情况下,Mapstraction 使用折线而不是标记来确定缩放级别。
结果地图显示在图 4-1 中,横跨大西洋,连接两个乔治亚。让我们看看通过在在线上添加更多点,我们能否使其变得更有用。

图 4-1. 在两个乔治亚首都之间绘制的线
绘制多个线段
当你想到线时,你可能想象的是我们在上一节中创建的东西:两点之间的连接。然而,折线可以有无限多的段,这意味着你可以使用它们来创建路径和路线。
实际上,谷歌使用折线来显示其驾驶方向。使用折线是有意义的,因为很少有街道是完全直的。大多数街道至少在两侧有轻微的蜿蜒。在旧金山,有几条街道甚至以其弯曲而闻名,最著名的是洛马街的一段短距离。
这里有一些代码,用于创建一个大致沿着洛马街曲线的折线,从上到下:
var lombard = [new LatLonPoint(37.802010, −122.419635),
new LatLonPoint(37.802036, −122.419463),
new LatLonPoint(37.802120, −122.419356),
new LatLonPoint(37.802010, −122.419184),
new LatLonPoint(37.802137, −122.419034),
new LatLonPoint(37.802053, −122.418841),
new LatLonPoint(37.802197, −122.418701),
new LatLonPoint(37.802087, −122.418519),
new LatLonPoint(37.802231, −122.418401),
new LatLonPoint(37.802120, −122.418186),
new LatLonPoint(37.802214, −122.417993)];
var poly = new Polyline(lombard);
mapstraction.addPolyline(poly);
mapstraction.autoCenterAndZoom();
如图 4-2 所示,这 11 个纬度和经度点创建了一个沿着洛马街八个转弯的折线。与乔治亚的例子一样,这些点存储在括号内,以创建 Mapstraction 制作折线所需的 JavaScript 数组。

图 4-2. 使用多个点追踪旧金山著名的洛马街的折线
洛马街仍然是一个相当简单的例子。我们覆盖的还不到四分之一英里——这包括转弯。你可以使用折线来追踪整个高速公路、数百英里流淌的河流,甚至中国的长城。
设置颜色和粗细
就像标记和其他地图方面一样,折线也带有你可能会想要更改的标准外观。例如,你可以改变你绘制的线的颜色和粗细。
Mapstraction 有单独的函数来设置你想要的标准。每个函数都可以在你创建折线后、将其添加到地图之前应用。这个过程与添加标记选项的顺序相似。
这里有一个示例,它创建了一个五像素粗的紫色折线(它假设你的折线存储在poly变量中):
poly.setColor(❶'#FF00FF');
poly.setWidth(5);
注意,颜色设置为十六进制值❶,类似于在 HTML 中声明颜色的方式。可以是六位或三位值,如果你愿意,甚至可以在其前面加上一个井号#。
注意
一些浏览器,尤其是较旧的浏览器,在处理彩色多边形时可能会遇到问题。请确保彻底检查你的用户群通常使用的所有浏览器,特别是如果多边形的颜色对你的应用程序至关重要的话。
由于你通常会同时设置多个属性,Mapstraction 有一个接受许多选项并存储它们值的 JavaScript 对象的函数:
poly.addData({color: '#FF00FF', width: 5});
大括号 { 和 } 很重要,因为它们声明了 JavaScript 对象。然后使用选项的名称、冒号和值设置一个属性。你将在下一个项目中看到更多添加样式选项到多边形的示例。
#17: 在地图上绘制形状
你准备好一点哲学了吗?我希望如此,因为我有一个哲学问题要问你:什么是形状?
在你回答之前,让我们列出一些形状的名称。形状包括圆形和三角形。正方形很受欢迎,以及它们的相对矩形。如果我们继续增加边的数量,名称可能会听起来很熟悉——五边形、六边形、七边形、八边形——然后名称会变得有点奇怪。
你的哲学答案可能不同,但为了我们的目的,我们将创建一个具有一些边的形状。这个形状被称为 多边形。多边形由起点和终点相同的线段组成。如果你已经掌握了前面的项目,你很可能对如何使用 Mapstraction 创建多边形有一个相当好的想法。
意想不到的是,在地图上创建形状的过程基本上与绘制多边形相同。看看这段代码,它绘制了美国国防部总部(“五角大楼”——明白了吗?)的轮廓:
var pentagon = `[new LatLonPoint(38.870253, −77.058491)`,
new LatLonPoint(38.872725, −77.057955),
new LatLonPoint(38.873059, −77.054715),
new LatLonPoint(38.870804, −77.053320),
new LatLonPoint(38.869000, −77.055616),
`new LatLonPoint(38.870253, −77.058491)`];
var poly = new Polyline(pentagon);
mapstraction.addPolyline(poly);
mapstraction.autoCenterAndZoom();
与标准多边形一样,我们需要使用方括号 [ 和 ] 声明一个 LatLonPoint 数组,以显示 JavaScript 数组。最大的不同是第一个和最后一个点(两者都加粗)是相同的,这向 Mapstraction 信号你正在创建一个多边形。
如图 4-3 所示,结果是多边形被填充以显示它不仅仅是一条线——它是一个形状。与普通线条相比,形状会引发更多样式问题,因此 Mapstraction 提供了一些额外的选项。

图 4-3. 使用多边形勾勒的五角大楼
设置填充颜色和透明度
现在既然你的多边形已经变成了多边形,让我们声明填充其中心的颜色。此外,由于填充覆盖了如此大的区域,你可能希望让多边形下的地图部分可见。这就是透明度发挥作用的地方——它决定了填充颜色的不透明度。
如果你熟悉图形程序,你可能会对透明度感到舒适。你用从 0 到 100 的百分比来声明它,其中 0 是不可见的,100 完全不透明。
让我们使用 Mapstraction 中的一个单一函数来设置我们之前制作的五边形的所有选项:
poly.addData({❶opacity: 0.9, ❷fillColor: '#00FF00', color: '#009900', width: 5});
在这里,我们声明了 90%的不透明度❶,所以我们几乎看不到它。请注意,我们使用 0 到 1 之间的十进制数来表示百分比。虽然从数学上是正确的,但你可能会觉得有点困惑。
填充颜色使用类似 CSS 的十六进制值声明❷。此值与折线颜色分开,可以是相同的也可以是不同的。在这个例子中,填充颜色是一种明亮的绿色,而边框则是一种略深的绿色。
#18:添加圆以显示搜索半径
绘制线和多边形相对简单。它们由一系列连接的点组成。圆稍微有点难以表达。圆没有组成其边界的点。相反,它通过一个中心点和半径来声明。
圆在地图上很有用,因为你可以用它们来显示你正在搜索的区域。例如,如果你正在寻找距离一个点五英里内的地点,你的圆将宽十英里(并且高——圆是完美圆形的),搜索点正好位于中心。
Mapstraction 提供了两种创建圆的方法。首先,我们可以用多边形进行近似。其次,我们可以使用图形并将其叠加在地图上。在本节中,我将向你展示如何做这两件事。
用多边形进行近似
通过连接圆上的点来创建一个假圆看起来比你想象的要好。当然,你使用的点越多,圆看起来就越好。Mapstraction 有一个内置函数来执行计算,你可以设置质量。
记住,一个圆需要两个信息:一个中心和半径。在这个例子中,我们将通过画一个直径为 500 英里的圆来展示德克萨斯州的大小,这个圆从州的首府开始,位于州中心的附近。
将这些行添加到你的地图初始化函数中,我在整本书中将其称为create_map:
var radius_object = ❶new Radius(new LatLonPoint(30.268259, −97.744674), ❷10);
var poly = radius_object.getPolyline(❸mxn.fn.milesToKM(250), ❹'#990066');
mapstraction.addPolyline(poly);
mapstraction.setCenterAndZoom(center, 5);
这段代码是添加类似圆形的多边形到地图上所必需的。首先,你需要创建一个Radius对象❶,它是 Mapstraction 库的一部分。这个对象进行一些重要的计算来确定圆的边缘。然后你传递给这个对象两个值:中心点(德克萨斯州奥斯汀市中心)和一个质量数字❷。
质量数字越低,你的多边形看起来就越像圆形。这个数字代表多边形中每两点之间的度数。你可以用这个来确定你的“圆形”将有多少边。一个圆总共是 360 度,所以如果你除以 10,那就是 36 个点,这意味着你在这个例子中创建了一个 36 边形。使用这种方法,你做得相当不错,可以近似一个圆,就像你在图 4-4 中看到的那样。边数越多,创建圆所需的时间就越长,所以你必须做出权衡。

图 4-4. 用 36 边形近似的圆
现在你有了半径对象,你可以创建多边形,这是一个多段线对象。你使用 Mapstraction 函数将英里转换为公里❸。在这里,我传递了半径的英里数,这是最终圆直径的一半。此外,我还传递了一个用于圆的颜色十六进制值❹,在这种情况下,是一种紫罗兰色调。
你可以从半径对象创建任意多的圆形,所以你可以显示从奥斯汀出发的几个不同级别的距离。然而,如果你想移动中心(或改变圆的质量),你需要重新创建半径对象。
在图像上叠加圆形
多边形化的圆可能对你来说仍然太粗糙,或者你可能想要对圆形的外观有更多的控制。在这种情况下,在你的地图上叠加图像是你的最佳选择。
首先,你需要一个圆形图像,可能是一个保存为透明 PNG 文件的图像。透明度很重要,因为图形是以矩形存储的,所以你绝对不希望圆外的区域可见。此外,由于你将在这个地图上作为矩形引用它,你的圆形应该紧靠图形的四边。我在书的网站上包含了几个示例圆形图形供下载,网址为mapscripting.com/circle-overlays。
一旦你有了圆形图像,你就可以确定它在地图上的位置。一个图形是通过其矩形的四边来引用的,所以你需要确定北、南、东和西的点。通常,一个圆是由其中心和半径确定的。你可以通过从中心测量四个方向来计算边值。
将以下行添加到你的地图代码中:
var center = new LatLonPoint(30.268259, −97.744674);
❶ var dist_lat = 250 / 69.2;
var dist_lon = ❷mxn.fn.metresToLon(❸mxn.fn.milesToKM(250)*1000, center.lat);
❹ var n = center.lat + dist_lat;
var s = center.lat − dist_lat;
var e = center.lon + dist_lon;
var w = center.lon − dist_lon;
mapstraction.addImageOverlay(❺'searchradius', 'circle.png', ❻75, w, s, e, n);
mapstraction.setCenterAndZoom(center, 5);
为了能够确定你的圆形图像的地理边界,你需要计算纬度和经度有多少度组成半径,这是 250 英里。纬度距离❶更容易计算,因为纬度在世界上几乎是一致的,每度 69.2 英里。
经度取决于你在地球上的位置,因为当你接近极点时,度数彼此更接近。Mapstraction 有一个方便的函数 ❷ 用于根据纬度将米转换为经度。为了将函数 250 英里转换为米,你必须首先将其转换为千米 ❸ 然后乘以 1000。
到目前为止,你已经有了距离,所以现在你只需要计算四条边。例如,图形的北部边界 ❹ 将在中心点纬度加上我们确定的 250 英里中的纬度数。南部也将使用那个纬度距离(只是从这个距离中减去中心点的纬度。东部和西部边界将使用经度距离。
拥有你的四个地理边界后,你可以应用你的图像叠加。Mapstraction 需要很多信息,包括图像的标识符 ❺ 和不透明度级别 ❻(在这个例子中,我选择了 75%)。结果如图 图 4-5 所示,你可以看到在德克萨斯州上方的完美圆形。你会发现,它覆盖的区域与 图 4-4 中多边形圆覆盖的区域相同。

图 4-5. 透明圆形图像叠加
无论你选择使用哪种类型的圆,德克萨斯州都是一个很大的州。
#19: 画一个矩形来声明一个区域
在 第二章 中,我讨论了边界框,一组大致描述地理区域的坐标。我写 大致 是因为边界在视觉上是矩形的,所以它们只能用来声明最简单的区域。当然,我们已经间接地为每个地图创建了边界。地图的可见部分是更大地图的一个矩形部分。
Mapstraction 使用 BoundingBox 类来描述一个区域。作为一个数据结构,这个类由两个 LatLonPoint 组成。一个声明西南角(左下角)和另一个声明东北角(右上角)。从这两个值中,我们可以推断出剩余的两个角。我们将在本项目中进行这样的操作。
你可能希望从视觉上声明地图上的一个区域。因为 BoundingBox 只是一个区域的简单数据表示,我们需要将其转换为 Polyline。将此函数添加到你的 JavaScript 中(在 create_map 函数外部)以执行转换:
function BoundingBox_to_Polyline(box) {
var points = [❶box.sw, ❷new mxn.LatLonPoint(box.ne.lat, box.sw.lon), box.ne,
new mxn.LatLonPoint(box.sw.lat, box.ne.lon),
new mxn.LatLonPoint(box.sw.lat, ❸box.sw.lon-.0001)];
var poly = new mxn.Polyline(points);
return poly;
}
在地图上绘制一个矩形需要五个点。起初,这个要求可能看起来有点奇怪——矩形不是有四个角吗?当然。我们并没有违反基本的几何规则。然而,我们需要分别声明起点和终点。而且因为这两个点是相同的(或者,正如你将看到的,几乎相同),所以我们将其包含两次。
点数数组简单地从西南点开始 ❶。然后我们想要直接向北绘制一条线,这意味着我们需要保持相同的经度同时增加纬度。我们只有两个点可以操作,因此我们使用西南点的经度和东北点的纬度创建一个新的点 ❷。
第三个点是东北点本身。然后我们可以使用类似的过程来确定第四个点。最后,我们需要将矩形的最后一侧画回到西南点。但这里事情变得奇怪。如果我们使用确切的西南点,Mapstraction 将填充该区域。为了得到一个空心的框,我们创建一个最终点,其经度几乎无法察觉地偏离 ❸ 起始点。
现在我们已经编写了我们的新函数,我们需要调用它。在你的 create_map 函数内部,添加以下行:
❹ var bounds = mapstraction.getBounds();
❺ var poly = BoundingBox_to_Polyline(bounds);
mapstraction.addPolyline(poly);
mapstraction.setZoom(mapstraction.getZoom()-1);
如果我们想要创建新的边界,我们可以这样做,但相反,我们是从地图本身获取它们 ❹。然后我们使用这些边界来调用我们的函数将其转换为 Polyline ❺。正如你之前看到的,创建对象只是第一步。我们还需要将其添加到地图上。最终的线会放大,这样我们就可以看到矩形,如图 图 4-6 所示。如果不放大,矩形就会位于我们地图的边缘。

图 4-6. 将边界框转换为折线
使用此代码片段清楚地显示边界。有关此项目的实际示例,请参阅 其他有用参数 中的 #7: 遍历所有标记。
#20: 沿点击绘制线条
在地图上创建自己的线路的最大障碍是找到经纬度点。通过这个项目,你(或你的用户)将能够通过点击地图来简单地绘制线路。Gmap Pedometer (gmap-pedometer.com/) 在地图 API 的早期阶段推广了这项技术,现在你也可以使用它了。
对点击事件的响应是这个方法的中心。你将在 第五章 中详细了解到事件。我们想要将所有点击的点存储在一个数组中。因为我们将从事件中访问它,所以我们创建的变量需要是公共的,这意味着它是在任何函数外部声明的。在你的 JavaScript 开头包含以下行:
var cpts = [];
这个变量将保存每个点击的点。目前它是一个空数组(方括号之间没有内容)。然而,每次点击,我们都会向数组中添加一个新的 LatLonPoint。
在你的地图初始化代码中,添加以下行以响应点击:
mapstraction.click.addHandler(function(event_name, event_source, event_args) {
var clickpoint = event_args.location;
❶ cpts.push(clickpoint);
if (cpts.length == 1) {
❷ var mk = new Marker(clickpoint);
mapstraction.addMarker(mk);
}
else {
❸var poly = new Polyline(❹cpts.slice(cpts.length-2));
mapstraction.addPolyline(poly);
}
});
在这里,我包括了当用户点击地图时运行的匿名内联函数中的代码。这几乎是我会编写的没有明确命名的函数的长度。
发生在第一件事是我们将新点“推入”数组❶。push函数是 JavaScript 中每个数组变量的内置函数,并且始终将其添加到数组的末尾。
在我们能够画线之前的最小点数是两个。这个要求引起了一些问题,因为我们需要让用户知道我们已经记录了第一次点击。为了解决这个问题,我们只在数组长度为 1 时在地图上添加一个标记❷。换句话说,只有在第一次点击时才会添加标记。
在随后的点击中,我们使用最近两个点创建一个新的折线❸。为此,我们将数组中的最后一个点❹连接到倒数第二个点(因为在 JavaScript 中数组从零开始计数,最后一个元素总是比长度少一个)。此外,请记住,折线是通过数组本身创建的,因此我们需要用括号包围这两个数字。代码开始看起来有点杂乱。
在多次点击之后,地图看起来可能像图 4-7。从内部来看,我们将所有点都存储在我们的数组中。然而,Mapstraction 将每个线段视为自己的折线。从视觉角度来看,它仍然看起来像一条大线。

图 4-7. 通过折线段连接点击
#21: 在地图上显示颜色状态/国家
如果你注意到了最近的美国政治,你很可能看到了红色和蓝色州地图。在 2004 年和 2008 年的总统竞选期间,这些地图在互联网上变得很常见。而且信不信由你,如果你读到本章的这一部分,你已经知道如何制作自己的彩色地图了。
你需要的只是构成每个州轮廓的点。然后,为每个州创建一个多边形,并给它适当的填充颜色。在你开始着色州之前,你需要构成每个州轮廓的点。你可以通过多种方式获取这些数据。例如,你可以使用与上一个项目类似的方法自己创建它。
你也可以直接从政府那里获取,但你可能需要将其转换为 Mapstraction 容易使用的格式。我在书的网站上提供了数据源mapscripting.com/state-boundaries。
如果你想尝试一下,先从几个州开始。在这个例子中,我将使用犹他州、科罗拉多州、亚利桑那州和新墨西哥州的四个角落州。它们有相对较少的点,并且它们都整齐地聚集在一起。第一步是声明构成每个州边界的点作为 JavaScript 数组:
var utah = [new LatLonPoint(36.99, −114.05), new LatLonPoint(36.99, −109.04),
new LatLonPoint(40.99, −109.05), new LatLonPoint(40.99, −111.05),
new LatLonPoint(41.99, −111.05), new LatLonPoint(41.99, −114.04),
new LatLonPoint(36.99, −114.05)];
var colorado = [new LatLonPoint(41.00, −102.05), new LatLonPoint(40.99, −109.04),
new LatLonPoint(37.00, −109.04), new LatLonPoint(36.99, −102.04),
new LatLonPoint(41.00, −102.05)];
var arizona = [new LatLonPoint(33.95, −114.52), new LatLonPoint(34.00, −114.47),
new LatLonPoint(34.02, −114.43), new LatLonPoint(34.08, −114.43),
...
new LatLonPoint(33.92, −114.53), new LatLonPoint(33.95, −114.52)];
var newmexico = [new LatLonPoint(32.00, −106.62), new LatLonPoint(31.99, −103.06),
new LatLonPoint(36.99, −103.00), new LatLonPoint(36.99, −109.04),
new LatLonPoint(36.99, −109.04), new LatLonPoint(31.33, −109.04),
new LatLonPoint(31.33, −108.21), new LatLonPoint(31.77, −108.20),
new LatLonPoint(31.78, −106.53), new LatLonPoint(32.00, −106.62)];
亚利桑那州过于复杂,无法在书中展示其所有点,但其他州的信息是完整的。就像使用点声明其他形状一样,我们使用方括号包含一个LatLonPoint列表来指定一个数组。为了创建一个完整的形状(因此包括填充颜色),数组中的第一个和最后一个点必须相同。相同的点告诉 Mapstraction 多边形开始和结束在同一个地方。
由于我们将多次执行相同的操作,这是一个创建我们自己的函数的合适时机。以下是填充状态的代码:
function color_state(❶pts, ❷color) {
var poly = new Polyline(pts);
poly.addData({❸opacity: 0.9, ❹fillColor: color, ❺width: 0});
mapstraction.addPolyline(poly);
}
我们将调用这个函数四次——一次针对每个州。或者,如果你要绘制整个美国,你需要调用这个函数五十次。这个函数的三行意味着我们的代码只需占用三分之一的篇幅。如果你打算多次运行相同的代码,你将想要避免重复并创建自己的函数。
我们需要将函数传递两个参数。首先,我们传递给它一个点的列表❶——一个状态边界数组。然后,因为我们将在地图上用不同颜色的状态填充,我们需要让函数知道当前状态应该使用什么颜色❷。
函数随后开始创建一个Polyline并添加数据到它。我们已将不透明度设置为 90%,这意味着填充的状态形状将略微透明,足以看到下面的州名。颜色设置为❹为我们接收到的参数。我们将宽度设置为零❺,这意味着州将没有边界。你可能更喜欢有边界,所以尝试几个不同的值。这个参数接受整数,表示像素厚度。
我们到目前为止所做的一切实际上还没有做任何事情。为了做到这一点,我们需要调用这个函数,传递一个状态边界数组和颜色。将以下代码添加到你的地图初始化部分:
color_state(utah, '#00ff00');
color_state(colorado, '#006600');
color_state(arizona, '#009900');
color_state(newmexico, '#00cc00');
mapstraction.autoCenterAndZoom();
如你所见,这调用了color_state函数四次,每次使用不同的点数组变量和颜色。为了保持这个例子不涉及政治,我使用了绿色的不同色调。你可以自由地插入你自己的红色('ff0000')或蓝色('0000ff')值。
为了确保每个州都在视野中,我在添加了四个州之后自动居中了地图。正如你在图 4-8 中看到的那样,我们的地图看起来相当漂亮。但如果你放大查看,每个州的边界可能不会完全接触。这是一个精度问题,你可能并不关心。它的重要性取决于你期望用户多接近地查看边界。对于查看国家级别的彩色选举地图的情况,我们不需要完美。
您可能还会注意到创建州地图时的问题,并不是所有州都是完美的形状。夏威夷是一系列岛屿,密歇根的两个部分被一个大湖分隔。再次强调,您如何处理这取决于这对您有多重要。您可能觉得将部分连接起来以形成一个州是可以接受的。或者,您可能使用多个多边形来表示这些复杂的州。
#22: 添加自定义控制
到目前为止,我们在地图上叠加的所有内容都进行了地理参照。换句话说,当用户将地图拖到一边时,我们叠加的内容也会移动。在本节中,我们将创建一些不移动,而是锚定在地图窗口特定位置的用户界面元素。
我们将要创建的控制类似于地图类型控制,它们位于地图的右上角。在添加缩放和其他控制中,我展示了如何包含这些(以及其他)控制。现在,我们将制作自己的按钮,它们将位于相同的位置。
对于这个例子,我们将创建一个按钮,用户可以通过它自动将地图居中,以便所有标记和线条都可见。更好的是,我们将编写代码,让您可以通过简单的函数调用创建任意数量的这些自定义控制。

图 4-8. 四个美国州的区域
这是创建控制的代码:
function create_control(❶txt, ❷func) {
❸ var newcontrol = document.createElement("a");
❹ newcontrol.className = 'googlecontrol';
❺ newcontrol.appendChild(document.createTextNode(txt));
❻ newcontrol.onclick = func;
❼ mapstraction.currentElement.appendChild(newcontrol);
}
为了创建控制并将其添加到地图中,此函数需要两份数据:它需要写入按钮内的文本❶,以及当按钮被点击时它将调用的函数❷。
有了这些信息,我们可以着手创建这个控制。从浏览器如何解释的角度来看,我们正在程序化地创建一个简单的<a>标签❸。我们给它一个类名❹,以便我们可以用 CSS 来样式化它。然后我们向它添加标签❺。
到目前为止,控制已经创建,但它还没有在地图上,也没有执行任何操作。为了解决这两个问题,我们设置了当用户点击时将被调用的函数❻,然后将对象作为地图对象的子对象❼添加。
现在我们可以为控制添加样式。将这些 CSS 行添加到您的样式表中:
a.googlecontrol {
`position: relative;`
`float: right;`
width: 63px;
height: 15px;
margin: 5px 3px 0 0;
border: 1px solid #b0b0b0;
background-color: white;
color: black;
font-size: 12px;
text-align: center;
}
a.googlecontrol:hover {
cursor: pointer;
}
这段 CSS 旨在使我们的控制看起来与谷歌地图类型控制相似。只有前两行(加粗)是必要的,用于将其定位在右上角。其余的都是样式。
所有艰苦的工作现在都完成了,我们准备好使用我们的自定义控制。在我们的地图初始化代码中,添加以下行以创建一个自动居中控制:
create_control("auto-center", function() {
mapstraction.autoCenterAndZoom();
});
这段代码传递了新控制(自动居中)的标签和一个匿名内联函数引用,该函数引用决定了当用户点击新控制按钮时应该执行的操作。在这种情况下,它会自动触发 Mapstraction 代码以显示地图上的所有标记和线条。请参见图 4-9,了解点击按钮前后的示例。
当用户点击你的自定义控制按钮时,你可以做任何你想做的事情。一个常见的选择可能是只显示特定类型的标记。我在第九部分:过滤掉某些标记中演示了如何做到这一点,第九部分:过滤掉某些标记。使用标记填充的地图,你还可以创建特定的区域进行缩放,就像我在第七十部分:显示全球最近的地震中做的那样,第七十部分:显示全球最近的地震。

图 4-9. 一个自定义控制看起来像 Google 控制
#23: 创建你自己的缩放界面
当你选择一个地图服务提供商时,地图的外观某些元素无法轻易更改。缩放界面可能是你接受为不可更改的这些元素之一。在这个项目中,我将展示如何包含你自己的缩放按钮,以提供对你地图外观的更多控制。
这种方法与上一个项目类似。我们将创建一个函数,它将向页面添加一个新对象,并使用 CSS 对其进行定位和样式设计。我们将使用图像按钮而不是文本按钮。而且,就像之前一样,每个新对象都会对点击做出反应。
首先,你需要两个图像:一个将用作放大按钮,另一个将用作缩小按钮。你可以在图 4-10 中看到我选择的两个不起眼的图形。

图 4-10. 两个用作自定义控制的缩放图形
这是创建图像控制的通用代码:
function create_image_control(❶src, ❷func) {
❸ var newcontrol = document.createElement("img");
❹ newcontrol.className = 'imgcontrol';
❺ newcontrol.src = src;
❻ newcontrol.onclick = func;
❼ mapstraction.currentElement.appendChild(newcontrol);
}
这个函数需要两份数据才能创建控制并将其添加到地图中。它需要一个图像源❶,这是一个指向我们将用于此控制的图像文件的路径。它还需要一个函数❷,当图像被点击时,它会调用这个函数。
现在我们已经准备好创建这个控制了。我们以编程方式创建一个图像元素❸,就像我们在创建自定义控制时使用<a>标签一样。然后我们给图像一个类名❹,这样我们就可以使用 CSS 对其进行样式设计。最后我们添加图像 URL❺。这个 URL 可以是完整的,也可以相对于当前页面。
到目前为止,图像已经创建,但它既不在地图上,也没有做任何事情。为了解决这两个问题,我们设置了一个函数 ❻ 在用户点击时被调用,然后将图像对象作为地图对象的子对象 ❼ 添加。
让我们确保我们的新图像控制位置正确。将这些 CSS 行添加到您的样式表中:
img.imgcontrol {
position: relative;
float: right;
margin: 2px;
}
现在我们可以将我们的自定义缩放控件添加到地图上。在您的地图初始化代码内部,添加以下行:
create_image_control(❶"zoom-plus.png", function() {
❷ mapstraction.setZoom(mapstraction.getZoom()+1);
});
create_image_control("zoom-minus.png", function() {
mapstraction.setZoom(mapstraction.getZoom()-1);
});
在这里,我创建了两个图像控制,如图图 4-11 所示。第一个用于放大。CSS 将控制放置在最右边。我发送了我的图像名称 ❶,这假设它存储在与 HTML 页面相同的目录中。然后我传递给它一个匿名内联函数。在这里,我使用 Mapstraction 设置了缩放级别 ❷,传递一个比当前缩放级别大一的数字。

图 4-11. 地图上的自定义缩放图像
第二个图像控制类似。它被赋予了一个不同的图像,当点击时,它将缩放级别设置为当前缩放级别减一 less。
#24: 在地图上绘制图像缩略图
一张图片可能不一定值一千个地理点,但它很接近。地图是展示被 地理标记 的照片的好方法。地理标记是将纬度和经度坐标与图像关联起来。然而,全尺寸照片可能不是理想的,因为它们会占用太多空间。相反,一种流行的方法是显示更小的版本——缩略图——供观众放大。
当然,你需要照片。你可以使用一些你有的作为测试,或者搜索照片分享网站 Flickr。我通过搜索奥兰多,佛罗里达找到了一些好的照片,它们已经地理标记。我特别寻找那些被许可为 Creative Commons 的照片,这意味着它们有较少的版权限制。
尽管我们将链接到每张图片的大版本,但我们还需要单独的文件来存储较小的版本。使用 Flickr 的另一个优点是它还自动创建这些缩略图和中等尺寸的图像。
对于我们绘制的每个奥兰多照片,我们需要以下内容:
-
照片缩略图
-
纬度和经度
-
中等尺寸的相片
-
中等相片的尺寸
-
完整照片的链接
至少你需要前两项,尽管你可以包含列表中的更多项来创建更好的用户体验。如果你像我一样使用 Flickr,那么你需要那个链接来避免违反 Creative Commons。
我的三个缩略图在图 4-12 中显示在地图上。正如在其他我们可能需要多次执行一个动作的情况中,创建一个函数是最好的。将以下代码添加到您的 JavaScript 代码中:
function plot_thumbnail(pt, thumbimg, medimg, medw, medh, link) {
var mk = new Marker(pt);
❶ mk.setIcon(thumbimg, [50, 50]);
❷ mk.setShadowIcon('outline.png', [52, 52]);
mk.setInfoBubble('<a href=\"' + ❸link + '\">' +
'<img src=\"' + ❹medimg + '\" width=\"' + ❺medw + '\"' +
' height=\"' + ❻medh + '\" /></a>');
mapstraction.addMarker(mk);
}
作为函数的参数,我们需要传递列表中提到的所有项目。然后我们使用缩略图创建一个自定义标记❶。注意,我将标记的尺寸设置为 50×50。你可以使用任何大小,但确保图像本身接近那个大小。Flickr 的是 75 像素的方形,所以稍微缩小是可以的。
接下来,我们想要设置一个阴影❷,否则将使用默认设置(在 Google Maps 中,默认是倒泪滴形状,放在方形照片下面看起来会很奇怪)。我创建了一个轮廓图形,给图像添加了一点点边框。我将大小设置为略大于缩略图本身。
最后,我们添加一个消息框。记住,你可以在其中包含任何 HTML,所以我们将链接到完整图像❸,并显示中等大小的图像❹。为了帮助地图提供商确定消息框的大小,我们包括中等大小图像的宽度❺和高度❻。
现在我们已经准备好调用该函数:
plot_thumbnail(new LatLonPoint(28.4736, −81.4651),
'http://farm3.static.flickr.com/2578/3769139951_954a782886_s.jpg',
'http://farm3.static.flickr.com/2578/3769139951_954a782886_m.jpg',
240, 180,
'http://www.flickr.com/photos/lancerrevolution/3769139951/');

图 4-12. 以自定义标记覆盖缩略图(照片由 Ron Miguel、Kok Leng Yeo 和 LancerE 拍摄)
此函数在地图上绘制单个照片缩略图。点击小版本,它会打开一个显示中等版本的消息框。然后,当你点击图像时,Flickr 链接会打开。
现在只需调用该函数两次——或者二十次。你可以使用我的地图中的 Flickr 照片作为示例:
对于一个更高级的项目,你可以通过调用 Flickr API 在地理点附近搜索图像来自动化查找图像的过程。API 随后会以 XML 或 JSON 格式响应,这两种格式你都可以使用第八章中展示的技术进行解析。
#25:在地图上覆盖图像
你可以用多种方式将图像添加到地图上。在“#5:创建自定义图标标记”(见第二章第五部分,ch02s05.html)和“#4:不点击标记显示和隐藏消息框”(见第二章第四部分,ch02s04.html)中,你使用图像作为 Placemark 的图标。在本节中,我们将做一些不同的事情——在地图的更大区域上覆盖图像,它将替换或增强现有地图。
你实际上已经熟悉这个过程,因为我们在本章前面已经使用过它,在第十八部分:添加圆圈以显示搜索半径和设置填充颜色和透明度中。在那里,我们使用了一个圆形图像并将其地理参照,使图像以一个点为中心并覆盖特定区域。那个例子比我们现在想要做的要简单,因为圆在各个方向上的距离相同,而且它指向哪里并不重要。
例如,考虑一下图 4-13 中纽约中央公园的地图。它包含了一些可能不在你标准网络地图上的建筑和地标。这张地图将公园完美地定位,使其南北方向延伸。实际上,环绕公园长边街道的位置稍微偏东(相对于北边,相对于南边)。

图 4-13. 中央公园源图形
Mapstraction 只能在其矩形顶部正好位于北边时覆盖图形。为了使中央公园地图与纽约的其他部分匹配,我们需要对其进行地理参照。
地理参照您的地图
地理参照技术的核心是确定图形上点的纬度和经度。然后,使用这些点,你可以弯曲和扭曲地图,使图形的顶部和底部边界成为静态纬度(以及另外两个静态经度)。
这个过程通常被称为橡皮膜法,因为你正在将二维参照拉伸到球形地球上,然后将其展开使其再次变平。结果是扭曲的图像,就像它是由橡皮制成的。
你可以通过多种方式将图形与地图进行地理参照。微软有一个名为 MapCruncher 的程序,效果很好。在这种情况下,你需要一台 Windows 机器,并且生成的图形只能用于非商业用途。MetaCarta 还有一个名为 Map Rectifier 的 Web 工具。
对于这个项目,我将使用一个名为 Map Warper 的 Web 应用程序,可以在warper.geothings.net/找到。Map Warper 是由 Tim Waters 开发的,是开源的,旨在让你摆脱对如何使用最终结果的担忧。你需要创建一个免费账户来存储你的地图图像。一旦你创建了账户,点击添加地图链接开始创建新地图。
在你为地图提供名称和其他元数据后,你可以包含一个图形。浏览你的硬盘以找到你想要使用的图像。你可以在本例中使用的中央公园图形在mapscripting.com/image-overlay/找到。
点击校正标签,你会在左边看到你的图像和右边的地图(见图 4-14
图 4-14. Map Warper 界面
例如,我在中央公园图形的左下角选择了我的第一个点,在哥伦布圆环处。一旦我能在两个屏幕上看到哥伦布圆环,我就点击标记按钮(见图 4-14 所示。

图 4-15. 地理参考的第一个控制点
要找到屏幕外的额外点,请记住从标记器切换到手,这样你就可以再次移动图像。你还需要用右边的地图做同样的事情。继续这个过程,直到你有一把点的数量。Map Warper 建议至少三个点,但我发现通常需要更多。
中央公园的例子比一些例子更容易,因为纽约市有一套很好的网格街道可以作为参考。图 4-16 显示了我在这个例子中选择的七个点。当你完成添加控制点后,滚动到底部并点击扭曲图像。

图 4-16. 更多控制点产生更好的校正地图
系统会旋转和嗡嗡作响一段时间。当它响应地图已经被校正时,你可以滚动到顶部并点击预览校正标签。一个带有你的图像扭曲并叠加在上面的地图将出现,如图图 4-17 所示。你可以移动底部的滑块来改变图像的不透明度。将标记完全移到右边会使图像完全不透明,这意味着它完全覆盖了原始地图。

图 4-17. 地理参考的中央公园地图预览
拖动滑块来确定你的图像与源地图的匹配程度。如果你对结果不满意,点击Rectify标签并添加几个更多点。
应用扭曲地图
当你对扭曲图像的结果满意时,点击Export链接下载图像以用于你自己的项目。你想要的是 PNG 格式的版本。图 4-18 显示了经过地理参考后我的中央公园图形现在看起来有多不同。

图 4-18. 从地理参考扭曲的中央公园图像
为了使用 Mapstraction 在地图上叠加扭曲的图像,你需要知道图像矩形的边界。要在 Map Warper 中找到这些信息,请点击Activity标签,在那里你会看到一个显示地理参考会话时间线的表格。最上面的条目可能是 Map Successfully Rectified。点击该行的Further Details链接,你会看到更多详细信息,包括一个包含四个十进制数字的框。突出显示并复制这些数字,它们应该已经按顺序排列:西、北、东、南。
现在你可以将叠加代码添加到你的 Mapstraction 地图中:
mapstraction.addImageOverlay(❶'centralpark', ❷'centralpark-warped.png', ❸100,
❹-73.9867415, 40.7622753, −73.9460798, 40.8032834);
为了将我们的扭曲图像添加到地图中,我们需要给它一个标识符 ❶。我们让 Mapstraction 知道扭曲图像的路径 ❷(这里,我假设图像与 HTML 文件在同一个目录中)。然后,我们给出一个介于 0(不可见)和 100(隐藏——图像下无法看到地图)之间的不透明度百分比 ❸。最后,我们添加 Map Warper ❹中的四个数字,这些数字描述了图像将驻留的地理框。
图 4-19 显示了完成的地图,中央公园的图形完全遮挡了其下的谷歌地图。

图 4-19. 在谷歌地图上叠加的中央公园地理参考图像
你也可以在更大的区域内使用你自己的地图图像。然而,将整个城市或更多的数据存储在单个图像中效果不会很好。相反,请看下一个项目,它展示了如何一次显示一点来创建自己的图像。
#26: 使用自定义瓦片
大多数提供商允许你为你的地图选择几种图像类型。你可以显示卫星视图、道路地图或混合版本。但你不是那种千篇一律的类型,对吧?你喜欢选择自己公园的绿色或自己道路的厚度。为此,你需要自定义瓦片。而且,哪里比拉斯维加斯大道更适合你的光彩和华丽呢?
要创建拉斯维加斯的瓦片,我们需要有关街道和其他特性的数据。尽管地图提供者在他们的 API 中允许你做的事情很宽松,但大多数像扑克玩家对待他们的牌一样对待底层数据。一个使其数据广泛可用的例子是 OpenStreetMap,这是世界上免费的可编辑地图。当然,分发地球上每条街道的详细信息意味着文件相当大,因此也提供了特定区域的下载。在这个项目中,例如,我们只需使用内华达州的数据,拉斯维加斯就位于这个州。
要创建瓦片,我们将数据输入到一个名为 Mapnik 的开源程序中。由于 Mapnik 的安装和配置可能有点复杂,我们将利用另一个名为 Tile Drawer 的项目,它提供了一个 Amazon EC2 机器镜像来执行大部分技术工作。首先,让我们了解一下地图提供者如何使用瓦片。
地球有多宽的像素?
如 第一章 所述,地图由排列成一个大图像的瓦片组成。每个瓦片是 256 像素的正方形,并组织成网格。大多数提供者从左到右和从北到南参考网格,从阿拉斯加上面的北极洋开始。瓦片通过其在网格中的编号进行引用,例如 (14, 34)。
显示整个地球所需的瓦片数量取决于缩放级别。例如,在 Mapstraction 中的最缩放级别 0,地球可以显示在一个单独的瓦片上。每次放大,需要四个瓦片来显示之前在一个瓦片上显示的细节。你可以通过确定 2 的 缩放级别 次方(2^缩放级别)来找到每个方向上使用的瓦片数量。表 4-1 显示了每个缩放级别的瓦片和像素信息。
当地图提供者加载地图瓦片时,它使用三个数字:缩放级别、从左侧的瓦片数量和从顶部的瓦片数量。所有这些数字都是从零开始的,因此每个缩放级别地图的左上角是 (0, 0)。例如,在级别 6 的右上角是 (16383, 0)。
表 4-1. 每个缩放级别的瓦片和像素
| 缩放级别 | 宽/高瓦片 | 宽/高像素 |
|---|---|---|
| 0 | 1 | 256 |
| 1 | 2 | 512 |
| 2 | 4 | 1,024 |
| 3 | 8 | 2,048 |
| 4 | 16 | 4,096 |
| 5 | 32 | 8,192 |
| 6 | 64 | 16,384 |
| 7 | 128 | 32,768 |
| 8 | 256 | 65,536 |
| 9 | 512 | 131,072 |
| 10 | 1,024 | 262,144 |
| 11 | 2,048 | 524,288 |
| 12 | 4,096 | 1,048,576 |
| 13 | 8,192 | 2,097,152 |
| 14 | 16,384 | 4,194,304 |
| 15 | 32,768 | 8,388,608 |
| 16 | 65,536 | 16,777,216 |
| 17 | 131,072 | 33,554,432 |
| 18 | 262,144 | 67,108,864 |
幸运的是,您不需要通过瓦片的网格位置来引用它们。映射提供商为您完成所有这些。了解它是如何工作的重要,因为您需要使用这些知识在项目后期创建自定义瓦片 URL。
关于本节顶部的疑问——地球的宽度是多少像素?这取决于缩放级别,但对于大多数提供商来说,地球的宽度在 256 到 67,108,864 像素之间。
启动 Tile Drawer EC2 实例
Tile Drawer 可以帮助您创建自己的自定义地图瓦片,并在云中运行瓦片服务器。它运行在 Amazon EC2 之上,即一个弹性计算云,也就是说,一个可扩展的 Web 服务器。EC2 的另一个特点是能够保存预配置的服务器、亚马逊机器镜像(AMIs),并使它们可供他人使用。这就是 Tile Drawer 的创造者所做的事情。方便!
您需要一个亚马逊账户(准备好提供您的电子邮件地址和一些其他信息),然后注册 EC2。亚马逊为此服务收费,但按每小时几分钱收费,所以您可以用不到一美元的价格尝试这个项目。此页面将指导您完成注册过程:aws.amazon.com/ec2/。
从此页面登录 AWS 管理控制台。点击启动实例按钮。然后在社区 AMIs 中搜索 Tile Drawer 机器镜像。寻找tiledrawer,或者找到在tiledrawer.com/上列出的 ID,例如ami-e1ea0a88。当您找到 Tile Drawer AMI 时,点击选择按钮。
在实例详细信息屏幕上,只创建一个实例,如图图 4-20 所示。因为您将使用一个非常小的区域开始,所以可以使用小型实例类型。点击继续按钮,然后在下一屏幕上,您大部分保持默认设置不变。但是,您需要添加一些用户数据。

图 4-20. 创建单个 Amazon EC2 Tile Drawer 实例
为您的实例声明用户数据
Tile Drawer 服务器相当容易安装。只需设置几个设置即可使其运行。您稍后会发现,这些设置还允许您完全自定义您的地图瓦片。
您可以使用tiledrawer.com/上的向导来帮助您自动创建用户数据。或者,为了继续跟随这个示例,将以下数据粘贴到 EC2 设置页面上的用户数据框中(见图图 4-21):
{
"style": "http://tiledrawer.com/mapscratch.mml",
"bbox": [−115.2, 36.10, −115.1, 36.15],
"source": "http://downloads.cloudmade.com/north_america/
united_states/nevada/nevada.osm.bz2",
"coast": "http://hypercube.telascience.org/~kleptog/processed_p.zip"
}
这组数据有四个偏好设置。首先,地图的样式,它使用类似 CSS 的样式表。在这个第一个例子中,我们将坚持使用瓦片抽屉的基本外观,稍后再进行更改。接下来,我们声明一个BoundingBox,类似于我们与 Mapstraction 一起使用的那些。这里的区别在于经度在纬度之前列出。我们使用的两个点仍然是西南,然后是东北。
最后两项是 Tile Drawer 将下载并使用的文件的 URL。第一个是数据本身。我们使用内华达州的数据,由 CloudMade 托管。您可以在 downloads.cloudmade.com/ 找到您需要的数据。寻找类型为 .osm.bz2 的文件。最后一个 URL 是海岸线数据,它被单独托管。

图 4-21. 向 EC2 实例添加用户数据
设置了用户数据后,再次点击继续。下一屏幕将提示您创建密钥对。密钥对使您能够连接到服务器的后端。这个高级功能可能在未来的某个时刻有用。现在,选择不使用密钥对继续,然后点击继续。
在防火墙屏幕上,请确保您选择了 web 服务器安全组。此组打开运行 web 服务器所需的适当端口,这对于从任何计算机访问您的瓦片,包括您自己的计算机,是必要的。点击继续,您将进入最终屏幕。深吸一口气,然后点击启动。
瓦片抽屉完成其工作
您的 EC2 实例不会立即启动。尽管它是一台虚拟计算机,但启动仍然需要几分钟。一旦可用,创建所有瓦片将需要额外的时间。这是一个休息的好时机!
但您不应该需要太长的休息时间。由于只需下载一个状态的数据并准备一个小区域,瓦片抽屉应在 15 分钟内准备好。从您的 EC2 仪表板,您应该能够点击“我的资源”下的运行实例链接。从那里,您可以查看您的新实例的状态。当服务器启动后,它应该从黄色和挂起切换到绿色和运行。
当服务器运行时,点击您的实例,下面面板中会出现一个描述。向下滚动并找到公共 DNS 地址,如图 图 4-22 所示。此地址是您新虚拟服务器的域名等效物。在您的网页浏览器中输入该地址,您应该会看到一个显示“它工作!”的页面。

图 4-22. 实例详情显示您的公共 DNS 地址。
接下来,将/status.php添加到您的地址末尾以获取 Tile Drawer 的状态。Tile Drawer 会创建瓦片:入门,下载源,提取数据,创建表格,导入海岸线,下载样式表,以及创建 TileCache。随着每个步骤的完成,它将变为灰色,如图图 4-23 所示。当 Tile Dawer 完成时,您可以点击链接或转到您的地址后跟/preview.php。

图 4-23. Tile Drawer 的每个部分完成时的状态更新。
您将在 Tile Drawer 服务器上看到您新瓦片的快速预览。您可以双击以放大。如果一切看起来都很好,那么是时候将这些瓦片放到 Mapstraction 地图上了。
将瓦片叠加到您的地图上
将您的瓦片包含到 Mapstraction 中的过程就像一行代码一样简单。设置瓦片服务器的工作已经完成——在创建样式方面,您的工作还在前方,这可能是一项繁琐的任务。
使用以下代码创建一个新的基本地图:
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'googlev3');
mapstraction.setCenterAndZoom(
new mxn.LatLonPoint(36.123, −115.167), 13);
mapstraction.addSmallControls();
`mapstraction.addTileLayer(`
`"http://```*`yourserver`*```.amazonaws.com/ tilecache/1.0.0/osm/`❶`{Z}/{X}/{Y}.png",` ❷1.0);
}
在这里,我们创建了一个以拉斯维加斯大道为中心的地图。然后,在加粗的行中,我们告诉 Mapstraction 在哪里找到我们的瓦片。我们的 URL 包含用于缩放级别❶和瓦片网格坐标的占位符。这些值,{Z}、{X}和{Y},用实际数字填充。您可以通过访问yourserver.amazonaws.com/tilecache/1.0.0/osm/13/1475/3213.png来查看一个示例瓦片。请确保用您的 EC2 实例的公共 DNS 地址替换yourserver。
我们传递给 Mapstraction 的瓦片层函数的第二个参数是透明度。就像在第二十五部分:在地图上叠加图像中一样,我们可以使我们的瓦片半透明,这样我们仍然可以看到下面的提供者图像。在这里,我通过选择值为一来使我们的瓦片完全不透明❷。介于零和一之间的数字设置百分比。例如,0.6 将是 60%不透明。
保存您的地图并在浏览器中加载。现在您应该看到定制的拉斯维加斯瓦片而不是谷歌图像,如图图 4-24 所示。在您的瓦片加载之前,您可能会看到默认的外观。这是因为定制的瓦片被放置在谷歌图像之上,所以这两组都需要加载。

图 4-24. 使用 Tile Drawer 的" scratch"样式表定制的瓦片
创建您自己的瓦片样式
如果您熟悉用于网页样式的 CSS,您可能会对 Tile Drawer 应用颜色和其他样式到地图的方式感到舒适。它使用 Cascadenik 将其转换为与 Mapnik 瓦片生成器一起工作的文件格式。在本项目的早期,我们使用了 Tile Drawer 提供的基本示例。现在我们将尝试更改一些颜色和道路宽度。
您首先需要做的是将tiledrawer.com/mapscratch.mml复制到您自己的服务器上。或者,您可以使用我为本节编辑的版本,在mapscripting.com/examples/tiledrawer/mapscratch-edits.mml。
由于拉斯维加斯以其霓虹灯而闻名,让我们尝试使道路从地图中突出出来。为此,我们将使用鲜艳的颜色和深色背景。找到以#land开头的行,并使用以下样式数据进行更改:
#land { polygon-fill: #333; }
这组数据将土地(基本上是背景颜色)从非常浅的颜色变为接近黑色。黑色非常符合拉斯维加斯的风格,尤其是当我们包括鲜艳的颜色时。对于接下来的更改,您需要使用以#lines开头的样式。确保您的代码与这些设置匹配:
#lines[❶highway=motorway],
#lines[highway=motorway_link]
{
❷ line-width: 6;
❸ line-color: #f00;
}
#lines[highway=primary],
#lines[highway=secondary],
#lines[highway=tertiary]
{
line-width: 4;
line-color: ❹#ff0;
}
#lines[highway=residential],
#lines[highway=unclassified],
#lines[highway=service]
{
line-width: 2;
line-color: ❺#00f;
}
Cascadenik 样式表使用方括号内的 OpenStreetMap 标签来确定您想要样式的元素。在所有这些示例中,我们正在样式化高速公路,这是一个任何道路的通用术语。在第一组中,我们只将样式应用于高速公路❶和高速公路“链接”(例如出口)。由于拉斯维加斯的一切都更大更亮,所以我们使高速公路更宽❷,然后将其染成鲜艳的红色❸。
在接下来的两个部分中,我们将较大街道设置为黄色❹。住宅和其他小街道设置为蓝色❺。随着街道变窄,地图上的街道宽度也相应变窄。然而,由于我们正在以拉斯维加斯风格进行,因此它们仍然比我们正在编辑的样式要宽。
您可以使用这些样式做一些强大的事情,使您的地图看起来与任何可用的图像都不同。作为样式如何具体的一个例子,尝试将以下行添加到您的样式表中:
#lines`[zoom=13]`[highway=motorway]
{
line-color: #ff8000;
}
初看,这段代码与我们之前做的一些代码类似。请注意加粗的部分;它告诉瓦片服务器仅在缩放级别为 13 时应用此样式。在所有其他缩放级别,我们的其他样式将优先。但是,当我们的地图处于 13 级时,高速公路将变为橙色而不是红色。
我没有重置线宽,就像在其他部分做的那样,这意味着为高速公路设置的线宽在 13 级缩放时将保持不变。只有颜色会改变。除了=之外,您还可以使用>, >=, <, 和 <=在特定缩放级别进行样式化。
通过更改一些样式,让我们看看 Tile Drawer 的实际效果。创建一个新的 EC2 实例(记得终止未使用的实例以避免每小时收费)并使用以下数据:
{
"style": "`http://mapscripting.com/examples/tiledrawer/mapscratch-edits.mml`",
"bbox": [−115.2, 36.10, −115.1, 36.15],
"source": "http://downloads.cloudmade.com/north_america/
united_states/nevada/nevada.osm.bz2",
"coast": "http://hypercube.telascience.org/~kleptog/processed_p.zip"
}
如果您做了我没有在这里包括的更改,那真是太好了!在这种情况下,用您自己的服务器上地图样式表的地址替换粗体中的 URL。为了了解这些少数更改如何改变我们瓦片的外观,请参阅图 4-25。

图 4-25. 大路和鲜艳的色彩风格化的拉斯维加斯地图
在您的虚拟服务器启动并 Tile Drawer 完成其工作后稍作等待,您新设计的拉斯维加斯瓷砖应该就绪了。更新您用于创建瓦片层的 Mapstraction 文件中服务器的地址。加载它,您应该会看到从您的新地图中跳出的明亮的拉斯维加斯道路。
第五章:处理地图事件

基于网络的地图高度交互。它们使用户想要拖动、点击和缩放地图。这些都是乐趣的一部分,但这种交互性也使它们更有用。通过利用这种交互性,你可以设计那些小小的用户动作,以提供更好的界面;例如,当用户拖动地图时更改可见标记,或者随着缩放级别改变搜索半径。
事件控制了用户与您的地图交互的所有潜在方式。用户拖动地图、放大或点击时,您都可以运行特殊代码。事件也针对标记、消息框和多段线。继续阅读,了解 Mapstraction 组织事件的方式以及每个事件的示例。
Mapstraction 的事件模型
事件发生,无论我们是否关注。为了能够对事件做出反应,我们需要告诉 Mapstraction 我们关心那个特定的事件。我们通过为 Mapstraction 在事件发生时调用的函数设置一个函数来实现这一点。这个函数被称为 handler。
要创建事件处理器,我们需要知道我们正在寻找的事件类型以及事件将从中起源的对象。我们使用以下形式注册我们对事件的兴趣:
*`object.event`*.addHandler(function (event_name, event_source, event_args) {
// Code to perform after the event
});
每个对象可能发生的事件潜力在 表 5-1 中显示。addHandler 函数接受一个参数——一个引用到将处理事件的函数。在前面示例以及本章中的大多数示例中,我使用的是匿名内联函数。您也可以使用命名函数,就像我在涉及标记的大多数示例中所做的那样。如果您要在许多对象上调用相同的函数,使用命名函数将发现更节省内存。
无论您使用匿名函数还是命名函数来处理事件,该函数都接受三个参数:事件名称、事件源和额外的事件参数。这些值由 Mapstraction 自动传递。名称将始终与 event 相同,而源将始终是 object。如果存在,附加数据将包含在参数对象中。
表 5-1. 对象及其事件
| 对象 | 事件 |
|---|---|
Mapstraction |
click |
Mapstraction |
endPan |
Mapstraction |
changeZoom |
Mapstraction |
markerAdded |
Mapstraction |
markerRemoved |
Mapstraction |
polylineAdded |
Mapstraction |
polylineRemoved |
Marker |
click |
Marker |
openInfoBubble |
本章的各个部分都深入探讨了每个事件。
#27:用户点击地图
开箱即用的交互性使地图 API 非常特别。使用内置控件,用户可以拖动和移动地图、放大并更改地图类型。用户也可以点击,但除非您帮助他们,否则不会发生任何事情。
Mapstraction 的点击事件是响应用户点击的。与本章中展示的其他一些事件不同,仅仅知道用户点击了是不够的。你需要知道他们点击了哪里。
要找出用户何时何地点击,请将以下代码添加到你的 create_map 函数中:
mapstraction.click.addHandler(function(event_name, event_source, event_args) {
var clickpoint = event_args.location;
var mk = new mxn.Marker(clickpoint);
mapstraction.addMarker(mk);
});
在这里,我们为 Mapstraction 的 click 事件添加了一个处理程序。为了响应这个事件,我使用了一个匿名内联函数。如果你有超过几行代码要运行,你将想要使用一个标准的命名函数,因为长的匿名函数很难阅读。我在这里正在挑战极限。
当用户在地图的任何地方点击时,Mapstraction 会调用我们的函数,并传递三份数据,其中最后一份数据包含事件参数。我们想要的关键信息就在这些参数中:点击位置,它被存储为 LatLonPoint。上一个例子就是使用这个点来创建一个新的标记。
每次点击都会在地图上添加另一个标记。这样做几次,你的地图看起来就会像图 5-1 那样。
#28: 用户拖动地图
如果你是一个喜欢控制的开发者,你可能不会立即欣赏地图的交互性。用户可以将地图的可视部分移动到他们想要的位置,从而放弃你精心设计的体验。多么令人沮丧啊!实际上,字面上来说就是令人沮丧,因为用户会这样拖动地图。然而,你可以使用一个事件来帮助你恢复一点控制。
你可以编写代码来响应地图的最小移动。你可以使用这个来强制地图回到你放置的位置,如果你喜欢这样做的话。一个更友好的反应是找到一种方法来识别用户移动的地图区域。例如,如果你正在显示搜索结果,尝试用新的中心重新加载它们。

图 5-1. 每次点击都会添加一个新的标记。
在这个例子中,我们将简单地创建一个 JavaScript 警告,每当我们发现地图已经移动时。那么我们如何知道地图已经移动了呢?请将这些行添加到你的基本地图中:
mapstraction.`endPan`.addHandler(function(event_name, event_source, event_args) {
alert('The map just moved!');
});
在这里,我们为 Mapstraction 的 endPan 事件添加了一个处理程序。为了响应这个事件,我使用了一个匿名内联函数。如果你有超过几行代码要运行,你将想要使用一个标准的命名函数。
实现了这段代码后,每次用户移动地图时,这个事件都会触发,并且会弹出警告框。程序性地设置中心也会触发事件,因为地图已经被平移(尽管不是直接由用户操作)。然而,当你最初设置地图中心时,事件不会触发,这仅仅是因为那段代码在 Mapstraction 知道要关注地图移动之前就已经执行了。
注意
对于一些提供者,例如 Google,当缩放级别改变时,也会触发 endPan 事件。
一旦endPan事件触发,你将可以通过 Mapstraction 对象访问当前地图数据,例如中心点。以下是一个示例,每当用户拖动地图时,都会在新的中心创建一个新的标记:
mapstraction.endPan.addHandler(function(event_name, event_source, event_args) {
var mk = new mxn.Marker(mapstraction.getCenter());
mapstraction.addMarker(mk);
});
将此代码输入到你的基本地图的create_map函数中,并在浏览器中加载它。尝试移动地图,每次你都会在中心找到一个全新的标记。
#29:缩放级别变化
"让我们仔细看看吧,"她心想,点击加号按钮将地图放大。假设你提供了这样的界面,你的用户将会进行大量的放大和缩小操作。在一些地图中,对地图边界有时剧烈变化做出反应是很重要的,这就是 Mapstraction 的缩放事件能成为你最好朋友的地方。
你可以让你的地图处理缩放事件并执行一些有用的操作,比如重置你的搜索半径。或者,如本章后面所示,确定用户是否将地图移动到预设边界之外。
对于这个例子,我们将在地图缩放时创建一个 JavaScript 弹窗,这样你可以感受到这个事件何时被触发。将以下代码添加到你的基本地图的create_map函数中:
mapstraction.`changeZoom`.addHandler(function(event_name, event_source, event_args) {
alert('Zoom level changed!');
});
在这里,我们为 Mapstraction 的changeZoom事件添加了一个处理程序。为了响应此事件,我使用了一个匿名内联函数。如果你有超过几行代码要运行,你将想要使用一个标准的命名函数。
尝试加载包含此代码的地图。在地图上改变缩放几次,并观察其响应。如果你的提供商将双击解释为放大(大多数都是这样),也试试这个。你的用户在没有你了解的情况下无法放大或缩小!
你想用更有用的事情来响应吗?比如显示新的缩放级别?尝试以下代码:
mapstraction.`changeZoom`.addHandler(function(event_name, event_source, event_args) {
alert('Changed zoom to level ' + mapstraction.getZoom());
});
现在,每次你的用户放大或缩小,消息不仅会说缩放级别已更改;还会显示新的缩放级别。如果你的地图有大的缩放控件,尝试从当前级别快速放大到一个非常接近的级别,或者尝试极度缩小。一个带有新缩放级别数字的弹窗将会弹出。如果你想更好地理解缩放级别,请参阅设置缩放级别。
#30:标记被添加到或从地图中移除
标记无处不在。一旦你的映射应用程序变得庞大,你可能需要从代码的许多不同区域添加标记。移除标记也可能如此。你可以利用两个事件,当标记被添加或移除时,将调用你指定的函数。
将以下代码添加到你的基本地图的create_map函数中:
mapstraction.markerAdded.addHandler(function(event_name, event_source, event_args) {
alert('Added marker at ' + event_args.marker.location);
});
mapstraction.markerRemoved.addHandler(function(event_name, event_source, event_args) {
alert('Removed marker at ' + event_args.marker.location);
});
当然,使用 JavaScript 弹窗来通知用户标记已被添加或移除可能并不那么有帮助。相反,你可能更新自己的标记元数据或根据新的或剩余的标记自动居中地图。
你可以通过event_args对象访问受影响的标记,该对象包含一个名为marker的属性。你可能觉得从event_source对象访问标记更直观。尽管这些事件与标记相关,但它们是由 Mapstraction 对象发起的,该对象作为源传递。
因为添加或删除标记的唯一方式是通过你的代码,所以有大量其他非事件驱动的方法可以在添加或删除标记时执行操作。正如提到的,你可能只有在大型应用程序中才会觉得这很有用。
#31:向地图添加或从地图移除折线
你在地图上使用了很多折线吗?你可以在每次添加或删除折线时运行一个特殊函数。这个特性对于大型地图应用程序特别有用,在这些应用程序中,许多不同的地方创建新的线条,或者当你需要删除不必要的线条时。
要为两个折线事件创建函数,请将以下代码添加到你的create_map函数中:
mapstraction.polylineAdded.addHandler(function(event_name, event_source, event_args) {
alert('Added polyline starting at ' + event_args.polyline.points[0]);
});
mapstraction.polylineRemoved.addHandler(function(event_name,
event_source, event_args) {
alert('Removed polyline starting at ' + event_args.polyline.points[0]);
});
我已经创建了一个 JavaScript 警告框来显示已添加或删除的折线的第一个点。你可能会想要包含比 JavaScript 警告框更有用的内容,在大多数情况下,用户可能会认为这是过度杀鸡用牛刀。例如,你可以选择根据新添加或删除的折线自动居中地图。
你可以通过event_args对象访问受影响的折线,该对象包含一个名为polyline的属性。你可能觉得从event_source对象访问折线更直观。尽管这些事件与折线相关,但它们是由 Mapstraction 对象发起的,该对象作为源传递。
与添加和删除标记一样,你的代码是向地图添加新折线(或从地图中删除不需要的折线)的唯一方式。由于这个原因,有其他方法可以在这些事件中运行代码。然而,对于大型应用程序,你可能更喜欢这种基于事件的方法。
#32:用户打开或关闭消息框
你的地图标记只能做到如此来传达地点的意义。其余的事情发生在消息框中,用户可以通过点击标记来打开它。如果你想对消息框的打开或关闭做出反应,你需要为每个标记编写事件处理器。
一个常见的用例是在消息框打开时更新地图外的网站部分。例如,你可能想在搜索结果中突出显示与该标记相关的结果。或者你可能想在查看过每个标记的消息框后隐藏每个标记。在这里,我们将坚持使用我们的方法来显示 JavaScript 警告框。
将以下代码添加到你的基本地图的create_map函数中:
var mk = new mxn.Marker(mapstraction.getCenter());
mk.setInfoBubble('Look ma, No Starch!');
mk.`openInfoBubble`.addHandler(myboxopened);
mk.`closeInfoBubble`.addHandler(myboxclosed);
mapstraction.addMarker(mk);
并且将此函数包含在 JavaScript 部分中,但不在其他函数之外:
function myboxopened(event_name, ❶event_source, event_args) {
alert('Opened bubble attached to marker at ' + ❷event_source.location);
}
function myboxclosed(event_name, event_source, event_args) {
alert('Closed bubble attached to marker at ' + event_source.location);
}
在这里,我在地图中心创建了一个简单的标记,并附带了一个信息框,类似于我们在第三部分:当您的标记被点击时显示信息框中创建的。打开和关闭信息框的事件以粗体显示。对于这个特定的标记,我们正在等待对这两个事件之一做出反应。
当事件触发时,我们调用命名函数,无论是myboxopened还是myboxclosed。您可以命名这些函数为任何您想要的名称。当适当的函数被调用时,标记会被传递到event_source变量❶。在这个时候,我们可以访问这个标记的任何属性,包括位置❷。我们可以用这个标记做任何我们可以对其他标记做的事情,包括隐藏它、移除它或设置新的属性(也许跟踪哪些信息框已被打开?)。
注意
与之前的一些示例不同,我们为这些事件添加了处理程序到标记对象,而不是 Mapstraction 对象。如果你想要为每个标记运行类似的代码,你需要为每个标记添加一个事件。
如您从比较两个事件的代码中看到的,对关闭信息框的反应使用了几乎相同的代码。您可以在本章后面的部分看到一个更高级的closeInfoBubble事件的示例。
#33: 用户点击标记
您的用户会看到地图上的所有标记,许多人会想要点击它们来了解更多关于特定位置的信息。这就是信息框的作用,对吧?嗯,你可能还想做更多。在这种情况下,你需要一种方法来告诉何时一个标记被点击。正如你可能猜到的,Mapstraction 有一个专门为此场合的事件。
标记点击事件,就像其他标记事件一样,是附加到标记本身的。所以,如果您想要为每个标记执行相同的操作,您需要单独添加处理程序。
对于这个示例,我们将简单地使用一个标记并创建一个 JavaScript 警告框来共享其位置。将以下代码添加到您的create_map函数的基本地图中:
var mk = new mxn.Marker(mapstraction.getCenter());
mk.`click`.addHandler(mymarkerclicked);
并且在 JavaScript 部分之外的其他函数之外包含此函数:
function mymarkerclicked(event_name, ❶event_source, event_args) {
alert('Clicked the marker at ' + ❷event_source.location);
}
当事件触发时,mymarkerclicked函数会带有三个参数被调用。然后标记会被传递到event_source变量❶。在这个时候,我们可以访问这个标记的任何属性,包括位置❷。我们可以用这个标记做任何我们可以对其他标记做的事情,但在这里我们只是创建了一个 JavaScript 警告框。
注意
这个事件与 Mapstraction 的主要点击事件不同,后者在地图被点击时随时触发。在这种情况下,事件是附加到一个特定的标记上的,并且只有当那个标记被点击时,您的代码才会运行。
事实是:你不会经常使用此事件,因为消息框是显示标记信息的最佳方式。然而,标记点击事件是一个有用的工具,尤其是在你想做一些不同的事情时。
#34:当消息框关闭时返回中心
我是否已经充分赞扬了消息框?它们有助于解释你的标记代表什么。它们提供了有关位置的额外信息,但仅在用户需要时提供。它们使你的地图更具信息性和交互性。但它们也可能妨碍,如图 图 5-2 所示。
根据你的提供商,打开消息框可能会导致你的地图不优雅地平移,当用户关闭消息框时留下一个混乱的地图。一点代码和本章中已经覆盖的事件的帮助可以改善用户体验。重新进入 closeInfoBubble 事件。
假设你已经布置了标记,使它们完美地位于地图内,如 #8:根据标记确定正确的缩放级别 所示,在 #7:遍历所有标记 中。如果你在用户关闭消息框和添加每个新标记后再次运行此代码,你的地图将始终保持有序。
在创建标记后添加此代码:
mk.closeInfoBubble.addHandler(myboxclosed);
添加处理事件的函数:
function myboxclosed(event_name, event_source, event_args) {
mapstraction.autoCenterAndZoom();
}
此代码假设有一个名为 mk 的标记,但你需要将其更改为与你的标记所选择的任何变量名匹配。还要记住,closeInfoBubble 事件是为单个标记设置的。为了使事件对每个标记都触发,你需要为每个标记设置它。
保留先前的中心
重新自动居中代码在大多数情况下可能非常有用,因为用户仍然可以看到每个标记。然而,在某些情况下,你可能希望给用户更多的控制权,同时仍然保持当消息框关闭时重新组织地图的好处。在这里,你想要记住用户打开消息框之前地图的位置。
要实现此结果,你需要同时使用两个消息框事件。这种方法是保留地图中心的一个简单而有效的方法。在创建每个标记后添加以下代码:
mk.openInfoBubble.addHandler(myboxopened);
mk.closeInfoBubble.addHandler(myboxclosed);
然后包括以下处理函数:
function myboxopened(event_name, event_source, event_args) {
❶ mapcenter = mapstraction.getCenter();
}
function myboxclosed(event_name, event_source, event_args) {
mapstraction.setCenter(mapcenter, ❷{pan: true});
}
与前面的示例一样,我假设有一个名为 mk 的标记。此外,我添加了一个名为 mapcenter 的全局变量来保存地图中心的 LatLonPoint。
每当打开消息框时,openInfoBubble 事件会在提供商移动地图以腾出消息框的空间之前触发。这很重要:现在我们有记录地图中心 ❶ 的机会,这将允许我们稍后重置它。
当消息框关闭时,会触发closeInfoBubble事件。而不是自动居中,我们将中心设置为打开消息框时存储的那个位置,如图 5-3 所示。为了更加体贴用户的眼睛,我们平移地图❷而不是直接设置中心。
现在你已经使用事件微妙地改善了用户与你的地图的交互。

图 5-2. 打开消息框可以移动你的地图。

图 5-3. 当消息框关闭时返回到上一个中心位置。
#35: 用户将地图移动到预设边界之外
用户可以随意拖动地图,但不要拖动得太远,否则我们就没有其他东西可以展示了。提供商已经绘制了整个地球的地图,但你的地图可能只包含特定街区或城市的资料。你可以使用事件来通知用户他们是否已经移动到了预设区域之外。
假设你正在创建黄石国家公园的观光地图。你会包括老忠实喷泉和大角温泉,因为它们都在黄石公园内。你不会包括自由女神像,因为它在数千英里之外。我们想提醒用户,如果他们把地图从黄石公园拖开,他们将不会在那里找到任何东西。
首先,让我们获取一个以黄石公园为中心的基本地图。将以下代码添加到您的 JavaScript 中,替换掉任何其他地图代码:
var mapstraction;
❶ var yellowstone = new mxn.BoundingBox(43.7, −111.8, 45.5, −109.3);
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.addLargeControls();
// Add Old Faithful marker
var ofmk = new mxn.Marker(new mxn.LatLonPoint(44.46270, −110.81153));
ofmk.setInfoBubble('Old Faithful');
mapstraction.addMarker(ofmk);
// Add Mammoth Hot Springs marker
var mammk = new mxn.Marker(new mxn.LatLonPoint(44.97682, −110.70425));
mk.setInfoBubble('Mammoth Hot Springs');
mapstraction.addMarker(mk);
mapstraction.autoCenterAndZoom();
// Event code below here
❷
}
// Additional functions below here
❸
首先,我们创建一个全局变量来保存描述黄石的BoundingBox。我们将在下一步中使用这些边界。在create_map函数中,你会看到我提到的两个景点的标记。之后,地图会自动居中,仅显示这些位置。我留下了两行代码,以指示我们很快将添加的额外代码。一个位置❷是用于事件代码,另一个位置❸是我们将用来确定地图是否在黄石之外的函数。
保存代码并将其加载到浏览器中。你的地图应该看起来像图 5-4。尝试拖动地图以显示更多北部的区域。拖动足够远,地图就会进入加拿大,远离黄石公园。让我们看看我们能做些什么来为用户提供警告。
拖动地图的行为是将用户从黄石公园带远,因此我们需要处理前面章节中提到的endPan事件。在为事件代码预留的部分中,在create_map函数内添加以下代码:
mapstraction.endPan.addHandler(function(event_name, event_source, event_args) {
if (❹!boundsInBounds(mapstraction.getBounds(), yellowstone)) {
alert('Watch out! You might be leaving Yellowstone...');
}
});

图 5-4. 黄石国家公园带有两个景点标记的地图
在这里,我们创建了一段代码,每次用户完成地图拖动时都会运行。它使用全局变量 yellowstone,这是描述黄石的 BoundingBox。边界仅是数据,在地图上不可见。图 5-5 显示了我们的边界覆盖的区域。你可以看到,我已经将边界做得足够大,以便在黄石周围包含一个合理的缓冲区。我们希望用户能够看到附近的东西。我们只想在他们显然偏离路线时警告他们。
要检查我们的地图是否仍然在黄石边界内,我们需要将新变量与地图的边界进行比较。为此,我们将这些传递给一个名为 boundsInBounds 的函数,我们还需要编写这个函数。如果该函数返回 false ❹(代码中的感叹号读作“不”),则我们创建一个 JavaScript 提示框来警告用户。

图 5-5. 黄石的边界
让我们在 create_map 之外包含新的函数,在为函数代码预留的部分:
function boundsInBounds(smaller, larger) {
if (larger.contains(smaller.sw) && larger.contains(smaller.ne)) {
return true;
}
return false;
}
该函数接受两个 BoundingBox 变量作为参数。第一个是我们 期望 较小的,在这种情况下是地图的边界。第二个是我们正在检查的边界,在这种情况下是黄石。因为 BoundingBox 由两个点(其西南角和东北角)组成,我们知道只有当较大的边界包含这两个点时,一个 BoundingBox 才在另一个内部。
我们之前编写的代码只有在该函数返回 false 时才会创建一个提示框,这意味着用户已经将地图拖出了黄石。试试看。将地图向北拖动。一旦你离开黄石,你将在每次拖动地图后收到警告,如图 5-6 所示。

图 5-6. 用户移出黄石边界时触发的 JavaScript 提示框
第六章. 探索邻近性

地图都是关于邻近性的。附近有什么?从一个位置到另一个位置有多远?本章将帮助你通过自己的地图来回答这些问题。
在某些情况下,你可能需要使用其他服务,例如 Yahoo!的本地搜索 API。在其他情况下,Mapstraction 提供了一些实用的函数。我们还将依赖一些比我聪明的数学家来帮助我们理解应用于地球三维球体的二维坐标系。
#36: 计算两点之间的距离
在纸质地图的古代,确定地图上两点之间的距离需要使用地图的比例尺和一些测量工具。我经常会用一张纸片或手指来复制所需的长度,以计算距离。没有尺子,计算距离并不是一门精确的科学。一些提供商仍然显示比例尺,但 Mapstraction 使得自己进行计算变得不再必要。
假设你有一张带有两个标记的地图:marker1 和 marker2。你可以确定这些标记的纬度和经度点,并据此计算距离。或者,你可以让 Mapstraction 为你完成这项工作:
var dist_km = marker1.location.distance(marker2.location);
结果是marker1位置到marker2位置之间的公里数。distance函数可以在任何LatLonPoint上调用,第二个LatLonPoint作为参数传递。标记的LatLonPoint存储在location属性中。
这个计算实际上发生了什么?这难道不就是我们在小学学过的简单勾股定理——a² + b² = c²吗?不幸的是,并非完全如此。毕达哥拉斯是在二维空间工作的,而地球是一个三维的椭球体——即一个略微变形的球体。
在 第一章 中,我描述了纬度和经度系统,其中随着点离赤道的距离越远,经度之间的距离越小。换句话说,如果你在厄瓜多尔,使用勾股定理可以得到足够接近的结果,但在瑞典你的计算就会偏差很大。
你还需要另一个奇怪命名的公式——哈弗辛公式。这个函数使用地球半径和一些复杂的球面三角学。以下是 Mapstraction 距离函数的一个略微修改版本:
function LatLonPoint_distance(pt1, pt2) {
❶ var rads = Math.PI / 180;
var diffLat = (pt1.lat-pt2.lat) * rads;
var diffLon = (pt1.lon-pt2.lon) * rads;
var a = Math.sin(diffLat / 2) * Math.sin(diffLat / 2) +
Math.cos(pt1.lat * rads) * Math.cos(pt2.lat * rads) *
Math.sin(diffLon/2) * Math.sin(diffLon/2);
return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) * ❷6371;
}
这个函数首先做的事情是计算将度数转换为弧度所需的乘数❶,这是三角函数用来计算所需距离的。弧度大约是 57 度(180 度除以π)。要将纬度和经度十进制度数转换为弧度,我们需要乘以一度中的弧度数,这大约是 1/57(π除以 180 度)。
然后我们进入哈弗辛公式,它确定了球面上两点之间的最短距离。为了得到一个可用的距离,我们必须知道球的半径。在这种情况下,我们使用地球的半径(千米)❷。要得到英里,使用半径 3958。或者将千米结果乘以 0.6213。Mapstraction 还有两个辅助函数,KMToMiles和milesToKM,用于执行这些转换。
你能扔一个物体过河吗?
这道数学题让我头疼,让我们来看一个实际例子。嗯,在你所在的地方扔东西可能不实用,但在波特兰,威拉米特河穿过城市的中心。自然地,我经常想知道是否有什么东西可以扔过河。
人类扔物体最远的距离是 Erin Hemmings 扔飞盘,距离为 1333 英尺,超过四分之一英里(0.4 公里)。在这个例子中,我们将看看从波特兰市中心西岸到威拉米特河东岸的距离是否小于 Hemmings 的投掷距离。
自然地,我们想在地图上可视化这个例子,所以我们向我们的基本地图的 JavaScript 部分添加以下代码,替换掉任何已有的 JavaScript:
var mapstraction;
var dist;
var wportland, eportland;
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.addSmallControls();
// Declare points for each side of the river
wportland = new mxn.LatLonPoint(45.52822, −122.67195);
eportland = new mxn.LatLonPoint(45.52933, −122.66957);
❶ dist = eportland.distance(wportland); // Calculate distance
// Show points on the map
mapstraction.addMarker(new mxn.Marker(wportland));
mapstraction.addMarker(new mxn.Marker(eportland));
❷ mapstraction.addPolylineWithData(new mxn.Polyline([wportland, eportland]));
mapstraction.autoCenterAndZoom();
var disttext = document.createTextNode("Distance is " + dist + " km.");
❸ mapstraction.currentElement.parentNode.appendChild(disttext);
}
确保在页面加载时调用create_map函数,并且你有一个div标签,其id设置为mymap,就像你在基本地图中做的那样。在函数内部,除了创建 Mapstraction 地图外,这段代码还创建了两个点,一个在河的一侧,一个在另一侧。然后它计算这两个点之间的距离 ❶。
我们可以在这里停止,但让我们在地图上添加一些视觉元素,使其更有意义。让我们为两个点中的每一个添加一个标记。然后,为了使事情更加清晰,让我们在这两点之间画一条线 ❷。你可以看到这个地图在图 6-1 中的样子。
在地图下方,我们向用户展示我们计算出的距离。我们通过在地图div下方添加一个新的文本节点来实现这一点 ❸。这个文本将显示距离,为 0.22 公里。与记录进行比较,你可以看到,如果你的名字是 Erin Hemmings,你确实可以扔一个物体过威拉米特河!

图 6-1. 两点及其之间的距离
#37: 使用路线查找真实距离
确定两点之间的距离是搜索的关键。这就是为什么 Mapstraction 让你可以访问之前项目中描述的距离函数。然而,这个函数只告诉你鸟飞的距离。我还没有见过任何乌鸦使用地图。
要确定两点之间的驾驶距离,你需要大量的信息。你需要有一个存储交叉点和街道是单向还是双向的地图底层数据。然后你需要一个算法来确定最佳路线。自己创建这些会是一项繁琐的工作,所以在这个例子中,你将利用 Google Maps API 的驾驶路线服务。
在这个例子中,你仍然会使用 Mapstraction,但你将依赖 Google 来计算距离。这意味着你需要加载 Google API,因此你可能会使用 Google 作为你的地图服务提供商。然而,你也可以加载第二个提供商,并在其地图上显示 Google 的路线结果。
你需要理解这里发生的事情。与依赖于公式的简单点间距离计算不同,这里你需要将点发送到 Google 并等待回复。由于结果等待时间,不建议对多个点执行此操作。
让我们来看看路线代码。在这个例子中,我将使用标准距离计算中的两个点,并比较结果。将以下代码添加到你的基本地图的 JavaScript 部分,替换任何其他代码:
var mapstraction;
var gdir;
var dist, ddist;
var wportland, eportland;
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.addSmallControls();
// Declare points for each side of the river
wportland = new mxn.LatLonPoint(45.52822, −122.67195);
eportland = new mxn.LatLonPoint(45.52933, −122.66957);
dist = eportland.distance(wportland); // Calculate distance
mapstraction.addPolylineWithData(new mxn.Polyline([wportland, eportland]));
// Google-specific code for driving directions
❶ gdir = new google.maps.DirectionsService();
❷ var diropt = {
origin: wportland.toProprietary(mapstraction.api),
destination: eportland.toProprietary(mapstraction.api),
travelMode: google.maps.DirectionsTravelMode.DRIVING
};
gdir.route(diropt, ❸setDDist);
}
function setDDist() {
if (status == google.maps.DirectionsStatus.OK) {
var directionsDisplay = new google.maps.DirectionsRenderer(
{map: mapstraction.getMap()});
❹ directionsDisplay.setDirections(response);
❺ ddist = response.routes[0].legs[0].distance.value / 1000;
// driving distance in km
var disttext = document.createTextNode("Normal distance is " + dist +
" km, but driving distance is " + ddist + " km");
❻ mapstraction.currentElement.parentNode.appendChild(disttext);
}
}
function handleErrors(){
// Handle errors in this section
}
由于大部分设置与之前距离项目中的地图相似,让我们先讨论一下 Google 特定的代码。由于 Google API 已经加载,我们可以创建一个 DirectionsService 对象 ❶。尽管我们通常通过 Mapstraction 与 Google 交流,但在这里我们直接与 Google 通信。
一旦我们创建了 DirectionsService 对象,我们就可以用它做些事情。我们首先准备选项 ❷,例如我们的起点和终点,用于我们的路线搜索。由于我们的点是为 Mapstraction 创建的,我们需要将它们转换为 Google 的专有格式。然后我们将这些选项连同回调函数 ❸ 一起发送给 Google,以接收结果。
当路线加载完成后,Google 会调用我们的 setDDist 函数。我们将驾驶路线添加到地图上作为一个专有的 Google 多段线 ❹,这将帮助我们直观地比较两种距离方法。然后我们可以获取这些路线的驾驶距离(以米为单位)❺。要将公里转换为米,只需除以 1000。
最后,我们将在地图 div ❻ 下方添加一个新的文本节点,它将向用户传达距离信息。

图 6-2. 与 Haversine 距离相比的驾驶距离
如图 6-2 所示,你必须驾驶的距离比懒惰的乌鸦飞行的距离要远得多。鉴于两点之间的最短距离是直线,路由结果总是会更远。在这种情况下,因为只有这么多桥梁跨越威拉米特河,这种影响被放大了,至少直到谷歌在其 API 中包含游泳路线。
#38:创建驾驶路线
地图网站最有用的功能可能一直是它们的驾驶路线。驾驶路线背后的路由技术比大多数开发者能够承担的更先进,但谷歌通过其 API 提供了驾驶路线的访问权限。在本节中,我将创建一个方向小部件,可以帮助任何企业向客户展示通往商店的路。在这种情况下,我将使用 La Bonita,一家墨西哥餐厅,我在那里写下了这本书的大部分内容。
这个示例严重依赖于 Google Maps API,所以这个项目是我不会使用 Mapstraction 的少数项目之一。首先,让我们向一个新文件添加一些基本的 HTML:
<html >
<head>
<title>Driving Directions with Google Maps</title>
<script type="text/javascript" src="http://maps.google.
com/maps/api/js?sensor=false"></script>
<style type="text/css">
div#mymap {
width: 400px;
height: 350px;
}
div#mydir {
width: 400px;
}
</style>
<script type="text/javascript">
❶var myaddress = "2839 NE Alberta St, Portland, OR";
// Google Maps Driving Directions Code Will Go Here
</script>
</head>
<body ❷onload="create_map()" onunload="GUnload()">
<h1>Venido a La Bonita</h1>
<div id="mymap"></div>
❸<div id="mydir"></div>
</body>
</html>
这段代码主要用于一个基本的、几乎空白的 HTML 页面。我们将用表单来接受用户输入,并用 JavaScript 请求谷歌的驾驶路线。首先,让我们看看几个新元素,我会解释它们的意义。
在 JavaScript 部分,我硬编码了 La Bonita 的地址❶。你可以用你的商业地址或派对的地点替换它。然后,客户或客人将稍后输入他们自己的地址。这两个地址将共同构成驾驶路线的起点和终点。
当页面加载时,我们调用create_map函数❷,就像我们在大多数 Mapstraction 示例中所做的那样。这是因为我想了这个函数;我可以叫任何名字,因为它与 Mapstraction 无关。
在 HTML 的下方,我包含了一个第二个div❸。这个标签将包含驾驶方向文本。这个第二个div在地图div之后,所以方向将列在地图下方。这个驾驶方向服务的部分是可选的(事实上,我在#37:使用路由找到真实距离中省略了它),但文本对这个示例很重要。
现在我们已经组装好了 HTML 页面的外壳,让我们添加使这个地图工作的组件。在地图div上方的空白区域(以及<h1>标签下方),包括以下表单:
<form onSubmit="❹loadDir();return false;">
Address: <input type="text" name="addr" />
City: <input type="text" name="city" value="Portland" />
❺<select name="state">
<option>OR</option>
<option>WA</option>
</select>
<input type="submit" value="Go" />
</form>
大多数表单都会将数据发送到服务器,但在这个示例中,我们想使用 JavaScript。这意味着当用户提交表单时,我们需要调用一个 JavaScript 函数❹。然后我们需要返回 false 以防止浏览器将数据发送到服务器。
此代码请求用户的位置信息分为三个部分:地址、城市和州。如果您愿意,可以使用一个或两个字段,但将它们分开成单独的字段有助于使您的格式期望更清晰。另一方面,准备调用驾驶路线服务将需要更多的工作。
州以下拉菜单的形式显示❺。在我的示例中,我只包括了俄勒冈州和华盛顿州。La Bonita 的食物很好,但没有人会为了它而走很远的路。您可以包括客户最有可能居住的州。
现在,我们将使用 JavaScript 将所有这些连接起来。至少,我们需要创建我们在 HTML 中已经引用的两个函数:create_map将使 Google 地图准备就绪,而loadDir将发送地址到 Google 的驾驶路线服务。
将以下内容添加到 La Bonita 地址下的 JavaScript 部分:
❶ var map, gdir;
function create_map() {
var opt = {center: new google.maps.LatLng(45.559192, −122.636049), zoom: 15,
mapTypeId: google.maps.MapTypeId.ROADMAP};
map = new google.maps.Map(document.getElementById("mymap"), opt);
❷ gdir = new google.maps.DirectionsService();
}
function loadDir() {
var stateobj = document.getElementById('state');
❸ var fromaddress = document.getElementById('addr').value + " "
+ document.getElementById('city').value + ", "
+ stateobj.options[stateobj.selectedIndex].value;
❹ var diropt = {
origin: fromaddress, destination: myaddress,
travelMode: google.maps.DirectionsTravelMode.DRIVING
}
gdir.route(diropt, ❺setDir);
}
function setDir(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
var directionsDisplay = new google.maps.DirectionsRenderer(
{map: map, panel: document.getElementById('mydir')});
directionsDisplay.setDirections(response);
}
}
首先,我们通过在函数外部声明gdir变量使其全局❶。这样,变量就可以在代码的任何地方被引用。在创建地图后,我们还需要初始化gdir变量❷,这样 Google 就知道我们将要求驾驶路线。
我们几乎准备好让用户与我们的表单进行交互了。让我们看看当表单提交时被调用的函数loadDir。首先,该函数将地址与城市/州组合在一起❸。您可能更喜欢检查这些字段中的空或格式不正确的内容,但这个简单的示例只是将它们连接在一起。
接下来,我们设置选项❹,包括两个地址,告诉 Google 要查找哪些路线。最后,该函数将选项连同回调函数❺一起发送给 Google。在setDir函数中,该函数接收驾驶路线,我们只是告诉 Google 在地图上以及地图下方显示路线,如图图 6-3 所示。
就这样。我们已经为任何人前往 La Bonita 创建了驾驶路线。要将其用于您的业务,只需更改myaddress变量并填写适当的州。要查看一个更深入探讨驾驶路线的示例,请参阅#73: 在中间找到一家咖啡店见面。
查看 Google 驾驶路线服务的完整文档:code.google.com/apis/maps/documentation/javascript/reference.html#DirectionsService
#39: 确定最近的标记
给定一个点和一大堆标记,你能找到离你的点最近的那个吗?在这个项目中,我们将遍历地图上的每个标记,并在用户点击的位置和最近的标记之间画一条线,我们将通过计算两点之间的距离来确定这个最近的标记。
然而,在我们找到最近的标记之前,我们需要一个带有一些标记的地图。为此,我们将获取五个随机点,如本章前面所示。在这里,我重新打印了您需要从该部分获取的get_random_by_bounds函数。将这些行添加到您基本地图的 JavaScript 部分顶部,但请确保它们在create_map函数外部:
function get_random_by_bounds(bounds) {
var lat = bounds.sw.lat + (Math.random() * (bounds.ne.lat − bounds.sw.lat));
var lon = bounds.sw.lon + (Math.random()
* (bounds.ne.lon − bounds.sw.lon));
return new mxn.LatLonPoint(lat, lon);
}

图 6-3. 来自 Google 的驾驶方向
在有了这个辅助函数之后,将这些行添加到基本地图的create_map函数中:
❶ var bounds = new mxn.BoundingBox(32.4, −113.9, 40.9, −103.0);
❷ for (i=1; i<=5; i++) {
var marker = new mxn.Marker(❸get_random_by_bounds(bounds));
mapstraction.addMarker(marker);
}
❹ mapstraction.setBounds(bounds);
mapstraction.click.addHandler(❺find_closest_marker);
假设您已经初始化了地图,您几乎可以开始创建标记了。首先,您需要创建标记的边界,这代表您将用于创建随机位置的面积。在这种情况下,我使用了一些点❶,这些点大致定义了美国“四个角落”的州:亚利桑那州、犹他州、科罗拉多州和新墨西哥州。
现在,我们可以创建一个循环❷来执行相同的代码五次。每次循环,我们都会得到一个新的随机点❸,因此我们的标记可以在边界内任何地方。
通常,我会要求 Mapstraction 在使用随机标记时自动居中和缩放。然而,在这里,我设置了边界为四个州的组合❹,这个区域可能包含一个标记。这样,我知道所有的标记都将可见。
最后,我们等待用户点击地图。点击后,我们告诉 Mapstraction 调用一个函数来找到最近的标记❺。现在我们需要编写这个函数。将这些行添加到您的 JavaScript 中,在create_map函数外部:
function find_closest_marker(event_type, event_source, event_args) {
if (mapstraction.markers.length > 0) {
var clickpoint = event_args.location;
❻ var closest_marker = mapstraction.markers[0];
❼ var closest_dist = clickpoint.distance(closest_marker.location);
❽ for (var i=1; i < mapstraction.markers.length; i++) {
var thismarker = mapstraction.markers[i];
var thisdist = clickpoint.distance(thismarker.location);
❾ if (thisdist < closest_dist) {
closest_dist = thisdist;
closest_marker = thismarker;
}
}
if (closest_marker) {
❿ var poly = new mxn.Polyline([clickpoint, closest_marker.location]);
mapstraction.addPolyline(poly);
}
}
}
为了找到用户点击的点最近的标记,我们需要检查每个标记和点击点之间的距离。在搜索过程中,我们需要保留两份数据:我们找到的当前最近标记及其到点的距离。
首先,我们创建这两个变量,并假设第一个标记(记住 JavaScript 数组索引从零开始)目前是最接近的❻。因此,我们知道要比较的距离,我们还计算了第一个标记到点的距离❽。现在我们准备好遍历所有其他标记❽,从第二个开始。
每次循环中,我们计算当前标记和点击点之间的距离。如果距离比我们迄今为止找到的最近距离更远,我们就不做任何事情。如果这个当前标记现在比之前的❾个更近,我们就用新值替换原始的两个变量。
循环结束后,我在用户点击的位置和我们所确定的最近的标记之间创建了一条新的折线标记❿。将此示例加载到网页浏览器中并点击几次。如果你移动得足够多,你会创建几条线,连接多个标记(参见图 6-4)。哪个标记离阿尔伯克基最近?丹佛呢?

图 6-4. 每次点击都连接到最近的标记。
#40:在一条线上找到一个点
假设你和你那位健忘的飞行员朋友从堪萨斯州的威奇托飞往俄克拉荷马州的塔尔萨,大约 140 英里。飞机起飞后,他漫不经心地说:“哎呀,我忘记加油了。”我知道你在想什么——掉头!但你的朋友向你保证,油足够你开 80 英里,应该能让你到达俄克拉荷马州,那里的油价更便宜。
知道两点之间的距离是有用的,但有时你想要知道这两点之间的故事。例如,两个城市之间的中点在哪里?或者,给定点 A 和点 B,沿着这条线九英里处的点 C 的坐标是什么?这是一个数学问题,但解决它会带来一些有趣的可能。
如果你真的是从威奇托飞往塔尔萨的几乎空油箱上,那么“乐趣”可能不是一个合适的词。但当你发动机开始噼啪作响时,你肯定想知道沿着那条路线 80 英里后你会在哪里。你最好再检查一下:你会到达你的朋友所说的俄克拉荷马州吗?毕竟,这个信息是从那个忘记给飞机加油的家伙那里来的。
为了找出这个问题的答案,我们将使用三个步骤的过程:
-
在地图上标出我们的起始点和终点。
-
使用两个点计算我们的航向(方向)。
-
使用航向和起始点找到一个 80 英里外的新点。
让我们在还有时间掉头之前开始。
绘制你的路线
由于飞机直线飞行,我们将使用一个简单的折线,类似于在第十六章:在地图上绘制线条中的初始示例。我们只需要两个机场的起始点和终点。
要在威奇托和塔尔萨之间画一条线,请将以下函数添加到你的基本地图中,替换你当前的create_map函数:
var mapstraction;
var wichita = new mxn.LatLonPoint(37.7454463, −97.4080747);
var tulsa = new mxn.LatLonPoint(36.0390101, −95.9936344);
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.setCenterAndZoom(wichita, 8);
mapstraction.addPolyline(new mxn.Polyline([wichita, tulsa]));
mapstraction.autoCenterAndZoom();
// Find bearing
// Find point X km along route
}
保存文件并加载它。你的地图应该看起来类似于图 6-5。
你认为你能到达俄克拉荷马州吗?油箱里的 80 英里油可以让你走完路线的大约 60%。这将非常接近。让我们继续并找出答案。

图 6-5. Wichita 和 Tulsa 之间的直接路线
找到你的方位角
为了找到直线上的一个点,你首先需要知道直线的指向。这个方向被称为方位角,它是一个用度数表示的数字,范围从 0 到 359。大多数指南针在周围标记这些度数,以及四个基本方向。
在本节中,我们将编写一个函数来计算方位角,这个函数基于 Movable Type Ltd. 的 Chris Veness 的工作。将以下代码添加到你的地图文件的 JavaScript 部分,但不要放在 create_map 函数内部:
function get_bearing(pt1, pt2) {
var lat1 = degrees_to_radians(pt1.lat);
var lat2 = degrees_to_radians(pt2.lat);
var lon_diff = degrees_to_radians(pt2.lon − pt1.lon);
var y = Math.sin(lon_diff) * Math.cos(lat2);
var x = Math.cos(lat1) * Math.sin(lat2)
- Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon_diff);
var bearing = Math.atan2(y, x);
return (radians_to_degrees(bearing)+360) % 360;
}
function degrees_to_radians(deg) {
return deg * Math.PI / 180;
}
function radians_to_degrees(rad) {
return rad * 180 / Math.PI;
}
除了计算方位角的函数外,我还包括了一些辅助函数,用于在度数和弧度之间进行转换。当我们使用弧度时,我们使用的三角学更简单(没错——数学可能更复杂)。然而,我们还需要转换回来,因为 Mapstraction 期望的是度数。
现在,在你的 create_map 函数内部,添加以下这一行:
var bearing = get_bearing(wichita, tulsa);
在这里,我们调用 get_bearing 函数,传递我们的两个点。结果应该是大约 146 度。注意,如果你交换函数参数的顺序,结果将不同:大约 326 度。这是因为从 Tulsa 到 Wichita 的旅行方向不同。由于它是完全相反的方向,这两个结果相差 180 度。
我们是从 Wichita 到 Tulsa,所以我们将使用这个结果在下一节中。
确定新点
现在我们知道了我们的行进方向。这是找到我们路线上的 80 英里处的点的关键步骤。是时候使用一些更复杂的数学,结合方位角和我们的起点 Wichita 的坐标了。
在本节中,我们将编写另一个函数,同样基于 Chris Veness 的工作。将以下代码添加到你的 JavaScript 中,注意不要将其放在任何其他函数内部:
function get_destination(pt, dist, bearing) {
var R = 6371; // radius of earth (km)
var lat1 = degrees_to_radians(pt.lat);
var lon1 = degrees_to_radians(pt.lon);
bearing = degrees_to_radians(bearing);
var cosLat1 = Math.cos(lat1);
var sinLat1 = Math.sin(lat1);
var distOverR = dist / R;
var cosDistOverR = Math.cos(distOverR);
var sinDistOverR = Math.sin(distOverR);
var lat2 = Math.asin( sinLat1 * cosDistOverR
+ cosLat1 * sinDistOverR * Math.cos(bearing) );
var lon2 = lon1 + Math.atan2( Math.sin(bearing) * sinDistOverR * cosLat1,
cosDistOverR Đ sinLat1 * Math.sin(lat2) );
lon2 = (lon2 + Math.PI) % (2 * Math.PI) Đ Math.PI;
lat2 = radians_to_degrees(lat2);
lon2 = radians_to_degrees(lon2);
return new mxn.LatLonPoint(lat2, lon2);
}
现在我们需要调用我们刚刚创建的函数。在 create_map 函数中,在计算方位角的行之后添加以下代码:
var newpt = get_destination(wichita, 128, bearing);
var mk = new mxn.Marker(newpt);
mapstraction.addMarker(mk);
注意,我们传递给新函数的距离是 128,而不是 80。这是因为该函数期望距离以 千米 为单位,而不是英里。这与 Mapstraction 计算距离的方式相匹配。要将英里转换为千米,将英里数乘以 1.6。
在加载你的更改之前深吸一口气。除了确定路径上的 80 英里处的点之外,代码还会创建并添加一个标记到地图上,如图 图 6-6 所示。

图 6-6. 从 Wichita 出发 80 英里就进入俄克拉荷马州!
你可以再深吸一口气,因为看起来 80 英里的油量可以让你从威奇托开到俄克拉荷马州,根据你健忘的飞行员朋友的说法,在那里你可以以更低的价格加油。至于找到着陆点,那是另一个问题。
现在你已经知道了如何找到线上的一个点,尝试一个不那么假设性的项目。#73: 在中间找到一个咖啡店见面 在 #73: 在中间找到一个咖啡店见面 中将你刚刚完成的项目与行车路线和本地搜索结果结合起来。
注意
你找到的点将只会在短距离内直接位于线上,就像这个例子中的 140 英里行程一样。对于更长的距离,点将是正确的,但线将是错误的。标准的多段线不考虑地球的曲率。为了得到两点之间“大圆”最短距离的线,你需要使用大地测量多段线,这是谷歌支持的。
#41: 在地图上绘制本地结果
当你在附近搜索时,迟早你会想要找到符合某些条件的企业。例如,在波特兰,我们总是在寻找咖啡。在这次示例发生的旧金山,这意味着要找到美味且价格便宜的墨西哥卷饼。
在这个项目中,我们将使用 JavaScript 来执行针对关键词 burritos 的本地搜索。谷歌和雅虎都提供了允许这种搜索的 API。在这个例子中,我们将使用雅虎,因为它简单直接。
我们从基本的 Mapstraction 地图开始,因此我们最小化了特定提供者的调用次数。因为我的大多数示例都使用了谷歌作为提供者,请确保你正在调用雅虎地图 API:
<script type="text/javascript"
src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=*`yourkeyhere`*"></script>
当外部 JavaScript 加载完成后,将任何现有的内联 JavaScript 代码替换为以下内容:
var mapstraction;
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'yahoo');
mapstraction.setCenterAndZoom(new mxn.LatLonPoint(37.7740486,-122.4101883), 15);
mapstraction.addLargeControls();
// Yahoo-specific calls
❶ var ymap = mapstraction.getMap();
YEvent.Capture(ymap, EventsList.onEndLocalSearch, ❷plotResults);
❸ ymap.searchLocal(ymap.getCenterLatLon(), 'burritos', ❹1, ❺5);
}
为了能够使用雅虎特定的调用,我们需要获取地图对象 ❶。然后我们需要让地图知道我们对一个事件感兴趣,这个事件将在我们的搜索完成后发生。我们还引用了一个可以发送结果的函数 ❷。我们稍后会创建这个函数。
现在,我们需要编写实际启动对雅虎进行本地搜索的代码 ❸。searchLocal 函数需要传递四个值:我们发送地图的中心、搜索查询、搜索半径(以英里为单位) ❹ 和结果数量 ❺。
然后我们编写 plotResults 函数。在 create_map 函数下方添加以下行:
function plotResults(❶results) {
if (results.Data) {
var places = results.Data.ITEMS;
❷ for (i=0; i < places.length; i++) {
var thisplace = places[i];
var lat = ❸parseFloat(thisplace.LATITUDE);
var lon = parseFloat(thisplace.LONGITUDE);
var marker = new mxn.Marker(new mxn.LatLonPoint(lat, lon));
marker.setInfoBubble(thisplace.TITLE + '<br />' + thisplace.ADDRESS);
mapstraction.addMarker(marker);
}
}
❹ mapstraction.autoCenterAndZoom();
}
搜索结果作为参数 ❶ 从雅虎传递到我们的函数。该参数是一个特殊对象,其中包含我们找到的每个企业的许多信息。我们想要从对象中提取纬度、经度、地址和商家名称,我们通过遍历 ❷ 结果来实现这一点。
在循环的每次迭代中,我们访问我们需要的四个商业信息。为了获取可用的经纬度值,我们需要使用 JavaScript 辅助函数 parseFloat ❸。这个函数将文本值转换为坐标所需的浮点数。
其余的现在可能对你来说已经很熟悉了。我们创建一个标记,在消息框中输入文本(地点的名称和地址),当我们添加了所有标记后,我们确保它们都可以在地图上显示出来❹,如图 图 6-7 所示。
#42: 使用 HTTP 获取本地结果
在许多情况下,你可能需要使用比 JavaScript 更强大的工具来执行本地搜索。例如,你可能希望将结果存储在数据库中或将它们输出到 RSS 源中。在任一情况下,之前项目中使用的方法都行不通。相反,我们将使用 Yahoo! 的本地搜索 API,并通过 PHP 访问它,这是一种在许多网络主机上可用的流行服务器端编程语言。

图 6-7. Yahoo! 地图上的本地结果
在我们将结果带入代码之前,让我们看看它们看起来像什么。我们使用的 API 中的一个优点是它输出纯文本 XML,这可以被网络浏览器解释,并且是可读的。
注意
本项目中的某些概念可能被认为是高级的。它们建立在 第八章 和 第九章 中介绍的概念之上。
就像访问普通网页一样,你可以通过访问一个 URL 来访问本地搜索 API。尝试在地址栏中输入以下搜索:在旧金山搜索玉米卷:
http://local.yahooapis.com/LocalSearchService/V3/localSearch?`appid`=
*`yourkeyhere`*&`query`=burritos&`location`=San+Francisco+CA
请求参数以粗体显示。第一个,你的 appid,是你的 Yahoo! API 密钥。query 是你正在搜索的内容,而 location 是你正在搜索的位置。
一旦结果被加载,你会看到类似以下的内容:
❶ <?xml version="1.0"?>
❷ <ResultSet ... totalResultsAvailable="553" totalResultsReturned="10">
❸ <Result id="21356805">
<Title>El Farolito</Title>
<Address>2777 Mission St</Address>
<City>San Francisco</City>
<State>CA</State>
<Latitude>37.752713</Latitude>
<Longitude>-122.41835</Longitude>
...
</Result>
<Result id="21342579">
...
</Result>
...
</ResultSet>
与大多数 XML 文档一样,结果在第一行声明自己是 XML ❶。第二行是文档的根元素 ❷,这意味着它包含其下所有其他标签。每个结果都存储在 <Result> 标签 ❸ 中,数据项存储在低于一级的标签中。
更多关于描述 XML 的术语,请参阅 #52: 使用 XML,其中还包含比我将在下一节中提供的更深入的 XML 解析描述。
使用 PHP 解析本地结果
在浏览器中查看 Yahoo! 返回的 XML 是一件事情。更有用的事情是将它读入 PHP,这允许你做更多的事情。在这个例子中,我们将获取与上一节相同的玉米卷结果,并打印出找到的第一家餐厅的名称。
创建一个新的 PHP 文件并添加以下行:
<?
$api_key = "*`yourkeyhere`*";
$search_term = urlencode("burritos");
$location = ❶"urlencode("San Francisco, CA");
$url = "http://local.yahooapis.com/LocalSearchService/V3/localSearch";
❷ $url .= "?appid=$api_key&query=$search_term&location=$location";
❸ $xobj = get_xml($url);
print $xobj->Result[0]->Title;
?>
我已经将这三个参数存储为 PHP 变量。这使得你可以轻松地包含自己的 API 密钥并更改搜索词。继续更改我们正在搜索的内容,或者包含你自己的城市。请注意,尽管我们正在硬编码搜索条件,但我已经使用了urlencode函数来确保 URL 的有效性。例如,位置的编码❶将替换空格和逗号,以使用这些字符的 URL 友好版本。
接下来,我将参数以 URL 形式组合在一起❷,这样我们就可以检索结果。实际下载网页的调用发生在另一个函数❸中,你可以在第六十一部分:检索网页中找到详细描述。
最后,PHP 代码打印出第一个结果的名字:示例结果中的 El Farolito。你可能更希望对结果做些更有趣的事情,而不仅仅是打印出第一个名字,但这给你提供了一个如何访问这些条目的想法。
其他有用参数
在上一个示例中,我展示了你可以使用 Yahoo!本地搜索 API 进行搜索的三个参数:appid(必需)、query(大多数搜索必需)和location。
有更多选项可用,其中最有趣的我已包括在表 6-1 中。
表 6-1. Yahoo!本地搜索接受的附加参数
| 参数名称 | 描述 |
|---|---|
city、state和zip |
这三个中的任何一个都可以单独使用或组合使用,以替换示例中使用的自由格式location参数。 |
latitude和longitude |
如果这两个都使用,它们将取代任何其他位置数据,并将搜索中心设置在由坐标创建的点。 |
radius |
设置搜索位置的最大距离(以英里为单位)。 |
results |
声明结果数量,从 1 到 20。默认值是 10。 |
sort |
指示结果将如何排序,使用以下四个选项之一:distance(距离)、rating(评分)、relevance(相关性)或title(标题)。默认值是relevance。 |
Yahoo!的文档中列出了更多参数^([2]),但通过这里提供的示例,你应该能够得到一些有趣的结果。你最近的动物标本制作师在哪里?
^([2]) developer.yahoo.com/search/local/V3/localSearch.html
#43:检查点是否在边界框内
在地图绘制中,矩形是最重要的形状之一。经过几个世纪,我们仍然以矩形形状查看地图,即使是在今天的 JavaScript 中也是如此。矩形也可以很容易地描述(你只需要两个点),判断一个点是否在特定的矩形内不需要复杂的数学计算。
正如你将在本项目的后续部分看到的那样,检查一个点是否在BoundingBox内是第一步。你也可以用它来确定,例如,所有标记是否都在屏幕上。
由于判断一个点是否在矩形内是一个非常实用的功能,Mapstraction 将其作为一个函数内置。给定任何BoundingBox对象,你可以传递一个LatLonPoint并返回 true(在框内)或 false。
这里是确定一个点是否在地图视图中的代码:
var box = mapstraction.getBounds();
var inview = box.contains(new mxn.LatLonPoint(37.7740486, −122.4101883));
发生了什么?这并不复杂。一个BoundingBox由其西南角和东北角点确定(见图 6-8)。所以要在框内,你的纬度必须在这两个点的纬度之间。经度是相同的。

图 6-8. 一个BoundingBox通过其西南角和东北角点来声明。
这里是 Mapstraction 的BoundingBox contains函数的一个稍微修改过的版本:
function check_bounds(pt, box) {
return (pt.lat >= box.sw.lat && pt.lat <= box.ne.lat
&& pt.lon >= box.sw.lon && pt.lon <= box.ne.lon);
是的,数学相当直接,但每次都要编写很多代码。我很高兴 Mapstraction 为我们做了这件事。
你能在框内点击吗?
现在我们知道了如何检查一个点是否在框内,让我们试试。这个例子将创建一个比可视地图小的边界框。当用户点击时,我们检查他或她点击的点是否在我们的框内。
为了使事情更清晰,我们将在边界框周围绘制一个多边形,这样就可以很容易地判断 Mapstraction 是否返回了正确的结果。你认为你能在框内点击吗?
将以下代码添加到基本地图的 JavaScript 部分,替换任何现有的代码:
var mapstraction;
var box;
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.setCenterAndZoom(new mxn.LatLonPoint(37.7740486,-122.4101883), 9);
mapstraction.addSmallControls();
❶ box = new mxn.BoundingBox(37.5, −122.8, 37.9, −122.2);
❷ var poly = BoundingBox_to_Polyline(box);
mapstraction.addPolyline(poly);
❸ mapstraction.click.addHandler(function(event_type, event_source, event_args) {
var reply = "";
var clickpoint = event_args.location;
❹ if (box.contains(clickpoint)) {
reply = "You clicked inside the box! ";
}
else {
reply = "Sorry--You missed the box. ";
}
// Create marker at click
var mk = new mxn.Marker(clickpoint);
mk.setInfoBubble(reply);
mapstraction.addMarker(mk);
❺ mk.openBubble();
});
}
function BoundingBox_to_Polyline(box) {
var points = [box.sw, new mxn.LatLonPoint(box.ne.lat, box.sw.lon), box.ne,
new mxn.LatLonPoint(box.sw.lat, box.ne.lon),
new mxn.LatLonPoint(box.sw.lat, box.sw.lon-.0001)];
var poly = new mxn.Polyline(points);
return poly;
}
在创建地图后,我们在旧金山周围创建了一个相当随意的边界框❶。然后我们用同样的框并沿着其边缘绘制使其在地图上可见❷。我们使用BoundingBox_to_Polyline函数,我在第十九章:绘制矩形声明区域中详细解释了它。为了方便,我在这里重新打印了它。
接下来我们需要监听地图上的点击❸。当用户点击时,我们需要查看点击的点是否在框内。为此,我们在我们创建的BoundingBox对象上调用contains函数❹。结果(true 或 false)将决定我们向用户显示什么信息。
为了将结果传达给用户,我们在用户点击的位置添加一个新的标记,并打开其消息框❺以显示点击是否在边界内。试几次。点击内部,点击外部——它每次都能正确识别!一个示例结果如图图 6-9 所示。

图 6-9. 点击边界框内的点
#44: 在边界框内获取一个随机点
快!想一个介于-122.9 和-122.8 之间的数字!虽然这不是一个普通的问题,但你可能会在创建地图时问自己。特别是对于测试,你希望能够生成随机地理点,通常在特定区域内。从某种意义上说,在矩形区域内找到一个随机点与上一个项目正好相反。
我创建了一个专门用于创建随机点的特殊函数。你可能甚至在其他部分这本书中见过它。在本节中,我将简要描述它,然后在一个示例中使用它。
下面是获取随机点的代码:
function get_random_by_bounds(❶bounds) {
var lat = bounds.sw.lat + (❷Math.random() * ❸(bounds.ne.lat − bounds.sw.lat));
var lon = bounds.sw.lon + (Math.random() * (bounds.ne.lon − bounds.sw.lon));
❸ return new mxn.LatLonPoint(lat, lon);
}
这个函数最需要的信息是知道你想要随机点的大致区域,即边界。这个信息作为该函数的单个参数❶传递,是一个 Mapstraction BoundingBox对象。BoundingBox对象由矩形区域的西南(SW)和东北(NE)角组成。在这两点之间,你可以确定边的最大和最小值。
现在,我们知道点将位于何处,但我们仍然需要使用内置的 JavaScript 函数❷来使点变得随机。Math.random返回的数字是一个介于 0 和 1 之间的十进制数,这很可能不是你想要的。然而,我们可以使用这个数字,乘以我们的范围❸,来确定随机坐标。
例如,要获取随机纬度,我们取东北纬度减去西南纬度,然后将这个答案(两个纬度之间以度为单位距离)乘以随机数。然后,我们将西南纬度(两者中最小的一个)加到结果上。因此,我们的最小纬度(当随机数为零时)将与西南纬度相同;最大纬度(当随机数为一时)将与东北纬度相同。
然后将相同的流程应用于经度,但使用一个新的随机数。现在我们有了纬度和经度,我们可以将这两个数字作为新的LatLonPoint❹返回。
下面是一个使用随机点的示例。确保你的 JavaScript 代码中有get_random_by_bounds函数,然后向基本地图的create_map函数中添加以下行:
var b = new mxn.BoundingBox(❶45.5, −122.9, 45.6, −122.8);
❷ var pt = get_random_by_bounds(b);
var mk = new mxn.Marker(pt);
mapstraction.addMarker(mk);
我们需要一些边界才能传递给随机点函数。在这种情况下,我虚构了一些点 ❶,大致位于我的家乡俄勒冈州波特兰市。在创建一个新的BoundingBox时,我们必须按以下顺序传递四个数字:SW 纬度,SW 经度,NE 纬度,和 NE 经度。
接下来,我们通过传递我们刚刚创建的边界来获取随机点 ❷。记住我们创建的函数使用了return来共享新的随机点。当我们调用该函数时,我们可以声明一个变量(我称之为pt)来存储返回的值。
为了显示随机点,我使用它创建了一个新的标记,并将其放置在地图上。如果你想直观地检查这个点是否真的在你的边界内,尝试将第十九部分:绘制一个矩形来声明一个区域与这个一起使用。
#45: 检查一个点是否在形状内
用户刚刚点击了堪萨斯州吗?这个地址是否在城市的界限内?这些问题是你希望用碰撞测试来回答的常见问题,碰撞测试是用来确定一个点是否在形状内的过程。要做到这一点,需要一些数据(形状的轮廓)和一些数学知识。在本节中,我将展示如何处理坐标并找到这些以及其他问题的答案。
首先,数据。一个形状可以描述为一系列纬度和经度点,其中起点和终点是同一个点,围成一个多边形。你可能可以通过绘制边界来创建你想要的形状。你很可能会在网上找到分享他们形状的人。例如,所有 50 个美国州的形状可以在mapscripting.com/state-boundaries找到。
我们将执行一个碰撞测试来查看用户点击的点是否在某个州内。鉴于堪萨斯州形状较为规则,让我们选择一个形状稍微复杂一些的州,比如犹他州(见图 6-10)。犹他州的边缘由六个点组成,这意味着描述犹他州作为一条线需要七个点(因为最后一个点需要与第一个点重新连接)。
这里有一些 Mapstraction 代码,用来描述犹他州的轮廓作为一系列坐标:
utah = [new mxn.LatLonPoint(36.99, −114.05), new mxn.LatLonPoint(36.99, −109.04),
new mxn.LatLonPoint(40.99, −109.05), new mxn.LatLonPoint(40.99, −111.05),
new mxn.LatLonPoint(41.99, −111.05), new mxn.LatLonPoint(41.99, −114.04),
new mxn.LatLonPoint(36.99, −114.05)];
现在我们想编写一个函数,用来确定特定点是否在由这些点创建的多边形内。我们的点可能位于地球上的任何地方。鉴于犹他州是一个相对较小的区域,我们的点很可能不在该州内。让我们快速排除这一点——在进入高级数学之前。

图 6-10. 犹他州手柄部分的状态非常适合进行碰撞测试。
找到多边形的边界框
确定一个点不在多边形内部的最简单方法就是证明这个点位于多边形边界框的外部。为了确定多边形的矩形边界,我们必须查看每个点,这样我们就可以找到纬度和经度的最小值和最大值。
一旦我们有了这些值,我们就会知道如何创建边界框。一个例子在图 6-11 中展示。
让我们编写一个函数来从一系列LatLonPoints创建一个新的Mapstraction BoundingBox。以下是整个函数的代码:
function points_to_bounds(pts) {
if (pts.length > 0) { var minlat = pts[0].lat
var maxlat = minlat;
var minlon = pts[0].lon
var maxlon = minlon;
❶ for (var i = 1; i < pts.length; i++) {
var pt = pts[i];
❷ if (pt.lat > maxlat) {
❸ maxlat = pt.lat;
}
if (pt.lon > maxlon) {
maxlon = pt.lon;
}
if (pt.lat < minlat) {
minlat = pt.lat;
}
if (pt.lon < minlon) {
minlon = pt.lon;
}
}
❹ return new mxn.BoundingBox(minlat, minlon, maxlat, maxlon);
}
return null;
}

图 6-11. 一个多边形的矩形边界
我们想要确定四个值:最小的纬度,最大的纬度,最小的经度,以及最大的经度。因为我们必须从一个地方开始,所以我们首先假设第一个点既是最小值也是最大值。这样做只是为了让我们有东西可以比较。
然后,从第二个点开始(因为 JavaScript 数组索引从零开始,所以它的索引为 1),我们遍历所有其他点❶。每次循环中,我们检查是否找到了新的最小值或最大值。例如,如果当前点的纬度大于我们目前认为的最大值❷,那么我们需要将最大值设置为这个值❸。
一旦我们完成循环,四个值将正确无误。这些值代表BoundingBox的四个角。西南角由最小值组成,东北角由最大值组成。我们可以创建BoundingBox并将其返回以供其他地方使用❹。
现在我们有了边界,我们可以通过围绕我们的多边形(简单矩形)来检查一个点是否在多边形内部。我之前已经详细介绍了这个方法,我将在接下来的几个部分再次演示,当我们执行完整的碰撞测试时。
将我们的点连接到外部点
好的,如果我们已经走到这一步,我们就已经确定我们的点在多边形边界框内。这并不意味着这个点在多边形内部,但至少它附近。在我们的犹他州例子中,我们的点位于边界框的非常东北角,但不在犹他州多边形内部。
现在开始有点棘手了,但在这个小把戏中,你会发现简单性。我们需要为测试制作一条临时线。这条线连接我们的点(可能位于多边形内部)到一个我们可以保证在多边形外部的点。
当我们绘制临时线时,它可能会与组成我们的多边形的线段相交。如果线条与多边形相交奇数次,则我们的点在内部。如果它以偶数次或根本不相交,则我们的点在外部。图 6-12 提供了犹他州示例的视觉表示。

图 6-12. 仅表示犹他州的多边形只与形状相交一次——该点位于形状内部。
只有当我们能保证我们创建的新点将位于多边形外部时,碰撞测试才是确定的。我们如何做到这一点?我们将为新点创建一个纬度,该纬度小于西南纬度。
如果我们要测试的点称为mypt,我们的BoundingBox称为box,以下是创建我们的测试点的代码:
var lat_change = (❶(box.ne.lat - box.sw.lat) / 100);
var pt2 = new mxn.LatLonPoint(box.sw.lat - lat_change, mypt.lon);
我们不需要移动到边界框外面很远。为了确定纬度需要改变多少,我取了西南和东北之间的度数差❶,然后除以 100。在犹他州的情况下,我们的新点将位于南部边界下方 0.05 度。南北纬度之间的差异越小,新点就越接近边界框——但该点始终位于框外。
对于新点的经度,我将其设置为与我们要检查的点的经度相同。这个决定是随意的,因为任何经度都会创建一个位于边界框外的点,因为纬度小于框的最小纬度。
检查线段交点
现在我们有一个边界框和一个位于框外的点,我们可以将这个点与我们要测试的点连接起来。最后一步是确定连接这两个点的线与多边形相交的次数。为此,我们需要知道如何检查两条线是否相交。
由于只有两个线段,但变量很多,代码变得有些混乱。我们有四个单独的点或八个不同的值。从两个到四个到八个。就像我说的,事情很快就会变得混乱。
加入一点向量数学(基于用 Visual Basic 编写的解决方案,可在www.vb-helper.com/howto_segments_intersect.html找到)后,代码开始变得复杂。以下是测试从点 A 到 B 的线段是否与从点 C 到 D 的另一个线段相交的 JavaScript 代码:
function check_intersection(A, B, C, D) {
❶ var latdiff1 = B.lat − A.lat;
var latdiff2 = D.lat − C.lat;
var londiff1 = B.lon − A.lon;
var londiff2 = D.lon − C.lon;
// Make sure lines aren't parallel
❷ if (londiff2 * latdiff1 - latdiff2 * londiff1 != 0) {
var segtest1 = (londiff1 * (C.lat - A.lat) + latdiff1 * (A.lon − C.lon))
/ ❸(londiff2 * latdiff1 - latdiff2 * londiff1);
var segtest2 = (londiff2 * (A.lat - C.lat) + latdiff2 * (C.lon − A.lon))
/ (latdiff2 * londiff1 - londiff2 * latdiff1);
❹ if (segtest1 >= 0 && segtest1 <= 1 && segtest2 >= 0 && segtest2 <= 1) {
return true;
}
}
return false;
}
代码首先计算每条线段的端点距离,例如点 A 和 B 的纬度之间的距离❶。这些值成为确定线是否相交的向量计算的基础。
仅使用纬度和经度距离,我们可以确保线不平行 ❷。确定这一点可以节省我们进一步的计算,因为平行线永远不会相交。更重要的是,我们不会在第一个线段测试中除以零 ❸。
segtest1 和 segtest2 变量比较线段 AB 和 CD,然后反过来。值决定了两条线的交点。因为它们不平行,它们将在某处相交。如果两个线段测试都在 0 和 1 之间 ❹,那么我们知道交点发生在我们的线段内。
执行碰撞测试
到目前为止,我们已经找到了多边形的边界框,从多边形外的点到我们关注的点画了一条线,并学会了如何确定两个线段是否相交。你是否觉得我们已经失去了找到点是否在多边形内的原始计划?好吧,那么让我们把所有这些都放在一起,执行碰撞测试。
记住,如果我们所创建的线与构成多边形的线相交的次数是奇数,那么我们的点就在多边形内。我们需要将每个线段与我们的线进行测试,我们知道至少开始在多边形外。
这里是函数,使用我们在本项目中早期组合的组件来确定点是否在多边形内:
function check_polygon(mypt, polypts) {
❶ var box = points_to_bounds(polypts);
❷ if (box.contains(mypt)) {
var lat_change = ((box.ne.lat - box.sw.lat) / 100);
❸ var pt2 = new mxn.LatLonPoint(box.sw.lat - lat_change, mypt.lon);
var intersections = 0;
❹ for (var i = 1; i < polypts.length; i++) {
var seg1 = polypts[i-1];
var seg2 = polypts[i];
❺ if (check_intersection(seg1, seg2, mypt, pt2)) {
intersections++;
}
}
❻ if (intersections % 2 == 1) {
return true;
}
}
return false;
}
碰撞测试函数接收要检查的点以及构成多边形的点的数组。从后者,我们能够确定多边形的边界框 ❶。当然,除非我们的点在这个框内 ❷,否则我们不需要做任何事情。如果点不在多边形的边界内,它就不在多边形内。
现在,我们准备检查点是否在多边形内。为此,我们在我们的点和边界框外的点之间创建一条临时线 ❸。然后我们需要检查这条线与多边形的交点。为此,我们将遍历多边形的点 ❹,检查与这条临时线的交点。
每次循环,我们从四个点制作两个线段。第一个线段是由多边形中的两个连续点组成的。另一个线段是用我们的点和我们在边界框外找到的点创建的。
我们将这些点传递给创建的函数以检查交点 ❺。如果两个线段相交,我们增加交点计数。无论如何,我们接着进行循环的下一轮。
当循环结束时,我们将知道我们的点是否在多边形内。如果交点计数是奇数,则点在内部。如果计数是偶数,则我们的点在外部。一个数是奇数,如果除以二时余数为 1。取模运算符 % 给我们余数 ❻。余数意味着我们有奇数个交点,我们返回 true 因为点在多边形内。在所有其他情况下,我们返回 false 因为点不在多边形内。
你在犹他州点击了!
现在我们能够检查一个点是否在多边形内,让我们将其整合到地图中。在本章的开头,我们创建了一系列形状像犹他州的多点。我们将使用这些点,以及到目前为止展示的所有其他代码,来报告用户是否点击了由utah变量描述的多边形内部。
创建一个基本的地图并添加多边形点。我们还需要之前创建的函数。确保包括check_polygon、check_intersection和points_to_bounds函数。然后添加以下代码,替换已经存在的create_map函数:
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.setCenterAndZoom(new mxn.LatLonPoint(39.5, −111.7), 6);
mapstraction.addSmallControls();
❶ mapstraction.click.addHandler(function(event_type, event_source, event_args) {
var clickpoint = event_args.location;
var intersects = check_polygon(clickpoint, utah);
❷ if (intersects) {
msg = "You clicked in Utah!";
}
else {
msg = "That's not Utah!";
}
❸ var m = new mxn.Marker(clickpoint);
m.setInfoBubble(msg);
mapstraction.addMarker(m);
m.openBubble();
});
}
前几行创建了一个新的地图,将其中心定位在犹他州,并添加了缩放控件。然后我们需要等待用户在地图上的某个位置点击 ❶。当用户点击时,我们初始化一个内联的匿名函数,将点击点作为参数传递,如第二十七部分:用户点击地图中所示。
检查一个点是否在多边形内的困难工作被传递给了check_polygon函数。这个函数,我们在前面的部分中编写过,返回true或false。如果函数返回true ❷,我们创建一个文本变量来告诉用户“你点击在犹他州内!”否则,用户会收到一条消息说“那不是犹他州!”
现在我们需要报告点击和结果。我们通过在点击点创建一个标记 ❸ 并将文本放入消息框中来实现这一点。然后我们将标记添加到页面并打开消息框。
尝试自己操作或查看图 6-13。看看你是否能通过在州东北角点击(看起来怀俄明州似乎侵入了犹他州的边界框)来欺骗测试。确实如此,如果你点击在犹他州之外,你会看到正确的信息。在点击在犹他州内部时也是如此。每次击中测试都会得到正确答案。

图 6-13. 你无法欺骗数学——那个点在犹他州之外!
#46: 从您的数据库中获取最近的位置
在本章的早期,我展示了如何计算两点之间的距离以及如何确定点到最近标记的位置。更有用可能是我们将在本项目中做的事情:从一个存储在数据库中的许多可能性列表中获取到点的最近位置。
要在数据库中查找位置,我们首先需要在数据库中有一些内容。在这个例子中,我们将使用 安装 MySQL 自己 中的 第六十三部分:将商店位置存储到数据库 的数据库表。尽管我们使用 MySQL 作为引擎,但大多数数据库都可以与这里的 SQL 语句一起工作。结构化查询语言 (SQL) 是与数据库服务器通信的语法。
因为我们正在寻找离单个点最近的位置,我们需要确定那个点是什么。我选择了一个位于俄勒冈州波特兰附近、纬度为 45.517、经度为 −122.649 的点。现在我们将这个点代入 Haversine 公式——这是我们在 JavaScript 中使用的相同三角函数,但这次我们将使用 SQL。
从 MySQL 命令解释器或 phpMyAdmin 中,输入以下查询:
SET @earthRadius = 6371;
SET @lat = 45.517;
SET @lon = −122.649;
SET @radLat = RADIANS(@lat);
SET @radLon = RADIANS(@lon);
SET @cosLat = COS(@radLat);
SET @sinLat = SIN(@radLat);
SELECT *,
`( @earthRadius❶ * ACOS( @cosLat * cosRadLat *`
`COS( radLon - @radLon ) + @sinLat *`
`sinRadLat ) ) AS dist`
FROM places
ORDER BY dist;
我们从 places 表中选择了所有字段,以及一个额外的字段,正如粗体部分所描述的那样。这需要很多代码!它通过使用每个地点存储的纬度和经度值来计算我们的点与数据库中点的距离。
距离,成为名为 dist 的列,以公里为单位表示。与之前实现的 Haversine 公式一样,我们乘以地球半径,即 6371 公里。对于英里,将数字 ❶ 替换为其英里等价值:3958。
当你运行这个 SQL 查询时,你的结果将与 表 6-2 中显示的结果相同。因为我们按距离排序,所以离我们的点最近的地点在表中排在前面。因此,离我们选择的点最近的地方是 Old Faithful。
表 6-2. Nearest Place SQL 的结果
| ID | 名称 | 纬度 | 经度 | 距离 |
|---|---|---|---|---|
| 1 | Old Faithful Geyser | 44.4605 | − 110.828 | 936.12 |
| 2 | 圣路易斯拱门 | 38.6247 | − 90.1851 | 2765.97 |
第七章。用户位置

创建一个基于位置网站需要一个起点。有时这个点来自您的数据库数据,或者来自您网站对一个小地理区域的关注。即使在这些情况下,了解用户的位置也能给您带来好处。
您可以通过多种方式检索这些信息,我将在本章中介绍。这些方法在复杂性和准确性方面有所不同。在某些情况下,您可能需要在 JavaScript 中做一个简单的调用。在其他情况下,您将安装一个数据库,以便您的服务器可以确定位置。
找到用户的位置也取决于您需要从他们那里获得多少权限。例如,IP 地址是每个互联网用户都有的东西。您可以在用户甚至不知道您在查找的情况下,为大多数用户找到城市级别的位置。对于浏览器访问其他数据源的方法,您将需要用户的权限。当然,在所有情况下,您都应该尽自己的一份力来确保用户隐私。
我们将从可能最简单的方法开始,确定用户的位置,这种方法与互联网本身一样历史悠久。
#47: 询问用户他们在哪里
这看起来明显吗?询问用户他们在哪里可能不是确定他们位置的一种引人注目的方法,但这种方法肯定会产生用户期望的结果。此外,它具有内置的好处,即只具体到用户想要的程度。换句话说,用户可以提供一个完整的地址、邮政编码,或者只是一个城市名称。如果您依赖于知道用户的所在地,至少在您无法确定他们的位置时,将此项目作为备选方案。
当然,您想要的是纬度和经度坐标。我们的地图需要这些坐标,这也是大量地理数据存储的方式。我们无法直接请求纬度和经度(您可以尝试在您的朋友身上这样做,看看有多少人知道他们当前的地理编码);相反,我们请求这些坐标的文本表示。一旦用户提供了这些信息,您可能需要将其输入到地理编码器中。本节不会涵盖这部分内容,但您可以在第三章中了解所有相关信息。在这个项目中,我将展示如何使用 JavaScript 和 PHP 将输入放入变量中。
使用 JavaScript 获取输入
当我们依赖 JavaScript 来接受输入时,您可以在不重新加载整个页面的情况下使用这些数据。此外,由于地图服务提供商与 JavaScript 一起工作,您只需要一种编程语言,这简化了一些事情。
为了接受用户输入,我们将把一个 HTML 表单集成到网站上。该表单将包含一个简单的文本输入框和一个提交按钮,如图图 7-1 所示。

图 7-1. 使用 JavaScript 请求位置或其他输入。
将以下代码添加到一个新的 HTML 页面中:
<!DOCTYPE html "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html >
<head>
<title>Get Input from a Form with JavaScript</title>
<script type="text/javascript">
var f;
❶ function prepare() {
f = document.getElementById("myform");
❷ f.onsubmit = function() {
var userinput = ❸f.wheretext.value;
// Do something with userinput
❹ return false; // avoids form submission
};
}
</script>
</head>
<body onload="prepare()">
<form id="myform">
<input type="text" name="wheretext" />
<input type="submit" name="butnew" value="Get Input" />
</form>
</body>
</html>
当页面加载时,会调用 prepare 函数 ❶。这种方法与其他地图项目中的 create_map 函数类似。这个函数的名字可以是任何你想要的,只要它与你用来调用它的名字匹配即可。
首先,你需要将提交事件附加到表单 ❷。你想要阻止表单提交到服务器,而是使用 JavaScript 中的数据。在这个例子中,我使用了一个内联的匿名函数来提交表单,而不是命名函数。否则,prepare 函数将只有一行。
此外,我通过索引引用表单,在这个例子中,索引是零。这是页面上的第一个也是唯一一个表单。如果页面上有其他表单,它可能是第二个,其索引将是 1(因为 JavaScript 数组索引从零开始)。你也可以给表单一个名字或 ID,然后通过这种方式访问它。
要获取用户输入,我通过表单对象中的名称来引用文本输入字段 ❸。包含 .value 部分很重要,因为它可以检索用户输入的文本,而不是输入字段对象。一旦你有了文本,你可以决定如何使用它;例如,你可以在网站上显示它或使用 #12: 使用 JavaScript 进行地理编码 来确定其坐标 #12: 使用 JavaScript 进行地理编码。
提交函数的最后一行 ❹ 也同样重要。这一行阻止浏览器将表单的输入发送到服务器,这是我们 希望 在下一节发生的事情。
使用 PHP 获取输入
让我们明确一下:用户在浏览器中输入他们的输入。使用 JavaScript 获取输入和使用 PHP(或任何服务器端语言)之间的区别在于,我们允许数据提交到服务器。HTML 表单的原始目的是将数据发送到服务器端的脚本,然后以额外的 HTML 进行回复。
由于大部分显著的操作都在 PHP 脚本中完成,因此获取输入的 HTML 代码相当简单。将以下行添加到一个新的 HTML 文件中:
<!DOCTYPE html "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html >
<head>
<title>Get Input from a Form with PHP</title>
</head>
<body>
<form ❶method="POST" ❷action="input.php">
<input type="text" name="wheretext" />
<input type="submit" name="butnew" value="Get Input" />
</form>
</body>
</html>
这段代码基本上与 JavaScript 示例中的 HTML 相同,但移除了 JavaScript。主要区别是 <form> 标签接收了几个额外的属性。
我声明了一个方法 ❶,它告诉浏览器如何将输入发送到服务器。这里有两个主要选项:GET,它将输入作为查询字符串放在 URL 中。我选择了 POST,它将数据放在 HTTP 消息的主体中,这样用户就看不到它。
另一个重要的属性是表单的 action ❷。这个 action 是表单数据的目标 URL。如果省略了 action,则默认为当前页面。如果你从一个普通的 HTML 文件发送,这个结果肯定不是你想要的,因为服务器端语言将无法解析数据。在这种情况下,我使用了 PHP 文件,因为这就是我将如何读取输入的。
现在我们需要编写 PHP 代码,因此将以下代码添加到名为input.php的新文件中(或与你的 action 属性匹配):
<?
$userinput = $_POST["wheretext"];
// Do something with userinput
?>
很简单,对吧?你只需要将输入框的名称传递给$_POST变量,它是一个关联数组。PHP 会做艰难的工作,解析头部文本和解码文本。
如果你打算在数据库中使用该值,请确保验证数据是好的。如果你期望邮政编码,请确保数据格式正确。如果你有一个地址,要注意地址中不属于地址的奇怪字符——分号就是其中之一。你不想成为 SQL 注入攻击的受害者,其中用户输入被用来创建有害的查询。
如果你打算在其他地方使用输入,比如在#13: 使用 HTTP 网络服务进行地理编码中,你可能会依赖它的安全性。但大多数时候,尽可能在你的端上做更多的工作,以确保数据完整性。
#48: 使用 JavaScript 获取位置
许多网络浏览器现在可以比使用 IP 地址提供更精确的用户位置信息。使用坐标,你可以省去用户告诉你他们位置的需要,例如,可以自动搜索附近的地点。
万维网联盟,通常被称为 W3C,是一个致力于制定网络标准的组织。在这些示例中,我将使用 W3C 推荐的语法。无论使用台式机、笔记本电脑还是移动电话,你将使用的代码都是相同的。
那么,让我们开始吧。从你的 JavaScript 代码的任何地方,添加以下行:
navigator.geolocation.getCurrentPosition(foundLoc, noLoc);
这个函数开始地理位置过程。浏览器将请求用户的许可,以便与你共享他的或她的位置。当用户批准时,浏览器将调用一个回调函数,这是传递给getCurrentPosition的第一个参数。当无法确定位置或用户拒绝你的请求时,还有一个可用的第二个回调函数。
我自己想出了那些函数的名字,你可以使用任何你想要的名称。然而,无论你叫它们什么,你都需要编写这些函数,这样浏览器就有东西可以引用。如果你更喜欢匿名内联函数,你可以用它们来代替命名函数。
在你用于发出位置请求的同一文件中,让我们添加这两个函数:
function foundLoc(❶pos) {
var lat = ❷pos.coords.latitude;
var lon = pos.coords.longitude;
❸ alert('Found location: ' + lat + ', ' + lon);
}
function noLoc() {
❹ alert('Could not find location');
}
第一个函数,当浏览器能够找到位置并且用户同意共享时会被调用,它接收一个位置对象 ❶。我们可以利用这个对象获取纬度 ❷、经度以及一些其他数据(关于这一点稍后详述)。一旦我们获取了这两个坐标,我们就会创建一个 JavaScript 警告框,显示位置 ❸。这个警告框并不特别有用,但我们现在只是证明它能够正常工作。
如果找不到位置(或者如果用户不想分享他的或她的位置),我们会创建一个 JavaScript 警告框,说明这一点 ❹。在一个完整的应用程序中,在这种情况下,您可能选择不采取任何行动。或者根据您网站的设计,您可以在无法确定位置时创建一个手动输入位置的选项。
在我们进行一些更有趣的操作之前,让我们谈谈一个普通的浏览器是如何访问您的位置的。
数据来自哪里?
位置数据的来源和准确性因来源而异。一款前沿的智能手机当然配备了全球定位卫星(GPS),因此如果这是数据来源,浏览器就能相当准确地知道您在哪里。那么在笔记本电脑或台式机上呢?
大多数计算机没有 GPS。相反,它们依赖 Wi-Fi 来找到您的位置。一些公司已经在全球最大的城市(以及一些小城市)周围开车,并“嗅探”无线互联网。每当他们发现信号时,他们就会将其唯一的 ID 添加到他们的数据库中,包括纬度和经度。
然后,当浏览器请求位置时,它会检测可用的 Wi-Fi。有了 ID,它会请求服务器回复坐标。您怀疑吗?在市区,这种方法在定位位置方面非常准确,因为有很多 Wi-Fi 接入点可用。此外,无线信号只应该传播几百英尺,所以对于大多数用途来说,它仍然相当准确。而且在很多情况下,Wi-Fi 比 GPS 更可靠,GPS 在室内获取信号比较困难。您可以在表 7-1 中看到不同方法的准确性。
表 7-1. 地理定位方法的准确性
| 方法 | 准确性 |
|---|---|
| GPS | 在几英尺范围内 |
| WiFi | 在一个街区范围内 |
| 蜂窝基站 | 在几英里范围内 |
| IP 地址 | ZIP 码或城市级别 |
当这两种选项都不可用时,您需要有一个后备计划。移动设备有时可以使用蜂窝基站三角测量。另一方面,标准网络浏览器始终有一个 IP 地址。是否使用后备计划取决于实现方式。
另一个随着使用的设备和浏览器实现位置代码的方式而变化的是,可用的附加数据。
我们可以获得哪些其他数据?
W3C 创建了一个可以在所有情况下工作的标准。正如您所看到的,可以使用多种数据源和设备来确定位置。特别是,移动电话可以提供一些有趣的数据。
之前,我描述了如何使用 JavaScript 获取位置坐标。这里有一些你可能能够访问的其他值:
-
获取位置的时间
-
位置的精度,以米为单位
-
位置的海拔高度
-
如果存在,海拔精度的米
-
速度,以米/秒为单位
-
航向,相对于真北的方向,以度为单位
并非每个设备都能提供所有这些信息。例如,如果用户在使用笔记本电脑,可能只有纬度和经度是可用的。然而,如果数据来自 GPS,你可能能够访问所有这些字段。
在地图上使用位置
现在你已经可以获取用户的位置,你可能想对它做一些事情。最简单的例子是将它用作新地图的中心。你还可以在那个位置添加一个标记。嘿,为什么不两者都做呢?
这个例子将与其他例子略有不同,因为地图将在我们知道有位置信息时才创建。因此,让我们从一个新的 HTML 文件开始,而不是基本地图。将以下行添加到你的新文件中:
<html>
<head>
<title>W3C Geolocation Map</title>
<script type="text/javascript"
src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript" src="mxn.js?(googlev3)"></script>
<style>
div#mymap {
width: 400px;
height: 350px;
}
</style>
<script type="text/javascript">
navigator.geolocation.getCurrentPosition(❶create_map, ❷function() {});
function create_map(❸pos) {
❹ var pt = new mxn.LatLonPoint(pos.coords.latitude, pos.coords.longitude);
mapstraction = new mxn.Mapstraction('mymap', 'googlev3');
mapstraction.setCenterAndZoom(pt, 15);
var marker = new mxn.Marker(pt);
marker.setInfoBubble('You Are Here');
mapstraction.addMarker(marker);
❺ marker.openBubble();
}
</script>
</head>
<body>
<div id="mymap"></div>
</body>
</html>
因为我们只想在拥有位置信息时调用create_map函数,所以在页面加载时不会调用任何东西。相反,getCurrentPosition的调用发生在裸 JavaScript 中,然后使用地图初始化函数作为回调❶。如果没有找到位置,我们不想做任何事情,因此我们创建了一个空的内联函数❷。将来,你可能想做一些事情,所以这留下了一个模板来提醒你这里可以放置某些内容。
当create_map函数被调用时,浏览器找到的位置❸会被传递给它。我们将首先使用这个位置,它是一个 W3C 格式的对象,将其转换为 Mapstraction 描述点的对象❹。然后,将地图居中并创建标记就像第一、第二章中展示的例子一样简单。
为了增加一些乐趣,我们给标记添加了一个消息框,上面写着“您在这里”。在将标记添加到地图后,我们会自动显示消息❺。当我在我家乡波特兰运行这个程序时,我得到了图 7-2 中所示的结果。
从这里,你可以在地图上绘制本地结果或使用找到的位置作为起点获取驾驶问题。
接收持续更新
如果你的应用程序将在移动设备上使用,那么用户的位置可能会随着他们的移动而改变。根据你对数据的处理方式,接收一个初始位置可能不够——你可能需要更新。
W3C 通过第二个函数来满足这一需求,以访问用户的位置。而不是使用getCurrentPosition,你可以使用以下这行代码:
var geoid = navigator.geolocation.watchPosition(foundLoc, noLoc);
作为参数传递的两个函数引用的使用方式与单个位置请求相同。这里的区别是,浏览器将定期调用您的函数,而无需代码执行另一个请求。频率由浏览器决定,可能是每分钟或更频繁。移动浏览器可能会更频繁地发送更新。

图 7-2. 在地图上绘制的位置坐标
您可以通过每隔一段时间调用单个请求来实现这一点。然而,浏览器可能会在每次请求时提示用户;watchPosition函数的权限将持续到用户重新加载页面。
如果您想停止跟踪用户的地理位置,可以使用此函数清除:
navigator.geolocation.clearWatch(geoid);
注意该函数接受一个参数。这个变量是watchPosition函数输出的标识符。为了停止监视,请将此标识符存储在变量中。
其他地理位置选项
在前几节中提到的两个函数接受一个尚未覆盖的第三个参数。您可以设置额外的选项,例如位置可以有多旧,以及您是否想要尽可能高的精度。
这里是一个包含选项的地理位置调用示例:
var locOptions = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 60000
}
navigator.geolocation.getCurrentPosition(foundLocation, noLocation, locOptions);
我创建了一个变量来保存位置选项,这些选项存储为 JavaScript 对象。选项可以按任何顺序输入,并且不必包含所有选项。在这种情况下,我使用了所有三个潜在选项。
首先,enableHighAccuracy是一个布尔值,期望的值是true或false。当设置为true时,浏览器将提供它能够提供的最准确的位置,即使这样做需要更多的时间(例如,锁定额外的 GPS 卫星)。在之前的例子中,我们没有使用选项参数,出于效率原因,高精度并未启用。
我们还可以使用timeout选项为位置查找设置一个时间限制。此选项以毫秒为单位设置。因为 1 秒钟有 1000 毫秒,所以示例显示了 5 秒的超时。
最后一个选项,maximumAge,为浏览器提供了发送缓存位置的指令。根据您的需求,您可能需要一个非常近的位置,或者您可能不太关心。就像超时一样,这个值以毫秒为单位传递。在这个例子中,我展示了maximumAge为 60,000 毫秒,即 1 分钟。
选项对象可以作为第三个参数传递给getCurrentPosition或watchPosition。如果您使用后者请求持续更新,则每次浏览器返回位置时都会跟随这些选项。
#49:使用 Fire Eagle 获取位置
您可以通过多种方式检索用户的位置,其中一些方式可以提供非常精确的输出。许多询问并存储用户当前位置的服务也都可以使用。雅虎的 Fire Eagle 就是这些服务的经纪人。当您编写 Fire Eagle 应用程序时,您可以设置或接收用户的当前位置,只要用户给予权限,这个位置信息就会对所有其他 Fire Eagle 应用程序可用。
在这个项目中,我将展示如何使用 Fire Eagle 获取用户的位置。一旦用户批准了您的应用程序访问权限,您将能够随时获取用户的最新位置,即使他或她不在线。
当然,某些应用程序必须设置位置才能准确。还记得我提到的那些帮助用户分享位置网站吗?我们将让他们在 Fire Eagle 中设置位置,让我们深入探讨如何使用 Fire Eagle 来检索位置。我们编写的应用程序将使用 PHP,我在第九章(第九章)中对其进行了更深入的介绍。
注意
有许多其他用于分享位置的服务。当您阅读此内容时,可能还有更多。请参阅mapscripting.com/location-apis中的列表和教程链接。
获取 Fire Eagle 基础知识
雅虎有一些工具可以使编写 Fire Eagle 应用程序变得容易得多。在您可以使用它们之前,您需要将应用程序注册到 Fire Eagle,这样您就可以获得使程序工作的代码。一个是 API 密钥,类似于其他服务使用的;另一个是“秘密代码”。如果您觉得有帮助,您可以将其视为加入一个秘密俱乐部。实际上,您只是在填写一个表格。
您需要一个雅虎账户。一旦您有了它,请转到此页面以注册您的应用程序:fireeagle.yahoo.net/developer/create。
不要担心完美。您稍后可以编辑此应用程序。此外,Fire Eagle 允许您注册多个应用程序,所以让这个成为您稍后编写的应用程序的彩排。
在创建您的应用程序时,Fire Eagle 有两个重要的设置需要注意。首先,您将使用基于 Web 服务的认证进行身份验证。接下来,您需要设置一个回调 URL。URL 可以命名为您想要的任何名称,但在这个简单的示例中,让我们使用callback.php。Fire Eagle 需要知道一个完整的 URL,所以您将插入类似yoursite.com/fireeagle/callback.php的内容。出于测试目的,您甚至可以使用您的本地机器而不是服务器。
一旦您注册了您的应用程序,Fire Eagle 将向您展示一些特殊的密钥,API 以及我之前提到的秘密代码。您可以通过访问此页面随时返回它们:fireeagle.yahoo.net/developer/manage。
在您离开网站一段时间之前,还有最后一件事:您需要 API 工具包。Fire Eagle 为各种网络编程语言提供了预包装的代码,这使得编写应用程序变得容易得多。从以下页面下载 PHP 工具包:fireeagle.yahoo.net/developer/code/php。
您绝对需要从这个包中获取两个文件:fireeagle.php 和 OAuth.php。将这些文件存储在名为 lib(代表库)的目录中,因为我们很快就会访问它们。
验证用户
正如其中一个文件的名字所暗示的,Fire Eagle 使用 OAuth 来验证用户。如果您之前没有使用过 OAuth,它可能看起来有点奇怪,但我已经学会了欣赏它的简单性和安全性。
验证用户的过程是从向 Fire Eagle 请求一个 令牌 开始的。这个令牌实际上由两部分组成:公开 和 私有。我们将用户和公开令牌一起重定向到 Fire Eagle。然后,当用户批准我们访问时,我们可以使用公开和私有令牌一起向 Fire Eagle 证明我们是批准的应用程序。
让我们看看代码中的样子。创建一个名为 authorize.php 的新文件,并添加以下行:
<?
require_once "lib/fireeagle.php";
$key = "*`YOURKEY`*";
$secret = "*`YOURSECRETCODE`*";
❶ $fe = new FireEagle($key, $secret);
❷ $response = $fe->getRequestToken();
session_start();
$oauth_token = $response["oauth_token"];
$_SESSION['oauth_token'] = $oauth_token;
$_SESSION['oauth_secret'] = $response["oauth_token_secret"];
❸ $_SESSION["signed_in"] = 0;
❹ header("Location: " . $fe->getAuthorizeURL($oauth_token));
?>
首先,我们使用密钥和密钥代码创建一个新的 Fire Eagle 对象❶。密钥和密钥代码与我所描述的令牌不同。密钥和密钥代码标识您的应用程序本身,并且永远不会改变。令牌对每个用户都是不同的。
使用我们创建的 Fire Eagle 对象,我们现在请求公开和私有令牌❷。我们将这些存储为会话变量,这是 PHP 在页面之间维护用户数据的方式。然后我们创建另一个会话变量,告诉我们用户尚未登录❸。
最后,使用公开令牌,我们将用户重定向到 Fire Eagle URL❹。这个页面是用户将批准我们的应用程序的地方。
回应召唤
一旦用户同意给我们访问权限,Fire Eagle 将重定向回我们的回调 URL。记住那些设置吗?当用户来到这个页面时,他或她很可能已经授权了我们的应用程序。
回调页面是用户正式登录的地方。您如何设置这取决于您正在创建的应用程序类型。例如,如果用户在数据库中存储了账户,您可能会在那里维护他们的 Fire Eagle 令牌。在这里,我们将简单地更新会话变量。
将以下代码添加到您的 callback.php 文件中:
<?
require_once "lib/fireeagle.php";
$key = "*`YOURKEY`*";
$secret = "*`YOURSECRETCODE`*";
session_start();
❶ $fe = new FireEagle($key, $secret,
$_SESSION["oauth_token"], $_SESSION["oauth_secret"]);
❷ $response = $fe->getAccessToken();
$oauth_token = $response["oauth_token"];
$_SESSION["oauth_token"] = $oauth_token;
$_SESSION["oauth_secret"] = $response["oauth_token_secret"];
❸ $_SESSION["signed_in"] = 1;
❹ header("Location: getloc.php");
?>
我们再次使用 Yahoo!提供的 PHP 工具包创建一个 Fire Eagle 对象❶。这次我们包括了用户的公开和私有令牌,以及我们应用程序的密钥和密钥。记住,这些信息证明了我们是用户刚刚批准的同一应用程序。
如果所有这些密钥和令牌都让人困惑,请做好准备。还有一对即将到来。一旦用户批准了一个应用程序,就不需要公共和私有令牌了。相反,它们被访问令牌❷所取代。我们用新的会话变量覆盖之前的会话变量。而且因为用户现在已经成功登录,我们可以适当地设置那个会话变量❸。
这个过程可能看起来很繁琐,需要遵循以访问用户的位置,但请记住,这只需要发生一次。而且,所有这些步骤都是为了安全起见,这是用户所欣赏的。此外,这个回调页面将立即显示给用户。
最后一行将用户重定向到我们将用于检索其位置的页面❹。毕竟,这就是整个事情的核心所在。所以,让我们开始吧。
获取用户的位置
用户已经给了我们访问其位置的权利。我们甚至可以检查我们设置的会话变量来确保。现在,我们将检索用户的位置。
将以下代码添加到getloc.php文件中:
<?
require_once "lib/fireeagle.php";
$key = "*`YOURKEY`*";
$secret = "*`YOURSECRETCODE`*";
session_start();
❶ if ($_SESSION["signed_in"] == 1) {
$fe = new FireEagle($key, $secret,
$_SESSION["oauth_token"], $_SESSION["oauth_secret"]);
❷ $loc = $fe->user();
❸ $toploc = $loc->user->best_guess;
❹ $lat = $toploc->latitude;
$lon = $toploc->longitude;
$datetime = date("M j, g:i A", strtotime(❺$toploc->located_at));
}
?>
<p>
❻ Your current location: <strong><?=$lat?>, <?=$lon?></strong>
(as of <?=$datetime?>)
</p>
代码的起始部分与其他页面类似,包括 PHP API 套件并声明我们的应用程序密钥和秘密代码。然后我们检查我们的会话变量以确保用户确实已经给了我们许可❶。如果已登录变量仍然是 0,我们可以重定向到认证页面,或者显示错误。
就像上一节一样,我们使用应用程序密钥和访问令牌创建一个 Fire Eagle 对象。因为我们相当确信我们现在可以访问用户的位置,所以我们向 Fire Eagle 请求位置对象❷。从那里,我们可以获取其对用户位置的最好猜测❸,这是用户愿意分享的最细粒度数据。
现在我们可以访问包括纬度❹和经度在内的几份数据。我们还可以了解用户分享该位置的时间有多久了❺。为了提高可读性,我使用了 PHP date函数来格式化时间戳。
最后,在 HTML 中,我们输出从 Fire Eagle 检索到的数据❻。当然,这只是一个例子,不是一个伟大的 Fire Eagle 应用程序。真正伟大的是您将编写的那个。对吧?
#50:通过 IP 获取位置
互联网上的所有用户都可以用一个在至少一个在线会话中保持不变的数字来识别,这个数字是静态的——永远不会改变。这个数字,称为IP 地址,属于互联网服务提供商,他们通常拥有一个(或更多)类似的数字块。通过将这些数字与位置相关的数据库绑定,您通常可以确定用户所在的城市,如果不是邮政编码的话。
我将在本章演示其他项目,这些项目可以获取更细粒度的数据,确定用户的精确位置。但了解一个地理区域通常就足够做一些有趣的事情。此外,您可以通过使用 IP 地址来定位大多数用户,而且您不必请求他们的许可。
什么是 IP 地址?IP 地址是一系列四个数字,每个数字从一到三位。例如,它可能是 208.54.34.3。每个连接到互联网的计算机都有自己的 IP 地址,尽管有时一个地址会在局域网中共享。
对于这个项目,我们将查看两个基于用户 IP 地址提供地理坐标的服务。一个必须在服务器上使用,而另一个则通过 JavaScript 在浏览器中运行。你选择哪个取决于你如何使用这些数据。例如,如果你将结果存储到数据库中,你可能会想要使用服务器端版本。但如果你只是想将地图中心定位在用户的位置,JavaScript 版本就足够了。
使用 HostIP 网络服务
对于这个示例,我将使用 hostip.info API。许多网络服务提供基于 IP 的地理位置服务,包括一个名为 MaxMind 的商业服务。然而,我选择了 HostIP,因为它易于使用,并且数据库由一群开发者和用户维护。
我们将使用 PHP 调用 API,包括在第八章(Chapter 8)和第九章(Chapter 9)中描述的一些概念。我会尽力使它们易于理解,但如果你发现某些内容看起来很奇怪,你可能需要提前翻阅。
在我们开始编写 PHP 代码之前,我们需要了解如何从 HostIP 获取数据。幸运的是,该网站使得在浏览器中尝试 API 变得非常简单。尝试将以下 URL 加载到地址栏中:api.hostip.info/?ip=12.215.42.19。
你的结果应该看起来像这样:
<?xml version="1.0" encoding="ISO-8859-1"?>
<HostipLookupResultSet ... >
<gml:description>This is the Hostip Lookup Service</gml:description>
...
<gml:featureMember>
<Hostip>
<gml:name>Sugar Grove, IL</gml:name>
<countryName>UNITED STATES</countryName>
<countryAbbrev>US</countryAbbrev>
<ipLocation>
<gml:PointProperty>
<gml:Point srsName="http://www.opengis.net/gml/srs/epsg.xml#4326">
<gml:coordinates>`−88.4588,41.7696`</gml:coordinates>
</gml:Point>
</gml:PointProperty>
</ipLocation>
</Hostip>
</gml:featureMember>
</HostipLookupResultSet>
在所有这些中间,以粗体显示的是我们想要的纬度和经度坐标(注意在这里经度在前)。获取这些数字意味着解析结果并提取数字。这个过程在 #52: 使用 XML 中有详细说明。让我们使用 PHP 将纬度和经度放入两个变量中。创建一个新文件并添加以下代码行:
<?
❶ $ipaddr = $_SERVER["REMOTE_ADDR"];
❷ $xmltxt = get_url("http://api.hostip.info/?ip=$ipaddr");
❸ $xmlobj = simplexml_load_string($xmltxt);
❹ $coords = $xmlobj->xpath("//coordinates");
❺ list($lon, $lat) = preg_split("/\,/", $coords[0]);
// cUrl functions
❻ function get_url($url) {
...
}
?>
在我们能够调用 HostIP 之前,我们需要确定用户的 IP。幸运的是,IP 通过服务器变量提供给 PHP,我们将它放入一个局部变量中 ❶。
尽管代码做了很多工作,但其余代码相对简洁,主要是因为它依赖于其他函数来完成繁重的工作。例如,我们通过一行调用加载 XML 文本 ❷,传递包含用户 IP 地址的 HostIP URL。为了将文本转换为可用的对象,我们调用SimpleXML解析函数 ❸,该函数将 XML 转换为对象。现在我们有一个易于访问的对象,我们使用xpath函数返回所有<coordinates>标签 ❹。我们只有一个,所以我们将其传递给字符串分割函数,将其插入到我们的两个变量中 ❺。
这些函数中的大多数依赖于内部 PHP,但其中之一 ❻ 是我自己编写的。你可以在 #61: 获取网页 中找到这个函数的完整内容。
使用 Google 的 ClientLocation JavaScript 对象
如果你不需要在服务器上获取用户的地理位置,如果你使用 Google 的 Ajax Loader,你可以轻松地获取它。这是加载 Google 的 JavaScript API 的另一种方式,包括 Maps 和 Search。即使没有加载特定的 API,ClientLocation 对象也是可用的,考虑到它提供的数据,这可以最小化开销。
与标准的 Google Maps API 一样,你需要在 HTML 文件的头部包含对 Google 的 JavaScript 的调用:
<script type="text/javascript" src="http://www.google.com/jsapi?key=
*`yourkey`*"></script>
然后,在你的 JavaScript 代码中的任何地方,你都可以访问 google.loader.ClientLocation.latitude 和 google.loader.ClientLocation.longitude(以及一些其他数据项,如城市名称)。
注意
在出版时,Ajax Loader 只支持 Google Maps 版本 2。要使用此示例,你需要一个 Google Maps API 密钥。你可以在 code.google.com/apis/ajax/documentation/ 找到它,以及有关 Ajax Loader 的更新信息。
在地图上看到它总是更容易,所以让我们创建一个新的 Mapstraction 地图,使用 Google 作为映射提供者。我们将使用 Ajax loader 而不是标准的 Google Maps。将以下代码添加到一个新的 HTML 文件中:
<html>
<head>
<title>ClientLocation Map</title>
<script type="text/javascript"
src="http://www.google.com/jsapi?key=*`yourkeyhere`*"></script>
<script type="text/javascript" src="mxn.js?(google)">
<style>
div#mymap {
width: 400px;
height: 350px;
}
</style>
<script type="text/javascript">
❶ google.load("maps", "2");
function create_map() {
❷ var pos = google.loader.ClientLocation;
❸ var pt = new mxn.LatLonPoint(pos.latitude, pos.longitude);
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.setCenterAndZoom(pt, 10);
var marker = new mxn.Marker(pt);
marker.setInfoBubble('You Are Here');
mapstraction.addMarker(marker);
marker.openBubble();
}
</script>
</head>
<body onLoad="create_map()">
<div id="mymap"></div>
</body>
</html>
JavaScript 的第一行代码告诉 Google Ajax Loader 加载 Maps API ❶。然后,一旦它以及页面的其余部分加载完成,就会调用 create_map 函数。
要访问位置,我们将 ClientLocation 对象复制到一个局部变量 ❷ 中,这使得访问它更加容易。然后我们使用新对象中的纬度和经度创建一个点 ❸。从那里开始,代码与之前的 JavaScript 示例非常相似,我们使用这个点作为地图的中心并创建一个标记。
#51: 创建自己的 IP 数据库
如果你在公共场所摔倒,你会拒绝帮助吗?那么你可能也是那种想要托管自己的 IP 数据库,而不是依赖他人服务的人。当然,你可能只是那种关心性能或不想担心网络延迟的人。无论哪种情况,这个项目将帮助你通过自己的服务器使用 IP 定位用户。
数据从哪里来?我们将使用一个名为 IPInfoDB 的免费服务,该服务每月更新一个精简的 MySQL 数据库转储。在 ipinfodb.com/ip_database.php 下载最新的数据库。
你有几种数据库类型可供选择。在这个例子中,我们将使用包含单个表的较小、城市级别的数据库。该表应包含大约 140 万条记录,这将导致数据库文件略大于 100MB。(当你下载压缩版本时,文件将小得多。)
导入 IP 数据
现在你已经下载了 SQL 文件,你需要导入它。这个文件太大,无法使用 phpMyAdmin,所以你需要使用命令解释器,你通常可以通过服务器上的 mysql 命令访问它。
你可能可以向你的系统管理员寻求帮助,以便访问你的 MySQL 数据库的命令解释器。或者,如果你使用的是 MAMP,你可以在 MAMP 目录中运行 Library/bin/mysql -u root -p(密码也是 root)。在 WAMP 上,位置可能有所不同,但通常可以在 C:\wamp\bin\mysql\mysql版本\bin\mysql 找到。在两种情况下,你都需要通过命令行:终端(在 Mac 上)或 CMD(在 Windows 上)来访问解释器。
打开命令解释器并选择你的数据库,或者创建一个新的数据库(参见 第六十三部分:将位置存储到数据库中 在 自己安装 MySQL)。要开始导入数据的过程,请输入以下命令:
\. /path/to/ipdata.sql
确保包含你下载的 SQL 文件的完整路径。在导入过程中,你会看到类似这样的消息:
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 11068 rows affected (0.35 sec)
Records: 11068 Duplicates: 0 Warnings: 0
当消息结束时,你可以离开命令解释器并返回 phpMyAdmin,如果你愿意的话。现在我们已经添加了数据,让我们来访问它。
查找 IP 的位置
IP 数据库中有什么信息可用,以及它是如何存储的?从 phpMyAdmin 点击到你的数据库,然后到你刚刚导入的 ip_group_city 表。点击浏览选项卡,你将看到数据库中的前 30 条记录。
可用的有趣字段包括城市、纬度和经度。你还会看到存储为非常大的数字的 IP 地址本身,这与你习惯看到的点分隔版本不同。这种差异是因为你需要进行一些预处理,这使得查找过程更加高效。
当 IP 地址为 A.B.C.D 时,该数字可以表示为 ((A*256+B)*256+C)*256。所以 71.59.208.255 会变成 1195102208。同样,71.59.208.X 块中的任何内容也会如此。注意公式中没有 D;这是因为 IP 地址以块的形式出现,所以该范围内的所有内容可能具有相同的地理位置。
将 IP 转换为数字的过程相当标准化。实际上,MySQL 有一个辅助函数,INET_ATON,这使得它变得非常简单。在 phpMyAdmin 中点击 SQL 选项卡,或者使用命令解释器,并添加以下查询:
SELECT * FROM ip_group_city
WHERE ip_start <= INET_ATON('*`71.59.208.255`*')
ORDER BY ip_start DESC LIMIT 1
记得将我使用的 IP 地址替换为你自己的。在这里,我请求了数据库中最近 IP 地址的所有字段。为了防止返回所有 1.4 百万条记录,我限制结果只返回一行——就是我想要的。
现在我们将把它连接到一个 PHP 脚本,这在 第六十五部分:从 PHP 使用 MySQL 中有介绍。
第八章:数据格式

与地图一起工作通常意味着与大量地理数据交互。除了描述性信息外,你还需要知道一个地方的位置,以便你可以在地图上绘制它。很可能大多数时候你都需要从其他人那里获取数据。你也可能需要与他人共享你的数据。
为了使地理数据的传递更加方便,已经采用了几种标准格式。在本章中,我将介绍几种分享地图上基本地理元素(点、线和形状)的方法。
我们还将介绍一些在网络上用于交换信息的流行格式。越来越多地,你的数据源将是提供 API 以使其数据可用的网站。在大多数情况下,你需要的格式在本章中都有涵盖。
让我们开始学习一些数据格式。
#52: 使用 XML
可扩展标记语言(XML)是本章讨论的许多 Web 数据和格式的基础。它看起来很像 HTML,因为一些 HTML 实际上就是 XML。然而,本节将避免具体 XML 变体,因为我会单独在其他章节中介绍它们。在这里,我将专注于如何识别和使用通用 XML。
首先,它看起来是什么样子?XML 由标签组成,这些标签是尖括号<和>内的单词。理解括号内的单词通常很容易,但有时它们是缩写或首字母缩略词。标签可以包含其他标签,以及key=value对,这些对被称为属性。包含其他标签或文本的标签以匹配的结束标签结束,该结束标签在标签名之前包含一个/。
考虑这个简短的 XML 文件示例:
❶ ?xml version="1.0" encoding="UTF-8"?>
❷ root>
<child ❸name="first">
❹<grandchild>text could go here</grandchild>
</child>
<child name="second" ❺/>
</root>
XML 文件通常以类似的方式开始,有一个处理指令声明其为 XML❶并提供版本和编码信息。这个特殊标签没有对应的结束标签。移除这个头部后,我们就可以直接进入数据部分。
根元素❷可以命名为任何名称,但只能有一个。例如,HTML 只有一个<html>标签。在起始和结束根标签之间是真正的 XML 内容。在这种情况下,XML 有两个子元素。再次强调,标签可以命名为任何名称,但在这个例子中,我使用了有助于描述 XML 术语的名称。
XML 是分层的,访问数据需要理解其结构。第一个子元素有一个单一属性❸以及它自己的子元素❹。第二个子元素也有一个属性,但它不包含任何子元素。在这种情况下,我们可以缩写结束标签❺。
现在我们想要获取 XML 中的这些数据。读取标签并将它们转换为计算机可以理解的结构称为 解析。大多数语言都有一些内置的解析 XML 的方法。接下来,我将展示两个 JavaScript 示例和一个使用 PHP 的示例,PHP 是一种服务器端编程语言。
使用 JavaScript 解析 XML
每个现代浏览器都提供了一种读取 XML 内容的方法,这在很大程度上是因为网络的大部分内容都是基于这项技术构建的。不幸的是,各种浏览器之间存在差异。此外,获取深层嵌套的元素可能会很麻烦。
在展示更简单的方法之前,我们将使用前面描述的 XML 示例在这个部分尝试一下。我们不会加载 XML 文件(我将在下一节中介绍),而是使用存储为文本字符串的 XML。
由于我们使用 JavaScript,文件需要位于网页内部。我们将在 JavaScript 部分进行所有操作,因此网页将保持空白。将这些行添加到新文件中:
<html>
<head>
<script>
❶ var xmltxt = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
xmltxt += "<root>\n";
xmltxt += " <child name=\"first\" />\n";
xmltxt += " <child name=\"second\">\n";
xmltxt += " <grandchild>text goes here</grandchild>\n";
xmltxt += " </child>\n";
xmltxt += "</root>\n";
var x = null;
var output = "";
❷ if (window.ActiveXObject) { // Internet Explorer
x = new ActiveXObject("Microsoft.XMLDOM");
x.async = false;
x.loadXML(xmltxt);
}
❸ else if (window.DOMParser) { // Other browsers
var p = new DOMParser();
x = p.parseFromString(xmltxt, "text/xml");
}
else {
// Can't load XML
}
if (x) {
❹ `var children = x.getElementsByTagName("child");`
❺ `for (i=0; i<children.length; i++) {`
❻ `output += children[i].getAttribute("name") + "\n";`
`}`
}
alert(output);
</script>
</head>
<body></body>
</html>
粗体行是获取数据的那一行。其余的都是纯设置。公平地说,准备 XML 文本 ❶ 确实需要七行代码。我们可以将其缩减为单行,但我将其展开以增强可读性。
解析 XML 需要一个多步骤的方法。首先,我们需要尝试 Internet Explorer 的方法 ❷。如果我们使用的是其他浏览器,这种方法将失败。然后,我们将使用更广泛采用的方法 ❸。希望这种方法能奏效,因为如果它不起作用,我们就无法解析 XML。
在这两次尝试之后,x 变量应该包含一个 XML 对象。这个变量被粗体部分用来提取子元素的名称。首先,我们遍历 XML 中的所有子标签 ❹。然后,我们循环遍历所有这些标签 ❺。我们可以通过 i 变量来判断我们处于哪个步骤,它从零开始,每次循环增加一步。每次循环中,我们将当前子元素的名称添加到我们将要输出的文本中 ❻。
如果你将此文件加载到网页浏览器中,你应该会看到一个包含 "first" 和 "second" 名称的 JavaScript 提示框。你已经成功使用纯 JavaScript 解析了 XML。现在让我们看看如何使用 JavaScript 库 jQuery 来解析它。
使用 jQuery JavaScript 库解析 XML
jQuery 的单一原则是编写更少的代码。艰苦的工作留给了库,而库本身非常小(目前小于 20K)。当涉及到获取和解析 XML 时,jQuery 保持简单且可预测。
与纯 JavaScript 示例类似,我们将在一个空白的 HTML 文件中解析 XML。然而,在这种情况下,我们将直接从文件中加载我们的 XML,这是一个常见的情况。确保你有一个名为 example.xml 的文件,其中包含之前提到的 XML,然后将这些行添加到同一目录下的新文件中:
<html>
<head>
❶ <script src="http://ajax.googleapis.com/ajax/libs/
jquery/1.3/jquery.min.js"></script>
<script>
var output = "";
❷ $.get("example.xml", {}, ❸function(xml) {
❹ $("child", xml).❺each(function(i) {
output += ❻this.getAttribute("name") + "\n";
});
alert(output);
});
</script>
</head>
<body></body>
</html>
你首先会注意到我们需要加载 jQuery JavaScript 文件 ❶。你可以从 jquery.com/ 下载到自己的服务器上,或者像我在这个例子中那样引用由 Google 托管的版本。在任一情况下,我们都可以访问库的许多功能,包括使用 JavaScript 通过 Ajax 加载文件以及解析 XML。
jQuery 库利用了许多技术来减少你需要编写的 JavaScript 量。其中之一是 美元符号对象,它允许你使用非常简单的语法访问 jQuery 的许多功能。例如,加载 XML 文件是通过调用 $.get 函数 ❷ 来实现的。
要在没有 jQuery 的情况下实现 Ajax 调用,需要根据浏览器尝试不同的方法,就像我们在上一节中的 XML 解析示例一样。相反,jQuery 做了工作,确保我们可以获取数据。
加载 XML 还展示了另一种减少代码的技术:内联的、匿名函数。这些函数是标准 JavaScript 语言的一部分,但与 jQuery 简化代码的方式结合使用时变得特别有用。当执行 Ajax 调用,例如我们用来加载 XML 文件的调用时,JavaScript 需要一个回调函数。而不是创建一个命名函数仅用于接收 XML 结果,我们可以直接编写一个内联函数 ❸。
在匿名函数(之所以这样命名是因为它没有名字)内部,我们使用另一个 jQuery 简写来解析 XML。解析发生得如此之快,你可能甚至都没有意识到它在发生。美元符号函数传递了我们想要的标签名称,以及包含我们从 Ajax 调用中获取的内容的 XML 变量 ❹。然后我们将 jQuery 的 each 函数链接到结果,我们可以遍历所有子元素 ❺。我们不需要使用显式的 for 循环,也不需要确定子元素的数量。这都在 jQuery 内部完成。
我们每次通过 jQuery 循环所做的事情由另一个匿名函数决定。同样,我们只是将一切保持内联,因为为了一行代码而创建一个命名函数是没有意义的。当然,代码只有一行,因为我们使用了 jQuery。this 变量持有当前子元素,然后我们使用在非 jQuery 示例中使用的相同的 getAttribute 函数来获取名称属性 ❻。
在大约是前一个例子一半的行数中,我们达到了相同的结果。如果你在网页浏览器中加载该文件,JavaScript 警告将打印出子标签的名称,“first” 和 “second”。jQuery 使得在处理 API 和解析数据格式时,你经常要做的事情变得容易,其中许多使用 XML。
使用 PHP 解析 XML
在许多情况下,你可能会想在服务器上检索 XML。为此,你不会使用 JavaScript,因为你通常在 Web 浏览器中编写 JavaScript;你会使用 PHP。PHP 是一种流行的编程语言,用于编写服务器端应用程序。有关 PHP 以及确保你的服务器上有 PHP 的更多信息,请务必阅读第九章。
让我们使用 PHP 解析上一节中的示例 XML。确保你的服务器上有一个名为example.xml的文件。在同一目录下创建一个新的 PHP 文件,并添加以下行:
<?
❶ $xmltxt = join("\n", file("example.xml"));
❷ $xmlobj = simplexml_load_string($xmltxt);
foreach (❸$xmlobj->child as $childobj) {
print($childobj->❹attributes()->name . "\n");
}
?>
我们首先将示例 XML 文件加载到$xmltxt变量中❶。在许多情况下,我们实际上是从 API 加载 XML。无论如何,XML 内容最终会存储在一个变量中,以便进行解析。
我们将解析 XML 的工作交给 PHP 的SimpleXML类,这个类在 PHP 5 中是自动包含的。simplexml_load_string函数将文本 XML 转换为有用的对象❷,以便访问 XML 内部的数据。还有一个simplexml_load_file函数,但通常情况下,你会将从一个 API 获取的字符串转换为字符串。
一旦 XML 以对象形式存在,我们就可以在子元素中查找名称属性。我们需要遍历所有子元素❸,将当前子元素放入它自己的对象中。然后,我们获取属性❹并找到名为 name 的属性。
如果你看到 PHP 创建的对象的样子,查询 XML 的代码将更有意义。使用print_r($xmlobj)来查看层次对象的文本表示:
SimpleXMLElement Object (
❶ [child] => Array (
[0] => SimpleXMLElement Object (
❷ [@attributes] => Array (
[name] => first
)
)
[1] => SimpleXMLElement Object
[@attributes] => Array (
[name] => second
)
❸ [grandchild] => text goes here
)
)
)
首先,所有内容都在一个SimpleXMLElement对象内部,就像所有 XML 都在根标签内一样。还包括额外的SimpleXMLElement对象,这类似于标签内有标签。SimpleXML 类本质上将 XML 转换为一系列数组。
首先,创建一个包含所有子元素的数值数组❶。在这种情况下,只包含两个子元素,编号为 0 和 1,因为,就像 JavaScript 一样,PHP 中的数组索引从零开始。每个子元素都有一个属性数组❷,它是关联数组,意味着它包含键值对。键是属性名,在这种情况下是name。
最后,如果标签内部存在标签,它们会被列出。在这种情况下,第二个子标签包含一个孙子标签❸。这个标签只包含文本,所以它也被表示为一个键值对。如果它包含下层的标签或属性,我们就会有另一个SimpleXMLElement。再次强调,SimpleXML类主要是为了在 PHP 对象中找到表示 XML 的方法。
更简单的 XML 与 XPath
在基本情况下,如果 XML 文件较短且不包含深层嵌套的标签,遍历SimpleXML对象是可行的。如果你被大量的 XML 内容淹没,你可能会发现使用 XPath 查询更简单。
与 XML 一样,XPath 是一个网络标准。您可以使用 XPath 遍历 XML 到您想要的数据。您需要做的就是调用 SimpleXML 对象上的 xpath 函数,并告诉它要访问的“路径”。
以下三个示例都找到了相同的元素,即孙子标签,它嵌套在两层层次结构中。
您可以使用元素的完整路径:
$xmlobj->xpath("/root/child/grandchild")
或者,在前面加上一个双斜杠以获取所有孙子标签,无论它们周围有什么标签:
$xmlobj->xpath("//grandchild")
或者混合匹配。在这里,我们获取任何位于子标签下的孙子标签:
$xmlobj->xpath("//child/grandchild")
XPath 可以帮助您以更多方式快速访问 XML,例如查询特定值,但这里不会涵盖它们。您可以在 php.net/simplexml 上了解更多关于 XPath 和 SimpleXML 的信息。
我已经向您展示了几个访问 XML 的方法:JavaScript、jQuery 库和 PHP。您使用什么取决于您从哪里获取 XML,XML 的复杂程度以及您已经使用的语言。
或者,您可能会厌倦解析 XML 并将其移植到您的 JavaScript 地图。许多程序员更喜欢直接使用一种称为 JSON 的格式,这种格式更接近真正的 JavaScript。继续阅读以了解该格式,并查看在 #57: 将 XML 转换为 JSON 中的 #57: 将 XML 转换为 JSON,了解如何将 XML 转换为更易于使用的 JSON。
#53: 使用 JSON
随着 JavaScript 在网络上的日益流行,JSON 正迅速成为开发者的首选数据格式。这是因为 JSON 代表 JavaScript Object Notation,在 JavaScript 中使用它几乎不需要解析。此外,JSON 比表示相同数据所需的 XML 字符少,因为它没有闭合标签。
您不受任何一种语言的限制。您可以在许多服务器端编程语言中解析 JSON。我将在稍后使用 PHP 举一个例子。大多数现代语言都有一种数据结构,使得将 JSON 转换变得容易。这,加上 JavaScript 的流行,使得这种格式在数据交换中得到了广泛的应用。
关于 JSON 的有用性就说到这里:让我们看看 JSON 的一个例子。以下显示了上一个项目中 XML 的表示方式:
❶ {"child": [
❷ {"attributes": {"name": "first"}},
❸ {"attributes": {"name": "second"}, "grandchild": "text goes here"}
]}
这个基本示例比它需要的复杂一些,但它展示了数据在 JSON 中可以组织的多种方式。构建块是一系列键值对,位于大括号内,在 JavaScript 中称为对象。乐趣在于值的定义。
在这个例子中,我们的主要对象 ❶ 只有一个键,child。值是一个数组,由括号声明。数组本身可以包含一系列值。在这种情况下,值是更多的对象。
数组中的第一个对象 ❷ 包含一个单独的键,attributes,以及它内部的另一个对象。最终,现在有三个级别的新的对象包含一个键 name 和值为 first。数组中的第二个对象 ❸ 有一个类似的第一键值对,然后是一个第二键,grandchild,它有一个文本值。
因此,一个值可以是一个数组、另一个对象或纯文本。它也可以是一个数字、布尔值或 null,尽管我没有在这个例子中展示这一点。
你是否被“值是什么”的循环定义弄糊涂了?这种复杂性是有意为之的,但实际上它实际上是一种表达多种类型数据简单的方法。因为一个对象可以包含数组、对象,甚至是其他对象的数组,所以许多类型分层的数据可以用 JSON 在非常小的空间内表达。
现在你已经了解了 JSON 的样子,让我们开始使用它。
使用 JavaScript 和 jQuery 解析 JSON
你还记得 JSON 代表什么吗?JavaScript 对象表示法。这种数据格式不仅是为了 JavaScript 而创建的,也是从它那里发展而来的。
如果你将 JSON 直接硬编码到 JavaScript 中,你不需要做任何事情来使用它内部的数据。它已经准备好了。在这里,我们使用 JavaScript 访问示例 JSON 中的第一个子元素:
`var obj =` {"child": [
{"attributes": {"name": "first"}},
{"attributes": {"name": "second"}, "grandchild": "text goes here"}
]}`;`
`alert(obj.child[0].attributes.name);`
我添加了粗体的代码部分。否则,这段代码就是之前提到的确切 JSON。我所做的就是将它赋给一个变量(obj),用分号(;)结束声明,然后从对象中警报一个特定的值。
当然,JSON 很可能不会直接写入 JavaScript。相反,你可能会从 API 接收它。换句话说,你可能会以文本形式拥有 JSON。
如果你信任数据,你可以使用 JavaScript 的 eval 函数将 JSON 从文本转换为对象。然而,确保你有良好的数据是一个明智的想法,因为 eval 将执行任何 JavaScript 文本,而不仅仅是 JSON 格式的文本。
为了避免可能的大型安全问题,一些浏览器中添加了 parseJSON 函数。但这个函数只有在它在每个浏览器中都有效时才真正有用。你可以在等待每个浏览器都支持最新的 JavaScript 版本的同时,使用 json.org/ 上可用的 JavaScript 文件来填补空白。
另一个选项是使用 jQuery JavaScript 库,它有一个简单的方法通过 Ajax 获取数据。实际上,你可以在 jQuery 中单行检索和解析 JSON。
将这些行添加到一个新的 HTML 文件中:
<html>
<head>
❶ <script src="http://ajax.googleapis.com/ajax/libs/
jquery/1.3/jquery.min.js"></script>
<script>
$.getJSON(❷"example.json", ❸function(jobj) {
alert(❹jobj.child[0].attributes.name);
});
</script>
</head>
<body>
</body>
</html>
要访问许多有用的 jQuery 函数,我们需要包含 jQuery JavaScript 文件 ❶。虽然你可以从 jquery.com/ 下载此文件到你的服务器,但你也可以引用一个由 Google 托管的版本,就像我在这里所做的那样。
jQuery 为你做了很多困难的工作,使得编写执行高级功能的非常短的 JavaScript 成为可能。它减少代码的一种更明显的方式是引入美元符号对象。jQuery 中发生的大部分事情都通过$进行。
例如,我们使用 jQuery 的$.getJSON函数来创建一个 Ajax 调用,下载并解析一个 JSON 文件。我们需要提供的重要信息是 JSON URL ❷。这个 URL 可以是一个本地文件,或者是对外部 API 的调用。
接下来,jQuery 需要一个函数引用。在这种情况下,我们使用一个内联的匿名函数 ❸ 来描述我们想要对 JSON 结果做什么。同样,jQuery 关于减少代码,但理解这里发生的事情仍然很重要。Ajax 获取我们的 JSON,然后将其解析为对象。该对象返回给匿名函数,我们可以在其中做任何我们想做的事情。在这种情况下,我创建了一个带有第一个子元素名称的警告 ❹,就像当数据是硬编码时一样。
注意
如果你调用返回 JSON 的外部 API,出于安全原因,该 API 需要接受一个回调函数名称。要查看这个功能的示例,请查看我在第六十九部分:创建天气地图中如何从 Yahoo! Pipes 检索 JSON 的#69: 创建天气地图。
在 jQuery 的基础上构建可以节省你的时间,并允许你专注于映射项目的高级问题。你也会得到一个额外的复杂性层,因为你需要在 HTML 中包含更多的 JavaScript。希望它的好处可以弥补这个加载时间上的小成本。
使用 PHP 解析 JSON
有时候你只需要服务器上的数据。如果数据是 JSON 格式,你将无法使用 JavaScript,因为它几乎总是写在网页浏览器中。尽管如此,大多数语言都可以轻松读取 JSON,所以你会发现它是一个在服务器上以及客户端上使用都合理的格式。
我将再次使用 PHP 作为示例服务器端编程语言,因为它在大多数网络主机上都是可用的。如果你是 PHP 的新手,我在第九章中提供了 PHP 用于地理项目的介绍。
在这个项目的开始阶段,我说过大多数语言都有类似 JSON 的数据结构。JavaScript 对象,通过其键值对,在 PHP 中表现为关联数组。同样,PHP 也有标准数组,除了文本和数字字符串。换句话说,所有这些部分都是为了完全表示 JSON。
下面是一些示例 PHP 代码,它声明了我在示例 JSON 文件中使用的数据:
<?
$obj = array("child" => array(
array("attributes" => array("name" => "first")),
array(
"attributes" => array("name" => "second"),
"grandchild" => "text goes here")
));
?>
既然我们知道在 PHP 中表示 JSON 是可能的,我们如何从文本解析到关联数组呢?从 PHP 5 开始,你可以通过单个调用解析 JSON。
这里有一个例子,通过硬编码的 JSON 文本来访问第一个子元素的名字。你的可能来自 API,或者可能是一个文件:
<?
$jtxt = "{\"child\":[" .
"{\"attributes\":{\"name\":\"first\"}}," .
"{\"attributes\":{\"name\":\"second\"}," .
"\"grandchild\":\"text goes here\"}]}";
$jobj = ❶json_decode($jtxt);
print ($jobj->❷child[0]->attributes->name);
?>
是的,所有的工作都交由内部 PHP 函数 ❶ 来执行。与之前我们使用的关联数组不同,json_decode 使用 PHP 对象。这个对象略有不同,但表达数据的方式相似。
如 child ❷ 这样的键是对象的实例变量,并且通过 -> 箭头引用。所有其他类型的数据,包括常规数组,都按原样传递。就像所有其他例子一样,第一个子元素的名字可以在三个级别以下找到。
好奇的读者可能会想知道是否还有另一个函数可以从 PHP 数据结构中创建 JSON 文本。当然!json_decode 的反义词是 json_encode。你可以传递第一个示例中的 $obj 变量或第二个示例中的 $jobj 变量,结果将与存储在 $jtxt 变量中的 JSON 文本相同。
你可能需要比编码更频繁地解码 JSON。话虽如此,当你需要它时,你会很高兴这个函数存在。关于编码 JSON 的例子,请查看 #71: 通过位置搜索音乐活动。
尽管我最近的例子使用了 PHP,但 JSON 是数据格式中的新星,因为它很容易与 JavaScript 集成——JSON 实质上 就是 JavaScript。现在你知道如何安全地读取 JSON 数据,你可能会寻找使用该格式的 API。JSON 使得从数据解析中解脱出来变得容易,这样你就可以做你真正想做的事情:创建充满数据的精彩网络地图。
#54: 使用 GeoRSS
位置只是网络中传递的众多信息中的一小部分。如果你包括它们的意义的上下文,一个点的列表就更有用了。GeoRSS 是一种将位置和其他地理信息添加到内容源的方法,从而创建地理标记的内容。
内容本身通常是博客文章或照片,尽管它可以是一切。博客是地理标记的理想候选者,因为大多数博客已经通过 RSS 源进行聚合,这是一种获取最新帖子而不必访问网站的方法。
虽然 GeoRSS 是以 RSS 命名的,但它可以用于除了 RSS 之外的其他格式。例如,美国地质调查局发布了一个最近的地震 Atom 源,包括每个地震的位置和深度。GeoRSS 可以添加到任何 XML 源,以将地理数据附加到其他内容。
让我们看看 RSS 源中 GeoRSS 的一个例子:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" >
<channel>
<link>http://mapscripting.com</link>
<title>Feed Title</title>
<description>Feed Description</description>
<item>
<pubDate>Thu, 01 Jan 2010 00:01:23 +0000</pubDate>
<title>Item Title</title>
<description>Item Description</description>
<author>Item Author</author>
**`<georss:point>45.256 −71.92</georss:point>`**
</item>
...
</channel>
</rss>
大部分文本是标准的 RSS。粗体部分是 GeoRSS 插件,它们为源添加位置数据。在顶部,你需要包含 GeoRSS 命名空间,这允许你使用 `georss:` 前缀为标签。
在这个例子中,我们声明了一个点,这是一个地理坐标。在标签内,我们首先放置纬度,然后是一个空格,接着是经度。有时你可能会看到数字之间有一个逗号。两者都是允许的。
GeoRSS 也有几种声明形状的方式。这些形状由多个点组成,通常代表路线、边界或其他边界。GeoRSS 将它们称为线条、多边形和矩形。
线条和多边形都声明为纬度和经度点的序列:
<georss:line>45.256 −110.45 46.46 −109.48 43.84 −109.86</georss:line>
<georss:polygon>
45.256 −110.45 46.46 −109.48 43.84 −109.86 45.256 −110.45
</georss:polygon>
正如这个例子所示,一条线至少有两个坐标,但可以有更多。这样,一条线可以描述一条路线。
多边形也是以类似的方式声明的,但最后一个点必须与第一个点相同。换句话说,多边形是一个环形路线。例如,它可以用来描述房屋的外墙或国家的边界。
另一方面,一个矩形将始终创建一个矩形形状,并且只需要两个坐标来声明:
<georss:box>42.943 −71.032 43.039 −69.856</georss:box>
如果你感到困惑,那没关系。矩形有四个角,所以箱子不应该是四个坐标吗?这就像 Mapstraction 的 `BoundingBox`,在 #19: Draw a Rectangle to Declare an Area 中有所介绍。GeoRSS 只使用两个角来确定箱子的位置。你需要的最小数据是西南角和东北角。从这两个点,你可以外推西北角和东南角。`
现在你对 GeoRSS 有了一些了解,让我们在另一种类型的源中使用它。以下是在 Atom 格式中 GeoRSS 的一个示例:
<?xml version="1.0" encoding="utf-8"?>
<feed
``> <title>Feed Title</title> <updated>2010-01-01T00:01:23Z</updated> <author> <name>Feed Author</name> <email>feedemail@example.com</email> </author> <id>tag:mapscripting.com,2009-01-01:feedid</id> <entry> <title>Entry title</title> <link href="http://example.org/entry_link"/> <updated>2010-01-01T00:01:23Z</updated> <summary>Entry summary</summary> **`<georss:point>45.256 −71.92</georss:point>`** </entry> ... </feed>``
`Atom 是 RSS 的替代品,并且是一个广泛支持的格式。正如你所见,对我们来说,它与 RSS 非常相似。同样,GeoRSS 的部分被加粗。`
`你通常会在 RSS 和 Atom 格式中看到 GeoRSS。然而,我展示的却是 GeoRSS 的简单版本。格式有时看起来略有不同,但它仍然是 GeoRSS。继续阅读以了解一些替代 GeoRSS 编码的示例。`
`使用替代 GeoRSS 编码`
`在上一节中展示的 GeoRSS 对于大多数需求来说是足够的,并且可能是你遇到的最常见的编码方式。然而,了解其不足之处并认识到其他表示位置数据的方式是很重要的。`
`*GML* 是 *地理标记语言*,并且是 GeoRSS 的超集。GML 是为了表达任何形式的地理信息而创建的,包括拓扑和除我们一直在使用的纬度/经度系统(称为 WGS84)之外的其他坐标系统。`
`为了让你的 GeoRSS 与 GML 兼容,你需要额外的标签。例如,声明一个点所需的单个标签变成了三个标签:`
`<georss:where> <gml:Point> <gml:pos>45.256 −71.92</gml:pos> </gml:Point> </georss:where>`
``The data communicated with these tags is the same as the simple GeoRSS example. The additional tags are not extraneous, but included because GML allows for more specific uses. For example, the `<gml:Point>` tag is where you would declare another coordinate system.``
``GML equivalents of the geometric objects used in the simple GeoRSS are available. The method for polygons, lines, and boxes is similar to points. The GML code is wrapped in `<georss:where>` tags. You can find out more about all the options at [`www.georss.org/gml`](http://www.georss.org/gml).``
`无论何时你编写包含冒号命名的标签的 XML,你都需要确保冒号前的单词(命名空间)在 XML 的顶部被声明。因为这个例子使用了 GeoRSS 和 GML,我们需要包含这两个命名空间:`
``Add this code inside the root tag of your XML. For RSS, the root is `<rss>`, and for Atom, it is `<feed>`. Both are necessary because the GML version of GeoRSS uses `georss:` and `gml:` tags.``
`到目前为止展示的两种 GeoRSS 格式是你在新数据源中最可能遇到的编码方法。尽管旧版本仍在一定程度上被广泛使用。`
`基本地理词汇表是由万维网联盟(W3C)开发的编码,该组织负责监督 HTML 和 CSS 等其他标准的发展。GeoRSS 的开发使得 W3C 的地理标签变得过时,但你经常会遇到它们,因此需要能够识别它们。`
`<geo:lat>55.701</geo:lat> <geo:long>12.552</geo:long>`
``The biggest difference between the W3C geo-tags and the ones shown earlier is that the latitude and longitude is declared separately. Because this encoding is popular, this method is yet another allowed to write GeoRSS. You'll need a different namespace to be able to use the `geo:` tags, however:``
`` `Now that you know about the many encodings of GeoRSS, let's see how to use GeoRSS directly with your map.` ``
`在地图上显示 GeoRSS`
`Mapstraction 使得将 GeoRSS 添加到地图变得简单。通过一个单一的功能,你可以将 GeoRSS 层叠到地图上,而无需自己解析 XML。`
`要显示 GeoRSS,你只需要一个公开可访问的源和一张可以显示它的地图。将这些行放入一个新的 HTML 文件中,以查看 GeoRSS 的实际应用:`
``<html > <head> <title>Example GeoRSS Map</title> <script src="*`http://maps.google.com/maps/api/js?`* *`sensor=false`*" type="text/javascript"></script> <script src="mxn.js?(googlev3)"></script> <style> div#mymap { width: 550px; height: 450px; } </style> <script type="text/javascript"> var mapstraction; function create_map() { mapstraction = new Mapstraction('mymap', 'google'); **`mapstraction.addOverlay(`** ❶ **`"http://mapscripting.com/example-georss.xml", ❷true);`** } </script> </head> <body onload="create_map()"> <div id="mymap"></div> </body> </html>``
``Most of this is standard map code; the important lines are in bold. You can see we use Mapstraction's `addOverlay` function. The first argument is the GeoRSS URL ❶. This address must be available on the public Web, not on your local computer or a password-protected development server. The reason the feed has to be accessible is the underlying mapping provider will make an Ajax call to load the feed. The mapping provider can't make the call if it can't access the URL. If you don't have your own feed, you can use my example from the companion website.``
``The second argument ❷ is optional. This argument is a boolean, meaning the value is either `true` or `false`. It controls whether the map is auto-centered and zoomed in to show only the GeoRSS content.``
`将之前的 HTML 文件加载到你的浏览器中,你应该能在地图上看到 GeoRSS 内容。如果你使用的是我的示例,你将看到跨越波特兰几座桥梁的路线,以及标识地标的位置标记。`
`现在你对 GeoRSS 格式及其三种编码方式有了更多的了解。在本节中,我展示了它们如何在 RSS 和 Atom 这两种最流行的网络订阅格式中使用。此外,你还学会了如何在 Mapstraction 代码的一行中,将你的 GeoRSS 数据叠加到地图上。要查看 GeoRSS 的一个示例,请阅读 #70: Display Recent Earthquakes Worldwide。`
````# #55: Use KML Google Earth, a three-dimensional geographic browser, popularized KML as a language to share geo-data. The acronym *KML* stands for *Keyhole Markup Language*, named after the company (acquired by Google) who invented it. Nevertheless, KML is an open standard based on XML. KML stores single locations, lists of points, and polygon shapes, among other features. The biggest factor that separates KML from other geographic data formats is that KML can also include styling information, so you can stipulate the color of lines or use custom marker icons. KML has a special schema, meaning elements are declared in a specific way. Here's a very basic KML file, containing one location, called a *Placemark*: ``` ❶ <?xml version="1.0" encoding="UTF-8"?> ❷ <kml > <Document> ❸ <Placemark> ❹ <name>Eiffel Tower</name> ❺ <description>The most recognizable place in Paris</description> <Point> ❻ <coordinates>2.29293460923931,48.85819570061303,0</coordinates> </Point> </Placemark> </Document> </kml> ``` As you put your KML files together, you can view them in Google Earth or on the Google Maps website, as long as the KML is accessible on the web. Try viewing this example at [`maps.google.com/?q=http%3A//mapscripting.com/example.kml`](http://maps.google.com/?q=http%3A//mapscripting.com/example.kml). Now let's examine what's inside that example KML file. As with every XML file, a KML file starts with the XML declaration ❶. Then the file points to the KML namespace ❷ to clearly specify we're speaking a particular XML language. With those technical bits out of the way, we can dive into the actual KML content. The geographic data in a KML file all falls within the `<Document>` tag. Inside that, I add a `Placemark` ❸, which will contain location and other data for a single place. Each Placemark has a name ❹, which is essentially the equivalent of a title in GeoRSS. Similarly, each Placemark also has a description ❺, which can be plain text (as shown) or HTML (with `<` and `>` brackets written as `<` and `>`). Arguably the most important piece of data for a Placemark is the actual geographic point. To declare this, we use a `<Point>` tag and then include the coordinates ❻ within it. Note we include three numbers, as opposed to the usual two. The last number represents altitude and is actually optional. We could increase the number, for example, if we wanted our Placemark to declare the top of the Eiffel Tower. One final, important note about the first two coordinates: Unlike most other geographic data formats, *KML lists longitude before latitude*. This setup is easy to recognize in examples like this and in North America, where longitudes are always negative. But you definitely want to make sure you get these numbers in the right order. A document generally contains multiple Placemarks, but in this simple example, I only use one. Adding another is easy—just include an additional pair of `<Placemark>` tags. Now that you've seen a simple location, let's look at some other ways KML marks up geographic data. ## Lines in KML The single point is the basic feature of geographic data—and that holds for KML, as well. Sometimes a point isn't the best way to describe a place. What single point represents a country or a city? A single point can't; you need many points. This is where lines and polygons become useful. Rather than use a different tag, KML declares both lines and polygons as Placemarks. Unlike the ones shown previously, these Placemarks do not contain a `<Point>` tag because we're actually declaring multiple points at once to represent a single place. Consider this example, which shows the path of the Golden Gate Bridge in San Francisco: ``` <Placemark> <name>Golden Gate Bridge</name> <description>A San Francisco landmark, for sure.</description> ❶ <LineString> ❷ <coordinates> ❸ −122.479485,37.827675,0 −122.477562,37.811028,0 </coordinates> </LineString> </Placemark> ``` I've omitted the tags that declare this a KML document and instead focused on the Placemark. I include a name and description, just as in the single point example. To describe the bridge, we first add some KML to say this Placemark is a line ❶, and then we insert a series of coordinates ❷ to describe the line. Of course, a bridge has two ends, so the line is a very simple one using two points. If this were a more advanced line, such as describing a trail or the route of a race, we would just continue adding coordinates in an order such that they could later be connected by line segments. As in the single point example, we declare three coordinates ❸, listed in longitude, latitude, and altitude order. The altitude is optional, but this coordinate may provide interesting data in some cases, showing the gradient of a trail, for example. This data is hard to show on a two-dimensional web map, but remember KML data is used in other ways, such as in Google Earth. ## Polygons in KML You may recall that in GeoRSS, as well as Mapstraction, a polygon is simply a line that ends at the same point where it begins. The same is true in KML, though the definition can also get a little more advanced with its description of a shape. Let's stick with something simple here and continue with the world landmark theme. Consider this bit of KML that describes the outer edges of the Parthenon in Greece: ``` <Placemark> <name>Parthenon</name> <description>A symbol of ancient Greece.</description> ❶ <Polygon> ❷ <outerBoundaryIs> ❸ <LinearRing> <coordinates> 23.726295,37.971539,0 23.726376,37.971287,0 23.727116,37.971420,0 23.727024,37.971672,0 23.726295,37.971539,0 </coordinates> </LinearRing> </outerBoundaryIs> </Polygon> </Placemark> ``` Notice that inside the polygon declaration ❶ I've include several levels of tags before getting to the familiar coordinates list. The reason for those additional levels is that KML's polygons are much more powerful than this example can communicate. For example, here I've declared an outer boundary ❷ for the Parthenon, meaning I'm describing the outside walls. If I also use an inner boundary, I can create a rectangular donut to show just the columns of the Parthenon. Inside a boundary, I use a linear ring ❸ to tell KML that I am creating a line that ends at its starting point. Here KML's polygon begins to look similar to GeoRSS, but with different syntax. As with the simple line example, the coordinates are a list of longitude, latitude, and altitude (optional) values. We have four corners of the Parthenon to connect, which requires five points. Why not four? The first and the last must be identical—to complete the ring. Now that you understand basic Placemarks, including lines and polygons, let's see where KML diverges from other geographic data formats: let's get stylish. ## Style KML Describing points, lines, and shapes is a basic building block of communicating geography. You can put these three types of data together and get plenty of information about a place. KML also lets you describe how you want the data to look, which separates it from other formats. Read on to learn how to style your KML. If you know how HTML and CSS interact, KML styles will seem familiar. As with HTML, you can create styles inline or reference them globally with declarations at the top of your KML file. A style tag is available for each of the three types of geographic data we've seen so far: `<IconStyle>`, `<LineStyle>`, and `<PolyStyle>`. Within the tag, you can declare an icon graphic, color, line width, and opacity. Here's an example of styles added inside the Eiffel Tower Placemark: ``` <Style> <IconStyle> <Icon> <href>http://mapscripting.com/icons/modernmonument.png</href> </Icon> </IconStyle> </Style> ``` This code is the KML equivalent of #5: Create a Custom Icon Marker in #4: Show and Hide Message Boxes Without Clicking the Marker. However, if you have a document with many points, you could end up with a lot of redundancy if most points share the same marker graphic, which makes declaring styles at the top of the code useful. You can move the entire `<Style>` block up, as an immediate child of the `<Document>` tag. Then, if you give the tag an `id` attribute, you can reference it below. For example, here is the Parthenon example filled in with white: ``` <?xml version="1.0" encoding="UTF-8"?> <kml > <Document> `<Style id="stoneBuilding">` `<LineStyle>` ❶ `<color>cccccc</color>` `</LineStyle>` `<PolyStyle>` `<color>ffffff</color>` `<fill>1</fill>` ❷ `<outline>1</outline>` `</PolyStyle>` `</Style>` <Placemark> <name>Parthenon</name> <description>A symbol of ancient Greece.</description> ❸ `<styleUrl>#stoneBuilding</styleUrl>` <Polygon> <outerBoundaryIs> <LinearRing> <coordinates> 23.726295,37.971539,0 23.726376,37.971287,0 23.727116,37.971420,0 23.727024,37.971672,0 23.726295,37.971539,0 </coordinates> </LinearRing> </outerBoundaryIs> </Polygon> </Placemark> </Document> </kml> ``` The portion of the KML that produces the style is in bold. As you can see, most of the styling is already declared by the time we get to the Placemark. Along with the white-shaded polygon, I add a `LineStyle` to give it a light gray outline ❶. Then, I also make sure the `PolyStyle` has outlines turned on ❷ via the boolean (`1` is on; `0` is off). Finally, we reference the styles ❸ from the Placemark itself. This reference is created much like a reference to an `id` within CSS, by prepending a `#` in front of the style `id`. ## Display KML on a Map With your KML file on the Web, you can display it on a map in many ways. Earlier, I showed how you can use the Google Maps website to show KML. You can also open it in Google Earth. In this section, we'll see instead how to layer a KML file into your embedded map using Mapstraction. Start with a brand-new HTML file and add the following code: ``` <html > <head> <title>Example KML Map</title> <script src="*`http://maps.google.com/maps/api/js?sensor=false`*" type="text/javascript"></script> <script src="mxn.js?(googlev3)"></script> <style> div#mymap { width: 550px; height: 450px; } </style> <script type="text/javascript"> var mapstraction; function create_map() { mapstraction = new Mapstraction('mymap', 'google'); `mapstraction.addOverlay(` ❶ `"http://mapscripting.com/example.kml",` ❷`true`); } </script> </head> <body onload="create_map()"> <div id="mymap"></div> </body> </html> ``` Most of this is a pretty basic map. The part that loads the KML file is in bold. Just like for the GeoRSS, we use Mapstraction's `addOverlay` function. The first argument is the KML URL ❶. As I've mentioned, this address needs to be available on the public Web, not on your local computer or a password-protected development server. The reason the feed has to be accessible is the underlying mapping provider will make an Ajax call to load the feed. This call won't work if the provider can't access the URL. If you don't have your own feed, you can use my example from the companion website. The second argument ❷, which is optional, is a boolean, meaning the value is either `true` or `false`. This controls whether the map is auto-centered and zoomed in to show only the KML content. Load this HTML into your browser and you should see the KML content on your map. If you're using my example, you'll see a polyline surrounding the Parthenon. # #56: Use GPX Are you a hiker, a runner, or a mountain biker? You could use the GPS exchange format, GPX, to track your favorite trails and routes. Most modern GPS devices, which use satellites to triangulate their location, can store this data and output it in GPX format. Even if the data did not originate on a GPS device, this format is useful for sharing any sequence of latitude and longitude points. GPX really is just another way to store *polylines*, a series of coordinates to connect on a map. Where the format is different is that it also contains useful metadata to make more sense of the dozens of points. GPX is separated into three types of data: | **Tracks** A record of a particular trip, including the time at each step | | **Routes** A suggested trip meant to be shared with others, which does not include time information | | **Waypoints** A single point, often used for landmarks or other points of interest | From a technical standpoint, GPX is just XML. Its schema is special, however, and elements are declared in a specific way. Here is an example GPX file: ``` ❶ <?xml version="1.0" encoding="UTF-8"?> ❷ <gpx version="1.0" xmlns:xsi="http://www.w3.org/2001 /XMLSchema-instance" xmlns="http:// www.topografix.com/GPX/1/0" xsi:schemaLocation="http://www.to pografix.com/GPX/1/0 http://www .topografix.com/GPX/1/0/gpx.xsd"> <trk> <name>Dog Walk</name> ❸ <trkseg> ❹ <trkpt lat="45.521270" lon="-122.626111"> ❺ <ele>7.125</ele> ❻ <time>2010-09-06T00:14:34Z</time> </trkpt> <trkpt lat="45.521292" lon="-122.625950"> <ele>6.831</ele> <time>2010-09-06T00:14:37Z</time> </trkpt> ... </trkseg> </trk> </gpx> ``` Most XML files begin in a similar way ❶ to show they contain XML. Then the code points to the GPX schema ❷. With the formalities out of the way, let's dive into the actual data. This example is showing a track, so we begin by wrapping everything in a `<trk>` tag. A track has at least one segment ❸, which contains the individual track points ❹. The latitude and longitude are stored as attributes of the `<trkpt>` tag, with the elevation ❺ and timestamp ❻ as subelements. A program going through the track points uses differences in the latitude and longitude to determine the distance between points. Similarly, the number of seconds or minutes between timestamps can be used to determine the approximate speed. The elevation numbers can determine the trail's grade. We can learn a lot from the metadata. ## Examples of GPX In addition to trail enthusiasts plotting and sharing their escapades, GPX has other even more useful applications. GPX is bringing geographic data that wasn't included before to rich content like photos and video. A world of volunteer cartographers also use it to map streets and cities. Walking around snapping shots is something most photographers do with regularity, whether hiking through nature or walking urban streets. By synchronizing the internal times on a GPS and camera, you can get latitude and longitude coordinates for your photos. Digital cameras usually timestamp each photo. If you have a record of a path, like the one stored in a GPX file, cross-referencing the two is straightforward. Just find the track point with a timestamp closest to the photo's. Update the photo metadata to include the coordinates, and you have now geo-tagged the photo. You can install many programs on your computer that will do this for you. OpenStreetMap uses GPX to map the world. It sends volunteers to walk the streets with GPS units. Then, the track points, along with other information like street names, are used to create maps that are available for anyone—without licensing fees. In many countries, such as the United States, much of this street data is already available. OpenStreetMap volunteers, in these cases, are checking accuracy and filling in what's missing. In some places, OpenStreetMap is all there is, so the GPX tracks become incredibly important to the project. ## Display GPX Tracks on a Map Once you have a GPX file, you'll want to do something with it, like show it on a map. Getting at the track points requires parsing the XML and then "connecting the dots" with polylines in Mapstraction. XML parsing can be painful. I discussed it in detail in the earlier in this chapter. We'll use the jQuery method here, which is as easy as it gets, but it does require a small JavaScript library. To start, let's lay the groundwork for the GPX map by preparing the basic HTML, CSS, and JavaScript to display a simple map. Put this code into a new HTML file: ``` <html > <head> <title>GPX on a Map</title> <script src="*`http://maps.google.com/maps/api/js?sensor=false`*" type="text/javascript"></script> ❶ <script src="http://ajax.googleapis.com/ajax/libs/ jquery/1.3/jquery.min.js"></script> <script src="mxn.js?(googlev3)"></script> <style> div#mymap { width: 600px; height: 450px; } </style> <script type="text/javascript"> var mapstraction; function create_map() { mapstraction = new Mapstraction('mymap', 'google'); ❷ mapstraction.setCenterAndZoom(new LatLonPoint(0,0), 2); mapstraction.addControls({"zoom": "large"}); ❸ parse_gpx("gpxfile.gpx"); } </script> </head> <body onload="create_map()"> <div id="mymap"></div> </body> </html> ``` We haven't quite parsed the GPX yet. This code just prepares a basic map. Make sure you include your Google Maps API key. Otherwise, everything is ready to go. Before moving on, however, I'd like to point out a few bits. First, I've included the jQuery JavaScript framework ❶. We'll use it for the Ajax call that will download the GPX file. Also, jQuery makes XML parsing easier, so we'll use it to get at the GPX data. The eventual location for the map will be determined by the latitude and longitude values inside the GPX file. Because we don't know what those are yet, I centered the map in the middle of the globe ❷. Assuming we're able to load data, that location will only stay for a short time. If you know what city the data will be in, a good practice is to use the coordinates of the city center. Finally, I make a call to the `parse_gpx` function ❸, passing a filename. Make sure *gpxfile.gpx* exists in the same directory and has some GPX data. You can find example files at [`mapscripting.com/gpx-files`](http://mapscripting.com/gpx-files). But wait . . . where is the `parse_gpx` function? We haven't added it yet, so let's write it! Add these lines below the `create_map` function: ``` function parse_gpx(filename) { ❶ jQuery.get(filename, {}, function(xmltxt) { var pdata = {"color": "blue"}; var pts = []; ❷ jQuery("trkpt", xmltxt).each(function(i) { var lat = ❸parseFloat($(this).attr("lat")); var lon = parseFloat($(this).attr("lon")); ❹ var thispt = new LatLonPoint(lat, lon); ❺ pts.push(thispt); }); ❻ mapstraction.addPolylineWithData(new Polyline(pts, pdata)); ❼ mapstraction.autoCenterAndZoom(); }); } ``` The `parse_gpx` function is really just a wrapper for the jQuery Ajax call ❶. It grabs the passed filename and returns the XML results to an anonymous, inline function. This is where the real stuff happens. You can see how one of my GPX files looks when added to the map in Figure 8-1.  Figure 8-1. Example GPX tracks from a walk in the park After creating a few variables, the function uses jQuery to look for every `<trkpt>` in the GPX file ❷. For each track point, it calls yet another anonymous function, which is sort of equivalent to a `for` loop. Each time through the loop, the function parses the latitude and longitude of the current track point. The `parseFloat` JavaScript function ❸ takes the text from the GPX file and turns it into the decimal number (also called a *floating point number*) needed. Again, the function uses jQuery to parse the GPX, but it uses the shorthand dollar sign method. Before we draw the track on the map, we need to have all the data points to pass to Mapstraction at the same time, as shown in #16: Draw Lines on a Map in #16: Draw Lines on a Map. We'll use an array to collect the points. Once we have the latitude and longitude of the current track point, we create a `LatLonPoint` with the two values ❹. Then we add the point to the `pts` array ❺. Once we're outside the loop, we pass our data to draw the Polyline ❻ and then zoom the map automatically to show our entire Polyline ❼. Now we have, in the case of most tracks, a tight view of the GPX data. # #57: Convert from XML to JSON As I've mentioned, XML is not always the easiest format to use with JavaScript. Yet, as this chapter has shown, most of the formats you'll be working with are flavors of XML. By now you probably prefer JSON, right? To make things easier on the JavaScript, we'll need to do a little extra work and convert from XML to JSON. A number of ways are available for getting our data from XML to JSON. And really, the conversion is not that difficult an operation. For example, once we load either XML or JSON into PHP, the data is very similar. In this section, I'll show how you can convert on your own server and also introduce you to a nifty service from Yahoo! called Pipes. ## Convert Using PHP Most Unix or Linux servers have a copy of PHP already running, which makes it a great server-side language to learn. We'll use PHP to read in and parse some XML and then turn around and encode it into JSON. The whole process takes just a few lines, thanks to helper functions included in PHP 5. Even if you don't have a server, or PHP is not accessible, you can likely install it on your own machine. For a more in-depth discussion of PHP, you'll want to check out Chapter 9. To start converting, create a new PHP file and add these lines: ``` <? $xmltxt = ❶get_url("http://mapscripting.com/example.xml"); ❷ $xmlobj = simplexml_load_string($xmltxt); $jtxt = ❸json_encode($xmlobj); print $jtxt; ?> ``` Like I said, this code isn't very complicated, is it? To get the XML text, I use a helper function ❶ I wrote, which is explained in #61: Retrieve a Web Page in #61: Retrieve a Web Page. You could also read in the file directly, as we did when parsing XML with PHP. Once we have the XML text, we convert it first to a PHP object ❷. This parses the hierarchy of the XML file into a format PHP can understand directly. Because JSON is so close to data structures found within PHP, a simple call ❸ is all that's needed to encode the JSON. Then we print it and voilà —we have converted data formats. ## Convert Using Yahoo! Pipes Don't have server-side chops, or just don't want to deal with additional code? Many folks are turning to a service from Yahoo! called Pipes. Pipes reads in various data formats, lets you massage the data if you want, and then outputs it as either XML or JSON. Why use Yahoo! Pipes instead of your own server? For one, the conversion process is even easier than using PHP. Plus, no code is required because Pipes has a graphical, drag-and-drop interface. You also get the benefit of Yahoo!'s infrastructure. Yahoo! decides how often to check for new content and deals with caching the most recent copy—something we didn't do at all in the PHP example. The downside to relying on Pipes? It introduces another point of failure. Even if your server is humming along, your map might not work if Pipes crashes. Although you can likely count on Yahoo! for uptime, what if the company decides the Pipes product isn't worth keeping around? You'd be hung out to dry. To me, the ease of using Pipes outweighs the detractions. Let's see just how easy converting from XML to JSON with Pipes is. You'll need a Yahoo! account to store your Pipes. Log in at [`pipes.yahoo.com/pipes`](http://pipes.yahoo.com/pipes) and click **Create a Pipe** to go to a blank canvas, with options on the left. First, Pipes requires a data source, so drag a Fetch Feed source on to the canvas. Paste your feed URL into the box, or use [`mapscripting.com/example.xml`](http://mapscripting.com/example.xml). Now a second box called Pipe Output should also appear at the bottom of the workspace. Drag the circle at the bottom of the Fetch Feed box to the circle at the top of the Pipe Output. This connects the elements to create a complete Pipe, as shown in Figure 8-2. Go ahead and try it and you should see sample output in the debugger at the bottom of the workspace.  Figure 8-2. Yahoo! Pipes feed connected Here's where things get really interesting: *You're done*. Save the Pipe, and then click **Run Pipe**. You should see your content within the Pipes interface. At the top of your content, you'll see a Get as JSON link. That's the URL to your converted data run through Yahoo!'s servers. Now you can order new business cards because you're a certified data plumber! # #58: Filter, Merge, and Sort Data with Yahoo Pipes! APIs and RSS feeds are becoming commonplace. So much of the Web's content is now available in a format that programmers are quickly able to use, which is great. The downside is that another problem has been created: Getting at just the right information can be tough. Sometimes a feed is a fire hose when a garden variety hose would do. Other times you have to combine multiple sources before you get the information you really need. Yahoo! Pipes has an easy interface for solving both issues—by filtering and merging data. Much of this data-munging is stuff programmers have been doing manually using server-side scripts. Sometimes that will still be necessary, but Yahoo! Pipes is able to solve the common scenarios. Plus, for reasons described in the previous project, offloading some of this work onto Yahoo!'s servers provides some major benefits. Before you create a Pipe, you'll need at least one data source. This source is often an RSS feed and, for map-related projects, may be GeoRSS. A Pipe is a way to transform data sources. The data comes in the Pipe, some stuff happens, and then the data flows out of the Pipe. If you have a data source, then let's start working on that "stuff" part. You can begin by editing the Pipe we created in the previous section. ## Filter Your Feed's Content Rather than simply running our feed through Pipes, let's try filtering out certain content. To do this, we'll need to drag a Filter box from the Operators menu. You can place the box anywhere in the workspace, but placing it between the Fetch box and the Output box may make the most sense. Next, you'll need to connect the feed to the filter. Drag the circle at the bottom of the Fetch box to the top of the Filter box. Then connect the Filter box to the Pipe Output by dragging the filter's bottom circle to the output's top circle. Of course, the filter isn't useful unless it's actually filtering some content. You can do this two ways with Pipes: You can filter *in* or filter *out*. At the top of the Filter box, you can select **Block** to filter out and **Allow** to filter in. Pipes can filter based on any fields in the feed. A common choice is the title, which, for RSS, is *item.title*. Click the arrow next to the first text area in the filter box (see Figure 8-3) and you'll see a list of available fields. Then you can type the words you want to filter in the second text field. The drop-down box lets you perform some basic types of filtering on the field, including greater-than/less-than for numbers.  Figure 8-3. Filtering out items with *map* in the title If you want additional filters, just click the plus sign next to Rules. Otherwise, your Pipe is complete. Save it, and then click **Run Pipe**. Your filtered feed will be shown within the Pipes interface. To get this new feed, choose **Get as RSS** or **Get as JSON**. If you are reading the feed in to use with JavaScript, JSON is probably your best choice. ## Merge Two or More Feeds What if you have two similar feeds that you want to combine? Pipes is very good at this! Create a new Pipe and drag two Fetch Feed boxes to the canvas (see Figure 8-4). Choose two feeds (if you're short of examples, you should be able to find an RSS feed from your favorite blogs or websites) and insert their URLs into the Fetch Feed boxes.  Figure 8-4. Merging and sorting two feeds with Yahoo! Pipes If the feeds are of the same variety and you don't plan to filter anything, you can actually use a single Fetch Feed box. Just click the plus sign next to URL. In many cases, you'll want to have the option to perform more advanced operations, so I recommend using a Fetch Feed for each individual feed. To combine the two feeds, we'll need a Union box from the Operators menu. Drag the Union box to the canvas below the two Fetch Feed boxes. Drag the circle below each feed to one of the five circles at the top of the Union box. Finally, drag the circle at the bottom of the Union box to the Pipe Output. Your feeds are combined in the order they were added to the Union box (left to right). In other words, the second feed's content is only seen after the entire contents of the first feed. This arrangement is not ideal. Most likely, you usually want to see the content in the order it was published. Pipes can do the sorting for you. Drag a Sort operator to the canvas. Connect the bottom circle of the Union box to the top of the Sort box. Then connect the bottom of the Sort box to the Pipe Output. You'll need to choose a field to sort by clicking the arrow next to the text field within the Sort box. To use the date, select **item.pubDate**. Now save the Pipe and you're done. You've now filtered, merged, and sorted with Yahoo! Pipes. You can transform data into whatever you want it to be. In fact, if you dig through the documentation a bit ([`pipes.yahoo.com/pipes/`](http://pipes.yahoo.com/pipes/)), you'll realize Pipes is even more powerful than the few examples I've shown. You can load feeds dynamically, use web services, and even extract location from text. Best of all, when you're done, the data is in a format that is easy to read in and plot on a map.````
第九章:GO 服务器端

地图 API 之所以受欢迎,是因为它们为网页浏览器带来了许多以前对普通开发者来说更难实现的功能。然而,仍然存在许多情况,你的应用程序将从运行在浏览器外的脚本中受益。这就是你需要转向服务器端的时候。
在本章中,我们将探讨两种在服务器上运行的技术:PHP,一种主要用于输出网页的编程语言;以及 MySQL,一个用于存储文本、数字和更多内容的数据库。与众多其他可用的技术相比,我选择了这两种技术,因为它们非常普遍——几乎所有的网络主机都支持它们。它们也是由包括 Yahoo!在内的许多公司使用的稳固平台。
已经有整本书专门介绍了 PHP 和 MySQL,包括它们各自的内容。我将只提供一个非常快速的基础知识,以帮助你入门。然后我们将深入探讨每个如何帮助你制作更好的地图,包括存储位置和从数据库中查找最近的位置。
#59:安装 PHP
PHP 是一种极为流行的编程语言,可以轻松创建动态网页和应用。你可以在电脑上安装它进行测试,或者在服务器上安装以供全世界共享。实际上,如果你在服务器上拥有网络托管服务,PHP 可能已经安装了。
你需要将 PHP 与一个网络服务器一起使用,其中最常见的是 Apache。请求从网页浏览器发送到你的网络服务器,然后服务器会查看请求的页面是否使用了 PHP。如果是,那么页面就会被发送到 PHP 进行处理。
接下来,我将展示几种安装 PHP 或确定其是否已安装的方法。只有在你需要安装 Apache 的情况下,你才需要担心安装 Apache。继续阅读,我们将帮助你快速掌握 PHP。
检查你的网络主机是否安装了 PHP
如果你已经拥有网络托管服务,那么你几乎肯定已经安装了 PHP。最简单的方法是创建一个简单的 PHP 文件,然后尝试用网页浏览器访问它。
你会希望将这个新文件放在你已知可以访问 HTML 文件的地方。你的网站管理员(他也可以确认你是否能够访问 PHP)可以指给你你的公共目录,其中存储着网页。
在你的网络目录中创建一个新的 PHP 文件,并添加以下几行:
<?php
print phpinfo();
?>
很简单。给文件起一个以.php结尾的名字(我创意性地选择了test.php),然后从你的网页浏览器中,访问其公共网络地址。例如,我可能会访问mapscripting.com/test.php。
如果您在加载该文件时看到自己的代码,那么您可能没有安装 PHP。您需要要求管理员安装它或找到另一种使用 PHP 的方法。然而,如果您看到一个像 图 9-1 中所示的信息页面,那么您就会知道 PHP 已安装。完成操作后,请记住从您的服务器上删除该文件。

图 9-1. 我本地机器输出的 PHP 信息
如果您是 PHP 新手,您可能想跳到 第六十部分:快速 PHP 介绍,在 自行安装 PHP 中。否则,您需要检查是否已安装 MySQL(参见 第六十二部分:安装 MySQL,在 在其他脚本中包含您的函数),或者将 PHP 连接到 MySQL(参见 第六十五部分:从 PHP 使用 MySQL,在 第六十五部分:从 PHP 使用 MySQL)。
使用 PHP 的打包安装
在您自己的计算机上安装 PHP 的最简单方法是下载适用于您操作系统的包。这种方法的好处是您还将同时获得 Apache 和 MySQL,这很有用。
这些包的名称是根据流行的服务器架构 LAMP 命名的,它代表 Linux, Apache, MySQL, 和 PHP*:
-
Windows (WAMP):
www.wampserver.com/ -
Macintosh (MAMP):
www.mamp.info/
这两个包中的任何一个都应该可以在您的计算机上安装,而无需从您那里获取太多信息(尽管您可能需要输入管理员密码)。
安装过程完成后,打开 WAMP/MAMP。程序应该打开两个窗口。首先,它将打开一个小的状态仪表板,如图 图 9-2 所示。其次,应用程序还将创建一个新的指向 localhost(您计算机上的服务器名称)的网页浏览器窗口。默认情况下,您将看到一个欢迎屏幕。

图 9-2. MAMP 控制台
您可能希望更改 WAMP/MAMP 查找文件所使用的目录。为此,请单击 首选项 按钮,这将打开一个带有更多选项的新面板。单击 Apache(Web 服务器)选项卡,然后选择一个新的文档根目录,如图 图 9-3 所示。

图 9-3. 更改 MAMP 文档根目录。
自行安装 PHP
如果你使用的是一个没有提供软件包的操作系统,你需要自己安装 PHP。由于存在太多的变体,本章中无法描述,但互联网上有许多教程可以帮助你。
PHP 网站是开始的地方:php.org/。
#60: PHP 快速入门
有些非常厚的书籍——比这本书厚得多——专门用于教你如何用 PHP 编程。这个项目将提供必要的推动力,以便你在一些地图项目中使用 PHP。然而,它不会使你成为大师。
在接下来的章节中,你将学习关于结构化 PHP 代码的非常基础的知识。我还会向你展示如何使用条件语句,通常称为 if 语句。接下来,你将学习如何处理数组,然后使用循环。最后,我将展示如何创建你自己的函数以减少需要编写的 PHP 代码量。
精细之处
PHP 代码通常被添加到以 .php 扩展名结尾的文件中。当程序运行完成后,输出通常是 HTML(尽管它也可以是 XML、JSON 或任何可以通过 HTTP 传输的文本)。因此,PHP 可以与纯 HTML 混合使用。
代码中的 PHP 部分被特殊的 双字符块 <?php 和 ?> 包围。例如,尝试以下一小段代码,它结合了 PHP 和 HTML:
<html>
<body>
<?php
print "This text comes from PHP!";
?>
</body>
</html>
这段代码是缩略的 HTML。但从中,你应该能够了解 PHP 的工作原理。并且代码将加载到浏览器中,并简单地显示引号内的文本,这是由 PHP 的 print 函数输出的。注意分号——大多数 PHP 行都以分号结束。然而,存在例外;如果你在一行中写一个命令,那么它很可能需要在末尾加上 ;。
变量存储可以更改或稍后访问的值。它们可以存储数字、文本等,并且通常有一个描述性的名称,总是以美元符号开头。例如,$mynum 可以是一个用于存储单个数字的变量。
这里有一些与之前代码类似的代码,但包含了一些变量:
<html>
<body>
<?php
❶ $msg = "This is text";
❷ $mynum = 5 + 1;
❸ $msg = $msg . " and a number: " . $mynum;
print $msg;
?>
</body>
</html>
当你运行这个示例时,它会生成以下输出:
This is text and a number: 6
代码从一个文本变量 ❶ 开始,通常称为 字符串。这个变量开始了一行,最终将成为代码的输出。接下来,你创建一个数字变量 ❷。我本可以直接将这个变量设置为 6,但我想要展示如何将两个数字相加。
在输出消息之前的最后一行 ❸ 中,我将字符串设置为等于自身,然后将其与更多文本连接起来,最后将数字附加到末尾。请注意,为了连接字符串,你使用点号,但为了连接数字,使用加号。
如果我们将第 ❷ 行中的加号替换为点号,则消息末尾输出的数字将是 51(5 后跟 1,而不是数字 5 加上数字 1)。如果我们替换第 ❸ 行中的第一个点号为加号,整个消息将是 06,因为文本的数字表示是零。
使用正确的操作符来组合数字和文本非常重要,因为所使用的操作符可以极大地改变你的输出。
获取输入
你可以通过两种方式从用户那里获取输入:从 URL 本身或通过表单。无论你选择哪种可能性,在 PHP 中访问这些数据的方法都非常相似。
如果输入是通过 URL 传递的,你会看到类似这样的内容:
yourfile.php?msg=Hello+there&num=42
要获取这些信息,你需要使用一个特殊的 PHP 变量,称为 $_GET。$_GET 是一个关联数组,它是一组通过方括号语法访问的键值对。通常,你希望从这个数组中取出值并将其放入一个新的变量中。
例如,以下是一些用于访问两个输入的 PHP 代码:
<html>
<body>
<?php
if ($_GET["msg"] != "" && $_GET["num"] != "") {
❶ $msg = $_GET["msg"];
❷ $mynum = $_GET["num"];
❸ print "Your message is " . $msg . " and your number is " . $mynum;
}
?>
</body>
</html>
这里是这个 PHP 程序的输出:
Your message is Hello there and your number is 42
为了获取消息❶,我把在 URL 中使用的键(即 msg=Hello+there)放在 $_GET 变量后面的方括号内。
注意
变量的名称不需要与 URL 中使用的名称匹配,尽管它们可以。
现在,我想获取数字,这是 URL 中的 num=42 部分。我使用与之前代码❷相同的方法,即使这是一个数字。最后,我打印出这两个新变量以及一些描述性文本❸。注意输出部分使用的点用于连接。
从表单中检索数据与从 URL 中检索数据非常相似。你使用变量 $_POST 而不是 $_GET。唯一棘手的部分是表单必须使用 HTML 中的 action 属性发送到这个页面。
如果这是真的,那么就做那件事
你的 PHP 程序需要做出决定。因为它们不能自己做出决定,你需要告诉它们如何决定要做什么。你这样做是通过使用一个 条件——或者 if 语句。
我们将基于之前的例子,其中我们在 URL 中接收输入,但我们将只使用数字部分。我们想查看这个数字,如果这个数字是两位数(10 或更大),则提供不同的输出。
我们的 URL 将看起来像这样:yourfile.php?num=42。
以下是包括我们两个条件的代码:
<html>
<body>
<?php
❶ $mynum = $_GET["num"];
if (is_numeric($mynum)) {
❷ if ($mynum < 10) {
print "Your number is less than 10!";
}
❸ else {
print "You have double digits!";
}
}
?>
</body>
</html>
首先,我们获取通过 URL 传递的数字❶。这个数字存储在一个变量中,现在可以在程序的其余部分中使用。我们使用这个变量来创建一个 if 语句❷,该语句检查数字是否小于 10。如果是,则输出相应的消息。
注意在 if 语句之后的花括号 { 和 }。这些花括号包含了当 if 语句为真时运行的代码。否则,将运行 else 后花括号中的所有代码。你可以有一个没有 else 的 if,但在这个例子中,我们想在两种情况下都做些事情,而不仅仅是其中一种。
表 9-1. 比较操作符
| 操作符 | 描述 | 使用情况 |
|---|---|---|
| == | 等于 | 数字或文本 |
| != | 不等于 | 数字或文本 |
| > | 大于 | 主要用于数字 |
| < | 小于 | 主要用于数字 |
| >= | 大于等于 | 主要用于数字 |
| <= | 小于等于 | 主要用于数字 |
比较变量时,有六个运算符,如表 9-1 所示,可以使用这些运算符。运算符主要用于比较数字,但也可以用于比较文本。
数组真多
到目前为止,我已经展示了两种类型的变量:文本和数字。本节还将介绍两种其他常见类型。
第一种是数组。数组是一个包含许多其他变量的单个变量,因此你不必单独命名它们。简单的数组通过索引跟踪它持有的值,索引从零开始计数。你可以这样声明一个数组:
$beatles = array("John", "Paul", "George", "Ringo");
在这个例子中,我使用了文本。数组也可以包含数字,或者数字和文本的组合。它还可以包含其他数组和对象,但让我们现在保持简单。注意array函数可以接受任意数量的值,用逗号分隔。
这里是打印乔治(第三个选项)的代码行:
print $beatles[2];
注意索引是 2,而不是 3。这是因为索引从 0 开始。访问值时,索引位于方括号[和]内。
我还想向你展示另一种类型的数组,称为关联数组。它不是使用索引来跟踪数组内的值,而是使用文本键。
例如,你可以通过以下方式使用关联数组访问 George:
print $beatles["guitarist"];
我们仍然使用方括号表示法,但不是使用我们的索引,而是使用我们的文本键。以下是关联数组可能声明的示例:
$beatles = array("singer" => "John", "bassist" => "Paul",
"guitarist" => "George", "drummer" => "Ringo");
与标准数组一样,项目在array函数内部通过逗号分隔。然而,在这种情况下,使用key => value对来声明如何访问数组中的每个项目。
在下一节中,我将展示如何遍历数组值。
感觉到循环了
如果你想多次执行相同的事情,你可以通过使用循环结构来实现。本节将包括 PHP 中的for和foreach循环。我们将遍历数组中的每个值并输出其内容。
考虑这个简单的数组:
$bandmates = array("Simon", "Garfunkel");
现在让我们遍历它并打印出每个名称:
$upperBound = count($bandmates) - 1;
for ($idx = 0; $idx <= ❶$upperBound; ❷$idx++) {
❸ print $bandmates[$idx];
❹ if ($idx < $upperBound) {
print " and ";
}
}
当你运行此代码时,它将打印出乐队的名称:
Simon and Garfunkel
我们通过创建一个索引变量,从零开始,来完成这个操作。程序会遍历循环,直到那个索引变量不再小于数组中的项目总数 ❶。递增运算符++用于每次将索引增加 1 ❷。
打印名称仅意味着访问数组变量中的当前索引 ❸。然后,它还打印一些文本在名称之间,但只有当索引小于总数减一 ❹时。因为总数是 2,这只会发生在第一次遍历循环时,当零小于一。
如果你将你的姓氏作为$bandmates变量的第三个项目并再次运行代码,你会看到另一个and被加入(并且你将自己在乐队中)。
让我们使用另一个循环来遍历一个关联数组。我们将保持相同的乐队,但将注意力集中在他们的头发上。它是直的还是卷的?考虑这个关联数组:
$bandhair = array("straight" => "Simon", "curly" => "Garfunkel");
现在让我们遍历这个数组,分享我们关于他们头发的知识:
foreach ($bandhair as $description => $name) {
print $name . " has " . $description . " hair. ";
}
最后,这是输出:
Simon has straight hair. Garfunkel has curly hair.
在这种情况下,我们正在访问关联数组中每个元素的键和值。然后,在每一步,我们都会打印出它们。
获得功能
在 PHP 中创建自己的函数可以节省你输入的时间。你还会发现这是一种良好的实践。创建 PHP 函数的方式与 JavaScript 非常相似,所以你可能比你想象的更接近能够使用自己的函数。
对于这个例子,我们将创建一个函数,它接受一个数字作为输入,并返回该数字的四倍。这个函数可能不是特别有用,但它足够简单,可以用来教授如何在 PHP 中工作函数。
在你的 PHP 括号<?php和?>中,包含以下代码:
function quadruple(❶$somenum) {
❷ $newnum = $somenum * 4;
❸ return $newnum;
}
就像我说的,一个简单的函数:它包含一个名称,并且传递一个单一变量 ❶。这个变量仅限于这个函数内部,意味着它不必在其他地方存在。你可以给变量取任何你想要的名称。
然后,我创建了另一个变量 ❷ 来保存四倍后的值。这个变量也是局部的,所以我们只是在函数内部使用它。请注意,乘法运算符是星号。有了我们新的值,我们需要将其传递回 ❸ 调用我们新quadruple函数的任何代码行。
现在让我们调用我们的函数。为了展示这个函数有多有用,我们将使用数组和循环在连续的几个数字上调用它。创建一个新的 PHP 文件,并包含以下代码:
<html>
<body>
<?php
$allnums = array(4, 18, 21);
foreach ($allnums as ❶$thisnum) {
❷ $quadnum = quadruple($thisnum);
print $thisnum . " quadrupled is " . $quadnum . "! ";
}
function quadruple($somenum) {
$newnum = $somenum * 4;
return $newnum;
}
?>
</body>
</html>
在这里,我使用了一个foreach循环在一个常规的非关联数组上。为了做到这一点,我简单地使用一个值 ❶ 而不是键值对。然后,对于数组中的每个项目,我使用新函数将数字乘以四 ❷。
一切结束后,这里是输出:
4 quadrupled is 16! 18 quadrupled is 72! 21 quadrupled is 84!
有了这些,你就知道了 PHP 的基础知识。你已经将它与 HTML 结合使用。你学习了多种类型的变量:数字、文本、数组和关联数组。你可以使用条件语句让你的代码思考,并且可以使用循环和函数来保持你的代码小且可重用。
要了解如何抓取网页或将 PHP 连接到 Web 服务,请继续下一节。若要将 PHP 连接到数据库,你将需要查看 #65:从 PHP 中使用 MySQL。
#61:检索网页
使用像 PHP 这样的服务器端语言的最常见原因之一是能够调用 Web 服务。一些服务提供的结果可以直接用 JavaScript 读取到浏览器中。然而,将这些调用放在服务器端给你更多的自由来使用或存储结果。
调用一个网络服务就像加载一个网页一样。你向服务器传递一个 URL,然后它以代码的形式响应(网页通常是 HTML,网络服务通常是 XML)。在 PHP 中,代码被放入一个字符串变量中,它包含一大堆文本。从那里,你可以对网页做任何你想做的事情——打印出来,分割它,解析成数据,存储起来。但首先,让我们先专注于获取网页。
PHP 5 附带一个名为 cUrl 的标准库。这个库全部关于建立网络连接。在 PHP 的早期版本中,你可能使用 file 函数来做这件事,它有点像用胶带粘上去的。切换到 cUrl——它是为抓取 URL 而设计的。
当然,file 函数非常简单。只需一行代码,你就有网页的内容了。正如你将看到的,cUrl 稍微复杂一些。然而,让我们创建自己的函数,这样我们就可以在许多地方重用它(正如我在这本书中多次做的那样)。然后我们将拥有 file 的便利性和 cUrl 的强大功能。
在一个空的 PHP 文件中,添加以下代码:
<?php
// Additional code may go here
// cUrl function
function get_url(❶$url) {
❷ $c = curl_init();
❸ curl_setopt($c, CURLOPT_URL, $url);
❹ curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
$content = trim(❺curl_exec($c));
❻ curl_close($c);
❼ return $content;
}
?>
我命名的 get_url 函数有一个参数,即 URL ❶。这个输入是它完成工作所需的内容,即抓取一个网页。一旦进入函数内部,我们需要初始化一个 cUrl 对象 ❷,这样我们就可以开始使用它了。
现在,让我们通过设置选项来告诉 cUrl 我们需要什么。首先,我们告诉它我们想要获取的 URL ❸。然后我们解释我们想要获取 URL 发送的内容 ❹。有些人使用 cUrl 仅发送信息,在这种情况下,他们不关心回复。我们则不同。
注意,每次我们设置选项时,我们都传递了当初始化 cUrl 时创建的 $c 变量。这个变量很重要,因为它跟踪我们设置的选项和 cUrl 会话的状态。
设置好选项后,我们通过一个普通变量存储的内容调用 URL ❺。首先,我们运行它通过 trim 函数,以移除文件开头和结尾的任何空格或换行符。否则,我们会得到与网络浏览器得到的确切数据相同的数据。
现在,我们将清理我们的混乱,所以告诉 cUrl 我们已经完成 ❻。然后最重要的部分:我们从这个函数返回内容 ❼。这就是我们如何将网页的结果从函数中提取出来,并放入我们声明的任何变量中。
让我们来测试我们的新功能。在“Additional code may go here”行下面,让我们添加这一行,它将抓取书籍网站的首页:
$htmlcode = get_url("http://mapscripting.com");
print $htmlcode;
如果你将 PHP 文件加载到浏览器中,你应该会看到一个网页。这个网页来自你的新函数!在另一个浏览器窗口中打开 mapscripting.com/,你会看到几乎相同的内容,尽管你的副本中可能缺少样式和图像——这是因为你只加载了 HTML。
使用 PHP 显示本书网站简化版的大惊小怪吗?这并不是什么大惊小怪的事情;这只是一个概念验证。真正精彩的部分出现在您从网络服务下载 XML 时。我在#52: 使用 XML 中展示了更多关于它是如何工作的。
在其他脚本中包含您的函数
使您的get_url函数在其他您编写的 PHP 脚本中可访问的一种方法是在每次需要时复制并粘贴它。当然,每次都剪切和粘贴是件麻烦事,如果您更改了函数,还很难保持更新;您必须更改您粘贴它的所有地方。
相反,创建一个可以从其他脚本中调用的包含文件。将之前的代码(不包括我们用来测试函数的两行)放入一个新文件中。我将其命名为net_func.inc.php,因为我可能将来会添加更多与网络相关的函数。我给文件添加了非.php扩展名,因为我不想让它被误认为是可以直接显示给 Web 浏览器的文件。
现在开始创建一个新的 PHP 文件,这样您就可以测试包含您的新函数文件:
<?php
include("net_func.inc.php");
$htmlcode = get_url("http://mapscripting.com");
print $htmlcode;
?>
这个测试的结果应该与之前的测试相同。但现在get_url函数的代码与调用它的代码是分开的,这将使我们能够轻松地在许多地方使用它。
#62: 安装 MySQL
MySQL 是一个流行的数据库平台,可在大多数操作系统上使用。您可以在自己的计算机上安装它,也可以在 Web 服务器上运行它,这意味着将其用作应用程序的数据源很容易。
对于初学者来说,一个数据库包含一个或多个用于存储数据的表。在非常粗略的层面上,您可以将数据库想象成一个电子表格,就像 Microsoft Excel 文件一样,可以部分或全部访问,并过滤出您关心的内容。您没有工作表,而是有表格。工作表中的每一行都是一个新的数据库记录。大多数工作表顶部的列是数据库字段——它们描述了放入该列的数据。实际上,当人们谈论数据库时,他们经常谈论行和列。
MySQL 中的 SQL 部分代表结构化查询语言,是与数据库服务器通信的语法。MySQL 和许多其他数据库服务器使用一组标准的语句(称为 SQL-92),尽管每个都有其自己的方言。
当 MySQL 与其他技术结合使用时,你会发现它非常有用。本章中的示例使用 PHP 作为编程语言,Apache 作为 Web 服务器。然而,继续阅读以获取关于单独使用 MySQL 的详细信息。
检查您的 Web 主机是否支持 MySQL
如果您有网络托管服务,您可能已经安装了 MySQL。您需要检查您的控制面板或与您的管理员确认 MySQL 是否可用以及是否已为您设置好账户。询问以下两种常见的使用 MySQL 的方法之一:
| phpMyAdmin 一个用于访问 MySQL 的网页仪表板。正如其名所示,PHP 也必须安装。使用此方法,您可以通过视觉的点击方式创建数据库表并查看数据,这是初学者通常更喜欢的。 |
|---|
| 命令解释器 一个命令行实用程序,允许您从文本界面访问 MySQL。创建数据库表和其他所有操作都通过键入命令完成。 |
本章中的所有示例都将使用这两种方法展示。在某些情况下,差异很小,因为您还需要在 phpMyAdmin 中输入命令。
如果 MySQL 已安装,您就可以继续到 #63: 将位置存储到数据库中,在 自行安装 MySQL 中。
使用 MySQL 的软件包安装
要在您的计算机上安装 MySQL,最简单的方法是下载适用于您操作系统的现成软件包。如果您选择此路径,您还将同时获得 PHP 和 Apache,这对您来说很有用。
软件包的名称是根据流行的服务器架构 LAMP 命名的,它代表 Linux、Apache、MySQL 和 PHP:
-
Windows (WAMP):
www.wampserver.com/ -
Macintosh (MAMP):
www.mamp.info/
这两种软件包都应该可以在您的计算机上安装,而无需您提供太多信息(尽管您可能需要输入管理员密码)。
安装过程完成后,请打开 WAMP/MAMP。程序应创建一个新的指向 localhost 的网页浏览器窗口,这是您计算机上服务器的名称。默认情况下,会出现一个欢迎屏幕,如图 图 9-4 所示。

图 9-4. MAMP 欢迎屏幕
要管理您的 MySQL 安装,请点击 phpMyAdmin 链接。现在您已准备好进行下一个项目。
自行安装 MySQL
如果您使用的操作系统没有可用的软件包,您将需要自行安装 MySQL。由于存在太多变体,本章无法一一描述,但互联网上有许多教程可以帮助您。
MySQL 网站是您开始的地方:mysql.org/。
当您开始运行时,继续阅读地理特定的说明。
#63: 将位置存储到数据库中
为了构建极其强大的映射应用程序,您将想要使用数据库。通过维护自己的数据,您的网站将更快,您可以做更多有趣的事情。首先,您需要创建数据库并向其中添加一些位置。本节将描述如何开始使用 MySQL 位置数据库。
在这一点上,我们需要明确我们的术语。实际上,存在于数据库中的数据实际上位于数据库的表中。在您创建表之前,您需要有一个数据库。MySQL 是一个数据库管理系统,它可以包含许多单独的数据库,而这些数据库本身又包含许多表。
创建新数据库
如果您通过托管提供商使用 MySQL,您可能已经有一个数据库分配给您。即使您的数据库中已经包含其他表(例如,用于博客软件),您仍然可以使用它来完成这个项目。我们只是会创建一个额外的数据库表。
如果您没有数据库,在 phpMyAdmin 中创建一个很简单。从主屏幕上,只需在表单中输入您数据库的名称(我选择了mapscripting)并点击创建按钮(见图 9-5)。或者,使用 MySQL 命令解释器,输入create database("mapscripting");。

图 9-5。创建新的数据库。
一旦完成,您需要选择数据库名称,通过点击它或在解释器中输入\u mapscripting来选择。现在您已经准备好创建数据库表了。
创建数据库表
现在您已经有了数据库,您可以开始添加表了。为了简化示例,我们只会添加一个表。
要在 phpMyAdmin 中创建新表,您需要在数据库主页面上的表单中填写信息。您需要为表指定一个名称并选择字段数量。我称我的表为places,它将包含四个字段,这些字段是每个地点都将包含的值类型。点击Go按钮以进一步定义表。
正如您在图 9-6 中看到的,我们需要为每个字段命名并指定其类型。我要添加到我们基本地点表中的四个字段是 id(唯一标识符)、label(地点的名称)、latitude和longitude。

图 9-6。创建具有四个字段的表。
对于任何数据库表来说,标识符都是一个好主意,因为它使得区分一块数据与另一块数据变得容易。我已经将标识符设置为bigint,这意味着它是一个可以变得非常大的整数(高达 9 千万亿——即 18 个零)。然而,我们不想自己计数,因此我们需要确保 MySQL 为我们完成这项工作。
在 phpMyAdmin 的右侧查看(可能需要水平滚动),你会找到“额外”列。选择id行的下拉菜单并选择auto_increment。然后,也点击直接位于其右侧的选项,图标看起来像一把钥匙。参见图 9-7 的示例。选择此选项将使标识成为主键。这两个步骤对于 MySQL 为您进行计数是必要的。
剩余的字段相当简单。标签是对地点的简短(100 个字符)描述,然后位置以两个浮点数(小数)形式存储:纬度和经度,现在无疑对你来说已经很熟悉了。
点击保存按钮,你就创建了一个表。要在命令解释器(或使用 phpMyAdmin 的 SQL 窗口)中做同样的事情,请输入以下内容:
CREATE TABLE places (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
label VARCHAR(100), latitude FLOAT, longitude FLOAT);
现在你已经有一个表了,让我们开始向其中添加数据。

图 9-7. 使标识字段自动递增。
向你的地点表添加数据
你的地点表,包含其四个字段,现在快乐地坐在你的数据库中,但你有一个问题。表是空的。它没有任何数据。让我们添加一些。
在 phpMyAdmin 中,点击表名然后点击“插入”标签。你应该会看到另一个表单(如图图 9-8 所示),包含四个字段。这次,你将输入值而不是名称,因为名称已经列出。
留空id值很重要。MySQL 将创建该值,因为它在为我们计数(自动递增)。在其他框中,输入一个描述性名称,然后是纬度和经度。然后点击Go。
你不必通过 phpMyAdmin 添加数据。你也可以使用纯 SQL(例如在命令解释器中)。要将老忠实泉添加到places表,请输入以下内容:
insert into places (label, latitude, longitude)
values ('Old Faithful geyser', 44.46054, −110.82834);
注意,我没有在这里包含id。我想让 MySQL 处理它。其他三个字段首先按名称列出,然后按我想要添加到它们的值列出。
执行这些任务后,你已向你的地点数据库表添加了一个位置。添加几个更多,改变标签、纬度和经度。现在你有一个正在兴起的位置数据库了。在 phpMyAdmin 中点击“浏览”标签查看你的地点,或者在命令解释器中输入以下 SQL:select * from places;。

图 9-8. 在你的地点表中输入值。
现在你可以对位置数据库做一些有趣的事情,比如从数据库中绘制位置或获取数据库中的最近位置。
#64: 从电子表格导入数据
如果你决定将你的位置数据导入数据库(我希望你是),你将想要一种使其变得简单的方法。填写表格或手动编写 SQL 插入语句是乏味的。在本节中,我将展示如何使用电子表格,将其转换为 CSV 格式,然后直接导入 MySQL。
之前我把数据库表比作电子表格。两者都有列,在数据库中这些列也被称为字段名。两者都有行,我们称之为数据库记录。为数据库准备大量数据的最简单方法是使用电子表格,例如 Excel 或 OpenOffice Calc。像平常一样在你的电子表格上工作。图 9-9 展示了我在上一个项目中创建的位置数据库作为电子表格可能的样子。
确保你有几行填写完毕,然后保存电子表格的副本。不要保存为常规的电子表格格式,而是滚动到你的文件类型列表,并选择逗号分隔的,.csv。这种格式是存储数据的一个简单、通用的格式。
我给我的文件命名为places.csv,当我在纯文本编辑器中打开它时,它看起来是这样的:
"label","latitude","longitude"
"Old Faithful geyser",44.46054,-110.82834
"St. Louis Arch",38.62470,-90.18510

图 9-9. 数据库表类似于电子表格。
现在,你已经准备好导入数据库表的这种逗号分隔值(CSV)版本了。在 phpMyAdmin 中,导航到位置表并点击导入标签。首先,你需要选择要导入的文件,所以点击浏览按钮并选择你的 CSV 文件。如果你的文件有一个标题行,就像我的那样,确保填写表格以跳过一条记录,因为你不想将字段名作为值导入。
滚动到文件导入部分。它可能设置为 SQL,因为 MySQL 主要期望通过这种方式读取文件。选择CSV,然后填写如图图 9-10 所示选项。字段由逗号分隔;字段由双引号字符包围;字段由反斜杠转义。你可以让行终止符自动确定。

图 9-10. 将逗号分隔的文件导入 MySQL。
最后一个选项是导入的字段列表。如果你正在导入表中的所有字段,你可能不需要输入这些。然而,我们有一个自动生成的标识符,因此我们需要指定我们正在导入的列,除了id列之外的所有列。
点击Go按钮,你的数据应该会被导入。为了检查,点击浏览标签,它将显示位置表当前的内容。
你也可以使用命令解释器导入:
LOAD DATA LOCAL INFILE 'places.csv' INTO TABLE places
FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n'
IGNORE 1 LINES (label, latitude, longitude);
CSV 文件需要可供运行 MySQL 的机器访问。如果您在服务器上使用解释器,您需要将文件上传到服务器。
当您有不止几个记录时,使用电子表格来创建数据是最简单的方法。此外,您可能会发现许多其他数据也以这种格式提供,因此了解如何从 CSV 导入可以帮助您使用他人的数据。
#65:从 PHP 使用 MySQL
许多编程语言都有连接到 MySQL 的方法。PHP 在其默认安装中包含了多个函数,这使得 PHP 和 MySQL 成为一对流行的组合。在本节中,我将向您介绍从 PHP 访问 MySQL 数据库的最常用函数。
以下示例假设您有一个位置数据库表,就像我们在 第六十三部分:将位置存储到数据库 中创建的那样,在 自行安装 MySQL 中,并且至少已经将一个位置添加到数据库中。一旦您的表准备就绪,让我们编写一些 PHP 代码来获取所有位置描述。
创建一个包含以下内容的新的 PHP 文件:
<?php
❶ $db = `mysql_connect`('localhost', 'username', 'password');
❷ `mysql_select_db`('mapscripting', $db);
$sql = "select label from places";
❸ $res = `mysql_query`($sql, $db);
while (❹$row = `mysql_fetch_assoc`($res)) {
print ❺$row["label"];
}
❻ `mysql_close`($db);
?>
以 mysql_ 开头的所有内容(粗体显示)都是与从 PHP 访问 MySQL 相关的函数。更多函数可以在 php.net/mysql 找到。
在此示例中,您首先使用服务器、用户名和密码连接到 MySQL❶,这些信息很可能由您的网站管理员提供。如果您使用 MAMP,您可以在安装的起始页面上找到登录信息。这些值建立了与 MySQL 的连接,并确保不是任何人都可以访问您的数据。
由于 MySQL 可以有多个不同的数据库,我们需要告诉它使用哪一个❷。在这里,我选择了之前创建的 mapscripting 数据库。我还传递了上一行的数据库连接变量。
现在我们已经准备好从数据库中获取一些数据了。我们使用 SQL 来完成这项任务,这与在 phpMyAdmin 的浏览标签页背后发生的情况类似。当使用 PHP 与 MySQL 结合时,您需要自己编写 SQL 语句,我已经将这些语句放入了变量 $sql 中,并将其与数据库连接变量一起传递给查询❸。
您可以通过多种方式从查询中获取数据,所有这些方式都使用上一行返回的对象。在此示例中,我们使用 while 循环逐个查看每个结果。每当 $row 变量不再有值时,while 循环就会结束。
每个结果的价值是一个关联数组❹,因此结果以键值对的形式存储。表的字段名是键,因此我们将它放在方括号中以获取值❺。最后,我们关闭与数据库的连接❻,这样可以释放资源。
当你运行这段 PHP 代码时,你应该会看到你添加的每个地点的描述依次打印出来,没有特定的格式。输出可能看起来不太美观,但你现在已经从 PHP 中建立了一个非常简单的与 MySQL 数据库的连接。更好的是,现在你可以将任何数据连接到网络上。你感觉强大了吗?你应该会的。
继续阅读,以在位置数据库中的数据上做一些更有用的操作。
#66:从数据库绘制位置
这个项目正是你一直在等待的。你耐心地投入时间学习 MySQL 和 PHP 的基础知识。你真正想要的只是将一些点放在地图上,而不需要手动编码。一旦你掌握了这个项目中的技能,你的地图项目将会更加强大,因为它们将由数据驱动。
信不信由你,你已经拥有了从数据库中绘制位置所需的知识。我们将使用结合了 #65:在 PHP 中使用 MySQL 和数据格式项目中的解析部分,即 #53:使用 JSON 的概念,后者让 JavaScript 框架 jQuery 为我们工作。
这个项目分为两部分:在服务器上使用我们的数据库创建 JSON,然后在浏览器中解析 JSON。让我们按顺序来做。
以 JSON 格式输出所有地点
我们将使用比其他项目中写的更高级一点的 PHP。而不是在遇到每个结果时打印出来,我们将存储它。一旦我们有了所有数据,我们将使用内置的 PHP 函数将其以 JSON 格式打印出来。
每次从数据库获取数据时,考虑你需要什么。在这里,我们将要在地图上绘制我们的位置,所以至少需要每个地点的纬度和经度。如果每个地点都有描述那就更好了,因此我们也会获取标签。根据你所做的事情,你可能还希望获取唯一的id。
你想要多少行?在我们的例子中,让我们获取所有行。记住,如果你的表非常大,你可能会让用户下载一个相当长的文件。在这种情况下,你可能只想从数据库中获取最近的位置,但这又是另一个项目。继续前进!
创建一个新的 PHP 文件,并添加以下代码:
<?php
$db = mysql_connect('localhost', 'username', 'password');
mysql_select_db('mapscripting', $db);
❶ $sql = "select label, latitude, longitude from places";
$res = mysql_query($sql, $db);
❷ $allrows = array();
while ($row = mysql_fetch_assoc($res)) {
❸ array_push($allrows, $row);
}
mysql_close($db);
❹ print json_encode($allrows);
?>
我们从连接细节开始,然后直接进入 SQL ❶。我已经列出了我们想要从数据库中获取的三个字段名。在遍历结果之前,我创建了一个空数组 ❷。我们将使用这个数组来存储所有结果。
当我们遍历数据库返回的每一行时,我们使用 PHP 函数将结果“推”到我们的数组末尾❸。你可能记得,数据库中的每个结果本身就是一个关联数组,值以字段名作为键存储。因此,包含所有行的数组实际上是一个数组的数组。可能有点令人困惑,但它非常适合 JSON。
在所有结果都存储在我们的巨大数组中之后,我们将变量转换为 JSON 文本并打印出来。如果我们有 JSON 扩展(包含在所有最新的 PHP 安装中),我们可以在一行中完成所有这些操作❹。
这是我的数据库的 JSON 输出,它有两行:
[
{"label":"Old Faithful geyser","latitude":"44.4605","longitude":"-110.828"},
{"label":"St. Louis Arch","latitude":"38.6247","longitude":"-90.1851"}
]
很可能你的结果都会挤在一起成为一行,但我已经将它们拆分,以便更容易理解。当我们将其解析到 JavaScript 中时,我们将有一个对象数组(由方括号定义),每个对象(由花括号定义)。
一旦数据被放入 JavaScript 中,我们就能在地图上绘制它。让我们开始吧。
从 JSON 中绘制地点
使用 PHP,我们已经将我们的位置数据库中的所有地点输出为 JSON。现在我们需要创建一个地图,该地图读取数据并为每个地点创建一个新的标记。表面上,这个地图就像我们之前创建的任何地图一样。然而,在这种情况下,我们甚至不会初始化地图,直到我们从数据库收到回复。
在我们解析 JSON 结果之前,我们需要包含 jQuery JavaScript 框架。将以下代码添加到基本 Mapstraction 地图的头部代码中:
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
现在用以下函数替换任何 JavaScript 代码以初始化地图:
var mapstraction;
function create_map() {
❶ $.getJSON("allplaces.php", function(jobj) {
❷ mapstraction = new Mapstraction('mymap', 'google');
❸ for (var i=0; i < jobj.length; i++) {
❹ var place = jobj[i];
var mk = new Marker(new LatLonPoint(place.latitude, place.longitude));
mk.setInfoBubble(place.label);
mapstraction.addMarker(mk);
}
mapstraction.autoCenterAndZoom();
});
}
与我们之前使用create_map函数的其他时候不同,我们不是先创建一个新的 Mapstraction 地图。我们将其保留到收到数据之后。相反,我们首先使用 jQuery 创建一个 Ajax 调用到我们的 PHP 文件(我将其命名为allplaces.php)❶。
一旦我们有了结果,然后我们创建地图❷,这样我们就可以开始向它添加数据。在这里,我们使用了一个for循环来遍历每个地点❸。然后我们可以保存这个对象❹,它有三个属性:标签、纬度和经度。这些值用于创建标记并给标记一个信息框。
最后,在for循环外部(所以它只发生一次),我们自动根据我们的标记来居中地图。图 9-11 显示了具有两个地标作为地点的我的地图的样子。
如果我在数据库中添加第三个地点并重新加载页面,我将有一个第三个标记。这就是数据库驱动地图的美丽之处!

图 9-11. 数据库驱动的地点地图
#67: 从数据库获取最近的位置
在第六章中,我们探讨了几个关于附近地点的项目。我展示了如何计算两点之间的距离以及如何确定离某一点最近的标记。更有用的是我们将在本项目中所做的工作:从一个数据库中存储的许多可能性中选择离某一点最近的位置。
为了能够查看数据库中的位置,我们首先需要在数据库中有所作为。在这个例子中,我们将使用第六十三部分:将位置存储到数据库中中的 places 数据库表,来自自己安装 MySQL。尽管我们一直在使用 MySQL 作为引擎,但大多数数据库都将与以下 SQL 语句一起工作。
因为我们正在寻找离一个单独的点最近的位置,我们需要确定那个点是什么。我选择了一个位于波特兰附近的点,纬度为 45.517,经度为−122.649。现在我们将这个值代入 Haversine 公式——这是我们在第六章中用来确定距离的同一部分三角学。在那里,我们使用了 JavaScript,在这个例子中,我们将使用 SQL。
要查看数据库中的位置,我们首先需要在数据库中有所作为。在这个例子中,我们将使用第六十三部分:将位置存储到数据库中中的 places 数据库表,来自自己安装 MySQL。虽然我们一直在使用 MySQL 作为引擎,但大多数数据库都将与以下 SQL 语句一起工作。
SELECT *,
`( 6371❶ * ACOS( COS( RADIANS( 45.517 ) ) * COS( RADIANS( latitude ) ) *`
`COS( RADIANS( longitude ) - RADIANS( - 122.649 ) ) + SIN( RADIANS( 45.517 ) ) *`
`SIN( RADIANS( latitude ) ) ) ) AS dist`
FROM places
ORDER BY dist;
在这里,我们选择了 places 表中的所有字段,以及一个额外的字段,由整个加粗部分描述。这是一段相当多的代码!这段代码使用每个地点存储的纬度和经度值来计算我们的点与数据库中点之间的距离。
距离,成为名为 dist 的列,单位是千米。与之前实现的 Haversine 公式相同,我们乘以地球半径,即 6,371 千米。对于英里,将数字❶替换为其英里等价物(3,958)。
当你运行 SQL 查询时,你的结果将如表 9-2 所示。因为我们按距离排序,所以离我们点最近的地点在表中排在前面。因此,离我们选定的点最近的地方是老忠实喷泉。当然,更有用的例子将来自包含许多地点且都在同一城市的地点数据库。然后,基于该城市内的一个点,你可以找到最近的地点,这些地点可能就在几英里之内。
表 9-2。SQL 中最近地点的结果
| ID | 标签 | 纬度 | 经度 | 距离 |
|---|---|---|---|---|
| 1 | 老忠实喷泉 | 44.46 | − 110.83 | 936.12 |
| 2 | 圣路易斯 Gateway Arch | 38.62 | − 90.19 | 2765.97 |
提高查询性能
数据库专家会对我们在上一节中使用的 SQL 有一些大问题。你知道我们使用了哪些数学:COS 这个,RADIANS 那个?这些值正在为数据库中的每一行计算,即使值没有变化。结果是,一旦你存储了许多地点,这个查询将不会让你的服务器感到高兴。在计算机科学术语中,这个查询有较差的性能。
让我们通过在 SQL 变量中存储一些不会改变的价值来提高性能。这个过程与 PHP 变量类似,但语法略有不同。将上一节中的查询修改如下:
SET @earthRadius = 6371;
SET @lat = 45.517;
SET @lon = −122.649;
❶ SET @radLat = RADIANS(@lat);
SET @radLon = RADIANS(@lon);
SET @cosLat = COS(@radLat);
SET @sinLat = SIN(@radLat);
SELECT *,
`( @earthRadius * ACOS( @cosLat * COS(RADIANS(latitude)) *`
`COS( RADIANS(longitude) - @radLon ) + @sinLat *`
`SIN(RADIANS(latitude)) ) ) AS dist`
FROM places
ORDER BY dist;
SET命令用于将值存储到变量中。变量本身以@符号开头。前三个变量用于存储之前直接写入查询中的数字,这使得查询的距离部分(再次强调,加粗)更容易阅读。
其他四个变量,从❶开始,也提高了可读性,并使查询大大缩短。更好的是,性能也得到了提升。存储在变量中的值只计算一次——当变量被创建时。
在查询运行过程中,仍然在进行许多计算,这将会拖慢你的服务器。像COS(RADIANS(latitude))这样的代码可以被避免,但前提是我们需要更改我们的数据库表。在下一节中,我将向你展示如何进一步优化你的查询性能。
在新列中预先计算值
你对 SQL 所做的更改在你数据库中存储了许多位置之前不会很明显。另一方面,随着存储的位置增多,应用这个查询变得更加有趣,这可能会吸引更多用户到你的网站。这绝对不是你希望服务器变慢的时候。
有些事情必须即时计算,否则每个位置的结果都会相同。你将为每一对纬度和经度坐标返回不同的结果。我们将专注于预先计算上一节中的这三个值:
-
COS(RADIANS(latitude)) -
RADIANS(longitude) -
SIN(RADIANS(latitude))
对于数据库中存储的每个地方,这些值都会不同。然而,一旦值被设置,它们将保持不变。这正是添加三个列到你的表中的完美案例。我们将它们命名为cosRadLat、radLon和sinRadLat,并将它们都设置为FLOAT类型。
这是通过命令解释器添加这些列的 SQL 版本:
ALTER TABLE places ADD cosRadLat FLOAT, ADD radLon FLOAT, ADD sinRadLat FLOAT;
一旦你添加了这三个列,你需要为当前表中所有的地方填充值:
UPDATE places set
cosRadLat = COS(RADIANS(latitude)),
radLon = RADIANS(longitude),
sinRadLat = SIN(RADIANS(latitude));
最后,现在你已经为你表中的新列添加了值,你可以使用这个优化的查询从数据库中获取最近的位置:
SET @earthRadius = 6371;
SET @lat = 45.517;
SET @lon = −122.649;
SET @radLat = RADIANS(@lat);
SET @radLon = RADIANS(@lon);
SET @cosLat = COS(@radLat);
SET @sinLat = SIN(@radLat);
SELECT label, latitude, longitude
`( @earthRadius * ACOS( @cosLat * cosRadLat *`
`COS( radLon - @radLon ) + @sinLat *`
`sinRadLat ) ) AS dist`
FROM places
ORDER BY dist;
结果是一个更短、更快的查询。仍然需要即时进行一些计算,但其余的已经被预先计算并存储在地点表中的变量或列中。
唯一的其他变化是我们明确命名了要选择的字段,这是另一个数据库最佳实践。此外,现在我们添加了三个预先计算的字段,我们不想让结果变得混乱。这些数字只是为了提高速度,并不特别有用。
#68: 获取与邮政编码最近的地点
大型企业通常在其网站上设有商店定位器。要使用它,你输入你的邮政编码,然后你会得到一个离你最近的地点列表。在这个项目中,我将向你展示如何执行 SQL 数据库调用,以便在你的网站上重现此功能。
一方面,查找与邮政编码附近的地点的代码与上一个项目相同。你只需要确定特定邮政编码的纬度和经度点,这可以从大多数地理编码服务中获得,然后你就能找到最近的地点。
当然,在你的数据库中拥有邮政编码表甚至更有用。这个过程在第十五部分:获取邮政编码坐标中描述,我在第十五部分:获取邮政编码坐标中提供了数据库源链接 mapscripting.com/postal-code-database。安装你自己的数据库将节省一个步骤,这样你就可以在数据库的一次调用中找到 ZIP 编码的坐标及其最近的地点。
创建一个新的数据库表,我将称之为 postals。它将有三列:code、latitude 和 longitude。正如你在图 9-12 中看到的,邮政编码本身是一个五字符的字符串(varchar),因为这是在美国使用的,ZIP 编码是五位数字。

图 9-12. 创建 ZIP 编码表。
邮政编码以文本形式表示,以应对代码以零开头的情况。如果你将代码存储为数字,这更有效率,你需要在每次查找时考虑起始零。纬度和经度是浮点数,就像在地点数据库中一样。
这里是使用命令解释器创建此表的 SQL 版本:
CREATE TABLE postals(code VARCHAR(5), latitude FLOAT, longitude FLOAT);
现在,你可以开始向数据库添加一些邮政编码了。为了这次测试,你可以手动添加几个,使用地理编码器来确定正确的纬度和经度点。更好的长期解决方案是下载完整的邮政编码数据库,并使用第六十四部分:从电子表格导入数据。
到目前为止,你的数据库应该有两个表:places 和 postals。我们将在一个 SQL 调用中使用这两个表来确定最近的地点。
从 MySQL 命令行解释器或 phpMyAdmin 中,输入以下查询:
SELECT `@lat:=latitude, @lon:=longitude` FROM postals WHERE code='90210';
SET @earthRadius = 6371;
SET @radLat = RADIANS(@lat);
SET @radLon = RADIANS(@lon);
SET @cosLat = COS(@radLat);
SET @sinLat = SIN(@radLat);
SELECT label, latitude, longitude
( @earthRadius * ACOS( @cosLat * cosRadLat *
COS( radLon - @radLon ) + @sinLat *
sinRadLat ) ) AS dist
FROM places
ORDER BY dist;
大部分查询与之前项目中优化的版本相同。而不是硬编码纬度和经度,第一行从我们的 postals 数据库中选择它。粗体部分将坐标存储在 @lat 和 @lon 变量中。
我们示例查询的结果显示在 表 9-3 中。再次强调,因为我选择的是美国西海岸的位置,所以老忠实喷泉是最接近的。至于查找靠近某个地点的地方,真正有趣的事情发生在你有一个城市内多个地点时。
表 9-3. Nearest Place SQL 的结果
| 标签 | 纬度 | 经度 | 距离 |
|---|---|---|---|
| 老忠实喷泉 | 44.46 | − 110.83 | 1322.32 |
| 圣路易斯 Gateway Arch | 38.62 | − 90.19 | 2566.23 |
第十章:MASHUP 项目

在本书中,我演示了如何使用网络地图和地理数据执行一些常见任务。现在,我将把这些课程中的许多结合到示例项目中。本章中的 mashup 展示了如何检索、转换和利用外部数据源。
我们还将创建超越仅显示地图上几个位置的界面。使用事件来捕捉点击和鼠标移动,我们将为地图添加一些交互性。
什么是 Mashup?
互联网远不止是一系列相互连接的文档。数百万个应用程序正在不断创建、收集和消费数据。当这些应用程序通过应用程序编程接口(API)相互交流,共享这些数据时,产生的功能被称为mashup。
这个名字来自音乐。在音乐上,当 DJ 将两首或多首歌曲叠加在一起以创造新事物时,就会产生 mashup。这个类比描述了程序员和设计师如何使用网络 mashup。他们结合 API 来创造新事物,通常是一种启发性的数据可视化方式。
在本书中,我们一直在使用不同类型的 API。除了图表 API 之外,地图还用于可视化数据,这些数据可能来自你自己的或其他 API。然而,大多数 API 都是用来向开发者提供数据的。在本章中,我将使用提供公开可用位置数据的 API。
项目
本章中的五个项目将帮助你开始创建 mashup。它们被选为概念多样性,因此你可以在单个章节中学习到许多技术。以下是项目的简要概述:
天气
就像你的日常报纸一样,这张地图显示了美国的天气状况——只是这张天气图是交互式的,并且每次页面加载时都会更新。请参阅mapscripting.com/weather。
地震
你将获得地质信息并创建一张显示上周全球地震分布的地图。使用里氏震级衡量地震的强度,你可以看到大地震发生的位置,并放大查看全球的地震热点。请参阅mapscripting.com/earthquakes。
音乐会
将这个 mashup 调到 11!我们将创建一个工具来搜索城市或周边地区的音乐会。我们甚至允许用户在搜索前声明他们的预算。我们只会过滤出符合他们标准的结果。请参阅mapscripting.com/concerts。
帮助用户找到他们所在位置附近的推文,或者他们搜索的任何地方。用户可以选择添加一个关键词到搜索中,以缩小 Twitter 状态消息中存储的什么和哪里的范围。请参阅mapscripting.com/twitter。
中点搜索
在中间相遇!仅仅搜索咖啡是不够的。最终的混合显示了你如何首先找到路线的中点,然后在该中点附近搜索咖啡。这样,当你找到中间相遇的地方时,你就不必开车穿越整个城镇。参见mapscripting.com/middle。
这五个项目使用了本书前面学到的许多概念。完成它们,你将肯定走向地图混合大师的道路。让我们开始混合吧!
#69: 创建天气地图
一张显示当前状况的大地图不仅对当地气象学家有用。使用 Yahoo!天气 API,你可以制作一个区域、国家或整个世界的视觉预报。在这个例子中,我将展示如何创建一个带有主要区域城市当前状况图标的美国天气地图。
正如你所看到的,创建天气地图的大部分工作都涉及数据处理。一旦数据以 JavaScript 可以轻松访问的格式,我们只需要一个基本的地图和一些自定义标记。当我们放大到城市并显示其覆盖层中的预报时,事情会变得有点棘手,但对于像你这样的地图大师来说,这应该不成问题。在这个项目结束时,你的地图将看起来像图 10-1。

图 10-1. 完整的美国天气地图
准备一张基本的美国地图
网络地图的基本要素是什么?一个中心点和缩放级别。好吧,你还需要带有div的 HTML 以及一些基本样式。此外,你还需要加载一些 JavaScript 文件。然而,一旦这个基础建立起来,你只需要调用setCenterAndZoom。
我很早就决定只展示美国大陆。抱歉阿拉斯加和夏威夷——我将在本章后面的地震示例中弥补这一点。我通过一点猜测和检查找到了中心和缩放级别。将地图拖动到你想要的位置,并将缩放设置为完美的级别。然后,调用getCenter和getZoom(或者每当地图被拖动时自动执行此操作)。
我到达的中心纬度为 38 度,经度为-98 度。虽然不是美国的精确中心,但看起来最好。我选择了 4 倍缩放级别,足以看到主要城市和高速公路。以下是创建基本地图的代码,我们将在接下来的章节中在此基础上进行构建:
<html >
<head>
<title>Weather Map Mashup</title>
<style>
div#mymap {
width: 800px;
height: 450px;
}
</style>
❶ <script src="http://ajax.googleapis.com/ajax/libs/
jquery/1.3/jquery.min.js"></script>
<script type="text/javascript" src="http://maps.google.com/
maps/api/js?sensor=false"></script>
<script src="mxn.js?(googlev3)"></script>
<script type="text/javascript">
var mapstraction;
❷ var center = new mxn.LatLonPoint(38,-98);
❸ var zoom = 4;
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'googlev3');
mapstraction.setCenterAndZoom(center, zoom);
mapstraction.addControls({"zoom":"large"});
}
</script>
</head>
<body onload="create_map()">
<div id="mymap"></div>
</body>
</html>t
这段代码的大部分内容可能看起来与其他简单地图相似。我们很快就会添加更多内容,但你已经可以看到这个代码有前瞻性的迹象:我包含了jQuery ❶,这是一个使应用效果和使用 Ajax 检索数据变得容易的 JavaScript 框架。我还全局设置了center ❷和zoom ❸变量,这样我们就可以在放大后返回用户到原始视图。
基本地图,如图 10-2 所示,现在可以添加标记。然而,在我们能够在地图上绘制天气条件之前,我们需要一些可访问格式的数据。

图 10-2. 基本美国天气图
将天气结果转换为 JSON
要使我们的地图成为一个混合体,我们需要一些数据,特别是当前的天气状况数据。Yahoo!有一个易于使用的天气 API,它接受邮政编码或专有位置标识符。结果以 GeoRSS 形式出现,这是一个纯文本 XML 文件。
让我们看看对 Yahoo!天气 API 的一个示例调用。为了获取明尼苏达州明尼阿波利斯市的当前条件和预报,我们获取这个 URL:weather.yahooapis.com/forecastrss?p=USMN0503。
URL 中的粗体部分是位置 ID。API 也可以接受邮政编码,但在这个例子中,我们将使用 ID。当我们把条件绘制在地图上时,我会解释如何获取位置 ID。
如果你访问这个 URL,结果将类似于以下 XML 缩略版本的返回结果:
<rss version="2.0" xmlns:yweather="http://xml.weather
.yahoo.com/ns/rss/1.0" >
<channel>
...
<item>
<title></title>
<geo:lat></geo:lat>
<geo:long></geo:long>
</item></channel></rss>
对于免费数据的格式不必过于挑剔,但正如我在第五十二部分:使用 XML 中讨论的那样,使用 JavaScript 解析 XML 可能会有点麻烦。许多 API 现在提供 JavaScript 对象表示法(JSON),这是一种可以直接插入 JavaScript 的格式。Yahoo!天气只提供 XML,但另一个 Yahoo!产品可以为我们转换数据。
Yahoo! Pipes 可以执行许多复杂的操作来合并、过滤和排序数据。在这个例子中,我们不会触及边界。我们只想从 API 读取 XML 并输出 JSON。使用 Pipes 很容易做到这一点。你也可以使用 Yahoo!查询语言或在你的服务器上运行 XML 处理。我将在本章后面的项目中展示第二种方法。
创建一个新的管道
因为我们只需要将 XML 转换为 JSON,我们将创建尽可能简单的管道。你可以在第五十七部分:从 XML 转换为 JSON 中看到这个例子的更深入版本,在那里你也会找到 Pipes 的更多高级用法。
从Pipes 首页,点击创建管道以获取一个全新的、空的管道。在左侧,你会看到你可以使用的“管道”组件。将一个获取源拖到工作区。这是数据流入的点。
那个馈送的 URL 是什么?我们将对天气 API 进行多次调用,一次针对我们想要查询的每个城市。这些调用将通过管道路由,这意味着馈送 URL 不能是静态的。为了接受管道的输入,将一个文本输入拖到工作区。将输入命名为位置,并将提示设置为类似于输入一个位置的内容。对于默认和调试值,可以使用你的邮政编码或城市名称。你也可以使用位置 ID,例如明尼阿波利斯的 ID,USMN0503。这有助于你确认管道是否正常工作。
现在你已经设置了文本输入以提供位置值,你准备好创建 URL 了。为了合并 URL 的静态和动态部分,我们将使用最后一部分管道。将一个字符串构建器拖到工作区。作为第一个值,添加天气 API URL 的开头:weather.yahooapis.com/forecastrss?p=。
将文本输入的输出拖到字符串构建器的第二个值。这个动作会将位置 ID 添加到对天气 API 的调用末尾。现在你有一个完整的馈送 URL。将字符串构建器的输出拖到获取馈送 URL行。最后,将获取馈送的输出拖到工作区底部的管道输出。
你刚刚创建了一个 Yahoo! Pipe!最终产品应该看起来像图 10-3。在屏幕底部的调试输出部分,你应该能看到一些基于你输入的文本输入调试值的样本结果。
要使用你的管道,你需要保存它。一旦保存,你就可以通过输入不同的位置来运行它以获取更多示例结果。从样本结果屏幕,点击获取 JSON。你可能需要右键单击(在 Mac 上按住 Ctrl 键单击)并复制链接。Pipes URL 将类似于这样:pipes.yahoo.com/pipes/pipe.run?_id=sGDQu...&_render=json&location=USMN0503。

图 10-3. 完整的 Yahoo! Pipe 用于获取天气数据
这个 URL 将以 JSON 格式检索明尼阿波利斯的天气状况。以下是它返回的简略版本:
{"count":1,"value":{"items":[{"geo:long":"-93.26","geo:lat":"44.98",
"description":"<img src=\"http:\/\/l.yimg.com\/a\/i\/us\/we\/52\/26.gif\"\/>
<br \/>\n<b>Current Conditions:<\/b><br \/>\nCloudy...","yweather:condition":
{"temp":"50","text":"Cloudy",...,"code":"26"}}]}
那个 URL 是我们将用来获取几个城市的天气状况,并替换明尼阿波利斯特定的位置 ID。实际上,这正是我们在将 XML 结果转换为 JSON 后要做的。
在地图上绘制条件
我们的 JSON 数据源已经准备好了,现在让我们深入 JavaScript 来获取天气状况并将它们绘制在我们的美国地图上。首先,你需要决定要绘制哪些城市。我选择了 11 个具有代表性的地方,注重地理多样性。
为了收集我城市的地点 ID,我去了weather.yahoo.com/并在搜索框中输入每个城市的名称。结果会转发到一个类似这样的 URL:weather.yahoo.com/forecast/USMN0503.html。
文件名中不带扩展名的那部分,即粗体部分,是位置 ID。一旦我有了所有城市的 ID,我在 JavaScript 顶部创建了一个变量来保存这些值:
var weatherids = [
"USIL0225", // Chicago
"USTX0327", // Dallas
"USCO0105", // Denver
"USFL0316", // Miami
"USMN0503", // Minneapolis
"USTN0357", // Nashville
"USNY0996", // New York City
"USAZ0166", // Phoenix
"USMO0787", // Saint Louis
"USCA0987", // San Francisco
"USWA0395" // Seattle
];
weatherids变量是一个数组,它包含一系列值。我将变量声明分散到多行,以提高可读性并使添加或删除城市变得容易。城市名称是注释,因此你可以轻松地知道哪个位置 ID 对应哪个城市。
当地图加载时,我们想要遍历每个城市并在我们的 Yahoo!天气 API 管道版本中查找它。将此循环代码添加到create_map函数中:
var pipeid = "Sbcb8u8J3hGNdOcopgt1Yg";for (var i=0; i < weatherids.length; i++) {
var pipeurl = "http://pipes.yahoo.com/pipes/pipe.run?_id=" + pipeid;
pipeurl += "&_render=json&location=" + weatherids[i] + "&_callback=?";
jQuery.getJSON(pipeurl, add_weather);}
变量i持有数组的索引,它从零开始,数到 10(数组中的第 11 项是纳什维尔)。每次循环时,我们都会创建一个 URL 来调用我们的管道,使用当前天气站的地点 ID 值。
要从我们的管道中获取 JSON,我们使用 jQuery,一个小的 JavaScript 框架。当我们之前设置基本地图时,我们包括了 jQuery 文件的引用,所以我们已经准备好了;创建 Ajax 调用的大部分工作都由 jQuery 的getJSON函数和刚刚创建的 URL 为我们完成。
除了location参数外,我们还在 URL 中添加了一个新参数,_callback=?。这个安全特性让我们可以从除我们自己的网站之外的网站获取 JSON。Yahoo! Pipes 会将结果包装起来,这样只有我们的回调函数才能访问数据。问号是一个占位符,用于我们作为getJSON的第二个参数传递的函数。
当 jQuery 从天气 API 获取结果时,这些结果会被传递给我们的add_weather函数,我们现在需要编写这个函数:
function add_weather(data) {
jQuery.each(data.value.items, function(i, item) {
var lat = item["geo:lat"];
var lon = item["geo:long"];
var code = item["yweather:condition"].code;
var imgsrc = "http://l.yimg.com/a/i/us/we/52/" + code + ".gif;
`add_marker`({"lat":lat, "lon":lon, "code":code, "imgsrc":imgsrc});
}
JSON 数据会自动作为参数传递给我们的回调函数。在这种情况下,我使用了一个名为data的变量来保存管道的响应。我们想要获取的天气条件在名为items的数组的第一个结果中,而items本身又在一个名为value的对象中。是的,我们有一些不必要的开销,但这些是 XML 残留物。
我们需要的主要数据是纬度、经度和描述。尽可能的情况下,我使用 JavaScript 点表示法,如item.description。在这个数据源中,有几个字段名包含冒号,这在使用点表示法时会被错误地解释。在这种情况下,我使用括号表示法从item中检索属性。
每种天气状况都有一个与特定描述匹配的编号代码。数字允许机器轻松地解释预报,而无需解析文本。该代码也由 Yahoo! 天气用于调用为每种状况指定的图像。例如,代码 30 表示天空部分多云。相应的图形存储在 l.yimg.com/a/i/us/we/52/30.gif。我将此图像 URL 拼接到 imgsrc 变量中。
一旦我们有了所需的数据,我们就将其包裹在一个带有花括号的对象中,并将其传递给 add_marker 函数来完成在地图上绘制此标记的工作:
function add_marker(options) {
var marker = new mxn.Marker(new mxn.LatLonPoint(options["lat"], options["lon"]));
marker.setIcon(options["imgsrc"], [52,52]);
marker.setShadowIcon('❶blankshadow.png', [0,0]);
mapstraction.addMarker(marker);
}
为什么我传递数据为 JavaScript 对象(options 变量)而不是单独的参数?你可以使用参数,但四个参数传递给函数会很多。每次调用它时,我们都需要检查顺序。描述是第三个还是第四个?此外,JavaScript 对象通常用于在 JavaScript 函数之间共享数据,因此使用它们是良好的实践。
add_marker 函数的其余部分创建了一个简单的自定义标记。唯一可能看起来奇怪的行是阴影图标 ❶,我希望它是空的。因为没有无阴影标记的选项,所以我使用了一个透明的像素作为阴影图标。
现在我们有了绘制我们城市列表状况的所有部件。将它们组合起来,你就有了一个完整的天气地图,每个城市上方都有当前天气的简略图形表示。以下是混合体加载时发生的所有事情的简要回顾:
-
HTML 页面加载后,调用
create_map函数,该函数设置基本地图并为weatherids数组中的每个位置 ID 调用get_weather函数。 -
JSON 数据是从 Yahoo! Pipe 中检索的,数据被传递到
add_weather函数。 -
重要信息从 JSON 中提取到 JavaScript 对象中,该对象本身被传递到
add_marker函数。 -
创建并放置在地图上的自定义标记。
现在让我们使这个混合体更具交互性。当用户点击标记时,地图将放大显示该位置的预报详情。
添加预报详情面板
当前条件的视觉表示很棒,但我们无法从天气 API 中展示太多内容。在本节中,我们将添加一个预报详情面板。为了增加交互性,当用户点击标记时,面板将出现,如图 图 10-4 所示。

图 10-4. 我们天气地图的预报面板
首先,我们需要为新内容添加 HTML 壳。在地图 div 下方添加此行:
<div id="forecast"></div>
就像地图一样,外壳是空的。我们将使用 JavaScript 来填充它。因为我们想在地图本身上叠加预报详情,所以我们将使用 CSS 来定位新的div。在页眉的<style>部分添加以下行:
div#forecast {
position: relative;
width: 200px;
height: 400px;
background-color: #fff;
top: −435px;
left: 550px;
padding: 10px;
}
此 CSS 在地图右侧创建了一个细长的白色框。实际上,保存你的文件,在浏览器中加载它,从密歇根州向东将会被这个框遮挡。这显然不是我们想要的。我们只想在选中城市时显示预报面板。默认情况下,我们需要将其隐藏。
我们可以通过在create_map函数中添加这一行 jQuery 来实现这一点:
$('#forecast').hide();
在这里,我们调用 jQuery 的hide函数在预报div上。当地图首次加载时,面板将被隐藏,等待用户点击。
现在我们可以使用 Mapstraction 来填充预报详情面板的内容。当标记被点击时,我们不会创建 infoBubbles,而是使用 API 中的预报数据调用setInfoDiv。
首先,我们实际上需要获取那些数据。整个当前条件的描述以及预报都通过 Weather API 的描述字段传递。在add_weather函数中,我们需要第五个变量。在imgsrc行之后添加:
var desc = item.description;
现在你将通过向传递的对象中添加另一个选项来修改对add_marker的调用:
add_marker({"lat":lat, "lon":lon, "code":code, "desc":desc, "imgsrc":imgsrc});
然后你将在add_marker函数内部使用新值。在调用addMarker之前添加这一行:
marker.setInfoDiv(❶options["desc"] + "<p><a href=\"javascript:return_center()\">"
+ ❷"<img src=\"usmap.png\" border=\"0\" alt=\"Return to full map\" />"
+ "</a></p>", ❸"forecast");
在这里,我们将预报详情面板设置为包含 API 中的描述❶,以及一个可点击的美国地图图像❷,该图像调用一个新的 JavaScript 函数以返回地图到中心并清除预报详情面板。Mapstraction 如何知道使用哪个div?我们通过将其id作为第二个变量❸传递。
要查看预报详情面板的实际效果,我们需要编写代码在标记被点击时显示隐藏的div。在调用addMarker之后添加此行,因为我们将与刚刚创建的标记对象一起工作:
marker.click.addHandler(marker_clicked);
现在我们正在监听一个标记上的点击事件,然后通过函数引用进行响应。让我们编写marker_clicked函数,该函数将在我们任何标记被点击时被调用:
function marker_clicked(event_name, event_source, event_args) {
❹ mapstraction.setCenterAndZoom(event_source.location, 6);
var bounds = mapstraction.getBounds();
var diff = ((bounds.ne.lon − bounds.sw.lon)/4);
❺ mapstraction.setCenter(
new mxn.LatLonPoint(mapstraction.getCenter().lat, bounds.ne.lon − diff));
❻ $('#forecast').show();
});
当任何标记被点击时,我们将它设置为地图的中心并放大❹。我们使用被点击标记的位置作为中心,该位置通过event_source参数传递给函数。然后,为了考虑到地图右侧的预报详情面板,我们将地图中心向西移动❺,这样标记就会出现在地图可见部分的中心。最后,我们确保预报详情面板是可见的❻。
保存你的文件并在浏览器中加载它。现在你应该能够点击一个标记并放大以查看预报。唯一没有连接的部分是能够缩回到整个连续的美国地图。为此,我们需要编写一个return_center函数,该函数将在你点击美国地图时被调用。
将此函数添加到 JavaScript 部分:
function return_center() {
mapstraction.setCenterAndZoom(center, zoom);
$('#forecast').hide();
}
这个维护函数将地图返回到其原始中心和缩放级别,然后隐藏预报详情面板。现在我们又能看到整个美国地图了。
房子打扫干净后,混合应用就完成了。你使用 Yahoo! Pipes 从 Yahoo!天气中提取数据,并将其转换为 JSON。然后你在地图上显示几个城市的条件,以及一个描述性的图形。最后,点击标记后,你将地图缩放到城市,并在自己的叠加信息面板中显示预报。
当然,最好的部分是,你再也不需要阅读报纸上的天气页面了。你有一个在线的、始终更新的替代品。
#70: 显示全球最近地震
你是一个有抱负的地质学家吗?或者你可能只是想找一个快速查看世界上发生什么震动的方法?在这个混合应用中,我将向你展示如何使用公共数据源来理解你周围的世界。我们将绘制美国地质调查局(USGS)统计的一周内的地震。
幸运的是,美国地质调查局不仅记录美国的地震数据,还记录了全球的地震数据。更幸运的是,他们熟悉最新的数据格式。该组织发布了一个地理编码为 GeoRSS 的 XML 数据流。它涵盖了里氏震级 2.5 或以上的地震。这应该仍然给我们提供足够的地震数据,以便在我们的地图上揭示一些有趣的趋势,如图 10-5 所示。

图 10-5. 显示最近地震震级的地图
你可以将数据流加载到网络浏览器中直接查看内容:earthquake.usgs.gov/eqcenter/catalogs/7day-M2.5.xml。
许多浏览器默认会显示一个“美观”版本。查看其源代码,以了解它发送的所有数据。以下是 USGS 的 XML 文件的简略版本:
<feed xml:base="http://earthquake.usgs.gov/"
>
<title>USGS M2.5+ Earthquakes</title>
<subtitle>Real-time, worldwide earthquake list for the past 7 days</subtitle>
<entry>
<id>...</id>
<title>M 2.6, Washington</title>
<updated>YYYY-MM-DDTHH:MM:SSZ</updated>
<link rel="alternate" type="text/html" href="/eqcenter/recenteqsww/Quakes/..."/>
<summary type="html">...</summary>
<georss:point>46.4078 −119.2521</georss:point>
</entry>
<entry>
...
</entry>
...
</feed>
这是一些很好的内容。让我们开始在我们的地图上使用这些数据。首先,我们将自动解析 GeoRSS,这是 Mapstraction 使它看起来很容易实现的功能。然后,如果基本的可视化还不够,我们将创建一个完全定制的解决方案。
使用 GeoRSS 显示地震
好吧,你已经选择了快速版本。你只想看到这些地震被扔在地图上。你希望用尽可能少的代码来实现这一点。
将这几行代码添加到一个新的 HTML 文件中:
<html >
<head>
<title>Earthquake GeoRSS Map</title>
<script src="http://maps.google.com/maps?file=api&v=2&key=
*`YOURKEY`*" type="text/javascript"></script>
<script type="text/javascript" src="mxn.js?(google)"></script>
<style type="text/css">
div#mymap {
width: 550px;
height: 450px;
}
</style>
<script type="text/javascript">
var mapstraction;
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.setCenterAndZoom(❶ new mxn.LatLonPoint(0, 0), 0);
mapstraction.addControls({zoom: 'large'});
`mapstraction.addOverlay(`
`"http://earthquake.usgs.gov/eqcenter/catalogs/7day-M2.5.xml");`
mapstraction.autoCenterAndZoom();
}
</script>
</head>
<body onload="create_map()">
<div id="mymap"></div>
</body>
</html>
和往常一样,记得使用你自己的 API 密钥。否则,代码就准备好了。在浏览器中保存和加载它,可以看到类似于图 10-6 的显示。标记的实际位置将根据过去一周的地质活动而有所不同。

图 10-6. 使用直接 GeoRSS 叠加显示的地震
您是否注意到了我们设置中心和缩放时线上的所有零❶?这些不是打字错误。纬度为零是赤道。经度为零大约是国际日期变更线,穿过伦敦。最后,缩放级别为零显示整个世界。
加载地震数据的“金钱线”被加粗显示。Mapstraction 的addOverlay函数做了大部分工作。它创建了数十个标记,甚至为它们添加了 infoBubble 内容。不幸的是,乐趣在这里就结束了。一旦我们将所有这些工作外包给一个单独的函数,我们就限制了制作我们想要的确切内容的能力。
为了给这个地震地图添加个人风格,我们需要更深入地研究代码。我们需要定制化。
创建自定义地震地图
如果您只想可视化地震的位置,使用 Mapstraction 的内置 GeoRSS 支持可以轻松完成任务。另一方面,如果您想预先过滤内容或根据地震强度显示不同的图标,您将需要一个更定制化的解决方案。
在本节中,我将向您展示如何使用 Yahoo!查询语言将 GeoRSS 转换为 JSON。然后您将根据地震的里氏值选择一个标记图标。最后,您将变得更加聪明,并放大通常有相当多地震活动的区域。
首先,您需要在地图上有一个基本的世界视图。
准备基本世界地图
世界的基本地图不需要与地震映射器的 GeoRSS 版本中的地图有很大不同。我们需要加载地图,然后设置中心和缩放级别。
将以下代码添加到一个新的 HTML 文件中:
<html >
<head>
<title>Earthquake Map Mashup</title>
<style type="text/css">
div#mymap {
width: 550px;
height: 450px;
}
</style>
❶ <script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
<script src="http://maps.google.com/maps?file=api&v=2&key=
*`YOURKEY`*" type="text/javascript"></script>
<script type="text/javascript" src="mxn.js?(google)"></script>
<script type="text/javascript">
var mapstraction;
❷ var defaultloc = {"point": new mxn.LatLonPoint(14.6048
47155053898, −177.1875), "zoom": 1};
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.setMapType(mxn.Mapstraction.HYBRID);
mapstraction.addControls({"zoom":"large"});
view_world();
}
function view_world() {
❸ mapstraction.setCenterAndZoom(defaultloc["point"], defaultloc["zoom"]);
}
</script>
</head>
<body onload="create_map()">
<div id="mymap"></div>
</body>
</html>
保存文件,将其加载到浏览器中,您将看到一个没有标记的世界地图,就像图 10-7 所示。我已经对代码做了一些修改,以便我们在定制地图时提前做好准备。其中之一是包括了 jQuery❶,这是一个 JavaScript 框架,它使得应用效果和使用 Ajax 检索数据变得容易。

图 10-7. 空的世界地图,等待标记
我创建了一个对象变量来保存默认的位置和缩放级别❷,而不是单独的变量。我选择这些值是基于猜测然后检查,以找到一个好的位置,使得整个世界都能可见,并且地震的位置最有意义。因为世界地图是三维物体的二维视图,所以可能会出现标记和多边形缠绕的问题。我选择了我们基本地图的中心,这样我们的热点区域在全球范围内都能完全在视野中。
最后,我创建了一个完整的函数 ❸ 来使用默认的中心和缩放值。稍后,在这个混合应用中,我们将放大那些热点区域,但之后我们需要一种方式来将缩放回原始中心。我们不妨重用代码。
将地震数据转换为 JSON
现在基本地图已经设置好了,我们需要开始考虑数据。USGS 提供了一个 XML 源。JavaScript 可以解析 XML,但出于安全原因,我们首先需要将 XML 下载到服务器上。如果我们能以 JSON 格式获取数据,我们的工作就会容易得多。
我们有多种将地震数据从 XML 转换为 JSON 的选项。在天气混合应用中,我们使用了 Yahoo! Pipes。在这种情况下,我们将使用另一个 Yahoo! 产品,即 Yahoo! 查询语言 (YQL)。YQL 技术的许多其他用途中,它使得将任何 GeoRSS 源转换为 JSON 变得非常简单。
YQL 的语法类似于 SQL,这是查询数据库所使用的语言。您可以在 developer.yahoo.com/yql/console/ 的 YQL 控制台中尝试命令。我们不会查询数据库表,而是将针对我们在上一节中使用的 USGS GeoRSS URL 进行操作。以下是获取该源所有数据的查询:
select * from atom where
url='http://earthquake.usgs.gov/eqcenter/catalogs/7day-M2.5.xml'
在 YQL 控制台中输入该命令,选择 JSON 输出,并点击 测试 按钮。如图 图 10-8 所示,您将在下面的面板中看到结果。如果您更喜欢看到更结构化的视图,请点击 树视图 选项卡来浏览数据。从那里,您可以看到当我们将数据加载到地图中时我们将拥有的可用项。

图 10-8. YQL 控制台显示地震源的结果
复制控制台底部“REST 查询”下的长 URL 并将其存储在某个地方。我们将在下一节中使用该 URL。
在地图上绘制地震
如您所见,仅通过调用 GeoRSS 源在地图上绘制地震只需要一行代码。然而,这对我们来说太基础了。这个混合应用的目的是要创建一些更定制化的东西。我们希望通过使用更大图标的方式,在里氏值较高的地方提供一种直观查看地震强度的方法。
以下两个 JavaScript 函数将从我们在上一节中找到的 YQL JSON URL 加载数据,确定要使用哪个图标,并在世界地图上绘制标记。请将此代码添加到您的 create_map 函数后的基本地图中:
// Data and marker functions
function get_quakes() {
❶ var jsonurl = "http://query.yahooapis.com/v1/public/yql"
+ "?q=select+*+from+atom+where+url%3D'http%3A%2F%2Fearthquake.usgs.gov"
+ "%2Feqcenter%2Fcatalogs%2F7day-M2.5.xml'"
+ "&format=json&diagnostics=true&callback=?";
jQuery.getJSON(jsonurl, function(data) {
❷ jQuery.each(data.query.results.entry, function(i, item) {
// Get Lat/Lon point
var lltxt = item.point;
❸ var llarr = lltxt.split(" ");
// Get Richter value
var richter = item.title;
❹ richter = richter.substr(2, 3);
var majorrichter = richter.substring(0, 1);
var iconvals = get_icon(majorrichter);
// Find link
var link;
if (item.link[0]) {
link = item.link[0].href;
}
else {
link = item.link.href;
}
// Create marker
var marker = new mxn.Marker(new mxn.LatLonPoint(llarr[0], llarr[1]));
marker.setIcon(iconvals.name, iconvals.size);
❺ marker.setShadowIcon('blankshadow.png', [0,0]);
var eqdate = item.updated.substr(0, 10);
❻ marker.setInfoBubble('<strong>' + item.title +
'</strong><p>On ' + eqdate + ' (<a href="' + link + '">more info</a>)</p>');
mapstraction.addMarker(marker);
});
});
}
function get_icon(majorrichter) {
var identifier;
var size;
if (majorrichter < 4) {
identifier = "low";
size = [10, 10];
}
else if (majorrichter == 4) {
identifier = "med";
size = [15, 15];
}
else {
identifier = "high";
size = [20, 20];
}
return {"name": "richter-" + identifier + ".png", "size": size};
}
get_quakes函数实际上是一行,但它非常长。它调用 jQuery 中的getJSON函数。JavaScript 框架需要两个变量来从 YQL 检索我们的 JSON。第一个是调用的 URL。第二个是回调函数的引用。我使用了一个匿名内联函数。这个回调函数占据了get_quakes函数的大部分空间。
YQL URL❶被展开成几行作为单个长字符串,几乎与你从 YQL 控制台复制的内容完全相同。唯一的区别是回调参数(URL 的最后一部分)是一个问号。通过包含一个问号,jQuery 会为我们替换成回调函数。
一旦我们得到结果,它就被作为data变量传递给匿名函数,它将是一个 JavaScript 对象。在对象中,data.query.results.entry指的是所有地震结果的数组。使用 jQuery 的each函数❷,我们遍历每一个结果。每个地震都被传递给另一个匿名函数。
对于每一次地震,我们首先从point值中检索地理坐标。如您从 USGS XML 中回忆起来,纬度和经度都存储为单个值,中间有一个空格。我们的代码将它们分成两个值❸并将结果存储在数组中。数组中的第一个元素(索引为零)是纬度;第二个是经度。
接下来,我们需要找到里氏值。里氏值是衡量地震强度的测量值,通常给出小于 10 的十进制值。不幸的是,USGS 并没有直接传递这个值,尽管它就在标题中。例如,M 2.6, Washington。通过从标题中提取子字符串,我们可以找到里氏值。在这种情况下,我们想要 2.6,不想要更多。
里氏值从字符串的第三个字符开始,这被称为槽位二,因为文本字符串,就像数组一样,从零开始计数。然后里氏值继续三个字符。因此,我们的substr调用❹从槽位二开始,继续三个字符。在这个例子中,richter变量的值现在是 2.6,正如我们想要的。
拿到里氏值后,我们可以通过将其传递给get_icon函数来确定使用哪个图标。我选择了三个级别,并为每个级别创建了一个图形。如果地震相对较小,小于四,我分配给它最小的图标。如果地震在四到五之间,它得到一个中等大小的图标。任何五以上的地震都得到最大的图标。这些图标在不同级别上也有不同的颜色,因此地震标记随着震级的增加而变大变红。
使用我们从数据中分割出的纬度/经度数组,我们为这次地震创建了一个标记。我们根据里氏值给它一个自定义图标,然后给它不显示阴影 ❺。我使用了一个透明的像素作为阴影图标,并将宽度和高度设置为零。您将看到,我们的地图将包含如此多的地震,以至于我们没有空间为阴影留出位置。
最后,我在 infoBubble ❻中添加了一条非常简单的信息。这条信息显示了完整的标题、日期以及一个链接,用户可以通过该链接访问 USGS 网站上的页面,获取更多关于这次地震的信息。
创建图例
只需几行代码,这张地图比 GeoRSS 生成的地图更容易阅读。由于标记较小,许多地震占据的空间更少。我们已经通过改变图标的大小和颜色表明,一些地震比其他地震更有意义。这张地图对不知道我们基于里氏震级方法的某人来说有意义吗?
让我们创建一个图例,就像在完成后的地图(图 10-5)中显示的那样,在地图下方显示每个图标的意义。在地图div之后添加以下有序列表,它描述了不同的图标:
<ol id="legend">
<li><img src="richter-low.png" /> 2.5 − 3.9</li>
<li><img src="richter-med.png" /> 4.0 − 4.9</li>
<li><img src="richter-high.png" /> 5.0+</li>
</ol>
我们不希望图例看起来像有序列表,因为这会占用太多空间。大多数时候,类似这样的内容会显示在一行中。输入一些 CSS 来使其看起来符合您的需求。将以下内容添加到您的样式表中:
ol#legend {
list-style: none;
margin: 0;
padding: 0;
}
ol#legend li {
display: inline;
padding-right: 30px;
}
现在有序列表存在于一行中。每个列表项都向右填充,因此哪个图标与哪个描述相对应仍然很明显。既然我们已经清楚地说明了图标的含义,那么让我们给这个地图添加一点交互性。
放大热点区域
如您从这次混合体中可以看出,世界上某些地区的地震活动性比其他地区更强。这些区域是相对可预测的区域。有些,比如加利福尼亚,可能比其他地区更明显。由于这些区域有许多标记的聚集,因此有一种方法可以放大以获得更好的视图将会很有用。
我确定了四个这样的区域,并创建了一个系统,使得添加其他区域变得容易。在您的代码变量部分,添加以下行以创建包含区域边界的对象:
var regions = {
"California": new mxn.BoundingBox(30, −136, 45, −101),
"Alaska": new mxn.BoundingBox(48, 164, 68, −125),
"Latin America": new mxn.BoundingBox(−47, −112, 24, −15),
"Southeast Asia": new mxn.BoundingBox(−33, 52, 39, −167)
};
在最基本层面上,regions变量包含与 Mapstraction BoundingBox对象对应的文本键。在我们的用途中,键是区域的唯一标识符。用于创建边界的四个数字是描述该区域所需的最小数字。第一对描述了盒子的西南角。第二对描述了东北角。盒子的其他两个角可以从这些值中推断出来。
当用户在地图上移动鼠标时,我们想要确定光标是否悬停在这些区域之一上。如果是,我们将通过围绕它绘制一个框来突出显示该区域。然后,如果用户点击,我们将放大到该区域,就像在图 10-9 中所示。
要实现这一点,我们需要监听两个事件:鼠标移动和点击。将这些行添加到你的create_map函数中:
❶ google.maps.event.addListener(mapstraction.getMap(), 'mousemove', `check_hover`);
❷ mapstraction.click.addHandler(function() {
if (highlighted) {
❸ `set_region`(highlighted);
}
});
第一个❶监听鼠标移动,并在鼠标在其边界框内时突出显示区域。我们必须使用谷歌的本地addListener函数,因为 Mapstraction 不支持mousemove事件。一个事件对象被传递给check_hover函数。

图 10-9. 放大到地震热点区域
尽管我们使用了少量的谷歌特定代码,但我们仍然能够使用 Mapstraction 处理其他所有事情。第二个❷事件监听地图上的任何位置的点击。如果某个区域已经被突出显示,它就会❸放大以更仔细地查看地震。
现在,让我们编写从我们的事件代码中调用的两个(加粗)函数。首先,我们将编写确定鼠标是否悬停在识别区域上的代码。将此代码添加到你的 JavaScript 中:
// Region highlight functions
var highlighted = "";
function find_region(cpt) {
for (var k in regions) {
if (k != "World") {
❶ if (regions[k].contains(cpt)) {
return k;
}
}
}
return "";
}
function check_hover(google_event) {
// Google-specific code to convert event to Mapstraction LatLonPoint
pt = new mxn.LatLonPoint(google_event.latLng.lat(), google_event.latLng.lng());
// Mapstraction code to highlight appropriate region
var regionin = find_region(pt);
if (regionin) {
❷ if (highlighted != regionin) {
highlighted = regionin;
highlight_region(regionin);
}
}
❸ else if (highlighted) {
highlighted = "";
❹ mapstraction.removeAllPolylines();
}
}
除了check_hover函数外,我们还有一个辅助函数。这两个函数一起确定用户是否悬停在区域上,如果是,是哪个区域。find_region函数做了大部分工作。它通过使用方便的contains函数将鼠标的纬度/经度与边界框的四条边❶进行比较,遍历区域的数组。
如果用户悬停在某个区域上,该区域的名称将被返回到check_hover。假设我们还没有突出显示该区域❷,我们将名称传递给highlight_region函数,该函数绘制框。如果鼠标没有悬停在区域上,但之前已经突出显示❸,那么我们知道用户将鼠标移出了该区域。因此,我们可以从屏幕上移除该框❹。
我们还没有创建添加框的函数,所以我们现在就做:
function highlight_region(name) {
var bounds = regions[name];
if (bounds) {
mapstraction.removeAllPolylines();
❺ var pdata = {"color": "white"};
var poly = BoundingBox_to_Polyline(bounds);
mapstraction.addPolylineWithData(poly, pdata);
}
}
function BoundingBox_to_Polyline(box) {
var points = [box.sw, new mxn.LatLonPoint(box.ne.lat, box.sw. Lon),
box.ne, new mxn.LatLonPoint(box.sw.lat, box.ne.lon),
new mxn.LatLonPoint(box.sw.lat, box.sw.lon-.0001)];
var poly = new mxn.Polyline(points);
return poly;
}
突出显示的过程相当简单,尽管我已经将其分为两个函数。我已经从第十九部分:绘制矩形以声明区域中重新打印了第二个函数,BoundingBox_to_Polyline,在第十九部分:绘制矩形以声明区域。
在highlight_region函数中创建的框使用白色折线❺。你可能希望将折线颜色改为其他颜色。其余的只是简单地绘制地图上的框。
到目前为止,当用户在地图上移动鼠标时,区域将被突出显示。现在我们需要在用户在突出显示的区域点击时发生某些事情。换句话说,我们需要设置当前区域并放大,以满足事件监听器调用的第二个功能。
将以下代码添加到您的 JavaScript 中:
function set_region(name) {
var bounds = regions[name];
if (bounds) {
mapstraction.setBounds(bounds);
}
}
简单来说,如果我们的数组中存在该区域,我们将地图的边界设置为仅包含所选区域。地图会放大并居中显示该区域的地震。
现在我们需要一种方法回到世界地图。在这个 mashup 的早期阶段,我们创建了一个 view_world 函数。我们如何调用它?我们将使用世界地图的图形,并在点击图像时调用该函数。
添加此功能会影响 mashup 的几个部分。因此,我们需要在多个地方包含几行代码。首先,将图形添加到您的 HTML 中,在地图和图例之间:
<a href="#" id="reset"><img src="worldmap.png" /></a>
您可以在 mapscripting.com/earthquake-mashup 找到这张图片,以及我在此示例中使用的标记图标。
接下来,我们需要添加一些 CSS,以便图形出现在地图的右上角:
#reset img {
border: 5px solid white;
position: relative;
top: −435px;
left: 430px;
}
让我们使用 jQuery 来响应用户点击图像。将此代码添加到 create_map 函数中:
$("#reset").click(view_world);
这通过 CSS 选择器语法检索链接元素,并告诉浏览器在点击该对象时调用 view_world 函数。
如果我们不挑剔,我们就可以在这里停止。在一个完美的世界中,可点击的图形只有在地图放大时才可见。让我们看看我们是否不能使这个世界更加完美,再次使用一些 jQuery 函数。
因为我们希望图形在我们查看整个世界时消失,所以我们需要在 view_world 函数内部添加此行:
$("#reset").hide();
图像现在将始终不可见。当然,我们希望它在放大时出现。在 set_region 函数中,直接在 setBounds 行下方,我们添加此行:
$("#reset").show();
这样,我们就将一个更好的界面整合到了我们的交互式地震地图中。按区域放大很酷,只要你能够返回到地图。我们可能没有创造一个完美的世界,但这个世界地图 mashup 几乎是完美的。
我们将 USGS 数据从 XML 转换为 JSON。然后我们读取过去一周内的每一场地震,确定其强度,并给它一个适当的图标。最后,我们实现了热点区域的放大功能。剩下要做的就是监控构造运动;至少现在你有一个工具来做这件事。
#71:按位置搜索音乐活动
想要今晚查看一场音乐会吗?我们去哪里,谁在演奏?这些信息都在那里。让我们动手获取它并在地图上绘制。
对于这个混合应用,我们将使用雅虎的 Upcoming API。Upcoming 是一个显示会议、音乐会、用户组会议等活动的日历。我们只想搜索音乐,这是通过使用基于标签的搜索 API 实现的选项。
除非我们能确保所有用户都来自同一个城市,否则我们需要提供一种按位置搜索的方法。幸运的是,Upcoming 也提供了这个选项。因为有些人愿意比其他人开更远的车,所以我们还希望用户能够指定距离。是的,这个选项也得到了支持。
有些人比其他人更节俭。你可能愿意花 75 美元去看一位年迈的摇滚明星,但我更喜欢支付一小笔入场费去看在酒吧的当地乐队。我们需要为不同的价格范围做出计划。不幸的是,Upcoming 并不直接支持这个选项。我们可以查询免费音乐会,但我不算是个十足的吝啬鬼;我会支付五到十美元。然而,由于 Upcoming 确实传递了票价信息,我们可以自己处理价格过滤。
这个混合应用需要很多功能。你可以在图 10-10 中看到,它们都很好地结合在一起。
现在我们有了行动计划,让我们开始吧!在我们深入研究数据本身之前,我们先了解一下我们将为用户创建的界面。

图 10-10. 25 英里内显示的
准备搜索界面 HTML
由于这是一个地图混合应用,地图将是我们的体验中心。用户需要告诉我们他们的位置和价格方面的喜好。我们需要将搜索功能放在地图附近,这样就很明显一个控制另一个。
打开一个新文件,并添加以下代码以创建一个基本的地图和表单字段:
<html >
<head>
<title>Upcoming Music Map Mashup</title>
❶ <script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
<script src="http://maps.google.com/maps?file=api&v=2&key=
*`YOURKEY`*" type="text/javascript"></script>
<script type="text/javascript" src="mxn.js?(google)"></script>
<style type="text/css">
div#mymap {
width: 550px;
height: 450px;
}
</style>
<script type="text/javascript">
var mapstraction;
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.addControls({"zoom":"large"});
mapstraction.setCenterAndZoom(new mxn.LatLonPoint(45.5, −122.5)❷, 10);
}
</script>
</head>
<body onload="create_map();">
<p>
<form onsubmit="`search_upcoming()`; return false;">
Location: <input type="text" name="location" value="Portland, OR"❸ size="20" />
<select name="radius" onchange="`search_upcoming()`;">
<option value="1">1 mile</option>
<option value="5"❹>5 miles</option>
<option value="10">10 miles</option>
<option value="25" selected="selected">25 miles</option>
<option value="50">50 miles</option>
</select>
<select name="cost" onchange="`filter_select(this)`;">
<option value="9999"❺>Any price</option>
<option value="25">$25 or less</option>
<option value="10">$10 or less</option>
</select>
<input type="submit" value="Search" />
</form>
</p>
<div id="mymap"></div>
</body>
</html>
大部分 HTML 看起来都很熟悉。和大多数地图一样,我加载了 Google Maps 和 Mapstraction JavaScript 库。我还包括了 jQuery❶,这使得应用效果和使用 Ajax 变得简单。
在这个例子中,搜索位置文本默认为俄勒冈州的波特兰。当地图首次加载时,它以波特兰为中心。你可以将中心更改为你的城市,但你需要编辑多个地方。首先,我使用了地理坐标❷来设置中心。你现在已经记住了你所在城市的这些坐标,对吧?其次,你需要更改文本字段的值❸。当然,你的用户可能会编辑位置,但从一个合理的默认值开始是有意义的。对我来说,我使用了我的家乡。
半径和成本字段是下拉框。值包含最大值。例如,如果你选择五英里的半径❹,那么超过五英里甚至一英尺的距离都不会显示在结果中。价格值也是如此。对于显示不考虑成本的选项❺,我们希望没有音乐会收费超过 9999 美元。
创建一个像我们这里这样的基本界面,可以帮助您了解需要做的一切。回顾代码列表,并注意粗体显示的函数名。您至少需要编写这些函数——一个用于搜索,另一个用于过滤——以便将此界面转换为可工作的混合应用。
在我们开始构建所需的函数之前,让我们看看我们将使用的数据。为此,我们需要熟悉 Upcoming API。
执行 Upcoming API 搜索
Upcoming 使用 REST API,这意味着我们可以在编码任何东西之前在浏览器中对其进行尝试。作为第一步,您需要一个 Yahoo! 账户来登录 Upcoming。然后,您需要获取一个 Upcoming API 密钥。这个密钥与您用于任何其他 Yahoo! 服务的密钥不同。
在登录 Upcoming 后,请在此处请求密钥:upcoming.yahoo.com/api/url。
现在您有了 API 密钥,请通过将此 URL 复制到浏览器中尝试 Upcoming 搜索:upcoming.yahooapis.com/services/rest/?api_key=YOURKEY&method=event.search&location=Portland,+OR。
这里是简单搜索结果的示例:
<rsp stat="ok" version="1.0" resultcount="12">
<event id="1234567" name="Some Band Name" description="..."
start_date="2011-04-15" latitude="45.5409" longitude="-122.6637"
geocoding_precision="address" geocoding_ambiguous="0"
venue_name="Wonder Ballroom" venue_address="128 NE Russell St"
venue_city="Portland" venue_state_name="Oregon" venue_state_code="OR"
venue_zip="97212" ticket_url="..." ticket_price="$25-$40" ticket_free="0" />
<event .... />
...
</rsp>
搜索词仅限于位置。在找到我们寻求的数据之前,我们需要添加更多搜索选项。请参阅表 10-1 以了解我们将使用的参数描述。Upcoming 的文档中列出了更多参数。
表 10-1. Upcoming API 事件搜索选项
| 参数 | 描述 |
|---|---|
api_key |
您的 API 密钥(必需) |
location |
搜索事件的市和州 |
radius |
搜索位置中心多远范围内的搜索 |
min_date |
开始搜索事件的时间 |
max_date |
停止搜索事件的时间 |
tags |
用于过滤结果的元数据关键字 |
服务器端检索事件数据
现在我们已经了解了 Upcoming API 的预期结果,我们准备连接到它。我们不会直接使用 JavaScript 获取数据,而是将通过服务器端 PHP 脚本处理数据。如果您不确定是否安装了 PHP 或如何使用它,请查看第九章。
我们使用 PHP 将结果传递给 JavaScript 的两个原因是:首先,我们必须考虑到使用 JavaScript 直接访问外部 API 的安全问题。在许多情况下,浏览器不会允许这样做。其次,我们可以对数据进行一些预处理。我们需要找到音乐会成本并将价格格式化为便于过滤结果的形式。此外,尽管 API 提供了 XML,但我们将通过服务器端脚本输出 JSON 格式。JSON 可以直接读入 JavaScript 对象,这再次会使我们的工作变得更容易。
我们将用来访问即将到来的 API 的 PHP 代码大约有 60 行长。为了不一次性显示所有内容,我将一次处理一个部分。这样我就可以描述正在发生的事情,你会在继续到下一个部分之前理解每一部分。
首先,让我们在你的服务器上创建一个新的 PHP 文件,并从查询字符串中检索参数:
<?php
$apikey = "*`YOURKEY`*";
$dateformat = "Y-m-d";
// Get arguments from querystring
`$location = $_GET["location"];`
`$radius = $_GET["radius"];`
`$timeframe = $_GET["timeframe"];`
`$tags = $_GET["tags"];`
// Determine the timeframe as a timestamp, set max/min date variables
❶ $mindate = date($dateformat);
$maxdate = "";
switch($timeframe) {
case "1d":
$timestamp = ❷time();
break;
case "1m":
$timestamp = strtotime("+1 month −1 day");
break;
case "1w":
default:
$timestamp = ❸strtotime("+1 week −1 day");
break;
}
❹ $maxdate = date($dateformat, $timestamp);
尽管我们正在使用别人的 API,但编写这样的中间人 PHP 脚本有点像是创建我们自己的 API。我们大部分的查询字符串参数(粗体)将不变地传递给即将到来的 API。然而,timeframe参数是我自己的创造。
这个参数指定了搜索未来的时间范围。这个简单的版本允许三种选项:一天(1d)、一周(1w)和一个月(1m)。因为即将到来的 API 没有这个选项,我们需要通过找到代表未来一天的戳记来将时间范围转换为最大日期。
我们已经将最小日期设置为今天 ❶。PHP 的date函数,如果没有包含第二个参数,则假定当前日期。我们可以通过传递time()作为第二个参数来达到相同的结果。实际上,在一天的时间范围内,我们只需将时间戳设置为time() ❷。这样,最小日期和最大日期都是今天,正如我们想要的。
对于一周或一个月的情况,我们需要进行一些日期计算。PHP 有一个strtotime函数,它接受许多不同类型的输入。在这个例子 ❸ 中,函数从今天开始并增加一周。然后它减去一天。为什么?因为否则我们会有一周多八天,这比一周还多。对于月份选项也是如此。
最后,我们将我们的时间范围转换为计算机能理解的戳记格式。现在我们只需要通过将timeframe传递给date函数来设置$maxdate变量 ❹。
现在我们已经确定了要发送给即将到来的 API 的所有变量,我们已经准备好编写下一部分代码。在本节中,我们实际上检索数据并进行预处理:
// Get XML results from Upcoming
❺ $url = "http://upcoming.yahooapis.com/services/rest/?api_key=$apikey";
$url .= "&method=event.search&location=$location&radius=$radius";
$url .= "&tags=$tags";
$url .= "&max_date=$maxdate&min_date=$mindate";
❻ $xmlobj = get_xml($url);
$outobj = array();
// Loop through results
foreach (❼$xmlobj->event as $event) {
❽ $attribs = $event->attributes();
$id = (int) $attribs->id;
$lat = (float) $attribs->latitude;
$lon = (float) $attribs->longitude;
$title = (string) $attribs->name;
$date = (string) $attribs->start_date;
$cost = "";
// Convert ticket price range into number value we can use in JavaScript
❾ preg_match_all("(\\\$\d+)", (string) $attribs->ticket_price, $dollars);
if (count($dollars) > 0 && count($dollars[0]) > 0) {
$cost = $dollars[0][count($dollars[0])-1];
}
$cost = str_replace("$", "", $cost);
// Put all results into an array of associative arrays
$eventobj = array(
"id" => $id,
"latitude" => $lat,
"longitude" => $lon,
"title" => $title,
"date" => $date,
"cost" => $cost
);
❿ array_push($outobj, $eventobj);
}
使用我们在上一节中创建的变量,我们将 URL 拼凑起来以调用即将到来的 API ❺。URL 的内容将以 XML 格式通过,我们将它转换成一个SimpleXML对象。在第六十一部分:检索网页中,我们编写了get_xml函数来完成这个任务。我们不妨节省一些时间,并在这里重用那个函数 ❻。
现在 XML 数据很容易访问,让我们遍历 Upcoming API 返回的所有活动。我们将逐个抓取 <event> 标签 ❼。关于活动的数据存储为事件标签的属性。我们可以一次性抓取所有属性 ❽,然后挑选我们想要的:Upcoming 分配的唯一 id、纬度、经度、活动的标题、日期和成本。大多数这些都是直接的,但我们需要做一些魔法来获取我们想要格式的活动价格。
Upcoming 在票价前包含一个美元符号,许多活动有一个价格范围而不是单一金额。为了在我们的 JavaScript 代码中按成本过滤,我们需要我们的 PHP 代码返回一个简单的数字。
在这里,我使用正则表达式来查找所有以美元符号开头后跟一个或多个数字的实例 ❾。这样,我们可以取最后一个美元金额,如果有的话,它应该是在范围最高端。如果票价只有一个价格,表达式也会找到它。最后,我们移除美元符号,因此我们只返回一个数字。
现在我们已经得到了所需的活动数据,我们将它放入一个关联数组 $eventobj 中。然后这个新数组被“推”到结果对象 ❿ 的末尾,这是一个普通数组。我将其命名为 $outobj,因为我们将会打印它。实际上,在所有预处理完成之后,我们现在就可以这样做:
// Output values as JSON
print header("Content-type: application/json");
print json_encode($outobj);
?>
我们首先打印标题来声明我们正在发送纯文本。PHP 默认为 HTML。接下来,我们打印出结果对象,但我们确保它是 JSON 编码的。这样,我们给 JavaScript 代码提供了一些容易消化的东西。
记得我们曾经使用 get_xml 函数来检索即将到来的 URL 并将 XML 内容转换为 SimpleXML 对象吗?我们实际上从未在我们的代码中包含这个函数。现在让我们来做这件事:
<?
//cUrl functions
function get_url($url) {
$c = curl_init();
curl_setopt($c, CURLOPT_URL, $url);
curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
$content = trim(curl_exec($c));
curl_close($c);
return $content;
}
function get_xml($url) {
$xml = get_url($url);
return simplexml_load_string($xml);
}
?>
嘿!这是两个函数!
你抓住了我。因为一个函数调用了另一个,所以我们需要包含它们两个。如果你经常使用这些函数,将它们添加到自己的文件中可能是有益的。然后你可以根据需要使用 PHP 的 include 函数将它们作为项目的一部分。
就这样。我们已经编写了一些 PHP 代码来调用 Upcoming API。从 Upcoming 返回的 XML 中,我们只取我们想要的,并以 JSON 格式输出。我将 PHP 文件保存为 upcoming.php,与之前创建的 HTML 搜索界面在同一目录下。现在让我们回到那个 HTML 文件,这样我们就可以使用 JavaScript 连接到我们刚刚创建的 PHP 文件。
在地图上绘制事件搜索结果
现在你已经知道如何从 Upcoming 获取数据,让我们将那些数据放在我们的地图上。我们将连接到我们刚刚创建的 PHP 文件,发送它所需的信息。
你可能还记得在设置 HTML 时,我们需要创建两个函数。首先,我们编写 search_upcoming 函数来执行对 PHP 的 Ajax 调用。在 create_map 函数下方添加这些行:
function search_upcoming() {
❶ var tags = "music,concert";
var timeframe = "1w";
❷ var data = {
"location": f.location.value.replace(", ", ",").replace(" ", "_"),
"radius": f.radius.value,
"tags": tags,
"timeframe": timeframe
};
❸ $.get("upcoming.php", data, ❹plot_upcoming);
}
在我们能够搜索即将发生的事件之前,我们需要从表单中检索用户的值。为此,我使用了document.getElementById函数,并对位置进行了一些数据清理。
我硬编码了一些我们目前在 PHP 文件中不作为用户输入使用的选项值。例如,我们寻找的标签❶可以更改为其他内容,如果你不是在寻找音乐会。时间范围默认为一周,这似乎对计划即兴娱乐最有用。
我硬编码的值,加上表单中的几个值,被放入一个 JavaScript 对象❷中。调用我们的 PHP 文件并使用data变量包含值的 jQuery getJSON函数❸需要这种格式。最后一个参数是plot_upcoming❹函数的引用。这是我们需要编写的函数之一。
在这个新函数中,我们想要遍历 JavaScript 对象中的所有结果。当我们找到每个事件时,我们将其绘制在地图上。将此代码添加到您的 JavaScript 中:
function plot_upcoming(jobj) {
if (jobj.length > 0) {
mapstraction.removeAllMarkers();
❺ for (var i=0; i < jobj.length; i++) {
var ev = jobj[i];
❻ var url = "http://upcoming.yahoo.com/event/" + ev.id;
var marker = new mxn.Marker(new mxn.LatLonPoint(ev.latitude, ev.longitude));
var cost = ev.cost;
if (cost != "") {
❼ marker.setAttribute('cost', parseInt(cost));
cost = " ($" + cost + ")"; // Format cost for infoBubble
}
else {
marker.setAttribute('cost', 9999); // Set a way too high value
}
var bubbletext = ev.date + " <a href=\"" + url + "\">" + ev.title
+ "</a>" + cost;
marker.setInfoBubble(bubbletext);
mapstraction.addMarker(marker);
}
❽ filter_select(document.forms[0].cost);
}
else {
alert('no results for this search');
}
}
这个函数解包了 PHP 输出的变量,并使用它们为 API 返回的每个即将发生的事件添加一个标记。记住,我们使用了一个包含许多关联数组的数组。JavaScript 对象也是一个数组。我们使用for命令❺遍历它,将每个事件结果放入ev变量中。
使用从 Upcoming 返回的id,我们可以拼凑出用户可以找到更多事件信息的 URL❻。其他数据,如纬度和经度,我们直接放入我们用来创建标记的变量中。
如果 PHP 能够确定事件的成本,我们将在标记上添加包含该信息的属性❽。当按票价过滤时,这些信息将很有用。实际上,在这个函数的末尾,一旦所有标记都添加到地图上,我们调用那个过滤器函数❽,这意味着我们最好去编写它。
通过票价过滤结果
当结果返回时,它们包含该区域内的所有音乐事件,而不仅仅是符合用户预算的事件。尽管 Upcoming API 没有查询特定票价以下事件的方法,但它确实在结果中提供了价格(如果有的话)。我们编写的 PHP 代码将票价转换为我们可以用作过滤器的数值。
在上一节中,我们为每个包含票价属性的标记添加了一个cost属性。仅仅有这个属性还不够,我们需要应用过滤器,这个过滤器来自 HTML 表单中的下拉框。
当所有标记添加到页面或下拉框中的值更改时,会调用filter_select函数。将这些函数添加到您的 JavaScript 代码中:
function filter_select(selobj) {
❶ var cost = parseInt(selobj.options[selobj.selectedIndex].value);
filter_cost(cost);
}
function filter_cost(amt) {
❷ mapstraction.removeAllFilters();
mapstraction.addFilter('cost', ❸'le', amt);
mapstraction.doFilter();
❹ mapstraction.visibleCenterAndZoom();
}
与其他示例一样,两个函数执行这个任务。第一个从下拉框中检索值❶,然后将成本传递给第二个。filter_cost函数执行实际的过滤工作。
在创建新的过滤器之前,我们需要删除任何之前的过滤器❷。为什么?Mapstraction 的过滤是累加的,这意味着第二个过滤器不会替换第一个,而是附加到第一个之上。在这种情况下,我们只想使用一种过滤方法,所以我们删除所有过滤器后再添加新的。
应用过滤器需要三个信息:要过滤的属性、要使用的运算符(在这种情况下为le,表示小于或等于❸),以及最后要比较的值。标记实际上只有在 Mapstraction 的doFilter函数被调用时才会被过滤。
现在只显示与我们的过滤器匹配的标记,我们可以确保它们都在地图上可见。Mapstraction 有一个专门用于这种情况的函数❹。我们不想根据所有标记设置中心点和缩放级别;我们只想使用可见的标记。
这些过滤函数在我们的大杂烩中调用,每当用户搜索演出时。当用户更改票价下拉框中的值时,我们也会节省一点带宽。因为我们已经存储了所有价格范围的演出,所以我们调用这些过滤函数来仅显示匹配的演出,如图图 10-11 所示。

图 10-11。仅显示匹配搜索条件的演出结果
如果你回顾一下图 10-10,你可以看到旧金山 25 英里范围内的所有演出结果。然后,在图 10-11 中,你只能看到票价为 10 美元或以下的两个演出。地图自动缩放以仅显示符合我们过滤器标准的演出。相当酷。那么我们接下来要听哪支乐队呢?
#72:绘制 Twitter 地理推文
Twitter 是一个流行的服务,用于与朋友分享简短的消息。在 140 个字符或更少的情况下,人们发送牢骚、链接、照片或他们想分享的任何内容。可选地,这些消息(称为tweets)可以进行地理标记。当内容与位置相关联时,你可以用这些数据做一些有趣的事情。
在这个大杂烩中,我们将创建一个工具,让用户可以通过城市名称、ZIP 代码或地址搜索地理标记的推文。我们还将创建一个可选的按关键词搜索的方式。你想要查看附近所有提到lunch的地理标记推文吗?你可以做到!只需看看图 10-12。

图 10-12. 旧金山人喜欢在哪些地方发午餐推文
除了搜索推文,我们还将整合书中早期的一些其他项目。因为我们需要用户输入一个位置,所以我们需要一个地理编码器将地点名称转换为经纬度坐标。为此,我们将使用 #12: 使用 JavaScript 进行地理编码 在 #12: 使用 JavaScript 进行地理编码。而且因为我们需要从某个地方开始,我已经使用了 #50: 通过 IP 获取位置 在 #50: 通过 IP 获取位置 来猜测用户的位置。
但首先我们需要创建我们的地图将驻留的页面 HTML。让我们开始吧。
准备带有用户位置的地图
我们创建的大多数地图都以默认位置开始。这是此地图与其他地图之间最大的区别。在这里,我们将使用 Google 的ClientLocation来猜测用户的城市。如果不可用,我们将显示整个美国的地图。在任何情况下,地图下面的表单都可以用来设置新位置或其他搜索词。
在一个空文件中添加以下代码:
<html >
<head>
<title>Show Geocoded Tweets</title>
<style>
div#mymap {
width: 600px;
height: 400px;
}
</style>
❶ <script type="text/javascript" src="http://ajax.google
apis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
❷ <script type="text/javascript" src="http://www.google.com/
jsapi?key=*`YOURKEY`*"></script>
<script type="text/javascript" src="mxn.js?(google)"></script>
❸ <script type="text/javascript" src="mxn.google.geocoder.js"></script>
<script type="text/javascript">
❹ google.load("maps", "2");
var mapstraction;
var radius_field, location_field, kw_field;
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.addControls({"zoom":"large"});
// Store form field objects
radius_field = document.getElementById('radius');
location_field = document.getElementById('loc');
kw_field = document.getElementById('keyword');
// Get position from ClientLocation
var pos = google.loader.ClientLocation;
if (pos) {
var posloc = new mxn.LatLonPoint(pos.latitude, pos.longitude);
var cityname = pos.address.city + ", " + pos.address.region;
get_twitter_geo(posloc, 5);
mapstraction.setCenterAndZoom(posloc, 11);
❺ location_field.value = cityname;
}
else {
❻ mapstraction.setCenterAndZoom(new mxn.LatLonPoint(40, −92), 3);
}
}
</script>
</head>
<body onload="create_map()">
<div id="mymap"></div>
<form onsubmit="geocode_form();return false;">
<input type="text" id="keyword" name="keyword" size="25" /> within
<select name="radius" id="radius">
<option value="1">1 mi</option>
<option value="5" selected>5 mi</option>
<option value="10">10 mi</option>
<option value="25">25 mi</option>
</select>
of <input type="text" id="loc" name="loc" size="25" />
<input type="submit" value="Go" />
</form>
</body>
</html>
首先,我们包含 jQuery 库 ❶,我们将使用它来向 Twitter 发起 Ajax 调用。然后我们加载通用的 Google JavaScript API 脚本 ❷。与正常加载 Google Maps API 的方式相比,这种区别很重要;我们使用这种方法,以便我们可以检索用户的地理位置。因为我们还将使用 JavaScript 地理编码器,所以我们也需要包含它 ❸。
到目前为止,我们已以标准方式加载了所有脚本。然而,我们只从 Google 包含了加载其他脚本的脚本。在 JavaScript 部分,我们首先需要加载 Google Maps ❹。
当我们创建地图时,我们需要检查我们是否可以确定用户的地理位置。如果我们能,那么我们可以根据从ClientLocation检索到的坐标设置地图的中心。我们还将预先填充搜索表单,包含此位置的名称 ❺,并调用get_twitter_geo函数,该函数从 Twitter 检索搜索结果。
如果我们无法确定用户的地理位置,我们只需放大地图,使整个美国显示在地图上 ❻。从这种视图调用搜索结果是无用的,因此我们将等待用户手动搜索。这是如何发生的?请继续阅读。
地理编码用户输入
接受用户输入是此混合应用的重要部分。用户希望与地图和数据交互,这需要查找任何位置的能力。我们在上一节中创建的表单有关键词、半径和地点的输入。Twitter 可以使用所有这些来查找推文,但首先我们需要将地点转换为 Twitter 期望的经纬度坐标。
当用户提交表单时,浏览器将调用geocode_form函数,该函数用于启动对 JavaScript 地理编码器的调用。让我们通过在 JavaScript 部分外添加以下行来创建此函数:
function geocode_form() {
var loctxt = location_field.value;
❶ if (loctxt == "") {
call_twitter_geo({point: mapstraction.getCenter()});
}
else {
geocoder = new MapstractionGeocoder(❷call_twitter_geo,
'google'); var address = { address: loctxt };
❸ geocoder.geocode(address);
}
}
如果用户留空了位置字段❶,我们假设他们想要使用当前地图的中心进行搜索。否则,我们创建一个地理编码器并设置回调函数❷。然后,我们使用用户输入的位置进行地理编码❸。
在任何情况下,接下来将被调用的函数是call_twitter_geo(直接或作为地理编码器的回调),它将点和其他标准传递给我们的get_twitter_geo函数,该函数执行繁重的工作。现在让我们创建这两个函数中的第一个和更简单的函数。在 JavaScript 部分中添加以下代码,但不要在另一个函数内部:
function call_twitter_geo(❹loc) {
mapstraction.setCenterAndZoom(loc.point, 11);
var kw = kw_field.value;
var rad = radius_field.options[radius_field.selectedIndex].value;
❺ get_twitter_geo(loc.point, rad, kw, 1, 100);
}
预期参数是一个位置对象❹,它包含一个 Mapstraction LatLonPoint的point属性。尽管我们有时会直接调用此函数,但它设计为接受 JavaScript 地理编码器的结果。
我们收集表单中的其余字段(关键字、半径)并将它们传递出去以用于检索地理推文❺。
这样,我们就完成了准备工作。现在我们准备搜索推文。
从 Twitter 检索地理推文
到目前为止,我们只确定了(以各种方式)搜索的某个点,但尚未执行实际的搜索。这正是我们在这里要做的:将我们的要求发送给 Twitter,并接收回推文。
Twitter 的搜索 API 不需要密钥,因此您可以立即开始。基本 URL 是search.twitter.com/search.json,您可以使用多个参数来调用它。我们将使用的是表 10-2 中列出的参数。
表 10-2. 一些 Twitter 搜索参数
| 参数 | 描述 |
|---|---|
q |
搜索查询/关键字 |
geocode |
lat,lon,radius格式的坐标 |
page |
要检索的结果页 |
rpp |
每页结果数 |
您可以通过调整 URL 在浏览器中进行搜索。以下是一些简化的示例结果:
{"results":[
{"from_user":"mapscripting", "created_at":"Thu 15 Jul 2010 12:30:12",
"text":"This is an example tweet, shown in the API",
"geo":{"coordinates":[45.5228,-122.6485],"type":"Point"},
...},
{"from_user":"adamd", ...
"geo":null, ... },
...
], ... }
您可以看到结果包含用户名、推文文本和日期。此外,geo属性包含有关地理编码推文的信息。然而,并非每个推文都包含这些数据,即使我们在搜索查询中发送了geocode参数。在创建绘制地理编码推文的函数时,我们需要注意这一点。事实上,由于存在大量未地理编码的推文,我们可能需要浏览多页结果才能收集到足够多的推文来在地图上绘制。
与我展示的相比,推文中发送了更多的数据,例如创建推文的客户端和撰写推文的用户的个人资料图片。你可以在 Twitter 的网站上找到搜索 API 的完整文档,网址为dev.twitter.com/doc/get/search。
现在你已经更好地了解了从 Twitter 的 API 中获得的数据,让我们编写 get_twitter_geo 函数,这是我们从我们的混合应用中调用的。将以下代码添加到你的 JavaScript 中,任何其他函数之外:
function get_twitter_geo(loc, rad, kw, pg, rpp) {
// Set default values
if (rpp == null) {
rpp = 100;
}
if (rad == null) {
rad = 5; // radius in miles
}
// Clear the map on first page
if (pg == null || pg == 1) {
pg = 1;
mapstraction.removeAllMarkers();
mapstraction.removeAllPolylines();
❶ polygon_circle(loc, rad);
}
mapstraction.autoCenterAndZoom();
// Construct URL
var url = "http://search.twitter.com/search.json?page=" + pg;
if (kw != null && kw != "") {
url += "&q=" + kw;
}
url += "&geocode=" + loc.lat + "," + loc.lon + "," + rad + "mi" + "&rpp=" + rpp;
❷ url += "&callback=?";
$.getJSON(url, function(jobj) {
var resarray = jobj.results;
for (var i=0; i<resarray.length; i++) {
var res = resarray[i];
❸ if (res.geo) {
var coords = res.geo.coordinates;
var mk = new mxn.Marker(new mxn.LatLonPoint(coords[0], coords[1]));
mk.setInfoBubble(res.text);
mapstraction.addMarker(mk);
}
}
❹ if ((pg * rpp) < 1500 && resarray.length == rpp) {
get_twitter_geo(loc, rad, kw, ❺pg+1, rpp);
}
});
}
该函数包含五个参数,但只需要一个(位置)即可。其他参数——半径、关键词、页面和每页结果数——如果需要,则设置为默认值。
每当搜索结果的第一页时,我们知道这是一个新的搜索,因此我们必须从地图中移除之前的结果。然后,基于中心点,我们在搜索区域周围画一个圆。正如在第 18 条:添加圆形以显示搜索半径中描述的,我使用多边形来近似圆形,该描述在设置填充颜色和透明度章节中。为了方便,polygon_circle ❶ 函数在本文节的末尾重新打印。
使用函数参数(或默认值),我们然后创建一个 Twitter 搜索的 URL。最后,我们包括一个带有问号的 callback 参数 ❷,jQuery 将用生成的函数名填充它。
一旦我们得到查询结果,我们就循环遍历,直到找到一个地理编码的推文 ❸。然后我们获取其坐标(一个数组,纬度在经度之前)并创建一个带有这些坐标的标记。我已经给标记添加了一个包含推文文本的消息框。如果你想的话,也可以包含更多关于推文的数据。
当我们遍历完所有结果后,我们不一定已经完成。因为并非每条推文都有地理编码,我们需要查看许多页面。Twitter 只会返回 1,500 条推文。页数将取决于每页的结果数。只要我们低于限制并且仍然收到完整的推文集合 ❹,我们就想继续搜索。在某些地区,尤其是在没有按关键词搜索的情况下,你的推文地图可能会非常满,就像在图 10-13 中所示。
这个 get_twitter_geo 函数是计算机科学家所说的递归,意味着它调用自己。这可能会很危险,因为如果你在再次调用时没有注意条件,你可能会遇到无限循环。函数最重要的部分可能是我们每次调用时都会增加页面编号 ❺。这样做将确保我们最终会停止调用该函数。
就这些了!在你代码生效之前,你需要包含以下代码,这些代码是从第四章中重新打印的:
function polygon_circle(center, radius) {
var rad = new mxn.Radius(center, 10);
var poly = rad.getPolyline(mxn.util.milesToKM(radius), '990066');
mapstraction.addPolyline(poly);
}
现在你已经编写了一个混合应用,它将位置地理编码并搜索该地点附近的推文。你已经触动了地理蜂群智慧。现在开始使用它来揭示一些有趣的数据。在你的城市里,人们在哪里发推文谈论午餐?

图 10-13. 波特兰市中心附近有许多许多推文
#73: 在中间找一个咖啡店见面
在中间见面是幸福婚姻的秘密,也是通过幼儿园的关键。当涉及到在物理上找到一个见面地点时,中间见面是制作出色地图混合应用的好方法。无论你和朋友是城里还是相隔千里,我们都会制作一个地图,找到尽可能靠近你两个位置中点的咖啡店,如图图 10-14 所示。
我们创建的地图将使用本书前面提到的几个示例。首先,我们将从用户那里获取输入并确定路线,就像我在第 37 条:使用路线查找真实距离中展示的那样。然后,我们将遍历驾驶方向的每个步骤,以确定我们何时大约走了一半的路程。为了找到确切的中点,我们将使用第 40 条:在一条线上找到一点中描述的另一种方法。最后,我们将使用中点通过 Yelp 的 API 执行本地搜索。
带着这个行动计划,继续阅读以开始。

图 10-14. 在路线中点寻找咖啡店
准备地图和表单
在我们能够整合第六章中的其他示例之前,我们需要考虑在网页上包含哪些部分。显然,我们需要包含一个地图。我们还需要一种方法从用户那里获取两个位置。
打开一个新文件,并添加以下 HTML 来创建一个包含整个美国视图和获取用户输入所需输入字段的地图:
<html >
<head>
<title>Coffee in the Middle</title>
<style>
div#mymap {
width: 600px;
height: 450px;
}
</style>
❶ <script type="text/javascript" src="http://ajax.google
apis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
<script src="http://maps.google.com/maps?file=api&v=2&key=
*`YOURKEY`*" type="text/javascript"></script>
<script type="text/javascript" src="mxn.js?(google)"></script>
<script type="text/javascript">
var mapstraction;
❷ var gdir;
function create_map() {
mapstraction = new mxn.Mapstraction('mymap', 'google');
mapstraction.addControls({"zoom":"large"});
mapstraction.setCenterAndZoom(new mxn.LatLonPoint(40, −92), 3);
}
</script>
</head>
<body onload="create_map()">
<form id="myform" onSubmit="❸goDir();return false;">
Your location: <input type="text" id="start" />
Friend's location: <input type="text" id="end" />
<input type="submit" value="Go" />
</form>
<div id="mymap"></div>
</body>
</html>
我已经包含了 jQuery JavaScript 库❶,我们稍后会用它来连接到 Yelp。展望未来,我添加了gdir变量❷来保存来自 Google 的驾驶方向。保存并加载文件,它将看起来像图 10-15。

图 10-15. 混合应用的基础:我们的地图和表单
注意,当前这个表单没有任何功能。如果你点击提交按钮或在文本字段中按下回车键,它将尝试调用goDir()函数,正如 HTML ❸中指示的那样。然而,该函数目前还不存在,所以下一步是创建它。我们将在下一节中完成这个任务,并从用户的输入中检索行驶方向。
检索行驶方向
这个混合应用可以找到两个起点和终点之间的中点,但这只有在你和你的朋友都是乌鸦或飞行员的情况下才成立。你更有可能开车而不是飞行,所以我们将使用谷歌的行驶方向 API。
如您从第六章中可能记得的,要检索行驶方向,我们首先需要告诉谷歌加载适当的代码。在create_map函数中,添加以下行以准备行驶方向:
❶ // Google-specific code for driving directions
❷ gdir = new google.maps.DirectionsService();
首先,我们创建了一个注释❶来标记我们正在编写特定提供商的代码;在这种情况下,这段代码只与谷歌兼容。如果你需要将这个混合应用转换为使用不同的地图提供商,这将是有帮助的。
为了使用行驶方向,我们必须创建一个DirectionsService对象❷。稍后,我们可以调用该对象上的函数或将它传递给其他函数。
现在我们已经准备好查找行驶方向,让我们编写goDir函数。当用户填写表单时,这个函数会被调用。将此代码添加到 JavaScript 部分,但不在create_map函数内:
function goDir() {
var start = document.getElementById('start').value;
var end = document.getElementById('end').value;
// Remove Markers and Polylines
mapstraction.removeAllMarkers();
mapstraction.removeAllPolylines();
// Google-specific: load directions
❸ var diropt = {
origin: start,
destination: end,
travelMode: google.maps.DirectionsTravelMode.DRIVING
}
❹ gdirroute(diropt, addDir);
}
goDir函数的目的是将两个位置传递给行驶方向服务。我们首先将用户输入的文本存储到变量中。如果这是一个后续搜索,并且地图已经包含结果,在调用方向之前,我们需要清除地图上的标记和折线。
现在我们已经准备好将位置传递给谷歌的方向服务。我们将它们包含在一个特殊选项对象❸中。然后我们可以使用这些选项调用方向服务,并给谷歌提供一个回调函数❹。
在我们找到中点之前,让我们将方向添加到地图上。以下是回调函数的代码:
function addDir(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
❺ var gpts = response.routes[0].overview_path;
var polypts = [];
for (var i=0; i<gpts.length; i++) {
❻ polypts.push(new mxn.LatLonPoint(gpts[i].lat(), gpts[i].lng()));
}
// Add polyline to map
var poly = new mxn.Polyline(polypts);
mapstraction.addPolyline(poly);
mapstraction.autoCenterAndZoom();
// Find distance
❼ var dist = response.routes[0].legs[0].distance.value / 1000;
// Find midpoint
❽ findMidpoint(polypts, dist);
}
}
此函数接收谷歌行驶方向服务的输出结果。从结果中,我们可以获取构成路线的点❺。然后,遍历这些点,将每个点转换为 Mapstraction LatLonPoint后,添加到新的点数组❻中。
当我们有了路线上的所有点后,我们可以使用它们来创建一个新的折线。这条线将作为两个位置之间整个路线的视觉表示,如图图 10-16 所示。我们几乎准备好找到中点,但我们需要从谷歌方向中获取一个额外的信息:总路线距离(公里)❽。然后,我们将新的点数组和距离传递给findMidpoint函数❽。
在下一节中,我们将创建那个函数,它完成了这个混合应用中大部分的实际工作。

图 10-16. 在找到咖啡店时显示的完整方向
找到路线的中点
现在我们已经设置了网页并从 Google 的方向服务中检索了路线,我们需要编写代码来遍历返回的路线数据。当我们遍历方向中的每个点时,我们将添加到目前为止的总行程距离,直到我们走了一半的总距离。
findMidpoint函数接收两个参数,即路线点和总距离。将此函数添加到您的 JavaScript 代码中,但放在其他函数之外:
function findMidpoint(allpts, totaldist) {
var midicon = 'http://chart.apis.google.com/chart?chst=d_map_pin_icon
&chld=star|00FF00';
// Determine distance needed
var halfdist = totaldist / 2;
var distsofar = 0;
// Loop through points, adding up distance so far
for (var i=1; i < allpts.length; i++) {
var pt1 = allpts[i-1];
var pt2 = allpts[i];
var thisdist = pt1.distance(pt2);
❶ if ((distsofar + thisdist) < halfdist) {
distsofar += thisdist;
}
else {
❷ var distneeded = halfdist − distsofar;
// Determine point that is "distneeded" along the line between pt1 and pt2
var bearing = get_bearing(pt1, pt2);
var midpt = get_destination(pt1, distneeded, bearing);
❸ var midmk = new mxn.Marker(midpt);
midmk.setIcon(midicon);
mapstraction.addMarker(midmk);
// Determine search radius
var radius = 1+ totaldist / 10;
if (radius > 25) {
radius = 25;
}
❹ loadYelp("coffee", midpt, radius);
break; // stop the loop, we're halfway!
}
}
}
每次通过循环,我们查看两个点——前一个点和当前点。因为我们需要两个点,所以我们的循环从 1(第二个点)开始,而不是从 0 开始。我们计算这两个点之间的距离。只要到目前为止所有点之间的距离小于总距离的一半 ❶,我们就将这两个点之间的距离加到运行总和中,然后继续循环到下一个点。
一旦我们找到了中点,真正的任务就开始了。我们找到的点很可能实际上比中点更远。然而,因为我们按顺序查看每个点,所以我们知道中点在两个点之间。我们可以计算出我们需要走多远 ❷ 才能到达中点。
现在我们有了两个点以及从第一个点到第二个点的距离,我们拥有了使用第四十部分:在直线上找到一点中示例所需的所有信息。第四十部分:在直线上找到一点项目中的两个函数get_bearing和get_destination在本节末尾重新打印。
当我们有了中点,我们可以用它来创建一个标记 ❸,并将其添加到地图上。然后我们也将使用那个点来搜索 Yelp。但首先我们需要确定要发送给搜索的半径。Yelp 可以接受 25 英里或更小的任何东西。如果我们用户只是在镇上搜索路线,25 英里的搜索可能比整个路线更长。然而,如果我们选择半径太小,当中点在更乡村地区时,附近可能没有咖啡的风险。
我选择将搜索半径至少设置为 1 英里,然后再加上总距离的 10%。这样,小距离会有相应较小的搜索半径。而且,如果我的用户正在穿越整个国家,25 英里的半径是合理的。
最后,我们将所有这些信息发送到 Yelp ❹。在我们到达那里之前,这里有一些从第六章重新打印的函数:
function get_bearing(pt1, pt2) {
var lat1 = degrees_to_radians(pt1.lat);
var lat2 = degrees_to_radians(pt2.lat);
var lon_diff = degrees_to_radians(pt2.lon − pt1.lon);
var y = Math.sin(lon_diff) * Math.cos(lat2);
var x = Math.cos(lat1) * Math.sin(lat2)
- Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon_diff);
var bearing = Math.atan2(y, x);
return (radians_to_degrees(bearing)+360) % 360;
}
function get_destination(pt, dist, bearing) {
var R = 6371; // radius of earth (km)
var lat1 = degrees_to_radians(pt.lat);
var lon1 = degrees_to_radians(pt.lon);
bearing = degrees_to_radians(bearing);
var cosLat1 = Math.cos(lat1);
var sinLat1 = Math.sin(lat1);
var distOverR = dist / R;
var cosDistOverR = Math.cos(distOverR);
var sinDistOverR = Math.sin(distOverR);
var lat2 = Math.asin( sinLat1 * cosDistOverR
+ cosLat1 * sinDistOverR * Math.cos(bearing) );
var lon2 = lon1 + Math.atan2( Math.sin(bearing) * sinDistOverR * cosLat1,
cosDistOverR − sinLat1 * Math.sin(lat2) );
lon2 = (lon2 + Math.PI) % (2 * Math.PI) − Math.PI;
lat2 = radians_to_degrees(lat2);
lon2 = radians_to_degrees(lon2);
return new mxn.LatLonPoint(lat2, lon2);
}
function degrees_to_radians(deg) {
return deg * Math.PI / 180;
}
function radians_to_degrees(rad) {
return rad * 180 / Math.PI;
}
现在唯一缺少的是对 Yelp 的调用。我们将在下一节中编写它。
在 Yelp 上搜索咖啡
这个混合应用不仅仅是找到中点;它是在中点附近找到一个会面的地方。正如混合应用的标题所暗示的,我们正在搜索咖啡馆,尽管你的混合应用可以搜索任何你想要的关键词。为了执行搜索,我们将使用 Yelp,这是一个记录本地商业的网站。
Yelp 提供了一个 API,使得基于纬度和经度点的搜索变得简单。更好的是,Yelp 的响应以 JSON 格式返回,这种格式使得将其整合到我们的混合应用中变得快速。在使用 API 之前,你需要获取一个访问密钥,就像你为地图所做的那样。在 yelp.com/developers 上注册成为 Yelp 开发者。现在你已准备好对 Yelp API 进行第一次调用。基本 URL 是 api.yelp.com/business_review_search,但我们还会包括 表 10-3 中显示的参数。
你可以在浏览器中尝试一些搜索并查看 JSON 结果的文本。例如,这里是一个响应片段:
{"message": {"text": "OK", "code": 0, "version": "1.1.1"},
"businesses": [
{"name": "Somewhere Coffee", "latitude": 12.3456, "longitude": 123.4567, ... },
{"name": "Someplace Jo", "latitude": 12.3456, "longitude": 123.4567, ... },
...
]
}
表 10-3. Yelp API 搜索选项
| 参数 | 描述 |
|---|---|
ywsid |
你的 API 密钥(必需) |
num_biz_requested |
请求的搜索结果数量 |
term |
搜索的关键词(s) |
lat |
搜索点的纬度 |
long |
搜索点的经度 |
radius |
搜索半径(以英里为单位—25 或更少) |
让我们深入探讨并创建之前代码中已经调用的 loadYelp 函数。将以下函数添加到你的 JavaScript 部分,注意要将其放置在其他所有函数之外:
function loadYelp(kw, loc, rad) {
var url = "http://api.yelp.com/business_review_search";
url += "?ywsid=*`YOURKEY`*&num_biz_requested=10&term=" + kw";
url += "&lat=" + loc.lat + "&long=" + loc.lon + "&radius=" + rad;
❶ url += "&callback=?";
$.getJSON(url, ❷function(x) {
if (x.message.text == "OK") {
if (x.businesses.length != 0) {
var res = x.businesses;
var allpts = [];
for (var i = 0; i < res.length; i++) {
❸ var place = res[i];
❹ var thisloc = new mxn.LatLonPoint(place.latitude, place.longitude);
allpts.push(thisloc);
var html = "<strong>" + place.name + "</strong><br />" + place.address1;
html += "<br />" + place.city + ", " + place.state;
// Create and add marker to the map
var mk = new mxn.Marker(thisloc);
mk.setInfoBubble(html);
mapstraction.addMarker(mk);
}
❺ mapstraction.centerAndZoomOnPoints(allpts);
}
}
});
}
loadYelp 函数需要三个参数:搜索的关键词、搜索的位置(作为一个 Mapstraction LatLonPoint),以及半径(以英里为单位)。函数的第一件事是使用参数创建 Yelp API 的 URL。我们包括一个带有问号的 callback 参数❶,jQuery 将用生成的函数名填充它。
为了解释 Yelp 的结果,我们将使用一个匿名内联函数❷,它接受一个名为 x 的单一参数来保存 JSON 对象结果。一旦我们确认我们有可用的数据,我们就循环遍历所有结果并获取每个商业列表❸。从那里,我们还可以找到商业的位置❹和其他信息。
在这个例子中,我创建了一个包含一些简单商业信息和基本样式的消息框。你可以在这里尽情发挥,在消息框内包含任何你想要的 HTML。Yelp 还在其响应中提供了一些有趣的数据,但我没有在这里包括。你可以获取搜索点的距离、平均评论,甚至是一些地点的图片。
当我们完成对结果的循环遍历后,我们将为每个商业机构设置一个标记。然后我们只针对这些标记进行居中和缩放❺,传递我们在循环过程中收集到的点数组。这个函数与在第七部分:遍历所有标记中展示的第八部分:根据标记确定正确的缩放级别不同。autoCenterAndZoom函数也考虑了多边形,这显示了整个路线。
你可以在图 10-17 中看到一个示例搜索,其中混合体在博尔德和 Little Rock 之间找到了几个堪萨斯州的咖啡店。

图 10-17. 在科罗拉多州的博尔德和阿肯色州的小石城之间的堪萨斯州,中间相遇的结果显示了咖啡店。
下次你想见一个朋友,无论是城里还是全国范围内,你们中的任何一个人都不需要开车全程。你们可以见个中点,多亏了行车路线、一点数学知识、Yelp 以及你刚刚创建的这个小小的混合体。
附录 A. JAVASCRIPT 快速入门

本书涵盖的所有映射 API 都基于 JavaScript。要使用它们,您需要从您的网页中编写 JavaScript 代码以与地图交互。然而,正如您在本附录中将会看到的,它是一种易于学习的编程语言。
我将介绍一种非常基础的 JavaScript 方法——足以不被书中其他示例所困惑。如果您想学习完整的细节,请拿起 JavaScript 权威指南(thau!, No Starch Press, 2000)。
JavaScript 的去向
JavaScript 通过 <script> 标签添加到 HTML 页面中。有时代码会放置在这个标签及其结束标签之间。其他时候,这个标签会通过属性调用外部文件。在这个部分,我将展示这两种版本。
从技术上讲,您可以将 <script> 标签放置在 HTML 文档的任何位置。对于分析服务,页面底部可能是最佳位置。对于小部件的 JavaScript,可能需要放置在您希望小部件出现的确切位置。然而,从标准合规性和最佳实践的角度来看,尽可能在文档的 <head> 中包含您编写的 JavaScript 是有意义的。
这里是一个在更基础的网页上一些非常基础的 JavaScript 的例子:
<html>
<head>
<title>My Simple JavaScript Example</title>
<script type="text/javascript">
alert('Testing JavaScript');
</script>
</head>
<body>
<h1>Hello World</h1>
<p>Did you see the JavaScript alert?</p>
</body>
</html>
JavaScript 代码位于 <script> 标签之间。这里我使用了 alert,这是一个 JavaScript 函数,它将在您的网络浏览器中使用括号内的文本创建一个消息窗口。使用 alert 是展示简单示例和调试的好方法,但对于更复杂的脚本和面向公众的网页来说,超越它才是最佳选择。
提示
想要向没有 JavaScript 的浏览器发送特殊消息吗?包含 <noscript> 标签,浏览器将显示其中的任何内容。
<script> 标签可以包含任意多的 JavaScript 代码行,尽管过多的代码可能会开始拖累您的 HTML 文件,所以大多数超过几行的脚本都存储在外部文件中。考虑以下略微修改的例子:
<html>
<head>
<title>My External JavaScript Example</title>
<script type="text/javascript" src="js/test.js"></script>
</head>
<body>
<h1>Hello World</h1>
<p>My JavaScript is stored in a file.</p>
</body>
</html>
在这里,我通过 <script> 标签的 src 属性加载了 JavaScript,这与在 HTML 中引用图像的方式类似。按照惯例,文件具有 .js 扩展名。此外,为了保持组织有序,大多数开发者将 JavaScript 文件存储在离网站根目录的目录中。在这个例子中,我使用了名为 js 的目录。
文件的内容与您直接在页面上包含 JavaScript 时相同。但您不需要 <script> 标签。继续上一个例子,test.js 将只包含一行:
alert('Testing JavaScript');
使用这种方法具有明显的优势,即脚本文件由用户的浏览器单独缓存。如果您在许多页面上使用相同的脚本,当用户在您的网站上导航时,脚本只会下载一次。
如第一个例子所示,此代码在加载时立即运行。要在不同时间运行代码,你需要使用函数,这些函数将在本附录的后面描述。但首先,让我们感受一下 JavaScript 语法的其他部分,如变量、条件语句和循环。
变量
变量存储可以更改或稍后访问的值。你可以在变量中保存数字、文本等。在本节中,我将展示一些简单的值,以及更复杂的结构,如数组和对象。
首先,你需要能够识别变量。它们看起来像普通单词。有时变量可能由多个单词组成,但它们不能包含空格。通常,变量第一次使用时将被设置为值,例如:
var age = 21;
在这里,我使用 JavaScript 的var关键字创建了一个新变量。这个变量被称为age,但你几乎可以使用任何你想要的名称,只要它不是语言保留的单词之一。像大多数 JavaScript 代码行一样,这一行以分号结束。
等号声明左侧的变量现在将被设置为右侧的值。在这种情况下,我使用了一个字面量数字。你也可以使用文本、另一个变量,甚至其他变量和字面量的组合。
这里有一个使用多个变量的例子,其中一个变量依赖于其他变量:
var cost_per_person = 3;
var num_people = 7;
var total_cost = cost_per_person * num_people;
var关键字应该只在你第一次引用变量时使用。实际上,你甚至不需要使用它,尽管如此,如果没有使用,变量将被假定为全局的。变量作用域在变量作用域中将解释这意味着什么以及为什么明确声明变量是一个好主意。你甚至可以在不创建值的情况下这样做:
var tbd;
当设置包含文本的变量时,需要在值的每端使用引号,例如:
var name = 'Adam';
你可以使用单引号(')或双引号(")来定义文本字面量,但不能混合使用。如果你想在你字符串中包含引号,你需要用反斜杠转义它。
var name = 'Adam\'s apple';
关于文本变量的另一个特性将在下一节中介绍。
算术
在上一节中,我展示了使用乘号(*)字符将一个变量乘以另一个变量来计算total_cost的例子。
+、−、*、/和=符号是改变变量值时的工具。我们已经看到=将变量设置为与值相等。加法使用加号+。减法使用减号-。乘法和除法分别通过星号*和斜杠/完成。
这里有一个更复杂的例子:
var num = 10 * 2 / 4 + 8; // num is 13
遵循数学运算顺序,这意味着先执行乘法和除法,然后从左到右进行计算。你可以通过将某些操作放在括号内来强制排序:
var num = 10 * (2 / 4 + 8); // num is 85
为了节省您的时间,表 A-1 中展示了几个简写算术约定。当您需要通过相对于当前值的某个量来更改变量的值时,请使用这些约定。
表 A-1. 算术简写
| 简写 | 长版本 |
|---|---|
num += 3; |
num = num + 3; |
num++; |
num = num + 1; |
num -= anothernum; |
num = num − anothernum; |
num--; |
num = num − 1; |
num *= 2; |
num = num * 2; |
num /= 4; |
num = num / 4; |
加号+也可以用来连接——合并——文本字符串,例如:
var her = 'Eve';
var him = 'Adam';
var together = him + ' and ' + her;
这里将三个单独的字符串全部合并在一起。第一个和最后一个分别是him和her变量。中间的一个是字面字符串,它以空格开始和结束。如果您要alert(弹窗显示)together变量,它将显示为亚当和夏娃。
注意
您还可以使用+=简写来连接字符串。例如:name += ' Smith'将字符串添加到name变量中的文本末尾。
数组
到目前为止,我已经介绍了两种类型的变量——数字和文本。在这里,我将介绍一种特殊的变量,它可以存储多个值。或者,换句话说,它可以包含一个数组的值。
值可以是数字、文本或它们的组合。您还可以在数组中包含其他数组,甚至对象(下一节将介绍)。以下是一个数组声明的示例:
var herbs = ['Parsley', 'Sage', 'Rosemary', 'Thyme'];
值之间用逗号分隔。列表被方括号[和]包围。相同的括号用于根据它们在列表中的位置访问单个值。例如,要从herbs变量中提取Rosemary,您可以使用以下代码:
herbs[2]
在 JavaScript 中,就像在大多数编程语言中一样,数组索引从零开始。因此,尽管Rosemary是列表中的第三个项目,但您使用数字2来访问它。
要确定列表中有多少项,您使用length属性:
herbs.length
在循环中,我将展示如何访问数组中的每个项目。
对象
与数组一样,对象可以存储多个值。区别在于,您可以表达比简单列表更结构化的数据。为此,对象包含键和值对。键是类似单词的标签,但值可以是数字、文本、数组或其他对象。
这里有一个表示汽车的简单示例:
var car = {
make: 'Ford', model: 'Mustang', year: 1965, color: 'red'
}
要访问对象内的值,您使用点符号:
car.model
现在您已经看到了在 JavaScript 中表示数据的不同方式,让我们来做些实际操作。
条件语句
条件语句帮助您比较变量与其他值(包括其他变量)。这种比较发生在条件语句中。使用条件语句是许多编程的基础,因为它们包含帮助计算机“做出决定”的逻辑。
最常见的条件语句是if语句。您使用它来提供仅在条件为真时才使用的代码,例如:
if (num == 5) {
// code to run if true
}
在{和}大括号内的代码只有在num变量正好是5时才会运行。==操作符用于提供等于比较——与用于赋值的单个等号相反。
你可以使用其他比较操作符,如表 A-2 所示,在你不希望值完全相等的情况下使用。
表 A-2. 条件操作符
| 操作符 | 描述 |
|---|---|
== |
等于 |
!= |
不等于 |
> |
大于 |
< |
小于 |
>= |
大于或等于 |
<= |
小于或等于 |
如果条件为假时你想做其他事情,你可以在if语句之后立即包含一个else语句:
if (num > 0) {
// code to run if true
}
else {
// code to run if condition is false
}
在else语句之后的{和}大括号内的代码只有在if语句中的条件不成立时才会运行。在这个例子中,当num为零或更少时,else代码会运行。在其他情况下(num大于零),会运行if代码。你不会遇到同时运行if和else代码的情况。
注意
一个else语句不能没有伴随的if语句。然而,你可以有没有else语句的if语句。
你也可以有一个没有任何操作符的if语句。在这些情况下,语句中的变量被称为布尔值,它可以是true或false。以下是一个示例:
var is_cool = true;
if (is_cool) {
// code to run if true
}
if (is_cool)代码是较长的if (is_cool == true)的简写。你还可以使用感叹号表示“不是”来简写比较变量为假,如下例所示:
if (!is_cool) {
// code to run if is_cool is false
}
有时,一个非布尔变量也可以作为条件。在这种情况下,空值被认为是假的。空值对于数字是0,对于文本是""(空字符串),对于对象是null。此外,任何未定义的变量也是假的。
循环
循环就像特殊的条件语句,它会反复运行代码,直到条件不再为真。在本节中,我将介绍两种类型的循环:for循环和while循环。
在大多数情况下,循环有一个数字来计数你使用了多少次循环。以下是一个运行 10 次的示例:
for (❶var i=0; ❷i < 10; ❸i++) {
// Code to run each time
}
for语句的括号内包含三个部分。初始值❶、循环继续的条件❷和增量部分❸。第一次通过循环时,i是 0。然后是 1,以此类推,只要i小于 10。当i等于 10 时,循环停止。但由于我们从 0 开始,循环将运行 10 次。
可以使用以下代码使用while循环实现相同的效果:
`var i=0`;
while (`i < 10`) {
// Code to run each time
`i++`;
}
事实上,如果你查看粗体代码段,你会注意到它们与for语句的三个部分相匹配。这个while循环在逻辑上是等价的。
注意
当你知道循环将运行多少次时,使用for可能最简单。当你事先不知道所需的迭代次数(例如,当你正在搜索一个值时),使用while。
记得数组部分中的 herbs 变量吗?为了刷新你的记忆,我们这样声明它:
var herbs = ['Parsley', 'Sage', 'Rosemary', 'Thyme'];
现在我们用一个循环来对数组中的每个值进行一些操作。因为我们知道迭代的总次数(数组的长度),所以我们将使用 for 循环。以下是代码:
var allherbs = '';
for (var i=0; i < herbs.length; i++) {
allherbs += herbs[i] + ' ';
}
在循环的每次迭代中,我们将当前草药(由数字 i 确定的索引处的那个)附加到 allherbs 字符串的末尾。循环结束时,这就是该变量中存储的内容:
Parsley Sage Rosemary Thyme
我们已经使用循环来连接数组中持有的四个值。这个概念在整本书中都有使用,只是通常不是用于添加字符串;相反,我们用它来在几行代码中创建数百个地图标记或输出搜索的所有结果。
循环是限制你需要编写的代码的重要工具。但函数可能是实现简单、可重用代码的最好工具。继续阅读以了解我的意思。
函数
如果你准备好提升你的 JavaScript 编程能力,编写函数是一个好的开始。函数是一段打包的代码,用于执行特定的操作。它们可以接受一个或多个变量,并在必要时返回一个值。使用函数,你可以使你的代码更具可重用性,并将应用程序分解成更小的部分。
为了一个基本的例子,我们将创建一个输出两个数中较大数的函数。这个函数已经以 Math.max 的形式存在(可以接受多个值进行比较),但它仍然是一个很好的例子。以下是该函数的完整代码:
function biggest(num, another) {
if (num > another) {
return num;
}
else {
return another;
}
}
每个函数都以 function 声明开始,后跟函数名。然后,在括号内,你声明函数将接受的参数——变量。在这种情况下,我们有两个,我分别命名为 num 和 another。
函数的其余部分,位于 { 和 } 大括号内,应该看起来很熟悉;它只是普通的 JavaScript 代码。当函数被调用时,你传递两个变量,代码根据输入运行。当你准备好发送一个值返回时,你使用 return 语句立即退出函数,并使其输出等于 return 后面的变量(或字面量值)。
我可能最好用一个例子来解释它。这是调用新的 biggest 函数的方法:
var somenum = 7;
var bigone = biggest(somenum, 11); // After the biggest function, bigone is 11
我传递了一个变量(somenum)和一个字面量数字到函数中。我可以使用任何字面量或变量值的组合。我的变量名不需要与函数内部使用的变量名匹配。它们有不同的 作用域,我将在下一节中解释。
对于普通的布尔型、数字型和文本型变量,JavaScript 会复制其值并将其发送到函数中。这意味着如果你在函数内部对值进行任何修改,原始变量外部将不受影响。对于对象和数组来说,情况并非如此;在这些情况下,你将修改原始变量的值——即使变量不在返回值中。
在本节的开始,我提到一个函数不需要接受参数。在这些情况下,函数需要空括号。以下是一个与映射相关的示例:
function recenter_map() {
mapstraction.setCenter(stored_center);
}
此函数将用于将地图设置为存储的中心点,只要你将变量保存在内存中。我在 #34: 当消息框关闭时返回中心 中使用了类似的方法。要使其按所示工作,唯一的方法是 stored_center 是一个全局变量,可以从页面上的任何 JavaScript 中访问。
变量作用域
在前面的章节中,我演示了如何创建变量。我还解释了 var 关键字虽然不是必需的,但建议使用。这些建议背后的原因与变量的可见性——或者说其作用域有关。
如果你使用一个没有包含 var 语句的变量,该变量将自动被认为是 全局的,而不是 局部的。全局变量可以从页面上的任何 JavaScript 中看到,包括其他函数。你还可以通过在函数外部声明变量来显式地将变量设置为全局,通常在 JavaScript 部分的开头。
考虑以下示例:
<html>
<head>
<title>JavaScript Variable Scope Example</title>
<script type="text/javascript">
❶ var firstvar = 1; // explicit global variable
function call_first() {
❷ secondvar = 2; // implicit global variable
❸ var thirdvar = 3; // local variable
then_me();
alert('First: ' + firstvar + '\nSecond: ' + secondvar
+ '\nThird: ' + thirdvar);
}
function then_me() {
❹ firstvar = 10;
❺ var secondvar = 20;
❻ thirdvar = 30; // implicit global variable
}
</script>
</head>
<body onLoad="call_first()">
...
</body>
</html>
当 JavaScript 的 alert 弹出时,三个变量的值将会是什么?让我们逐步分析代码。当页面加载时,我们创建一个全局变量❶,然后调用 call_first 函数。
在第一个函数内部,我们设置了一个变量的值而没有使用 var 关键字❷,这使得该变量隐式地成为全局变量。我们创建的第三个变量❸是一个局部变量,因为我们在函数内部使用了 var 关键字。
使用这三种不同方式创建的三个变量后,我们然后在第一个函数内部调用我们的第二个函数。在这个新的函数中,我们重置一个全局变量❹,创建一个新的局部变量❺,并创建一个新的全局变量❻。
当我们返回到第一个函数中的位置时,在这个作用域中唯一改变的变量是 firstvar。then_me 函数中的 secondvar 变量是局部的,因此它不会改变全局的 secondvar。尽管 then_me 的 thirdvar 是全局的,但这不会改变 call_first 函数内部具有相同名称的局部变量。
提示
为了让自己更容易理解,要明确并始终使用 var 关键字显式声明变量。
虽然可能看起来有些复杂,但好消息是这个过程是逻辑的,只要你能够理解它。
匿名函数
在现代 JavaScript 中,经常使用一种特殊类型的函数,尤其是在读取数据时(就像你经常使用地图那样)。这个函数被称为匿名函数,因为它没有名字。这些函数通常作为参数传递给其他函数。
这里有一个非常基础的示例:
function start_here() {
call_another(`function() {`
`alert('Anonymous function message!');`
`}`);
}
function call_another(❶fn) {
❷ fn();
}
粗体部分是匿名函数。它看起来就像任何其他函数一样,只是缺少一个名字。另一件奇怪的事情是,这里函数被放在了另一个函数调用 call_another 的括号内。在那个第二个函数中,我们有一个变量 ❶,它持有匿名函数的 引用。然后我们可以通过引用变量的名字 ❷ 来调用它。
确实如此,如果你加载它并调用 start_here 函数,你会看到来自你的匿名函数的警告消息——通过第二个函数。
这是一个匿名函数更常见用途的例子,使用 jQuery 从网络服务中读取 JSON:
jQuery.getJSON(url, function(obj) {
// Code to access the obj variable
});
我们向 jQuery 的 getJSON 函数传递了两个值。第一个是我们想要检索的 JSON 的 URL。第二个是我们匿名函数——这次传递了一个变量给它。当评估 JSON 数据时(参见第五十三部分:使用 JSON),jQuery 会将其放入一个 JavaScript 对象中,然后将其传递给我们的匿名函数。
使用匿名函数在某种程度上是一种简写。这是这个 jQuery 示例的更长版本:
jQuery.getJSON(url, get_results);
function get_results(obj) {
// Code to access the obj variables
}
当然,你打字的时间并没有节省多少,但你的代码通常更易于阅读。在 JavaScript 中使用单用途函数会使代码变得杂乱无章。但了解匿名函数的最好理由可能是你可能会在其他人的代码中看到很多匿名函数——包括在这本书中。
然而,匿名函数只应该在它们最有用的场合下使用。如果你发现自己正在匿名函数中编写大量代码,你可能应该将其改为命名函数。此外,注意在长循环中创建匿名函数。你实际上在每次循环中创建新的函数,这是低效的。而且这样做也与函数的目的相悖:编写一次代码,多次运行。
使用 jQuery
在这本书的几个地方,我使用了 JavaScript 框架 jQuery。jQuery 简化了常见任务,并为在不同浏览器中实现不一致的操作提供了一个统一的接口。jQuery 在这本书中最常见的用途,尤其是本节,是从网络服务中读取文件或其他数据。本节旨在快速介绍使用 jQuery。
在你能够使用 jQuery 之前,你需要将其包含在你的网页中。你可以通过在 jquery.com/ 下载最新版本,或者通过在你的 HTML 文件的 <head> 部分添加以下内容来获取 Google 托管的版本:
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
使用 Google 托管的版本的一个优点是,许多其他网站也是如此。这意味着当人们访问你的网站时,他们可能已经在浏览器缓存中存储了 jQuery。即使他们没有,他们也可以快速下载它——目前文件大小不到 20KB。
一旦你在文档中包含了 jQuery,你就可以通过两个全局变量来使用它的函数:jQuery及其缩写$。
查询文档对象
在其众多用途中,jQuery 非常擅长快速轻松地从你的 HTML 页面中提取元素到 JavaScript 代码中。你可以通过id、标签名、CSS 类或这些因素的组合来进行查询。jQuery 使这项任务变得更容易,这样你就可以专注于你想对这些元素做什么。
要获取页面上的单个元素,例如地图<div>,你可以通过它的id来调用它。jQuery 的大部分语法都借鉴了 CSS,这可能会让你感到熟悉。以下是如何调用一个id为mymap的对象:
var mapdiv = $('#mymap');
注意我正在使用美元符号缩写。然后我添加括号,因为$是一个变量和一个函数(jQuery 在 JavaScript 的边界上进行了扩展,你和我可能都不会这样做)。在函数内部,我们传递一个字符串,其中包含我们用来样式化地图div的 CSS,#mymap。
你现在已经使用了第一个 jQuery 函数来查询特定元素。你可以用 JavaScript 的标准document.getElementById函数达到相同的效果,但 jQuery 的方法输入更快。此外,使用$的结果允许你链式调用其他 jQuery 函数,例如视觉效果。
jQuery 不仅可以查询单个元素,还可以在一次调用中收集多个元素。以下是一些示例:
var imgs = $('img'); // all the images on a page
var allmaps = $('.map'); // every element with class="map"
var mapimgs = $('#mymap img'); // all images that are within #mymap element
var firstp = $('p:first'); // first paragraph on a page
这些示例展示了不同类型的查询——还有更多可能。你可以在api.jquery.com/category/selectors/了解 jQuery 所称为的选择器。
插入和隐藏内容
一旦你使用 jQuery 从页面上获取了一个或多个元素,你将想要对它们做些什么。在本节中,我将向你展示如何在页面元素内部添加或替换内容的方法。我还会演示一个效果,这样你就可以稍后使其重新出现。
假设你想要在页面上添加一个元素,你可以显示一些计算的结果。你可以使用类似以下方式使用 HTML 添加该元素:
<div id="results">Results:</div>
当你准备好添加结果时,你可以使用 jQuery 找到元素并显示结果。如果你想显示一个名为total的变量的内容,你可以使用以下单行 jQuery:
$('#results').html(total);
这行代码将找到页面上id为results的元素,并将其内部 HTML 替换为total变量的值。如果你只想向元素添加附加内容,请使用此行代码代替:
$('#results').append(total);
现在结果部分看起来可能像这样(如果total变量是42):
Results: 42
你已经看到了如何插入内容,但关于使相同内容消失呢?例如,在添加结果之前,一个只有Results:而没有其他内容的页面看起来有点奇怪。所以当页面加载时(你将在下一节中学习如何做到这一点),运行以下代码来隐藏结果元素:
$('#results').hide();
当你准备好向用户显示结果时,包括以下代码:
$('#results').show();
这两个函数是 jQuery 效果的简单(且非常有用)示例。更多效果可以在 jQuery 网站上找到,地址为 api.jquery.com/category/effects/。
使用浏览器事件
在本书的早期,我花了一整章来介绍在地图上发生的事件,例如点击或拖动。你还可以在浏览器中的任何位置响应事件,jQuery 使响应过程变得简单。
要响应事件,你首先需要监听它。当你的页面首次加载时,你注册你的意图来响应事件。你可以通过将 JavaScript 函数添加到 <body> 标签的 onload 属性来实现,或者你可以使用特殊的 jQuery 事件来完成。
虽然创建一个事件来注册其他事件可能看起来有些反直觉,但它确实有效。以下是一个等待页面准备好的示例:
$(document).ready(register_events);
注意,我们要查询的元素与过去略有不同。我们不再在括号内插入字符串,而是插入了一个标准的 JavaScript 对象,document。当页面加载足够多,浏览器知道它包含的所有对象时,我们调用 register_events 函数。然而,该函数不存在,因此我们需要编写它。或者,我们可以用匿名函数来响应相同的事件:
$(document).ready(function() {
// Code to register events
});
与大多数匿名函数一样,你通常希望将其保持为只有几行。如果你有很多事件需要注册,使用一个命名函数可能更适合你。
注意
使用浏览器 onload 属性和 jQuery 的 ready 事件之间存在很大差异。使用 onload,你等待整个页面加载完成,包括图像和其他外部文件。使用 $(document).ready,你可以在浏览器准备好时立即运行代码,例如注册事件和隐藏对象。在这里使用 jQuery 通常意味着更好的用户体验。
现在你已经等待浏览器准备好注册其他事件,让我们来注册它们。以下是 jQuery 事件的基模式:
$(*`element`*).*`event`*(*`function`*);
element 部分通常是选择器,例如元素的 id(尽管它也可以是任何浏览器对象,如 ready 示例所示)。event 部分是函数的名称,由 jQuery 声明。最后,function 是函数引用,可以是匿名函数或要调用的函数名称。
这是如何监听特定元素被点击的示例:
$('#myid').click(function() {
alert('Clicked on myid');
});
如果你熟悉 HTML 中包含的 JavaScript 事件,你可能想知道 on,如 onClick 去哪里了。在 jQuery 中,事件仅通过动作来引用。
你还可以获取有关事件的额外信息。可用的信息可能因事件而异。以下是如何找出用户在页面上双击的位置:
$(document).dblclick(function(❶e) {
alert('Double-clicked at ' + ❷e.pageX + ', ' + ❸e.pageY);
});
我没有使用click事件,而是使用了dblclick事件。因为我们想要更多关于事件的详细信息,我在匿名函数中包含了一个可选参数❶。在函数内部,我可以使用这个变量来获取信息,例如用户点击的位置。这些数据分为两部分:从页面左侧的像素数❷和从页面顶部的像素数❸。
你可以在表 A-3 中看到可用事件的样本,以及每个事件可选参数中传递的附加信息。
表 A-3. 一些有用的 jQuery 事件
| 事件名称 | 可用对象 | 其他信息 |
|---|---|---|
click |
任意 | 页面位置:pageX,pageY |
dblclick |
任意 | 页面位置:pageX,pageY |
mousemove |
任意 | 页面位置:pageX,pageY |
keydown |
document,window |
键码:which |
focus |
表单元素 |
事件会使你的网页更加互动,无论它们是否包含地图。有关事件的全列表(以及可用的附加事件信息),请参阅 jQuery 的文档api.jquery.com/category/events/。
加载文件和数据
如果你想要与当前 HTML 文件外的数据进行交互,jQuery 提供了一些出色的工具来进行这类 Ajax 调用。尽管 Ajax 代表的是异步 JavaScript 和 XML,但你可以使用 Ajax 访问任何类型的数据。
要使用 jQuery 加载文件,你使用get函数。get至少需要两个参数:你想要加载的 URL 以及 jQuery 想要发送结果的函数。以下是一个加载文本文件的简单示例:
$.get(❶"test.txt", function(txt) {
alert(txt);
});
我提供的 URL 只是一个文件的名称。在这种情况下,文件是文本,但它也可能是 HTML 代码或其他类型的内容。在第五十二部分:使用 XML 中,我展示了如何使用get函数解析 XML 结果。
你不仅限于只加载简单的文件。你可以包括目录名,甚至完整的 URL。无论你加载什么,都需要在同一个域名上。出于安全原因,浏览器不会让你从其他网站检索数据,即使这些数据是公开的。你可以通过在服务器端加载文件(如第六十一部分:检索网页中所示)然后访问本地副本来解决这个问题。
存在一个特殊情况,允许你从另一个网站加载数据。如果该网站提供 JSON 数据,并且它允许你在 URL 中指定回调函数,那么你可以使用 jQuery 的getJSON函数远程加载数据。以下是基本结构:
jQuery.getJSON(❷url, function(obj) {
// Code to access the obj variable
});
你发送给 getJSON 函数的 URL ❷ 需要包含一个问号来代替函数名称。然后 jQuery 将创建一个函数,并从其新函数中调用你的匿名函数(或者如果你选择,命名函数)。
getJSON 与 get 另一个不同之处在于传递给回调函数的参数类型。而不是返回给 URL 的纯文本,jQuery 会以 JavaScript 对象的形式将解析后的 JSON 传递给你。
你可以在 第五十三部分:使用 JSON 中找到 getJSON 的一个更完整的示例。此外,你还可以在 第十章 中几个 mashup 示例中看到该函数在实际项目中的应用。
附录 B. MAPSTRACTION 参考

Mapstraction 库使得创建与任何提供者兼容的地图变得容易。本附录提供了 Mapstraction 众多类和函数的详细信息。在您开发自己的地图时,请将其作为参考。
本附录按 Mapstraction 类组织——这是库本身分离的方式。在每个类中,我涵盖了其构造函数、其他函数以及任何类变量。这些类包括:
-
mxn.Mapstraction,用于创建和控制地图的主要类 -
mxn.BoundingBox,用于描述矩形区域(边界) -
mxn.LatLonPoint,一个用于存储和引用单个坐标的类 -
mxn.Marker,用于维护单个兴趣点的数据 -
mxn.Polyline,一个用于存储连接点的集合(形状或线条)的类 -
mxn.util,一个包含有用函数的实用类
本节内容基于从 Mapstraction 源文件生成的官方文档。尽管我已经通过示例和更详细的描述扩展了大多数部分,但我还是要向维护文档的 Mapstraction 开发者社区表示衷心的感谢。
与所有 Mapstraction 事物一样,您可以在mapstraction.com找到最新的文档。
类 mxn.Mapstraction
这是主类;它是您使用 Mapstraction 时的第一个停靠点。您使用mxn.Mapstraction创建地图对象,然后通过该对象提供的主要接口来更改地图。
在本节中,您将找到构造函数,它创建一个新的地图对象。其他函数用于执行地图级别的操作,例如设置中心点和向地图添加图层。本类的大部分内容在第一章中有详细说明。
函数 mxn.Mapstraction
这是Mapstraction类的构造函数。该函数将一些 API 选择初始化到给定的 HTML 元素中。每个 Mapstraction 地图都将从这一行开始:
mxn.Mapstraction(*`element`*, *`api`*, *`debug`*)
参数
*`element`* (字符串) 包含地图的 HTML DOM 元素或 HTML DOM 元素的id。 |
|---|
*`api`* (字符串) Mapstraction 使用的 API 名称。选项包括:'cloudmade'、'geocommons'、'google'、'googlev3'、'map24'、'mapquest'、'microsoft'、'multimap'、'openlayers'、'openstreetmap'和'yahoo'。 |
*`debug`* (布尔值) 一个可选参数,用于开启调试支持。如果此参数为 true,Mapstraction 将使用警告面板来处理不支持的操作。这在开发期间可能很有用,但在生产中并不实用。默认为 false。 |
返回值
Mapstraction 对象,可用于调用mxn.Mapstraction字段和方法。在本节中称为mxnobj。
示例
var mxnobj = new mxn.Mapstraction('mymap', 'googlev3');
函数 addControls
此函数通过仅调用一次,向地图添加多个控件,如缩放和地图类型。您可以在唯一的参数对象中指定要添加的控件。
mxnobj.addControls(*`args`*)
参数
*`args`* (对象) 要显示哪些地图控件。选项包括:pan,zoom('large' 或 'small'),overview,scale,和map_type。除了zoom之外,所有选项都是布尔值(true 或 false)。 |
|---|
示例
mxnobj.addControls({map_type: true, zoom: 'small'});
函数 addFilter
此函数添加一个标记过滤器,根据您创建的属性自动显示或隐藏标记。addFilter 函数准备过滤器并需要 doFilter 执行过滤。要了解更多关于 Mapstraction 的过滤选项,请参阅 #9: 过滤掉某些标记 在 #9: 过滤掉某些标记。
mxnobj.addFilter(*`name`*, *`operator`*, *`value`*)
参数
*`name`* (字符串) 要放置过滤器的属性名称。通过 markerobject.addAttribute 添加。 |
|---|
*`operator`* (字符串) 用于比较属性与值的运算符。选项包括:'ge'(大于等于),'le'(小于等于),或'eq'(等于)。 |
*`value`* (数字,字符串) 要比较的值。 |
示例
mxnobj.addFilter('age', 'ge', 21);
mxnobj.doFilter();
函数 addImageOverlay
此函数在给定位置在地图上添加一个地理参考图像。图形显示在地图图像之上,但在标记和多段线之下,因此可以用作替代或增强图像。
为了确保你的图像覆盖正确的区域,你需要对其进行 校正,如 #25: 在地图上叠加图像 在 #25: 在地图上叠加图像 中所示。
mxnobj.addImageOverlay(*`unique`*, *`url`*, *`opacity`*, *`west`*, *`south`*, *`east`*, *`north`*);
参数
*`unique`* (字符串) 该覆盖层的唯一标识符,用作添加到地图中的新对象的 DOM id。 |
|---|
*`url`* (字符串) 要叠加的图像的文件位置。此 URL 可以是本地或远程的,文件可以是浏览器支持的任何格式。透明图形(如 PNG 文件)最适合增强(而不是替换)地图图像。 |
*`opacity`* (数字) 一个介于 0(透明)和 100(完全不透明)之间的值,用于确定原始地图图像背后显示你图形的程度。 |
*`west`* (数字) 图形边界框最西端的经度。 |
*`south`* (数字) 图形边界框最南端的纬度。 |
*`east`* (数字) 图形边界框最东端的经度。 |
*`north`* (数字) 图形边界框最北端的纬度。 |
示例
mxnobj.addImageOverlay('centralpark', 'centralpark.png', 100,
−73.9867415, 40.7622753, −73.9460798, 40.8032834);
函数 addLargeControls
此函数向地图添加提供者的大缩放控件。在某些情况下,它可能还包括其他控件,如平移控件和比例尺。
mxnobj.addLargeControls()
函数 addMapTypeControls
此函数允许用户使用提供者的控件从可用的地图类型中选择。
mxnobj.addMapTypeControls()
Function addMarker
此函数根据标记对象中的标准将标记对象(之前使用 mxn.Marker 创建)添加到地图中。标记的常见选项在第二章中进行了介绍。
mxnobj.addMarker(*`marker`*);
Parameter
*`marker`* (mxn.Marker) 要添加到地图中的标记。 |
|---|
Example
var mk = new mxn.Marker(new mxn.LatLonPoint(45, −122));
mxnobj.addMarker(mk);
Function addMarkerWithData
此函数将一个标记对象(之前使用 mxn.Marker 创建)添加到地图中,并附带一系列选项。
mxnobj.addMarkerWithData(*`marker`*, *`options`*);
Parameters
*`marker`* (mxn.Marker) 要添加到地图中的标记。 |
|---|
*`options`* (object) 标记的选项哈希对象,包括 draggable、groupName、hover、hoverIcon、infoBubble、icon、iconShadow、infoDiv、label 和 openBubble。 |
Example
var mk = new mxn.Marker(new mxn.LatLonPoint(45, −122));
var opt = {infoBubble: 'Message box content', draggable: false};
mxnobj.addMarkerWithData(mk, opt);
Function addOverlay
此函数将 GeoRSS 或 KML 覆盖添加到地图中。这两种数据格式在第八章中进行了详细说明。
mxnobj.addOverlay(*`url`*, *`autoCenterAndZoom`*);
Parameters
*`url`* (string) GeoRSS 或 KML 文件的完整、公开 URL。 |
|---|
*`autoCenterAndZoom`* (boolean) 是否自动将地图中心对准覆盖内容。默认为 false。 |
Example
mxnobj.addOverlay('http://mapscripting.com/example.kml');
Function addPolyline
此函数根据多段线对象中的标准将多段线对象(之前使用 mxn.Polyline 创建)添加到地图中。多段线的常见选项在第四章中进行了介绍。
mxnobj.addPolyline(*`polyline`*);
Parameter
*`polyline`* (mxn.Polyline) 要添加到地图中的多段线。 |
|---|
Example
var poly = new mxn.Polyline(
[new mxn.LatLonPoint(45, −122), new mxn.LatLonPoint(46, −121)]);
mxnobj.addPolyline(poly);
Function addPolylineWithData
此函数将一个多段线对象(之前使用 mxn.Polyline 创建)添加到地图中,并附带一系列选项。
mxnobj.addPolylineWithData(*`polyline`*, *`options`*);
Parameters
*`polyline`* (mxn.Polyline) 要添加到地图中的多段线。 |
|---|
*`options`* (array) 多段线的选项哈希对象,包括 closed、color、fillColor、opacity 和 width。 |
Example
var poly = new mxn.Polyline(
[new mxn.LatLonPoint(45, −122), new mxn.LatLonPoint(46, −121)]);
var opt = {color: '#ffcc99', width: 3};
mxnobj.addPolylineWithData(poly, opt);
Function addSmallControls
此函数将提供者的小型缩放控件添加到地图中。在某些情况下,它还可能包括其他控件的小型版本,例如平移。
mxnobj.addSmallControls()
Function addTileLayer
此函数使用参数化 URL 将瓦片层添加到地图中。在第二十六部分:使用自定义瓦片中进行了详细说明。
addTileLayer(*`tile_url`*, *`opacity`*, *`copyright`*, *`min_zoom`*, *`max_zoom`*, *`map_type`*)
Parameters
*`tile_url`* (string) 瓦片的 URL 模板。需要参数化 URL,用于 {X} 和 {Y} 坐标以及缩放级别 {Z}。 |
|---|
*`opacity`* (number) 一个介于 0(透明)和 1(完全不透明)之间的十进制值,用于确定在您的瓦片层后面显示原始地图影像的程度。 |
*`copyright`* (string) 包含在地图版权中的文本。 |
*`min_zoom`* (number) 瓦片层可见的最小缩放级别。 |
*`max_zoom`* (数字) 瓦片层可见的最大缩放级别。 |
*`map_type`* (布尔值) 如果瓦片层是图层调色板中的可选地图类型,则为 true。默认为 false。 |
示例
mxnobj.addTileLayer('http://tile.openstreetmap.org/{Z}/{X}/{Y}.png',
1.0, "OSM", 1, 19, true);
函数 applyOptions
此函数应用当前选项设置。
mxnobj.applyOptions()
函数 autoCenterAndZoom
此函数将地图的中心和缩放设置为包含所有标记和多边形的最大边界框。
mxnobj.autoCenterAndZoom()
函数 centerAndZoomOnPoints
此函数将地图的中心和缩放设置为包含传递给函数的所有点的最小边界框。
mxnobj.centerAndZoomOnPoints(*`points`*);
参数
*`points`* (数组) 用于确定地图的新中心和缩放的点(作为 mxn.LatLonPoint 对象)。 |
|---|
示例
mxnobj.centerAndZoomOnPoints([new mxn.LatLonPoint(45, −122),
new mxn.LatLonPoint(46, −121)]);
函数 declutterMarkers
您可以使用此函数在地图上清理标记并将重叠的标记分组在一起。尽管如此,它并不广泛支持,但在 #11: 处理标记簇 中展示了另一种方法。
函数 doFilter
doFilter 函数根据使用 addFilter 函数先前创建的属性和标准隐藏或显示标记。有关 Mapstraction 的过滤选项的更多信息,请参阅 #9: 过滤掉某些标记。
mxnobj.doFilter()
示例
mxnobj.addFilter('age', 'ge', 21);
mxnobj.doFilter();
函数 getAttributeExtremes
此函数查找在标记属性中设置的最低和最高值。
mxnobj.getAttributeExtremes(attribute)
参数
*`attribute`* (字符串) 标记属性的名称。 |
|---|
返回值
包含传递的属性的最小和最大值的两个元素数组。
示例
var minmax = mxnobj.getAttributeExtremes('age'); // return [min, max]
函数 getBounds
此函数检索当前可视地图的 BoundingBox。
mxnobj.getBounds()
返回值
一个 mxn.BoundingBox 对象。
函数 getCenter
此函数检索当前可视地图的中心。
mxnobj.getCenter()
返回值
一个 mxn.LatLonPoint 对象。
函数 getMap
此函数检索当前地图的本地地图对象。如果需要执行专有地图调用,则非常有用。
mxnobj.getMap()
返回值
依赖于您所使用的提供者的本地对象。
函数 getMapType
此函数检索当前地图的图像类型,例如卫星或混合图像。
mxnobj.getMapType()
返回值
一个与地图类型相对应的数字。有关可用类型的说明,请参阅 设置地图类型。
函数 getZoom
此函数检索当前缩放级别。
mxnobj.getZoom()
返回值
与当前缩放级别相对应的整数。该主题在 设置缩放级别 中有详细说明。
函数 getZoomLevelForBoundingBox
此函数检索给定边界范围内的最佳缩放级别。
mxnobj.getZoomLevelForBoundingBox(*`bounds`*)
参数
*`bounds`* (mxn.BoundingBox) 您想要找到最佳缩放级别的边界。 |
|---|
函数 polylineCenterAndZoom
此函数将地图的中心和缩放设置为包含所有折线的最小边界框,忽略标记。
mxnobj.polylineCenterAndZoom()
函数 removeAllFilters
removeAllFilters 函数移除之前添加的所有过滤器,但不会显示之前过滤的标记(为此您需要再次调用 doFilter 函数)。因为过滤器是累加的,所以此函数在从一种过滤方法切换到另一种过滤方法时非常有用。
mxnobj.removeAllFilters()
函数 removeAllMarkers
此函数永久地从地图中移除所有标记。要临时移除,请在 mxn.Marker 中使用 hide 函数。
mxnobj.removeAllMarkers()
函数 removeAllPolylines
此函数永久地从地图中移除所有折线。要临时移除,请在 mxn.Polyline 中使用 hide 函数。
mxnobj.removeAllPolylines()
函数 removeFilter
此函数移除之前添加的过滤器,但不会显示之前由移除的过滤器过滤的标记(为此您需要再次调用 doFilter 函数)。
mxnobj.removeFilter(*`name`*, *`operator`*, *`value`*)
参数
*`name`* (字符串) 要移除过滤器的属性名称。 |
|---|
*`operator`* (字符串) 之前用于比较属性和值的运算符。 |
*`value`* (数字,字符串) 要比较的值。可以是数字或文本。 |
示例
mxnobj.removeFilter('age', 'ge', 21);
mxnobj.doFilter();
函数 removeMarker
此函数从地图中移除标记对象(之前使用 mxn.Marker 创建)。
mxnobj.removeMarker(*`marker`*);
参数
*`marker`* (mxn.Marker) 要从地图中移除的标记。 |
|---|
示例
mxnobj.removeMarker(mxnobj.markers[0]); // removes first marker
函数 removePolyline
此函数从地图中移除一个折线对象(之前使用 mxn.Polyline 创建),并永久删除。
mxnobj.removePolyline(*`polyline`*);
参数
*`polyline`* (mxn.Polyline) 要从地图中移除的折线。 |
|---|
示例
mxnobj.removePolyline(mxnobj.polylines[0]); // removes first polyline
函数 resizeTo
此函数通过 CSS 调整地图对象的大小到提供的尺寸。
mxnobj.resizeTo(*`width`*, *`height`*)
参数
*`width`* (数字) 地图的新宽度。 |
|---|
*`height`* (数字) 地图的新高度。 |
示例
mxnobj.resizeTo(800, 600); // 800 pixels wide, 600 pixels tall
函数 setBounds
此函数根据 BoundingBox 设置地图到适当的中心和缩放。
mxnobj.setBounds(*`bounds`*)
参数
*`bounds`* (mxn.BoundingBox) 用于确定地图新中心和缩放级别的 BoundingBox。 |
|---|
函数 setCenter
此函数根据提供的点设置地图的中心。
mxnobj.setCenter(*`point`*)
参数
*`point`* (mxn.LatLonPoint) 要用作地图新中心的点。 |
|---|
示例
mxnobj.setCenter(new mxn.LatLonPoint(45, −122));
函数 setCenterAndZoom
此函数根据提供的点和缩放级别设置地图的中心和缩放级别。
mxnobj.setCenterAndZoom(*`point`*, *`zoom`*)
参数
*`point`* (mxn.LatLonPoint) 要用作地图新中心的点。 |
|---|
*`zoom`* (数字) 要用于地图的新缩放级别。缩放级别在设置缩放级别中有详细说明。 |
示例
mxnobj.setCenterAndZoom(new mxn.LatLonPoint(45, −122), 10);
函数 setDebug
此函数确定是否开启调试支持。当开启调试支持时,Mapstraction 将使用警告面板来处理不支持的操作。
mxnobj.setDebug(*`debug`*)
Parameter
*`debug`* (boolean) 如果此参数为 true,则开启调试支持。这在开发期间可能很有用,但在生产环境中则没有用。 |
|---|
Function setMapType
此函数声明当前地图的图像类型,例如卫星图或混合图。
mxnobj.setMapType(*`type`*)
Parameter
*`type`* (integer) 对应地图类型的数字。有关可用类型的描述,请参阅设置地图类型。 |
|---|
Example
mxnobj.setMapType(mxn.Mapstraction.SATELLITE);
Function setOption
此函数设置单个选项,例如滚轮缩放或可拖动地图。
mxnobj.setOption(*`name`*, *`value`*)
Parameters
*`name`* (string) 要设置的选项的名称。目前支持选项:'enableScrollWheelZoom' 和 'enableDragging'。 |
|---|
*`value`* (boolean) 是否设置了选项(true 或 false)。 |
Example
mxnobj.setOption('enableDragging', false); // map cannot be dragged
Function setOptions
此函数使用对象一次性声明多个选项,例如滚轮缩放或可拖动地图。
mxnobj.setOptions(*`opt`*)
Parameter
*`opt`* (object) 包含要设置选项的名称和值的哈希对象。目前支持选项:'enableScrollWheelZoom' 和 'enableDragging'。 |
|---|
Example
mxnobj.setOptions({'enableDragging': false, 'enableScrollWheelZoom': true);
Function setZoom
此函数设置当前缩放级别。
mxnobj.setZoom(*`zoom`*)
Parameter
*`zoom`* (number) 要设置的缩放级别的数字。该主题在设置缩放级别中有详细说明。 |
|---|
Example
mxnobj.setZoom(13); // Roughly city-level zoom
Function swap
swap 函数隐藏之前的地图,并实时将活动地图更改为不同的 API 提供商。
mxnobj.swap(*`api`*, *`element`*)
Parameters
*`api`* (string) Mapstraction 用于新地图的 API 名称。选项包括:'cloudmade'、'geocommons'、'google'、'googlev3'、'map24'、'mapquest'、'microsoft'、'multimap'、'openlayers'、'openstreetmap' 和 'yahoo'。 |
|---|
*`element`* (string, DOM element) 包含新地图的 HTML 元素的 id,或元素本身。 |
Example
mxnobj.swap('yahoo', 'secondmapdiv');
Function toggleFilter
如果存在标记过滤器,此函数将移除它。否则,它将添加一个标记过滤器,根据您创建的属性自动显示或隐藏标记。它仍然需要 doFilter 函数来执行过滤。有关 Mapstraction 的过滤选项的更多信息,请参阅第九部分:过滤特定标记。
mxnobj.toggleFilter(*`name`*, *`operator`*, *`value`*)
Parameters
*`name`* (string) 要放置过滤器的属性的名称。通过 markerobject.addAttribute 添加。 |
|---|
*`operator`* (string) 用于比较属性和值的运算符。选项:'ge'(大于等于)、'le'(小于等于)或 'eq'(等于)。 |
*`value`* (数字,字符串) 要比较的值。可以是数字或文本(仅 'eq')。 |
示例
mxnobj.toggleFilter('age', 'ge', 21);
mxnobj.doFilter();
函数 toggleTileLayer
toggleTileLayer 函数控制先前添加的瓦片层的可见性。如果瓦片层是可见的,此函数将使其不可见(反之亦然)。
mxnobj.toggleTileLayer(*`tile_url`*)
参数
*`tile_url`* (字符串) 在 addTileLayer 函数中使用的确切 URL。 |
|---|
示例
mxnobj.toggleTileLayer('http://tile.openstreetmap.org/{Z}/{X}/{Y}.png');
函数 visibleCenterAndZoom
此函数将地图的中心和缩放设置为包含所有 可见 标记和多段线的最小边界框。忽略已隐藏的标记或多段线。
mxnobj.visibleCenterAndZoom()
mxn.Mapstraction 类中的变量
这些变量可以通过 Mapstraction 对象直接访问。例如,您可以使用 mxnobj.markers 变量遍历添加到地图上的所有标记,如第七部分:遍历所有标记中所示。
可用的变量有:
*`api`* (字符串) 此 Mapstraction 对象的当前活动 API 名称。此值与 mxn.Mapstraction 构造函数中传递的 api 参数相同。 |
|---|
*`CurrentElement`* (对象) 包含地图的 DOM 元素。Mapstraction 将此值设置为 document.getElementById(mxnobj.element)。 |
*`element`* (字符串) 将作为 element 参数传递给 mxn.Mapstraction 构造函数的原始元素 id。 |
*`markers`* (数组) 包含当前地图上所有已加载的标记(包括隐藏的)。 |
*`options`* (对象) 当前使用 setOptions 函数设置的地图选项。 |
*`polylines`* (数组) 包含当前地图上所有已加载的多段线(包括隐藏的)。 |
类 mxn.BoundingBox
此类创建一个 BoundingBox 并提供访问框信息的方法。BoundingBox 对象用于描述由两个经纬度点定义的区域,如第十九部分:绘制矩形以声明区域中所述。
Mapstraction 还内部使用 BoundingBox 类来自动定位地图或检索当前地图的边界。此外,您还可以使用 BoundingBox 对象执行简单的碰撞测试(参见其他有用参数中的第七部分:遍历所有标记)。
函数 mxn.BoundingBox
这是 BoundingBox 类的构造函数。使用此函数根据西南角和东北角的坐标创建一个新的 BoundingBox 对象。
mxn.BoundingBox(*`swlat`*, *`swlon`*, *`nelat`*, *`nelon`*);
参数
*`swlat`* (数字) 西南角的纬度。 |
|---|
*`swlon`* (number) 西南点的经度。 |
*`nelat`* (number) 东北点的纬度。 |
*`nelon`* (number) 东北点的经度。 |
返回
一个新的BoundingBox对象,可以用来调用mxn.BoundingBox函数。在本节中称为bbobj。
示例
var bbobj = new mxn.BoundingBox(45, −122, 46, −121);
函数包含
此函数用于执行简单的碰撞测试。也就是说,它查找给定点是否在BoundingBox内。
bbobj.contains(*`point`*)
参数
*`point`* (LatLonPoint) 要测试的点。 |
|---|
返回
一个布尔值(true或false),描述点是否在边界框内。
示例
var pt = new mxn.LatLonPoint(45.5, −121.5);
var hittest = bbobj.contains(pt);
扩展函数
如果一个点不在边界内,extend函数将扩展BoundingBox以包含该点。
bbobj.extend(*`point`*)
参数
*`point`* (LatLonPoint) 要包含在边界内的新点。 |
|---|
示例
var pt = new mxn.LatLonPoint(45.5, −121.5);
bbobj.extend(pt);
获取东北点函数
此函数检索边界的最东北点。
bbobj.getNorthEast()
返回
一个mxn.LatLonPoint对象。
获取西南点函数
此函数检索边界的最西南点。
bbobj.getSouthWest()
返回
一个mxn.LatLonPoint对象。
函数 isEmpty
此函数检查边界是否有零面积——也就是说,西南点和东北点是否相同。
bbobj.isEmpty()
返回
一个布尔值(如果为空则为true,如果边界有面积则为false)。
mxn.LatLonPoint类
没有这个类,你无法在地图上放置很多东西。一个mxn.LatLonPoint对象描述地球上的一个点,并提供了一些用于使用数据的函数,例如在第 36 号:计算两点之间的距离中所示。
通常,mxn.LatLonPoint是一个实用程序。其他每个类都依赖于它来保存纬度和经度坐标。
mxn.LatLonPoint函数
这是mxn.LatLonPoint类的构造函数。使用它来创建新的LatLonPoint对象,你可以将其作为地图的中心、标记的位置等传递。
mxn.LatLonPoint(*`lat`*, *`lon`*)
参数
*`lat`* (number) 你正在创建的点的纬度。 |
|---|
*`lon`* (number) 你正在创建的点的经度。 |
返回
一个新的LatLonPoint对象,可以用来调用mxn.LatLonPoint函数。在本节中称为llobj。
示例
var llobj = new mxn.LatLonPoint(45, −122);
距离函数
此函数计算当前LatLonPoint与第二个LatLonPoint之间的距离,单位为公里。
llobj.distance(*`point`*)
参数
*`point`* (LatLonPoint) 第二个LatLonPoint对象。 |
|---|
返回
一个十进制数字,表示llobj LatLonPoint和参数之间的公里数。
示例
var llobj = new mxn.LatLonPoint(45, −122);
var newpt = new mxn.LatLonPoint(44, −121);
var kms = llobj.distance(newpt); // 136.578 km
函数 equals
equals函数测试此点是否与第二个LatLonPoint相同——纬度和经度是否都相等。
llobj.equals(*`point`*)
参数
*`point`* (LatLonPoint) 第二个 LatLonPoint 对象。 |
|---|
返回值
一个布尔值(true 或 false),回答点是否相同。
示例
var llobj = new mxn.LatLonPoint(45, −122);
var newpt = new mxn.LatLonPoint(44, −121);
var same = llobj.equals(newpt); // false
函数 latConv
此函数提供纬度转换——基于当前投影的一个纬度度数的距离。它在 Mapstraction 中用于诸如搜索半径等功能(参见在“设置填充颜色和透明度”中添加圆圈以显示搜索半径 设置填充颜色和透明度)。
llobj.latConv()
函数 lonConv
此函数类似于 latConv,但它返回基于当前投影的经度转换——一个经度度数的距离。
llobj.lonConv()
函数 toString
此函数给出点的文本表示。
llobj.toString()
返回值
一个显示包含在 LatLonPoint 中的纬度和经度的字符串(如 45, −122)。
类 mxn.Marker
虽然 mxn.LatLonPoint 声明点,但 mxn.Marker 是在地图上显示点的最常见方式。使用此类创建新的标记图钉,包括自定义图标图形。标记在第二章中有详细说明。
函数 mxn.Marker
这是 mxn.Marker 类的构造函数。使用它来创建新的标记,然后设置其选项。然后使用 mxn.Mapstraction.addMarker 函数将其添加到地图上。
mxn.Marker(point)
参数
*`point`* (LatLonPoint) 在地图上放置标记的点。 |
|---|
返回值
一个 mxn.Marker 对象,在本节中称为 mkobj。
示例
var mkobj = new mxn.Marker(new mxn.LatLonPoint(45, −122));
函数 addData
使用一个便捷的函数向标记添加数据,例如自定义图标图形。addData 函数类似于 mxn.Mapstraction.addMarkerWithData 函数。
mkobj.addData(*`options`*)
参数
*`options`* (array) 标记的选项哈希对象,包括 draggable、groupName、hover、hoverIcon、infoBubble、icon、iconShadow、infoDiv、label 和 openBubble。 |
|---|
示例
var opt = {infoBubble: 'Message box content', draggable: false};
mkobj.addData(opt);
函数 getAttribute
此函数从标记中检索特定属性的值。属性通常用于过滤,并包含有关特定标记的数据。
mkobj.getAttribute(*`name`*)
参数
*`name`* (string) 你想要检索的属性名称。 |
|---|
返回值
属性的值。
示例
var num = mkobj.getAttribute('age');
函数 setAttribute
这是 getAttribute 的另一面,此函数设置标记中特定属性的值。
mkobj.setAttribute(*`name`*, *`value`*)
参数
*`name`* (string) 你想要设置的属性名称。 |
|---|
*`value`* (varies) 你想要设置的属性的值。 |
示例
mkobj.setAttribute('age', 21);
mkobj.setAttribute('address', '123 Main St');
函数 setDraggable
此函数声明标记是否可拖动——如果提供者支持此功能,则用户可以将其从一处移动到另一处。
mkobj.setDraggable(*`draggable`*)
参数
*`draggable`* (boolean) 如果标记应该可拖动(否则为 false),则设置为 true。 |
|---|
函数 setHover
此函数确定当鼠标悬停在标记上时,标记的消息框是否应该显示。
mkobj.setHover(*`hover`*)
参数
*`hover`* (布尔值) 如果你想设置一个可悬停的标记,则设置为 true。否则,设置为 false。 |
|---|
函数 setHoverIcon
此函数添加一个特殊图标,当鼠标悬停在标记上时显示。悬停图标继承主图标的大小。
mkobj.setHoverIcon(*`iconurl`*)
参数
*`iconurl`* (字符串) 使用作为悬停图标的图形的文件名或完整 URL。 |
|---|
示例
mkobj.setHoverIcon('highlighted.png');
函数 setIcon
此函数向标记添加自定义图标。它包含在 #5: 创建自定义图标标记 中,在 #4: 不点击标记显示和隐藏消息框。
mkobj.setIcon(*`iconurl`*, *`iconSize`*, *`iconAnchor`*)
参数
*`iconurl`* (字符串) 使用作为图标的图形的文件名或完整 URL。 |
|---|
*`iconSize`* (数组) 包含图标宽度和高度(以像素为单位)的数组。 |
*`iconAnchor`* (数组) 一个可选参数,用于描述图形指向地图的位置。该数组包含从图形的左上角到图形左下角的像素数。 |
示例
mkobj.setIcon('pin.png', [24, 36], [12, 36]);
函数 setIconAnchor
此函数直接设置图标的锚点,即图标指向地图的位置。
mkobj.setIconAnchor(*`iconAnchor`*)
参数
*`iconAnchor`* (数组) 包含从图形的左上角到图形左下角的像素数。 |
|---|
示例
mkobj.setIconAnchor([12, 36]);
函数 setIconSize
此函数直接设置图标的尺寸。
mkobj.setIconSize(*`iconSize`*)
参数
*`iconSize`* (数组) 包含图标宽度和高度(以像素为单位)的数组。 |
|---|
示例
mkobj.setIconSize([24, 36]);
函数 setInfoBubble
此函数声明当标记被点击(或如果启用了悬停选项,则鼠标悬停)时,在标记内显示的文本。大多数提供商允许此文本为 HTML。
mkobj.setInfoBubble(*`text`*)
参数
*`text`* (字符串) 在消息框内显示的文本或 HTML。 |
|---|
示例
mkobj.setInfoBubble('<em>This</em> is my message!');
函数 setInfoDiv
此函数声明当标记被点击时,在地图外部的 div 中显示的文本。
mkobj.setInfoDiv(*`text`*, *`div`*)
参数
*`text`* (字符串) 在 div 内显示的文本或 HTML。 |
|---|
*`div`* (字符串) 显示消息的 div 的 id。 |
示例
mkobj.setInfoDiv('<em>This</em> is my message!', 'msgbox');
函数 setLabel
此函数声明当用户将鼠标悬停在标记上时,在标记上方显示的简短文本。
mkobj.setLabel(*`text`*)
参数
*`text`* (字符串) 在悬停时显示的文本。 |
|---|
示例
mkobj.setLabel('Name of Location');
函数 setShadowIcon
此函数在标记图标下方添加阴影图标(如果提供商支持),作为阴影图标。
mkobj.setShadowIcon(*`iconurl`*, *`iconSize`*)
参数
*`iconurl`* (字符串) 使用作为阴影图标的图形的文件名或完整 URL。 |
|---|
*`iconSize`* (数组) 包含阴影图标宽度和高度(以像素为单位)的数组。 |
示例
mkobj.setShadowIcon('pin-shadow.png', [36, 42]);
类 mxn.Polyline
当一个单独的点不能描述你的数据时,转向mxn.Polyline类来描述线和形状。最常见的用途是描述路线,但在第四章中给出了许多其他示例。
函数 mxn.Polyline
这是mxn.Polyline类的构造函数。使用此函数创建一个新的折线,然后设置其选项。然后使用mxn.Mapstraction.addPolyline函数将其添加到地图上。
mxn.Polyline(*`points`*)
参数
*`points`* (数组) 描述折线的mxn.LatLonPoint对象。 |
|---|
返回值
一个mxn.Polyline对象,在本节中称为pobj。
示例
var pobj = new mxn.Polyline(
[new mxn.LatLonPoint(45, −122), new mxn.LatLonPoint(44, −121)]);
函数添加数据
在一个方便的函数中添加数据,如线条颜色和透明度。addData函数类似于mxn.Mapstraction.addPolylineWithData函数。
pobj.addData(*`options`*)
参数
*`options`* (对象) 一个包含折线选项的哈希对象,包括closed、color、fillColor、opacity和width。 |
|---|
示例
var opt = {color: '#ffcc99', width: 3};
pobj.addData(opt);
函数设置闭合
此函数声明折线是否意味着要成为一个多边形——一个完整的形状。
pobj.setClosed(closed)
参数
*`closed`* (布尔值) 如果折线表示一个完整的形状,设置为true。否则,false。 |
|---|
函数设置颜色
此函数直接声明线的颜色。它不影响多边形内部的颜色(如果Polyline是闭合的)。
pobj.setColor(*`hex`*)
参数
*`hex`* (字符串) 颜色值的十六进制表示,包括前面的#。 |
|---|
示例
pobj.setColor('#ff0000'); // red
函数设置填充颜色
此函数直接声明多边形的内部颜色。它不影响未闭合的Polyline。
pobj.setFillColor(*`hex`*)
参数
*`hex`* (字符串) 颜色值的十六进制表示,包括前面的#。 |
|---|
示例
pobj.setFillColor('#0000ff'); // blue
函数设置宽度
此函数直接声明折线(或多边形边框)的宽度。
pobj.setWidth(*`pixels`*)
参数
*`pixels`* (数字) 宽度(以像素为单位)。 |
|---|
示例
pobj.setWidth(4);
函数简化
simplify函数减少了折线中的点数,基于你提供的容差删除那些靠近其他点的点。如果你在宽缩放级别显示一条大线,简化点是好主意。
pobj.simplify(*`tolerance`*)
参数
*`tolerance`* (数字) 一个点需要与上一个点保持的距离(以公里为单位),以便包含在简化的折线中。 |
|---|
示例
pobj.simplify(1.5);
命名空间 mxn.util
Mapstraction 有几个函数不适合放入之前的任何类中。这些实用函数位于mxn.util命名空间中。与类不同,mxn.util在使用其函数之前不需要用构造函数初始化。
函数 mxn.util.$m
$m函数类似于 jQuery 的$函数,但它仅用于通过id查找 HTML 元素。这是一个更优雅的document.getElementById函数。
mxn.util.$m(*`id [, id]*`*)
参数
*`id`* (字符串) 元素的标识符。与 jQuery 不同,此参数不需要前面的#CSS 语法。可以传递多个id参数。 |
|---|
返回
与元素id对应的 HTML 对象。如果提供了多个id参数,则返回匹配的元素数组。
示例
var mapobj = mxn.util.$m('mymap');
函数 mxn.util.getAvailableProviders
此函数检索当前加载的所有提供者。大多数时候,你只会使用一个提供者,但如果进行复杂的多映射,此结果很有用。
mxn.util.getAvailableProviders()
返回
包含提供者标识符的数组。
示例
var loaded = mxn.util.getAvailableProviders();
函数 mxn.util.getStyle
此函数检索 CSS 样式信息。由于不是所有浏览器都以相同的方式访问样式,Mapstraction 提供了这个跨浏览器函数。
mxn.util.getStyle(*`htmlobj`*, *`style`*)
参数
*`htmlobj`* (对象) HTML 元素的对象版本,例如mxn.util.$m返回的值。 |
|---|
*`style`* (字符串) 要检索的样式名称。 |
返回
包含样式信息的字符串。
示例
var this_style = mxn.util.getStyle(mxn.util.$m('mymap'), 'border');
函数 mxn.util.KMToMiles
当然,你可以自己进行数学计算,但 Mapstraction 提供了几个函数来在测量系统之间进行转换。此函数将千米转换为英里。
mxn.util.KMToMiles(*`km`*)
参数
*`km`* (数字) 千米数。 |
|---|
返回
英里数。
函数 mxn.util.loadScript
此函数加载 JavaScript 文件。这用于内部读取适当的 Mapstraction 文件以供提供者使用,但你也可以使用它。
mxn.util.loadScript(*`scripturl`*)
参数
*`scripturl`* (字符串) 脚本文件的地址,可以是单个文件或完整 URL。 |
|---|
函数 mxn.util.loadStyle
此函数加载 CSS 样式表。
mxn.util.loadStyle(*`styleurl`*)
参数
*`styleurl`* (字符串) 样式表的地址,可以是单个文件或完整 URL。 |
|---|
函数 mxn.util.lonToMetres
地球的曲率使得经度一度长度的变化取决于位置的纬度。此函数将经度转换为海平面上的米。注意函数名称的英国拼写。
通过传递纬度距离作为lon参数和 0 作为lat参数,可以实现纬度到米的转换。
mxn.util.lonToMetres(*`lon`*, *`lat`*)
参数
*`lon`* (数字) 要转换为米的经度度数。 |
|---|
*`lat`* (数字) 位置的纬度。 |
返回
在指定纬度下,指定经度度数的米数。
函数 mxn.util.metresToLon
这是mxn.util.lonToMetres函数的逆过程。它将米转换为经度度数。
纬度度数与纬度为 0 时的经度度数相同,因此可以将lat参数的值设为 0 以转换纬度的米。
mxn.util.metresToLon(*`meters`*, *`lat`*)
参数
*`meters`* (数字) 要转换的米数。 |
|---|
*`lat`* (数字) 位置的纬度。 |
返回
经度度数。
函数 mxn.util.milesToKM
这是 mxn.util.KMToMiles 的逆操作。此函数将英里转换为千米。
mxn.util.milesToKM(*`miles`*)
参数
*`miles`* (数字) 要转换的英里数。 |
|---|
返回值
千米数。
函数 mxn.util.stringFormat
在 JavaScript 中将变量与文本混合连接可能会很麻烦,所以 Mapstraction 提供了这个函数来简化这个过程。传递一个参数化字符串以及一些变量,Mapstraction 就会创建一个单独的文本字符串。
mxn.util.stringFormat(*`text`*, var1, var2, ...)
参数
*`text`* (字符串) 完整的文本,其中包含用 { 和 } 括号表示的特殊编号参数。 |
|---|
*`var1, var2, ...`* (可变) 任意数量的变量或值,与 text 变量中参数化值的数量相匹配。 |
返回值
完整的文本字符串,其中插入了变量/值。
示例
var message = mxn.util.stringFormat('There are {0} tiny {1}', num, 'eggs');


浙公网安备 33010602011771号