精通-JavaScript-ArcGIS-服务器开发-全-
精通 JavaScript ArcGIS 服务器开发(全)
原文:
zh.annas-archive.org/md5/c997927188810ea905d8285a4c85d23a译者:飞龙
前言
在这个数字时代,我们期望地图无论在哪里都能可用。我们在手机上搜索驾驶路线。我们使用应用程序查找商业地点和附近的餐厅。维护人员使用数字地图定位地下几英尺的资产。政府官员和高级管理人员需要最新的位置数据来做出影响许多人生的重要决策。
ESRI 在数字制图领域一直是 30 年的行业领导者。通过 ArcGIS Server 等产品,他们使公司、政府机构和数字制图师能够将地图和地理数据发布到互联网上。使用 ArcGIS API for JavaScript 编写的网站和应用使这些地图在桌面和移动浏览器上均可访问。该 API 还提供了创建用于数据展示、收集和分析的强大应用程序所需的构建块。
虽然有很多示例、博客文章和书籍可以帮助你开始使用 ArcGIS API for JavaScript 开发应用程序,但它们通常不会深入探讨。它们没有讨论使用这些工具开发应用程序的陷阱、限制和最佳实践。这本书试图实现的就是这一步。
本书涵盖内容
第一章, 你的第一个地图应用程序,介绍了 ArcGIS Server 和 ArcGIS API for JavaScript。在本章中,你将学习创建地图和添加图层的基础知识。
第二章, 深入 API,概述了 ArcGIS API for JavaScript 中可用的许多 API 组件、小部件和功能。本章提供了如何使用这些组件的解释。
第三章, Dojo 小部件系统,探讨了 ArcGIS API for JavaScript 构建在其中的综合 JavaScript 框架——Dojo 框架。本章探讨了异步模块设计(AMD)的内部工作原理以及如何为我们的地图应用程序构建自定义小部件。
第四章, 在 REST 中寻找平静,探讨了 ArcGIS REST API,该 API 定义了 ArcGIS Server 如何与浏览器中的应用程序通信。
第五章, 编辑地图数据,介绍了如何在 ArcGIS Server 中编辑存储的地理数据。本章探讨了 ArcGIS JavaScript API 提供的模块、小部件和用户控件。
第六章, 绘制你的进度图,探讨了如何通过图表和图形传达地图要素信息。本章不仅讨论了使用 Dojo 框架制作的图表和图形,还探讨了如何集成其他图表库,如 D3.js 和 HighCharts.js。
第七章, 与其他人合作,讨论了将其他流行的库集成到使用 ArcGIS API for JavaScript 编写的应用程序中的方法。本章还探讨了将框架与 jQuery、Backbone.js、Angular.js 和 Knockout.js 结合使用。
第八章, 美化您的地图,涵盖了在映射应用程序中使用 CSS 样式。本章检查了 Dojo 框架如何布局元素,并探讨了添加 Bootstrap 框架来美化映射应用程序。
第九章, 移动开发, 探讨了在移动设备上开发的需求,并讨论了尝试使您的应用程序“移动友好”的陷阱。本章实现了由 Dojo 框架提供的 dojox/mobile 框架,作为为移动使用样式元素的方法。
第十章, 测试, 讨论了测试驱动开发和行为驱动开发的需求。本章讨论了如何使用 Intern 和 Jasmine 测试框架测试应用程序。
第十一章, ArcGIS 开发的未来,探讨了新的映射和应用服务,如 ArcGIS Online 和 Web AppBuilder。您将学习如何围绕 ArcGIS Online 网上地图开发应用程序。
您需要这本书什么
对于本书的所有章节,您需要一个现代浏览器和一个文本编辑器来创建文件。您可以选择任何文本编辑器,但具有语法高亮和某种 JavaScript 代码高亮的编辑器会很有帮助。对于第五章, 编辑地图数据 和 第九章, 移动开发 中的练习,您还需要访问一个网络托管服务器来测试用 Java、PHP 或 .NET 编写的文件。最后,对于第十章, 测试 中的练习,您需要在您的机器上安装最新版本的 Node.js。
这本书适合谁
本书适合有一定 ArcGIS Server 经验的 Web 开发人员,或者有一定 HTML、JavaScript 和 CSS 编写经验的地理空间专业人士。本书假设您已经查看了一些由 ESRI 提供的 ArcGIS API for JavaScript 示例。更重要的是,本书适合希望更深入地使用 ArcGIS Server 进行应用程序开发的读者。Packt Publishing 提供的其他书籍,如 Eric Pimpler 的 Building Web and Mobile ArcGIS Server Applications with JavaScript 或 Hussein Nasser 的 Building Web Applications with ArcGIS,可能更适合作为 ArcGIS Server 和 ArcGIS API for JavaScript 的入门。
惯例
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称将如下所示:“我们正在处理人口普查数据,让我们称它为census.html。”
代码块设置如下:
<!DOCTYPE html>
<html>
<head></head>
<body></body>
</html>
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
<title>Census Map</title>
<link rel="stylesheet" href="http://js.arcgis.com/3.13/esri/css/esri.css" />
<style>
html, body {
border: 0;
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
</style>
<script type="text/javascript">
dojoConfig = {parseOnLoad: true, debug: true};
</script>
<script type="text/javascript" src="img/" ></script>
</head>
新术语和重要词汇将以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“我们将其精确地定位在右上角,并为人口普查按钮留出一点垂直居中的空间。”
注意
警告或重要提示将以这样的框显示。
小贴士
小技巧和技巧如下所示。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中获得最大收益的书籍。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 图书的骄傲拥有者了,我们有一些东西可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您的账户中下载示例代码文件,这些文件适用于您购买的所有 Packt Publishing 书籍。www.packtpub.com。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/6459OT_ColorImages.pdf下载此文件。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。这样做可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将在勘误部分显示。
侵权
侵权是互联网上所有媒体持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌侵权材料的链接。
我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。
询问
如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章. 你的第一个地图应用
假设你有一张地图。你已经使用 ESRI 提供的一种桌面地图和数据分析软件 ArcGIS Desktop 对其进行了数字化处理。你已经经历了绘制点、连接线和检查多边形边界的繁琐过程。你已经添加了漂亮的背景航空影像,并应用了使地图可读的所有部分。
你将如何与公众分享这张地图呢?你可以在公共信息办公室张贴它,但市民们抱怨说那个地方太偏远,而且地下有太多的楼梯。你也可以制作一千份印刷品,但这会非常昂贵。
如果你运行并连接到 Web 服务器的 ArcGIS Server 软件,你可以将你的地图在线发布,并通过运行 ArcGIS JavaScript API 的网站提供服务(developers.arcgis.com/javascript)。
ArcGIS JavaScript API 是一个与 ArcGIS Server 一起工作的 JavaScript 库,它将地图制作者与公众连接起来。地图制作者可以使用 ESRI 的产品,例如 ArcMap,来生成地图文档。然后,该地图制作者可以通过 ArcGIS Server 发布地图文档。从那里,加载了 ArcGIS JavaScript API 的网页可以在浏览器中绘制地图,并让公众平移、识别和与地图交互。
在本章中,我们将涵盖以下主题:
-
使用 ArcGIS Server 和 ArcGIS API for JavaScript 创建网络地图应用的要求
-
使用 JavaScript API 提供的 HTML head 和 body 内容来服务地图
-
如何使用 ArcGIS JavaScript API 创建地图并添加内容
-
如何制作一个交互式地图
API 的功能
ArcGIS JavaScript API 提供了许多构建健壮网络地图应用所需的工具。它可以生成可滑动的地图,即允许用户平移和缩放的交互式地图。其行为类似于 Google 或 Bing 地图,但使用的是你的数据。你控制着内容,从背景影像到标记和弹出内容。使用 ArcGIS Server,你可以控制地图的布局方式,以及你使用的颜色、样式和字体。该 API 还附带了一些自定义元素,允许你从在地图上绘制、搜索数据、在地图上测量事物到以多种格式打印地图等一切操作。
ArcGIS JavaScript API 是基于 Dojo 框架构建的 (www.dojotoolkit.org). 由于 Dojo 与 API 一起打包,您还可以访问一个广泛的免费 HTML 表单元素、控件和布局元素库,用于您的 Web 应用程序。这些 Dojo 用户控件已在多个浏览器中进行了测试,并包括一个完整的库,可用于制作移动应用程序。虽然 ArcGIS JavaScript API 是用 Dojo 构建的,但它也与 jQuery 和 AngularJS 等其他库很好地协同工作。
ArcGIS JavaScript API 是与 ArcGIS API 一起设计和构建的,用于 Flash 和 Silverlight。与其他需要专用编译器、插件和相关软件的 API 不同,ArcGIS JavaScript API 可以用简单的文本编辑器编写,并在大多数常见浏览器上查看,无需任何特殊插件。由于移动浏览器,如 iPad 的 Safari 和 Android 的 Chrome,不支持第三方插件,因此 ArcGIS JavaScript API 是创建移动平台交互式地图网站的优选选择。
小贴士
现在,使用 Windows Notepad 编写网站代码成为可能,就像在没有向导的情况下攀登珠穆朗玛峰一样。但一旦出现问题,您可能会想使用具有语法高亮和其他功能的免费文本编辑器,例如 NotePad++ (notepad-plus-plus.org/)、Aptana Studio 3 (www.aptana.com/products/studio3.html) 或 Visual Studio Code (code.visualstudio.com),适用于 Windows;Brackets (brackets.io) 或 Textmate (macromates.com/),适用于 Mac;Kate (kate-editor.org/)、Emacs (www.gnu.org/software/emacs/) 或 vim (www.vim.org/),适用于 Linux。如果您想使用不免费但提供更多功能和支持的文本编辑器,可以查看 Sublime Text (www.sublimetext.com/) 或 Webstorm (www.jetbrains.com/webstorm/)。
ArcGIS JavaScript API 社区
ArcGIS JavaScript API 拥有一个活跃的开发者社区,他们愿意在您学习过程中提供帮助。ESRI 有博客,在那里他们发布更新,并在全国乃至全球的各个城市举办聚会。许多 ArcGIS JavaScript 开发者,无论是 ESRI 内部还是外部,都在 Twitter 和其他社交媒体平台上非常活跃。
注意
您可以通过书籍和网站找到许多资源来帮助您学习 ArcGIS JavaScript API。对于书籍,您可以查阅 Eric Pimpler 的《Building Web and Mobile ArcGIS Server Applications with JavaScript》,Hussein Nasser 的《Building Web Applications with ArcGIS》,以及 Rene Rubalcava 的《ArcGIS Web Development》。对于在线资源,您可以访问 ESRI GeoNet(geonet.esri.com/community/developers/content),查看 GIS StackExchange 上的 arcgis-javascript-api 标签(gis.stackexchange.com/questions/tagged/arcgis-javascript-api),或访问 ESRI GitHub 页面(github.com/esri)。
我们的第一张 Web 地图
现在介绍部分已经结束,我们应该开始使用 API 进行工作了。在本章中,我们将查看一些代码,这些代码将创建一个简单的交互式地图。示例将是一个单页应用程序,所有的样式和编码都在同一页上。在现实世界中,我们希望将这些内容分别存放在不同的文件中。对于这个示例,项目由以下内容组成:
-
设置 HTML5 网页
-
添加必要的样式和 ArcGIS JavaScript 库
-
框架我们的 HTML 内容
-
设置创建地图的脚本
-
加载图层文件
-
添加点击事件以从地图服务收集数据
-
在地图上显示这些数据
我们的作业
我们刚刚被 Y2K 历史学会要求制作一个关于 2000 年左右美国的交互式地图应用程序。他们希望该应用程序能够显示该年的美国人口统计,包括性别、年龄和种族。在审查客户的要求后,我们确定 2000 年的人口普查数据将提供我们所需的全部地图和人口统计数据。
经过一番研究,我们找到了一个提供 2000 年人口普查数据的 ArcGIS Server 地图服务。我们可以使用 ArcGIS JavaScript API 在 HTML 文档上显示这些数据。用户将能够点击地图,应用程序将按州、人口普查区和人口普查区块组显示人口普查数据。
设置 HTML 文档
让我们打开我们最喜欢的文本编辑器并创建一个 HTML 文档。由于我们正在处理人口普查数据,让我们将其命名为census.html。我们将从一个 HTML5 模板开始。浏览器将通过页面顶部的适当文档类型将其识别为 HTML5。我们的 HTML5 页面开始如下:
<!DOCTYPE html>
<html>
<head></head>
<body></body>
</html>
从头部开始
HTML 文档的头部包含有关页面信息,包括标题、页面内容的元数据、层叠样式表(CSS)链接,告诉浏览器如何渲染输出,以及开发人员需要浏览器在读取页面其余部分之前运行的任何脚本。以下是一个简单网页的示例:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
<title>Census Map</title>
<link rel="stylesheet" href="http://js.arcgis.com/3.13/esri/css/esri.css" />
<style>
html, body {
border: 0;
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
</style>
<script type="text/javascript">
dojoConfig = {parseOnLoad: true, debug: true};
</script>
<script type="text/javascript" src="img/" ></script>
</head>
…
小贴士
下载示例代码
您可以从您在www.packtpub.com的账户中下载示例代码文件,以获取您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
让我们分别查看每个项目。
元标签和标题标签
文档头部的标题和元标签向浏览器和搜索引擎提供了更多关于页面的信息。以下代码是一个示例:
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
<title>Census Map</title>
一些标签告诉搜索引擎如何读取和分类你网站的内容。其他标签,如本文件中的元标签,告诉浏览器如何显示和操作页面。在前面代码中,第一个元标签我们设置了用于渲染文本的字符集。第二个元标签告诉 Internet Explorer 浏览器使用最新版本加载页面。第三个元标签告诉移动浏览器内容已缩放到正确大小,禁用了在屏幕上用手指捏或展开来放大或缩小文本的能力。这与放大或缩小地图比例尺不同,并且大多数移动浏览器需要此标签才能放大或缩小地图。
级联样式表
网站的样式由 CSS 决定。这些样式表告诉浏览器如何将元素放置在页面上,为每个元素指定颜色,如何分配空间,等等。您可以在当前文档中看到它们的排列方式:
<link rel="stylesheet" href="http://js.arcgis.com/3.13/esri/css/esri.css" />
<style>
html, body {
border: 0;
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
</style>
以下是三种使用 CSS 组织和控制元素样式的办法:
-
首先,我们可以通过向 HTML 元素(例如
<div style="..."></div>)添加样式属性,使用内联样式为页面上的单个元素应用样式。 -
其次,我们可以通过使用内部样式表(由
<style></style>标签表示)来为整个页面应用样式。 -
第三,我们可以通过引用外部样式表(由
<link rel="stylesheet" … />表示)来为多个样式表应用样式。对于我们的单页应用程序,我们将使用我们自己的内部样式表和由 ESRI 提供的外部样式表。
ArcGIS JavaScript 库需要它自己的样式表来正确地在屏幕上定位地图。没有这个文件,地图将无法正确渲染,因此无法显示预期结果。要加载库所需的样式表,请添加一个link标签,并将href属性设置为指向esri.css文件。
如果你正在使用 3.2 或更高版本,并且你的地图在页面上以棋盘图案显示,每隔一个方块显示地图瓦片,那么最可能的问题是esri.css样式表没有加载。请确保你引用了正确版本的esri.css样式表。以下图片展示了这种行为示例:

Dojo 配置脚本
我们的 JavaScript 代码添加了一个变量,告诉 Dojo 如何加载,在第一个脚本标签中。从这个脚本中,我们可以告诉浏览器如何解释我们文档中特别定义的 HTML 元素,我们是否希望浏览器缓存所有文件,甚至如何将包和其他库加载到 Dojo 构建系统中:
<script type="text/javascript">
dojoConfig = {
parseOnLoad: true,
cacheBust: true
};
</script>
在这个例子中,我们告诉 Dojo 在页面加载时解析 body 中任何特别装饰的 HTML 元素,并用适当的 Dojo 小部件替换它们。使用 cacheBust 参数,我们还要求浏览器在加载文件时使用时间戳,这样文件就不会在浏览器中缓存。添加时间戳会强制浏览器加载 JavaScript 文件的最新副本,而不是依赖于缓存的副本。处于开发中的缓存脚本可能不会显示您所做的最新更改,这会减慢开发速度并增加故障排除时间。
小贴士
加载 dojoConfig 对象的脚本必须在你加载 ArcGIS JavaScript API 之前。如果 dojoConfig 对象是在 API 脚本引用之后创建的,则 dojoConfig 的内容将被忽略。
ArcGIS JavaScript API 脚本
ArcGIS JavaScript 库是您将用于从 ArcGIS Server 渲染、操作和与地理数据交互的主要工具:
<script type="text/javascript" src="img/" ></script>
本应用以及其他书中的应用都使用了 ArcGIS JavaScript API 的 3.13 版本。这是在编写本书时可用的新版。在维护这些应用时,请注意版本号更新。ESRI 经常发布新版本以添加新功能,修复旧版本中的错误,并确保 API 与最新浏览器兼容。
从 head 移动到 body
在设置好 HTML head 之后,我们可以专注于应用的身体部分。我们将在身体中添加 HTML 元素,其中包含地图和其他信息。我们将从内联样式表中设置这些功能。最后,我们将编写一个脚本以处理地图创建、人口数据检索以及对地图事件的响应。
框架 HTML body
我们的客户指定他们希望应用显示两个面板,一个主地图面板和一个单独的面板,解释用户应该做什么。我们将通过使用 HTML div 元素划分区域来满足这一请求。div 元素是 HTML 中的通用内容块。在第一个 div 中,我们将添加一个 instructions 样式类,并填充适当的说明。在第二个 div 中,我们将应用特定的 id 元素 map,以告诉我们和 ArcGIS JavaScript API 应该在哪里放置地图:
<body>
<div class="instructions">
<h1>U.S. Census for 2000</h1>
<p>Click on the map to view census data for the state, census tract, and block.</p>
</div>
<div id="map"></div>
</body>
添加一些样式
我们需要为我们添加的新元素添加一些样式。为此,我们将修改应用程序头部部分的原始内部样式表。我们的客户希望地图占据整个屏幕,在右上角留出一点空间用于地图标题和说明。客户还没有决定颜色,但他们要求使用今天大家都在网站上使用的圆角。
因此,在审查需求并查找如何设置元素样式后,让我们在<style></style>元素内添加以下内容。以下代码片段中的更改已被突出显示,以帮助您看到以下代码中发生了什么变化:
<style>
html, body, #map {
border: 0;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
.instructions {
position: absolute;
top: 0;
right: 0;
width: 25%;
height: auto;
z-index: 100;
border-radius: 0 0 0 8px;
background: white;
padding: 0 5px;
}
h1 {
text-align: center;
margin: 4px 0;
}
</style>
这里是对我们添加的样式的解释。我们希望 HTML 文档,地图<div>没有边距、边框或填充,并占据整个页面的高度。我们还希望具有说明类<div>元素精确地定位在右上角,占据页面宽度的百分之二十五,然后其高度将自动确定。说明块将浮起 100 z-index 单位向用户(将其置于我们的地图之前),其左下角将有一个 8 像素半径的曲线。它将有一个白色背景,并在左右两侧有一些填充。最后,标题<h1>将水平居中,上面和下面有一些填充。
在末尾添加脚本
如果我们现在查看我们的网页,我们不会看到太多。只有一个标题和右上角的说明。我们需要将这个普通的页面转换成一个功能齐全的地图应用程序。为此,我们需要通过 JavaScript 指导浏览器如何将我们的地图<div>转换成地图。
在我们的<body>标签结束之前,我们将添加一个脚本标签,在其中我们将编写我们的 JavaScript 代码。我们将脚本放在 HTML 文档的末尾,因为与图像和样式表不同,浏览器一次只加载一个脚本标签。当浏览器正在加载脚本时,它不会加载其他任何内容。如果您将其放在页面的开头,用户可能会注意到一点延迟,即在页面加载之前的延迟。当我们把脚本放在末尾时,用户会被页面上的图像和其他元素分散注意力,以至于不会注意到您的脚本何时加载。这使得它看起来加载得更快:
<div id="map"></div>
<script type="text/javascript"></script>
</body>
那么,为什么我们没有在页面末尾加载 ArcGIS JavaScript 库呢?有时,尤其是如果您正在使用 Dojo 的其他部分,我们需要在浏览器加载页面时操纵库。在这种情况下,我们将库引用放在 HTML 文档的头部。
现在我们有了用于编写一些 JavaScript 的脚本标签,我们可以编写一个交互式地图。但是,在我们开始编写代码之前,我们需要了解如何使用 ArcGIS JavaScript API 和 Dojo。我们将从一个简短的历史课开始。
在网络开发的美好时光里,甚至直到今天,JavaScript 库试图通过创建一个全局对象(例如 JQuery 的$或 Yahoo 的 YUI)来避免相互冲突。库的所有功能都会集成到这个对象中。如果你使用过 Google Maps API,你可能使用过google.maps.Map()来创建地图,以及google.maps.LatLng()来在地图上加载一个点。库的每个子部分都通过点(.)分隔。
ArcGIS JavaScript 库的旧版本并无不同。所有 ESRI 的地图库都被加载到主"esri"对象中。你可以使用esri.map创建一个地图,并通过esri.layer.FeatureLayer加载它来显示一些数据,例如。Dojo 框架也是类似的,使用dojo、dijit和dojox全局对象。
但这种类似树状结构的 JavaScript 库设计方法有其缺点。随着库的扩展和成熟,它会积累很多开发者并未经常使用的部分。我们可能只使用库中的一个或两个特定功能,但可能不会使用库提供的每一个工具和功能。我们未使用的库部分浪费了客户端带宽,增加了内存负担,并使得我们的应用看起来加载速度更慢。
异步模块定义
ArcGIS JavaScript API 和 Dojo 都决定通过引入异步模块定义(AMD)的概念来解决库膨胀的问题。在 AMD 中,库被分解成模块化组件。开发者可以选择他们想要包含在应用程序中的库的部分。通过只加载所需的模块,我们可以减少下载时间,释放浏览器内存中未使用的功能,并提高性能。
AMD 的另一个优点是避免命名冲突,或者说是控制库加载时变量名的开发者。此外,加载的库的作用域限制在调用函数内部,就像一个自执行的语句。
在基于 AMD 的应用程序中,我们列出我们想要使用的库模块,通常是一个字符串数组,库知道如何解释它。然后我们跟随一个函数,该函数加载大多数或所有这些模块到 JavaScript 对象中。我们可以在函数中使用这些模块来获取我们想要的结果。
加载所需模块
要利用 Dojo 的 AMD 风格,我们将使用 Dojo 的require函数。在旧例子中,我们会创建多个dojo.require("")语句来加载我们需要的 ArcGIS JavaScript 库的部分(并且希望在我们想要使用它们的时候)。但使用 AMD 风格时,我们使用一个单一的require函数来请求我们请求的库列表,并在所有库在浏览器中加载后,在函数中加载它们:
<div id="map"></div>
<script type="text/javascript">
require([], function () {});
</script>
</body>
require 函数接受两个参数,一个字符串数组,对应于我们库中的文件夹位置,以及一个在那些库加载后运行的函数。在第二个函数中,我们添加参数(函数括号内的变量)与列表中加载的库对应。
因此,对于这个应用程序,我们需要从 ArcGIS JavaScript API 中获取几个模块。我们需要创建一个空地图,并以我们称之为层的格式向地图添加数据。我们需要在地图上识别事物,从我们点击地图的位置检索所需的普查数据,然后显示它:
<div id="map"></div>
<script type="text/javascript">
require([
"esri/map",
"esri/layers/ArcGISDynamicMapServiceLayer",
"esri/tasks/IdentifyParameters",
"esri/tasks/IdentifyTask",
"esri/InfoTemplate",
"dojo/_base/array",
"dojo/domReady!"
], function (
Map, ArcGISDynamicMapServiceLayer,
IdentifyParameters, IdentifyTask, InfoTemplate,
arrayUtils
) {
// code goes here
});
</script>
注意 require 语句中加载库的顺序以及以下函数中的参数。列表中的第一项对应于函数中的第一个参数。在创建更复杂的应用程序时,混淆元素顺序是一个常见的错误,尤其是如果它已经被修改多次。确保列表中的项目随着列表的下降而对应。
你可能已经注意到,尽管加载了七个库,但只有六个参数。最后加载的库,即 dojo/domReady! 库,告诉 require 语句的第二个函数不要运行,直到所有 HTML 元素都已加载并在浏览器中渲染。
地图对象
现在我们有了组件,我们需要创建一个交互式地图;让我们将它们组合起来。我们将首先构建一个 map 对象,它为我们提供了所需的基础和交互平台。地图构造函数接受两个参数。第一个参数,要么是 HTML 节点,要么是节点的 id 字符串,表示我们想要放置地图的位置。地图构造函数的第二个参数是一个 options 对象,我们在其中添加使地图按预期工作的可配置选项:
function (
Map, ArcGISDynamicMapServiceLayer,
IdentifyParameters, IdentifyTask, InfoTemplate,
arrayUtils
) {
var map = new Map("map", {
basemap: "national-geographic",
center: [-95, 45],
zoom: 3
});
});
在前面的代码中,我们正在创建一个具有 id 为 map 的 div 元素中的地图。在地图中,我们添加了一个 基础地图,或背景参考地图,采用国家地理的风格。我们将地图中心定位在北纬 45° 和西经 95°,缩放级别为三。我们将在后面的章节中更深入地讨论这些配置。如果你使用桌面浏览器查看结果,你应该看到包括阿拉斯加和夏威夷在内的美国,如下面的图像所示:

小贴士
如果你有过使用 Dojo 的经验,大多数元素构造函数都有选项,然后是节点或节点的 id。这与我们构建地图的方式相反。记住,顺序很重要。
层
几年前,地图部门在透明的 Mylar 薄膜上绘制地图。绘制这些薄膜需要付出艰辛的努力,并且要保持相同的比例。当 Mylar 薄膜堆叠在一起,并且每一层的对应点对齐时,它们会提供重叠地图层的视觉混合。
今天,我们可以通过基于浏览器的地图应用程序创建相同的效果。而不是使用清晰的 Mylar 薄片,该应用程序利用分层图像文件和矢量图形来创建相同的效果。在 ArcGIS JavaScript API 中,我们将这些可堆叠的地图数据源称为层。
ArcGIS JavaScript API 可以接受来自不同来源的多种类型的层文件。有些层非常灵活,可以被重新定位和重新投影以与其他地图源对齐。这些通常被称为动态层。其他层由特定比例绘制的图像组成,并不设计得那么灵活。这些通常被称为瓦片层。
在我们的当前应用程序中,我们添加的国家地理背景被认为是瓦片层。它的预渲染内容在浏览器中快速加载,这是使用瓦片层的一个优点。另一方面,我们的普查数据的数据源是由 ArcGIS Server 地图服务提供的动态层。其动态特性有助于它拉伸并与我们的瓦片背景对齐。以下是我们将用于添加层的代码:
var censusUrl = "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/";
var map = new Map("map", {
basemap: "national-geographic",
center: [-95, 45],
zoom: 3
});
var layer = new ArcGISDynamicMapServiceLayer(censusUrl);
map.addLayer(layer);
因此,如果我们现在查看页面,我们应该看到一个世界地图,每个州、普查区和区块周围都有黑色线条。我们可以放大地图以查看更多细节。您的地图应该看起来像以下图像:

添加一些动作
到目前为止,我们有一个绘制所有州、普查区块和轨迹数据的地图。我们需要更多。我们需要一种让用户与网站交互的方式。ArcGIS JavaScript API 集成了由原生 JavaScript 语言提供的许多工具,以及由 Dojo 提供的新工具。
事件
当我们中的许多人学习传统编程时,我们了解到程序遵循线性设计。它们从第一行开始,到最后一行结束。计算机按顺序执行每个计算,并且不会继续到下一行,直到当前行完成。
但 JavaScript 添加了不同的东西。JavaScript 添加了一个事件循环,它监视特定的网站交互,例如文本框中的变化。我们可以将一个函数,通常称为事件监听器,附加到一个已知元素的事件。当该事件被触发时,事件监听器就会运行。
例如,按钮有一个点击事件,如果我们为按钮的点击事件附加一个事件监听器,那么该函数将在每次点击按钮时响应。我们还可以通过通过点击事件传递的数据来获取有关按钮的信息。
对于我们的应用程序,我们希望地图在被点击时执行某些操作。如果我们查看支持的地图事件列表,我们可以使用 .on() 方法附加事件监听器。.on() 方法接受两个参数,一个是事件的字符串描述,另一个是当事件发生时希望被调用的事件监听器函数。支持的事件列表可以在 ArcGIS JavaScript API 文档中找到:
map.addLayer(layer);
function onMapClick (event) {
}
map.on("click", onMapClick);
在我们可以对地图进行任何操作之前,我们需要知道它是否已经加载。如果我们在我们最快的计算机上运行更新的浏览器,并且有高速互联网连接,地图可能会立即加载,但如果我们从较旧的智能手机上的移动浏览器进行测试,互联网下载速度较慢,地图可能不会很快加载。
在我们分配点击事件之前,我们将测试地图是否已加载。如果是这样,我们将把 onMapClick 函数作为事件监听器添加到地图的 click 事件。如果不是,我们将等待地图触发其 load 事件来设置 click 事件。为了在添加地图点击事件时避免重复,我们将把这个赋值放在另一个函数中:
function onMapClick(event) {
}
function onMapLoad() {
map.on("click", onMapClick);
}
if (map.loaded) {
onMapLoad();
} else {
map.on("load", onMapLoad);
}
任务
虽然开发者可以编写使用 JavaScript 执行大量任务的代码,但有些任务最好留给服务器。ArcGIS JavaScript API 通过 任务 对象向服务器发送请求。有计算复杂形状面积和周长的任务,有查询并返回地图中特征的空间和非空间数据的任务,还有创建包含我们正在查看的地图的 .pdf 文档的任务,以及其他许多任务。这些任务减轻了浏览器执行可能非常慢的复杂计算的负担。它们还允许库更轻量,因此库不需要加载一个坐标系统到另一个坐标系统之间的所有转换因子的集合,例如。
大多数任务分为三个部分:任务对象、任务参数和任务结果。任务对象使我们能够向 ArcGIS 服务器发送特定任务的请求,并包括可能需要引用的一些任务的常量。任务对象通常接受一个 URL 字符串参数,告诉任务将请求发送到哪个 ArcGIS 服务器服务端点。任务参数对象定义了我们从任务中需要获取的信息。最后,在执行任务并从服务器接收响应后,我们得到一个结构化对象,或称为任务结果的列表。任务、任务参数和任务结果的格式可以在 ArcGIS JavaScript API 文档中找到,网址为 developers.arcgis.com/javascript/jsapi/。
在我们的代码中,我们将使用一个名为 IdentifyTask 的任务方法。我们将告诉 IdentifyTask 方法通过我们的普查 URL 联系 ArcGIS 服务器。在地图点击事件处理程序内部,我们将创建一个名为 IdentifyParameter 的任务参数对象。我们将使用关于我们点击的点以及地图上的数据来配置它。最后,我们将执行 IdentifyTask 方法,传入 IdentifyParameters 对象,以从我们点击的位置检索普查数据:
layer = new ArcGISDynamicMapServiceLayer(censusUrl),
iTask = new IdentifyTask(censusUrl);
function onMapClick (event) {
var params = new IdentifyParameters();
params.geometry = event.mapPoint;
params.layerOption = IdentifyParameters.LAYER_OPTION_ALL;
params.mapExtent = map.extent;
params.returnGeometry = true;
params.width = map.width;
params.height= map.height;
params.spatialReference = map.spatialReference;
params.tolerance = 3;
iTask.execute(params);
}
Deferreds 和承诺
几年前,当 FORTRAN 统治着计算机编程世界时,有一个语句让开发者们在调试代码时感到疯狂:GOTO。使用这条代码行,会打断应用程序的流程,使应用程序跳转到另一行代码。从一个代码段跳转到另一个代码段,在最好情况下也使得跟踪应用程序的逻辑变得困难。
随着现代 JavaScript 和异步开发的出现,跟踪某些异步应用程序的逻辑也可能变得困难。当用户点击按钮时,应用程序会触发一个 AJAX 请求以获取数据。在数据成功返回后,另一个事件被触发,这使得地图执行一些需要一点时间的事情。地图完成后,它会触发另一个事件,以此类推。
Dojo 通过创建 Deferreds 对象来应对这个问题。Deferred 对象返回一个承诺,表示异步过程的结果即将到来。等待结果的函数只有在承诺得到满足后才会被调用。使用返回 Deferred 结果的函数,开发者可以使用 .then() 语句将函数链接在一起。.then() 语句仅在结果得到满足后才会启动其参数中的第一个函数。可以将多个 .then() 语句与返回 Deferred 对象的函数链接在一起,从而实现有序且更易于阅读的编码逻辑。
在我们的 onMapClick 函数中,IdentifyTask 对象的 execute 方法返回一个 Deferred 对象。我们将把这个延迟结果存储在一个变量中,以便稍后可以被另一个工具使用:
function onMapClick (event) {
var params = new IdentifyParameters(),
defResults;
…
defResults = iTask.execute(params);
}
展示结果
一旦我们收到数据,我们应该向用户展示结果。如果我们查看通过网络传递的数据格式,我们会看到一个复杂的 JavaScript 对象表示法(JSON)对象的列表。这些数据以原始形式,在普通用户手中将毫无用处。幸运的是,ArcGIS JavaScript API 提供了将此数据转换为更易于用户使用的工具和方法。
地图的 infoWindow
在现代地图应用程序如 Google Maps、Bing Maps 和 OpenStreetmaps 的时代,用户已经学会了,如果你在地图上点击某个重要位置,应该会弹出一个小窗口并告诉你更多关于该物品的信息。ArcGIS JavaScript API 为地图提供了一个类似的弹出控制,称为infoWindow控制。infoWindow在地图上突出显示特征形状,并叠加一个弹出窗口来显示与特征相关的属性。
infoWindow可以作为地图的一个属性来访问(例如,map.infoWindow)。从这一点起,我们可以隐藏或显示弹出窗口。我们可以告诉infoWindow要突出显示哪些特征。infoWindow提供了一系列可配置和控制点,以帮助创建更好的用户体验。
在我们应用程序的地图点击处理程序中,我们需要将搜索结果转换为infoWindow可以使用的形式。我们将通过向IdentifyTask对象的execute函数添加一个.addCallback()调用来实现这一点。我们将从IdentifyResults中提取特征,并创建一个特征列表。从那时起,我们可以将处理后的结果传递到infoWindow对象所选特征的列表中。我们还将提示infoWindow显示用户点击的位置:
function onIdentifyComplete (results) {
// takes in a list of results and return the feature parameter
// of each result.
return arrayUtils.map(results, function (result) {
return result.feature;
});
}
function onMapClick (event) {
…
defResults = iTask.execute(params).addCallback(onIdentifyComplete);
map.infoWindow.setFeatures([defResults]);
map.infoWindow.show(event.mapPoint);
}
你现在可以从你的浏览器中运行应用程序,并尝试点击地图上黑色轮廓线中的一个特征。你应该会看到一个用青色(浅蓝色)轮廓的形状,以及一个指向你点击位置的弹出窗口。弹出窗口会告诉你那里至少有一条记录(可能更多)。如果你点击弹出窗口上的小前后箭头,可以浏览所选结果(如果有多个)。
InfoTemplate对象
到目前为止,我们可以看到我们数据的外形。问题是,我们看不到里面是什么。与我们在结果中看到的形状相关的重要表格数据,但弹出窗口还没有被告知如何显示信息。为此,我们可以使用一个InfoTemplate对象。InfoTemplate对象告诉弹出窗口如何格式化用于显示的数据,包括使用什么标题,以及我们希望如何显示搜索结果。InfoTemplate对象与特征数据、特征的几何形状和属性连接在一起。
InfoTemplate对象可以通过不同的方式构建,但最常见的是使用一个字符串来描述标题,另一个字符串来显示内容。内容可以包含任何有效的 HTML,包括表格、链接和图片。由于标题和内容是模板,你可以在模板字符串中插入特征属性。用${fieldname}包围你想要使用的字段名称,其中"fieldname"是你想要使用的字段的名称。如果你想在不进行任何特殊格式化的情况下显示所有字段名称和值,可以将InfoTemplate对象的内容值设置为${*}。
对于我们的应用程序,我们需要将InfoTemplates添加到IdentifyTask结果中。我们将使用onIdentifyComplete回调并将它们插入其中。我们将首先插入以下代码:
function onIdentifyComplete (results) {
return arrayUtils.map(results, function (result) {
var feature = result.feature,
title = result.layerName;
feature.infoTemplate = new InfoTemplate(title, "${*}");
return feature;
});
}
在这段代码中,我们正在提取结果的图层名称,并使用它作为标题。对于内容,我们使用“显示所有内容”模板来显示所有字段和值。如果你现在在浏览器中运行网页,并点击一个要素,你应该看到以下图像:

现在,有很多难以阅读的字段名和可能无法理解的字段值。我们可以根据要素的图层名称应用不同的内容格式。查看前面的示例,我们主要对人口、家庭数量和住房单元数量感兴趣:
function onIdentifyComplete (results) {
return arrayUtils.map(results, function (result) {
var feature = result.feature,
title = result.layerName,
content;
switch(title) {
case "Census Block Points":
content = "Population: ${POP2000}<br />Households: ${HOUSEHOLDS}<br />Housing Units: ${HSE_UNITS}";
break;
default:
content = "${*}";
}
feature.infoTemplate = new InfoTemplate(title, content);
return feature;
});
}
如果你再次在浏览器中运行页面,你应该看到更易读的结果,至少对于人口普查区块点来说是这样。其他要素,如州、县和区块组,将显示字段名及其对应值的列表,用冒号(:)分隔。我将把其他字段的模板留作你的家庭作业练习。
最后,你的代码应该如下所示:
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
<title>Census Map</title>
<link rel="stylesheet" href="http://js.arcgis.com/3.13/esri/css/esri.css">
<style>
html, body, #map {
border: 0;
margin: 0;
padding: 0;
height: 100%;
}
.instructions {
position: absolute;
top: 0;
right: 0;
width: 25%;
height: auto;
z-index: 100;
border-radius: 0 0 0 8px;
background: white;
padding: 0 5px;
}
h1 {
text-align: center;
margin: 4px 0;
}
</style>
<script type="text/javascript">
dojoConfig = { parseOnLoad: true, isDebug: true };
</script>
<script src="img/"></script>
</head>
<body>
<div class="instructions">
<h1>U.S. Census for 2000</h1>
<p>Click on the map to view census data for the state, census tract, and block.</p>
</div>
<div id="map"></div>
<script type="text/javascript">
require([
"esri/map",
"esri/layers/ArcGISDynamicMapServiceLayer",
"esri/tasks/IdentifyParameters",
"esri/tasks/IdentifyTask",
"esri/InfoTemplate",
"dojo/_base/array",
"dojo/domReady!"
], function (
Map, ArcGISDynamicMapServiceLayer,
IdentifyParameters, IdentifyTask, InfoTemplate,
arrayUtils
) {
var censusUrl = "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/",
map = new Map("map", {
basemap: "national-geographic",
center: [-95, 45],
zoom: 3
}),
layer = new ArcGISDynamicMapServiceLayer(censusUrl),
iTask = new IdentifyTask(censusUrl);
function onIdentifyComplete (results) {
return arrayUtils.map(results, function (result) {
var feature = result.feature,
title = result.layerName,
content;
switch(title) {
case "Census Block Points":
content = "Population: ${POP2000}<br />Households: ${HOUSEHOLDS}<br />Housing Units: ${HSE_UNITS}";
break;
default:
content = "${*}";
}
feature.infoTemplate = new InfoTemplate(title, content);
return feature;
});
}
function onMapClick (event) {
var params = new IdentifyParameters(),
defResults;
params.geometry = event.mapPoint;
params.layerOption = IdentifyParameters.LAYER_OPTION_ALL;
params.mapExtent = map.extent;
params.returnGeometry = true;
params.width = map.width;
params.height= map.height;
params.spatialReference = map.spatialReference;
params.tolerance = 3;
defResults = iTask.execute(params).addCallback(onIdentifyComplete);
map.infoWindow.setFeatures([defResults]);
map.infoWindow.show(event.mapPoint);
}
function onMapLoad() {
map.on("click", onMapClick);
}
map.addLayer(layer);
if (map.loaded) {
onMapLoad();
} else {
map.on("load", onMapLoad);
}
});
</script>
</body>
</html>
恭喜你,如果你这是第一次使用 ArcGIS Server 和 JavaScript 开发的网络地图应用程序。你已经完成了多个步骤,制作了一个工作状态良好的交互式地图应用程序。这个应用程序应该适用于所有最新的浏览器。然而,较旧的浏览器可能无法正确连接到服务器,可能需要代理。
关于代理的注意事项
如果你曾经长时间使用过 ArcGIS JavaScript API,或者不得不支持像 Internet Explorer 9 或更早的旧浏览器,那么你可能遇到过代理。代理是服务器端应用程序,代表网络浏览器进行网络请求,通常传输和接收浏览器本身无法收集的数据。浏览器需要代理与服务器通信有三个主要的原因。具体如下:
-
浏览器是较旧的浏览器,不支持跨源资源共享(CORS),服务器请求将发送到应用程序所在的服务器之外的其他服务器。
-
代理提供额外的安全密钥以访问特定数据,这可能包括开发者不希望公开的受保护令牌。
-
网络请求的长度超过了
GET请求的最大字符数 2048+。
第一个示例在较旧的浏览器中很常见,包括 Internet Explorer 9 或更低版本。由于安全限制,它们无法从与网络服务器分离的第三方服务器获取数据。根据 HTML5 CORS 规范,较新的浏览器可以检查是否允许从不在服务器上的脚本请求应用程序。
第二个示例在大型安全环境中很常见,需要跳过许多安全关卡。部门门户网站可以使用部门特有的令牌访问代理,提供额外的安全层。
第三个示例在用户传递具有许多不规则顶点的大几何体时很常见。例如,如果您使用绘图工具使用自由手绘工具绘制不规则形状,其顶点会在您在地图上移动时添加。由于有这么多点,并且这些点需要很多字符来显示它们的纬度和经度,所以形状请求可能超过浏览器的最大字符长度也就不足为奇了。
ArcGIS Server 代理可以从 GitHub 免费下载(github.com/Esri/resource-proxy)。它们有与 Java、.NET 和 PHP 兼容的版本。请使用最新版本,并确保它已正确配置以用于您将使用的所有服务。
小贴士
如果您的计算机和 ArcGIS Server 都位于网络防火墙之后,并且 ArcGIS Server 具有独特的公共和私有 IP 地址,您的网络防火墙可能会阻止对 ArcGIS Server 的代理连接。如果您仅在较旧的浏览器上看到网络流量失败,例如 Internet Explorer 8,并且仅针对内部请求,那么防火墙可能是问题所在。请联系您的网络管理员以解决问题。
摘要
在本章中,我们学习了使用 ArcGIS JavaScript API 编写简单 Web 应用的基础知识,并构建了一个基于地图的应用程序。我们学习了如何在 HTML 文档的头部设置应用程序。我们学习了如何在页面上创建地图以及如何添加图层以便我们可以查看数据。我们还学习了如何通过任务检索要素形状和属性,以及如何向用户展示这些数据。
在下一章中,我们将更深入地探讨 ArcGIS JavaScript API 中可用的工具。
第二章. 深入 API
应用程序编程接口(API)定义了从网络服务中可用的操作、格式和数据结构。开发者使用 API 编写代码,告诉服务器完成任务。API 建立在现有编程语言之上,并使用语言的语法和功能使与计算机的通信更容易编码。
在某个时候,ESRI 有三个基于 Web 的 API 用于与 ArcGIS Server 通信:Flash、Silverlight 和 JavaScript。随着移动网络的兴起和浏览器性能的提升,ArcGIS JavaScript API 成为了浏览器首选。为了最好地利用 ArcGIS Server 地图功能,最好学习 JavaScript API 提供的功能。我们将通过 API 进行一次巡游,以便熟悉它所能提供的内容。
与本书中的其他章节不同,本章提供更多的是参考而不是编程练习。添加了简短的代码片段,以展示如何在 API 中使用模块。如果您没有从本章中吸收所有信息,请不要担心。只需回来重新阅读其中的部分内容作为参考即可。
在本章中,我们将学习以下主题:
-
如何在 ArcGIS JavaScript API 中创建和配置地图
-
如何通过瓦片、动态和图形层显示来自 ArcGIS Server 的地理数据
-
地图上图形的构建块
-
如何通过任务与 ArcGIS Server 服务进行通信
-
如何通过名为dijits的打包 UI 控件节省开发时间
注意
有关 ArcGIS JavaScript API 的更多信息,包括您可以构建的示例,请参阅 Eric Pimpler 的《Building Web and Mobile ArcGIS Server Applications with JavaScript》或 Rene Rubalcava 的《ArcGIS Web Development》。
查找 ArcGIS JavaScript API 文档
您可以通过访问developers.arcgis.com/javascript/jsapi/找到 ArcGIS API for JavaScript 的文档。在那里,您可以找到有关 API 最新版本的信息、自上一版本以来的更改列表以及帮助您创建应用程序的相关文档。组织结构合理,信息相对容易通过其布局找到。
文档布局
ArcGIS API 中每个元素的文档都按组织方式排列。API 组件的链接位于左侧。每个元素的文档默认为 AMD 风格,但提供链接到较旧的遗产开发风格。使用 AMD 风格时,页面顶部显示如何将模块加载到您的代码中。接下来是模块的描述、使用该模块的示例链接以及显示当前模块继承的模块的类层次结构图。
对象构造函数
如果模块需要构造 JavaScript 对象,文档将提供有关调用模块构造函数所需信息。文档提供了创建对象所需的参数的详细信息,包括参数名称、对象类型、它们是否可选以及它们的作用。
CSS 类和数据-属性
在任何构造函数信息之后,文档提供了有关模块可能显示在地图上的任何内容的 CSS 类属性数据,以及模块可能使用的任何 HTML5 data-* 属性。CSS 类可以帮助您通过提供类钩子来修改模块中小部件和视觉元素的外观,以便使用您自定义的样式进行修改。data-* 属性使其他 JavaScript 脚本能够访问有关您的部件的信息,而无需将整个部件加载到内存中。例如,当使用另一个库点击地图图形时,您可以通过查看元素的数据-geometry-type 属性来访问几何类型。
属性
当使用构造函数创建 ArcGIS API 对象时,它将具有某些属性或分配给对象的变量参数。一些属性在对象创建时设置。其他属性在对象修改时设置,或运行某些方法时设置。API 文档列出了积极维护的对象属性,并且在没有足够的通知的情况下不会删除。
小贴士
如果您在浏览器中使用网络开发者工具,例如 Firefox 的 Firebug 或 Chrome 开发者工具,您可能会发现这些 ArcGIS API 对象具有比文档中列出的更多的属性。许多被认为是私有的属性在其名称前有一个或两个下划线 (_) 字符。如果属性和方法未在文档中列出,它们可能在 API 的下一个版本发布时不存在。不要依赖代码中的未记录属性和方法。
方法
除了某些属性外,使用 ArcGIS API 模块创建的对象可能还会包含方法,这些方法是分配给对象的 JavaScript 函数,通常使用它们分配到的对象的其他部分。文档将列出附加到对象上的公共方法,以及它们接受的参数、返回的值以及它们执行的任务或功能的描述。
事件
许多模块都有记录的事件,其中模块会发出通知表示发生了某些事情。这些类似于 HTML 按钮的 onClick 事件或浏览器的 onLoad 事件。在 API 的旧版本中,事件是通过 dojo.connect 监听的。但随着库的成熟,Dojo 正在淘汰 connect 函数,并用 dojo/on 库(dojotoolkit.org/reference-guide/1.10/dojo/on.html)来替换。实际上,dojo/on 模块提供了一个函数。
使用 dojo/on 函数,你可以通过以下参数调用它来附加事件:你想要监听的 HTML DOM 元素或 Dojo 小部件,事件名称作为字符串,以及当事件发生时将被调用的函数。如果你想停止事件,你需要一个变量来接受 dojo/on 调用的返回值,提前准备。当你准备好停止监听时,调用那个返回变量的 remove() 方法。一些模块包含它们自己的 on() 方法,该方法实现了用于事件处理的 dojo/on 模块。以下是一个使用 dojo/on 模块的示例代码片段:
require(["dojo/on", "dojo/dom", "dojo/domReady!"],
function (on, dom) {
var soupMe = on(dom.byId("deliver-soup"), "click", function () {
alert("Here's your soup. NEXT!");
});
on(dom.byId("no-more-soup"), "click", function () {
alert("NO SOUP FOR YOU!");
soupMe.remove();
});
});
现在我们已经了解了阅读文档时可以期待的内容,让我们来看看 ArcGIS API for JavaScript 中可用的不同工具。由于 ArcGIS 应用程序与地理数据一起工作,我们将首先查看地图。
绘制地图
当使用 ArcGIS JavaScript API 进行工作时,你首先想要做的事情之一就是创建一个地图。地图将一个简单的 div 元素转换成一个交互式画布,你可以在其中显示、请求甚至编辑地理数据。地图是一个复杂的对象,因此我们将花费相当多的时间来关注它。让我们学习如何在我们的脚本中创建地图。
构造函数选项
当使用 ArcGIS JavaScript API 首次创建地图时,开发者有多个参数可以用来配置地图。我们在上一章创建我们的第一个地图应用程序时接触到了一些这些参数。让我们更详细地研究其中的一些。
自动调整大小
自动调整大小功能,默认情况下为开启状态,它告诉地图是否应该在浏览器大小或方向改变时自动调整其大小。如果你有一个全屏地图应用程序,并且你最大化或调整浏览器窗口大小时,地图将自动调整并填充分配给它的空间。地图将保持相同的缩放级别,但在调整浏览器窗口大小时,地图的边缘将更多或更少地可见。
提示
如果你的应用程序隐藏、移动或动画化你的地图在屏幕上或屏幕外的外观,自动调整大小功能可能会导致地图中出现一些奇怪的行为。例如,如果地图是从右侧滑入的,鼠标滚轮和一些缩放功能可能会缩放到地图的中心左点,而不是地图的中心。当地图动画在屏幕上或屏幕下时,最好将自动调整大小设置为关闭。
底图
底图是一个可选的字符串值,它提供了放置数据的背景图像。ESRI 根据您如何想要呈现数据提供各种背景图像。所有 ESRI 底图都是在 Web Mercator 投影(WKID 102100)中发布的瓦片图像。为了最佳性能,添加到 ESRI 底图之上的图层应使用相同的投影。否则,ArcGIS Server 将不得不重新投影动态地图服务,如果空间参考不匹配,瓦片地图服务可能不会显示。您可以在以下代码片段中看到前一章中突出显示的示例:
var map = new Map("map", {
basemap: "national-geographic",
center: [-95, 45],
zoom: 3
});
ESRI 提供的背景图像选项包括:
-
street:ESRI 街道地图 -
satellite:卫星/航空摄影 -
hybrid:街道地图和卫星的组合 -
topo:带有高程和等高线的地形图 -
gray:浅灰色/简约背景 -
oceans:海洋和海底背景 -
national-geographic:国家地理主题 -
osm:OpenStreetMap 主题
解释中心、范围、缩放和比例
中心、范围、缩放和比例参数可以用来覆盖其他地图图层设置并设置在地图上显示的区域。中心可以是包含中心点经度和纬度的两个数字数组,也可以是 ESRI 点。范围描述了地图坐标中应显示地图的哪些边缘:右、左、上或下。如果存在瓦片图层,缩放指示地图应显示的显示级别,从最远到最近。比例告诉地图有多少个测量单位与现实中一个单位相关(例如,比例值为 500 表示地图上的 1 英寸代表现实中的 500 英寸)。
LODs
当地图加载图层时,细节级别(LODs)由地图中首先加载的瓦片图层定义。您可以在创建地图时通过传递包含级别(数字:0、1、2 等)、比例(数字:对于 1:500 的比例,您将使用 500)和相应分辨率(数字)的对象数组来覆盖这些 LODs。如果您想限制用户将地图缩放到视图之外的能力,或者如果您的一些瓦片数据具有比您的底图显示更精细的分辨率,这将很有帮助。
现在我们已经学会了如何调整地图对象的参数,让我们来看看它的不同部分。
属性
地图对象提供了一些属性,可以在映射应用程序中使用。其中一些是只读的,并告诉地图的状态。其他可以操作,并且更改可以显著影响地图。虽然属性有很多,但我们将查看其中的一些。
loaded
在您创建地图对象后,直到地图至少有一个图层加载,您都不能对地图执行某些操作。当地图加载其第一个图层时,它将加载属性设置为 true。然后您可以操作地图,平移,搜索图层,并修改其行为。
常见的编程错误是在对地图进行操作之前,例如添加来自查询的搜索结果图形之前,没有测试地图是否已加载。如果你拥有极快的互联网连接,并且你的 ArcGIS 服务器就在你身边,地图可能在你添加图层后立即加载。但是,对于服务器室外的互联网用户来说,地图可能不会那么快加载。在继续之前最好检查加载属性,如下面的示例所示:
if (map.loaded) {
doSomethingwithMap(map);
} else {
map.on("load", doSomethingwithMap);
}
layerIds
地图对象保留了一个引用列表,指向添加到其中的图层。地图图层引用存储在一个名为 layerIds 的字符串数组中。列表中的第一个 layerId 指的是堆栈中最底层的图层,而列表中的最后一个字符串 id 指的是地图上可见的最顶层。如果你需要搜索列表中的特定图层,或者需要对所有加载到地图中的图层执行某些操作,这会很有用。
spatialReference
地图的 spatialReference 属性指的是用于在平面上表示圆形地球的数学方程式的长列表。这个属性通常(但不总是)与地图以及包含在其中的图层和图形共享。我们将在 几何空间参考 部分深入研究空间参考。
方法
地图对象具有许多有用的方法,这些方法是绑定到地图对象的功能。这些方法可以帮助你向地图添加项目,移动,缩放,以及操纵地图行为。让我们看看其中的一些。
添加、删除和检索图层
根据需要可以向地图中添加和删除图层。如果你想一次添加一个地图图层,可以调用地图的 addLayer() 方法。要一次添加多个图层,你必须将这些图层放入一个数组中,并调用地图的 addLayers() 方法。可以使用 removeLayer() 方法删除地图图层。
检索图层就像调用地图的 getLayer() 方法一样简单。getLayer() 方法传递一个图层 id 字符串。有效的 id 列表可以在地图的 layerIds 数组中找到,或者在地图的 GraphicsLayerId 属性中。如果没有找到与 getLayer() 参数匹配的图层,该方法返回 null。
在地图上移动
地图包含用于导航地图的几个方法。有些影响地图上的位置,而有些影响用户缩放的程度。以下是用于操纵你在地图上位置的最常用方法的列表:
-
setZoom(): 这将设置地图的缩放级别,使地图进一步放大或缩小。 -
setScale(): 与setZoom()类似,但试图将地图的比例尺更改为传递的值。 -
setLevel(): 与setZoom()类似,但仅在具有瓦片服务的地图上工作。根据传递给函数的整数,地图将缩放到相应的缩放级别,如果地图有瓦片图层。 -
setExtent():当传递一个边界框时,此消息将尝试将地图的边界设置为与边界框匹配。这会影响缩放级别和位置。 -
centerAndZoom():接受一个中心点和缩放级别,然后将地图中心定位在该点上,并尝试缩放到该级别。这会影响位置和缩放级别。 -
centerAt():当此函数接受一个点时,它尝试平移地图,使该点位于地图中心。
注意
请注意,对于 setExtent()、centerAndZoom() 和 centerAt() 方法,点和边界框需要与地图相同的坐标系。
更新设置
有许多地图导航操作可以通过地图方法启用或禁用。你可以控制用户点击、双击或 shift 双击地图时会发生什么。你可以启用或禁用使用鼠标进行平移,以及使用滚轮进行滚动。你甚至可以禁用或启用地图的导航事件。有时你可能想要禁用导航。例如,如果你的地图是通过点击链接进行导航的,你可能不希望用户在地图上走得太远。
记住,直到地图加载完成之前,这些设置都不能被修改。但如果地图尚未加载,我们该如何知道它何时加载呢?为了找到答案,我们将探讨大多数 JavaScript 库共有的一个特性——事件。
事件
在使用 JavaScript 的异步编程时,事件非常重要。地图对象在执行某些重要更改或完成某些任务之前、期间和之后会触发多个事件。这些事件可以通过地图的内置 on() 函数进行监听。让我们看看你将需要处理的一些更常见的事件:
-
load:在地图加载其第一层后触发的事件。 -
click:当用户点击地图时触发。在其它事件项中,它返回用户点击的点作为事件对象的mapPoint属性。 -
extent-change:当地图改变位置或缩放级别时触发。这两个因素都会影响地图范围的计算。 -
layer-add:当任何新的图层被添加到地图上时触发。 -
layers-add-result:在通过addLayers()方法加载多个图层后触发。 -
mouse-over:当鼠标移动到地图上时触发。 -
update-start:在加载图层之前触发此事件,无论是通过添加新图层,还是通过在地图上移动并触发地图下载更多地图数据。 -
update-end:在地图从其地图服务加载数据后触发此事件。
图层
没有数据来展示的地图是不完整的。数据通过图层添加到地图中。图层指的是包含地理、符号甚至表格数据的数据源。ArcGIS API for JavaScript 包含多个模块,用于解释不同类型的常见地理数据源。一些图层模块与 ArcGIS Server 提供的数据一起工作,而其他模块可以显示来自其他来源的数据,例如 KML、Web 地图服务(WMS)和 CSV 文件。
常见的 ArcGIS Server 层
ArcGIS Server 通过地图服务提供地图层数据。地图服务提供从 ArcMap 地图文档(.mxd)发布的数据。视觉地图数据、图例和底层数据表通过 ArcGIS Server 提供,并且当图层数据源被加载到地图中时,可以通过浏览器进行消费。不同的图层类型具有不同的功能。我们将在以下部分中回顾这些图层。
ArcGISDynamicMapServiceLayer
ArcGISDynamicMapServiceLayer是通过 ArcGIS Server 发布的地图的默认类型。它是一个动态地图,这意味着其内容在地图刷新时更新。因为它具有动态性,数据也可以重新投影以适应不在同一空间参考系中的另一个地图层。
require(["esri/map", "esri/layers/ArcGISDynamicMapServiceLayer"],
function (Map, ArcGISDynamicMapServiceLayer) {
…
var map = new Map("mapdiv", {…});
var layer = new ArcGISDynamicMapServiceLayer ("http://serv.er/arcgis/rest/services/MyDynamicLayer/mapserver");
map.addLayer(layer);
});
因为ArcGISDynamicMapServiceLayer中的数据是动态的,所以每当地图从不同的位置和缩放级别查看数据时,ArcGIS Server 都会重新绘制该层的外观。如果这个层包含大量的标签、注释、复杂的图形或栅格数据(例如航空影像),ArcGIS Server 将需要更多的系统资源来渲染地图层。这会导致性能明显变慢。对于那种类型的数据,建议使用其他服务,如瓦片地图服务。
ArcGISTiledMapServiceLayer
ArcGIS Server 允许地图服务进行瓦片化。ArcGISTiledMapServiceLayer在其内容在地图服务发布时已经绘制成图像,这些图像以定义的缩放比例存储在硬盘上,以便快速检索。这些预先渲染的瓦片可以快速且无需服务器大量努力地提供服务。可以在同一台机器上运行更多的地图服务,而对性能的影响很小。以下代码片段提供了一个示例:
require(["esri/map", "esri/layers/ArcGISTiledMapServiceLayer"],
function (Map, ArcGISTiledMapServiceLayer) {
…
var map = new Map("mapdiv", {…});
var layer = new ArcGISTiledMapServiceLayer ("http://serv.er/arcgis/rest/services/MyTiledLayer/mapserver");
map.addLayer(layer);
});
瓦片地图服务有一些缺点。首先,通过瓦片地图服务显示的数据在瓦片重建之前不会改变。如果数据中的公园边界发生变化,但瓦片没有重建,服务仍然会显示旧的边界。其次,瓦片地图服务将地图限制在特定的缩放比例上,没有简单的方法在缩放比例之间查看地图。第三,如果另一个瓦片地图堆叠在第一个瓦片之上,它不仅必须在相同的空间参考中,而且缩放比例必须在重叠的比例范围内完全重叠。这意味着使用 1:500 和 1:1000 比例构建的地图可以与使用 1:1000 和 1:2000 比例构建的地图一起加载,但比例 1:750、1:1500 和 1:3000 的瓦片层将不会被看到。
小贴士
您不能将动态地图服务层作为 ArcGISTiledMapServiceLayer 来加载,否则会抛出错误。然而,可以将瓦片地图服务层加载为 ArcGISDynamicMapServiceLayer。在缩放和平移时,您会失去性能,但它可以帮助您查看瓦片比例之间的比例。
我们已经看到了提供图像数据的 ArcGIS 层。让我们将注意力转向处理更多矢量数据的层。
图形层和特征层
有时候您不想与地图的代表性图像一起工作。有时候您想直接处理您数据的外形。这就是 GraphicsLayers 和 FeatureLayers 发挥作用的地方。这些层直接在您的浏览器中渲染矢量图形。您可以点击它们、修改它们,甚至可以通过它们将数据添加到 ArcGIS 服务中。让我们更仔细地看看它们。
图形层
GraphicsLayers 提供了一种简单的方法来将自定义矢量图形添加到您的地图中。您可以通过对那些图形调用 add() 和 remove() 方法来管理图形。您还可以通过搜索 GraphicsLayer.graphics 数组来搜索地图上的图形列表。
作为旁注,地图对象有一个内置的 GraphicsLayer。您可以通过 map.graphics 属性访问该 GraphicsLayer。
特征层
FeatureLayers 是基于 GraphicsLayer 构建的专用层,提供了更多的功能。它们具有 GraphicsLayer 的自定义矢量图形,并且还有一个 ArcGIS 服务器数据源来填充数据。您可以通过位置或属性查询数据,使用自定义图形将其添加到地图中,并控制弹出内容和外貌。您甚至可以在服务器上编辑数据。
与可以同时混合多个层的 ArcGIS Tiled 和动态层不同,每个 FeatureLayer 只能与一个地图服务层一起工作。FeatureLayers 可以与地图服务中的单个层一起工作,或者称为 Feature Services 的特殊可编辑地图服务。Feature Services 提供有关地图层的更多详细信息,包括查询结果的端点。如果 ArcGIS Server 配置为允许编辑特征,并且地图服务已发布以允许编辑,则使用这些服务的 FeatureLayers 将允许您修改形状和表格数据。
因为 FeatureLayers 从 ArcGIS Server 查询数据,它们返回的特征数量有限。通常,ArcGIS Server 会根据版本限制查询返回的结果数量为一千或两千。可以通过更改服务器设置来显示更多结果,但地图上更多的 FeatureLayer 图形意味着消耗更多的内存,应用程序可能会变慢且无响应。在使用 FeatureLayer 与您的数据时必须仔细考虑。
FeatureLayer 通过设置其模式来细化返回的特征数量。FeatureLayer 有三种主要的特征选择模式:
-
MODE_ONDEMAND:只有符合地图范围、时间范围和/或定义查询的特征才会被下载。这是与地图范围内可见特征交互的最常见方式。 -
MODE_SELECTION:只有通过交互地图(例如点击、触摸或查询)选定的特征才会被下载,并且它们的数据会在地图上显示。 -
MODE_SNAPSHOT:所有特征最初都会下载到内存中。这种方法适用于较小的数据集,并且需要较少的服务器调用。 -
MODE_AUTO:它会在MODE_ONDEMAND和MODE_SNAPSHOT之间自动选择,并且对于 ArcGIS API for JavaScript 来说是较新的功能,具体取决于数据的大小和内容。
当我们讨论编辑时,我们将在 第五章 编辑地图数据 中更详细地探讨 FeatureLayers。
其他图层类型
ArcGIS API for JavaScript 与通过 ArcGIS Server 发布的数据配合良好。但世界上并非所有地理数据都是通过 ArcGIS Server 发布的。API 有许多其他可以消费这些不同数据源的图层类型。以下是一些更常见的类型:
-
KMLLayer:使用 Keyhole Markup Language (KML) 编写的服务。KML 因 Google Earth 而闻名。该格式使用标记来存储和表示地理数据。许多服务发布 KML 数据,包括 国家海洋和大气管理局 (NOAA)。 -
CSVLayer:这是一个相对较新的功能,它将逗号分隔的文本文件转换为地图上的点。为了在地图上显示数据,CSVLayer预期一个纬度和经度列以显示十进制坐标。这个工具与 HTML5 的拖放 API 结合使用,可以使您将任何符合条件的数据映射到.csv文件中。 -
WMSLayer和WMTSLayer:使用 Open Geospatial Consortium (OGC)兼容标准发布的 Web 地图服务(WMS)和 Web 地图瓦片服务(WMTS)。注意
请注意,如果您仅使用 OGC Web 地图服务的 URL 加载图层,您需要使用代理(参见第一章,您的第一个映射应用程序)。这是因为模块首先在图层上请求
GetCapabilities以从地图服务中获取元数据。此请求需要一个代理。然而,如果您使用带有可选resourceInfo参数的WMSLayer加载,它会自动为您描述服务,因此不需要代理。 -
StreamLayer:一个显示来自 ArcGIS GeoEvent Processor 扩展的实时流数据的图层。StreamLayer利用 HTML5 WebSocket 的最新技术,从服务器向客户端提供实时更新。您可以看到对特定事件的 Twitter 响应,或带有跟踪设备的紧急响应车辆的实时位置。
注意
注意,大多数最新的现代浏览器支持 WebSocket 技术,但较旧的浏览器通常不支持。您可以通过访问 www.websocket.org/echo.html 来查看您的浏览器是否支持 WebSocket。
图形
图形对象代表在 web 地图上绘制的单个点、线或多边形要素。图形对象有四个主要部分:其几何形状、符号、属性和 infoTemplate。它们在 API 的许多部分中使用。如果您在地图上绘制某个东西,您就创建了一个图形。如果您查询地图服务以获取某个东西,它将返回一个图形列表。一些模块甚至接受图形列表作为其他函数的参数。
图形对象可以使用最多四个可选参数来构建:
-
geometry:它描述了绘制在地图上的图形的形状 -
symbol:它描述了图形的颜色、粗细和影响图形外观的特征 -
attribute:一个包含与要素对应的表格数据的名称-值对的 JavaScript 对象 -
infoTemplate:它格式化地图的InfoWindow高亮显示图形属性时的外观
在接下来的几节中,我们将更详细地研究这些图形功能。
介绍几何对象
几何对象是一组坐标的集合,用于描述某物在世界中的位置。它们为我们寻找的数据赋予了形状。它们可以用于在地图上显示图形,或为查询提供空间数据,甚至用于地理处理服务。几何对象有五种基本类型:点、线、多边形、扩展范围和多点。任何形状都是基于这些基本类型构建的。让我们更深入地了解一下。
点
在 ArcGIS API for JavaScript 中,点是最简单的几何对象。它包含点的x和y坐标,以及点的空间参考。点可以用x和y坐标以及空间参考来构造。它也可以用经度和纬度来定义。
多段线
多段线几何体是由一个或多个点坐标数组组成的集合。每个点坐标数组被称为一条路径,当绘制出来时看起来像一条线。可以在同一几何体中存储多个路径,从而产生多条线的视觉效果。单个点坐标存储为x和y值的数组。
多边形
多边形由一个或多个点的数组组成,这些数组会自己闭合。每个点的数组被称为一个环,环中的第一个点和最后一个点必须具有相同的坐标。由多个环组成的多边形被称为多部分多边形。
在控制多边形形状的许多属性和方法中,有两个有用的方法:.getCentroid()和.contains()。.getCentroid()返回一个大致位于多边形中间的点。计算质心位置的公式可以在维基百科的质心页面找到:(en.wikipedia.org/wiki/Centroid#Centroid_of_polygon)。.contains()方法接受一个点作为输入,并根据该点是否在多边形内部返回一个boolean值。
扩展范围
扩展范围是一个矩形多边形,它只使用四个数字来描述其形状。扩展范围由其最小x值(xmin)、最大x值(xmax)、最小y值(ymin)和最大y值(ymax)描述。在地图上查看时,扩展范围通常看起来像一个框。
扩展范围被 API 的许多不同部分使用。除了点之外的所有其他几何对象都有一个getExtent()方法,用于收集几何体的边界框。你可以通过设置地图的扩展范围来放大地图上的特定位置。扩展范围也用于在地图上识别事物时定义感兴趣的区域。
多点
有时候你需要将一组点显示在地图上。为此,多点就是你的答案。你可以使用addPoint()和removePoint()方法添加和删除点。你也可以使用getExtent()方法获取点覆盖的一般区域。它们对于收集区域内的随机点以及在地图上选择未连接的特征非常有用。
几何空间参考
除了几何位置坐标外,几何对象还包含有关对象空间参考的信息。空间参考描述了用于描述映射区域内地球的数学模型。空间参考不是包含所有那些公式和常数,而是通过一个已知 ID(WKID)或一个已知文本(WKT)来存储对这些公式的引用。
空间参考在地图上显示数据时起着至关重要的作用。瓦片地图服务层必须与地图具有相同的空间参考才能显示。动态地图服务层必须重新投影,这会增加服务器的时间和负载。图形也需要与地图具有相同的空间参考才能在地图上的正确位置显示。
符号和渲染器
ArcGIS API 提供了一些基本的图形工具来定义地图上要素的外观。如果你正在创建自己的自定义图形或修改现有的图形,API 的这部分内容会告诉你如何定义它们的符号。API 还提供了自定义渲染器,这有助于你根据渲染器应用的规则定义使用的符号。
简单符号
ArcGIS API 使用符号来定义图形的颜色、厚度和其他视觉特征,而与其形状无关。实际上,如果没有分配符号,图形将不会显示。API 定义了三个简单的符号来突出显示点、线和多边形。让我们看看简单符号。
SimpleLineSymbol
SimpleLineSymbol 定义了添加到地图上的线和折线图形的符号。从线符号开始可能看起来有些奇怪,但其他两个简单符号也使用了线符号。SimpleLineSymbol 的主要属性是其颜色、其宽度(以像素为单位)和其样式。样式包括实线、虚线、点线和这些的组合。
SimpleMarkerSymbol
SimpleMarkerSymbol 定义了地图上点和多点图形的符号。符号可以生成不同的形状,这取决于设置的样式。虽然此符号默认为圆形,但 SimpleMarkerSymbol 可以生成正方形、菱形、交叉和十字。此符号内还可以操纵的其他属性包括像素大小、其轮廓作为 SimpleLineSymbol,以及其颜色。
SimpleFillSymbol
SimpeFillSymbols 定义了地图上多边形图形的符号。SimpleFillSymbol 的样式反映了形状内部的显示方式。它可以填充(完全填充),为空(完全空),或者有水平、垂直、对角或交叉的线条。请注意,具有空样式符号的图形在中心不会响应用户点击。符号还可以通过 SimpleLineSymbol 轮廓进行修改,并且如果 SimpleFillSymbol 的样式是实心,其填充颜色也可以修改。
让我们进一步看看我们将用来定义这些符号的颜色。
esri/Color
esri/Color 模块允许你自定义添加到地图中的图形的颜色。esri/Color 模块是 dojo/_base/Color 模块的扩展,并添加了一些独特的工具,以不同的方式混合、创建和计算颜色格式。库的旧版本使用 dojo/_base/Color 模块来设置颜色,并且在当前版本中仍然可用。
esri/Color 模块的构造函数可以接受各种值。你可以分配常见的颜色名称,如 "blue" 或 "red"。你可以使用十六进制字符串(例如 #FF00FF 表示紫色),就像你在 css 颜色中找到的那样。你还可以使用三个或四个数字数组创建颜色。数组中的前三个数字分配红色、绿色和蓝色值,范围从 0 到 255([0, 0, 0] 是黑色,[255, 255, 255] 是白色)。数组中的第四个可选数字表示 alpha,或颜色的不透明度。这个范围内的值从 0.0 到 1.0,其中 1.0 是完全不透明,0.0 是完全透明。
esri/Color 模块在旧的 dojo/_base/Color 模块的基础上增加了一些有趣的增强功能。例如,你可以使用 blendColors() 方法混合两种现有的颜色。该方法接受两种颜色和一个介于 0.0 和 1.0 之间的十进制数,其中 0.5 是两种颜色的平均混合。这可能在投票地图上很有用,例如,绿色代表赞成,红色代表反对,投票区可以根据赞成或反对的百分比着色。
esri/Color 模块也有将颜色从一种格式转换为另一种格式的方法。例如,你可以点击一个要素,获取其图形颜色,并使用 toHex() 或 toCss() 获取颜色字符串。从那里,你可以将那种颜色应用到显示该图形属性的信息 <div> 的背景颜色上。
图片符号
如果图片对你来说比颜色更有吸引力,你可以在图形中使用图片符号。使用这些符号,你可以使用图片图标和自定义图形在你的图表上添加有趣且难忘的数据点。点和多边形可以使用图片符号进行符号化,分别使用 esri/symbols/PictureMarkerSymbol 和 esri/symbols/PictureFillSymbol 模块。
PictureMarkerSymbol 模块为地图上的点添加简单的图片图形。它通常使用三个参数构建,一个指向图像文件(如 .jpg、.png 或 .gif)的 URL,以及像素宽度和高度。
PictureFillSymbol 模块使用重复的图形填充多边形的内部内容。PictureFillSymbol 接受四个参数。第一个是一个指向图像文件的 URL 字符串。第二个输入是一个线符号,如 esri/symbols/SimpleLineSymbol。最后两个参数是图像的像素宽度和高度。
渲染器
有时候,你不想逐个分配图形符号。如果你知道你要添加的图形类型以及它们应该提前看起来如何,提前分配符号是有意义的。这就是渲染器发挥作用的地方。可以将渲染器分配给 GraphicsLayers,作为为其中所有图形分配共同样式的一种方式。渲染器依赖于一个图形层接受一种几何类型(例如,所有多边形)。
随着 ArcGIS API 的成熟,它已经向其库中添加了许多不同的渲染器。我们将探讨三个常见的:SimpleRenderer、UniqueValueRenderer 和 ClassBreakRenderer。每个都有其合适的用例。
SimpleRenderer
SimpleRenderer 是渲染器中最简单的一种,因为它只分配一个符号。它接受一个默认符号,并假设你将只插入与该符号相同类型的图形。与其他渲染器一样,如果你将 SimpleRenderer 分配给 GraphicsLayer,你可以在不分配符号的情况下向 GraphicsLayer 添加图形对象。让我们看看一个代码片段,展示如何创建并应用 SimpleRenderer 到地图的图形层:
require(["esri/map", "esri/renderers/SimpleRenderer",
"esri/symbols/SimpleLineSymbol", "esri/Color",
"dojo/domReady!"],
function (Map, SimpleRenderer, LineSymbol, Color) {
…
Var map = new Map("map", {basemap: "OSM"});
var symbol = new LineSymbol().setColor(new Color("#55aadd"));
var renderer = new SimpleRenderer(symbol);
…
map.graphics.setRenderer(renderer);
});
自 API 的 3.7 版本以来,SimpleRenderer 已得到扩展。可以根据字段中预期值的一个范围以不同的方式修改符号。例如,你可以通过设置 SimpleRenderer 的 colorInfo 属性在两种颜色之间改变符号的颜色。你也可以通过设置其 proportionSymbolInfo 属性根据字段值改变点的大小。最后,你可以通过设置其 rotationInfo 属性根据字段值旋转符号。
唯一值渲染器
有时候,你希望根据图形的某个属性显示不同的符号。你可以使用唯一值渲染器来实现这一点。可以突出显示特定的唯一值,而列表外的值可以分配一个默认符号。
唯一值渲染器的构造函数接受一个默认符号,以及最多三个不同的字段来比较值。如果使用多个字段,则需要一个字段分隔符字符串。唯一值和相关的符号可以作为对象加载到渲染器的 info 参数中。该 info 对象需要以下内容:
-
唯一值(
value) -
与该值关联的符号(
symbol) -
符号标签(
label) -
唯一值的描述(
description)
类别断点渲染器
有时候,你希望根据图形在特定值范围内的位置对它们进行分类,例如人口或国内生产总值(GDP)。类别断点渲染器可以帮助你以视觉方式组织图形。此渲染器查看图形中的特定字段值,并找出图形的字段值适合哪个类别断点范围。然后,它将该符号分配给该图形。
当构建类间隔渲染器时,您分配一个默认符号,以及将被评估以修改符号的字段。类间隔通过渲染器的信息属性分配。信息数组接受一个包含以下内容的 JavaScript 对象:
-
类间隔的最小值(
minValue) -
类间隔的最大值(
maxValue) -
符号(
symbol) -
描述间隔的标签(
label) -
类间隔的描述(
description)
InfoTemplates
InfoTemplates,正如我们在第一章您的第一个映射应用程序中提到的,描述了您希望在弹出窗口中显示数据的方式。InfoTemplate可以使用两个字符串构建,即标题和内容。标题和内容字符串代表 HTML 模板,您可以在模板中替换图形属性。对于图形的infoTemplate,如果您想显示图形的总属性,您可以在模板中插入值,使用${total},这显示了插入到替换字符串${}括号内的字段名。
如果您想在弹出窗口中显示包含图形属性中所有名称/值对的弹出窗口,您可以在内容值中使用通配符${*},如下所示:
graphic.infoTemplate = new InfoTemplate("Attributes", "${*}");
对于内容,您可以在字符串中添加 HTML 内容,包括表格、列表、图像和链接。例如,您可能使用以下内容来描述海洋栖息地:
graphic.infoTemplate = new InfoTemplate("Marine Habitat
(${name})", "<table><tbody><tr><th>Type:
</th><td>${type}</td></tr><tr><th>Ocean
zone:</th><td>${zone}</td></tr><tr><td colspan='2'><img
src='${imageSrc}' alt='${name}' /></td></tr></tbody></table>");
在前面的示例中,我们将图形的名称属性添加到标题中,并创建了一个两列的表格,显示图形的类型、图形所在的区域以及与地图图形连接的两列宽图像。
工具栏
ArcGIS API for JavaScript 使用工具栏来处理转换光标的控件。API 有三个不同的工具栏:一个用于地图导航,一个用于绘制图形,一个用于编辑这些图形。这些工具可以通过activate()方法打开,该方法包括描述您想要使用哪个工具的参数。可以使用deactivate()方法关闭工具;让我们更详细地看看这些工具。
导航工具栏
导航工具栏处理高级地图导航。默认情况下,地图提供缩放滑块和在地图上平移的能力。通过激活导航工具栏的缩放/缩小功能,用户可以绘制一个框定的范围,地图将在此范围内放大或缩小,与缩放框相对于地图当前范围的缩放比例相同。一旦激活,此缩放/缩小框始终存在,因此您必须开发某种关闭它的方法,或者让用户激活平移工具,这将禁用缩放/缩小。
导航控件还有三个可以激活的操作。当从导航工具栏调用zoomToFullExtent()函数时,会触发地图缩放到其原始地图范围。工具栏还记录了用户进行了多少次缩放、缩放和平移操作。通过激活zoomToPreviousExtent()函数,地图范围会回到之前的范围。如果地图回到之前的范围,地图也可以缩放到未来的位置。使用zoomToNextExtent()函数,用户可以撤销他们在查看之前范围时所做的缩放范围更改。
绘图工具栏
绘图工具栏允许用户在地图上绘制以提供输入。根据激活了哪个绘图工具,用户可以在地图上绘制一个形状,并且任何附加到draw-end事件的监听器都会被调用,运行一个接收用户绘制的形状的函数。如果需要,可以给事件监听器附加多个draw-end事件。常见的draw-end事件可能包括将绘图添加到地图、测量工具栏绘制的形状,或使用绘制的形状进行查询。这些工具还会修改地图的工具提示,提供有关如何使用它们的说明。
ArcGIS API 提供了在地图上绘制时使用的多种形状。可以通过简单的绘图工具常量(如点、折线和多边形)激活工具栏,这些常量通过在地图上点击每个点并双击来完成激活。折线和多边形有手绘版本,允许你点击并拖动鼠标来绘制连续的线条或形状,并释放鼠标以停止绘制。API 还包括用于绘制常见形状(如三角形、矩形、圆形、椭圆形和箭头)的绘图工具,仅举几例。
小贴士
不同的工具栏之间互不感知。如果你不小心,你可能会同时激活绘图和导航工具,并且它们的行为会同时发生。你可能会为工具绘制一个形状,并因为导航工具没有被要求停用而放大到该区域。提前计划好如何在启用另一个事件时禁用其中一个事件。
编辑工具栏
当你在地图上绘制某物并犯了一个错误时会发生什么?你如何纠正地图上的图形?编辑工具栏允许你更改地图图形的形状、大小和方向。它还适用于文本符号,允许你更改地图上文本图形的文本。
当在某个功能上激活编辑工具栏时,它允许你移动、旋转、缩放或更改该功能的顶点。移动功能让你可以抓取功能并将其放置到你想要的位置。旋转功能提供了一个点击并拖动以重新定位图形的点。缩放功能允许用户使图形变大或变小。对于编辑顶点,用户可以在地图上拖动点,右键点击“删除”点,并在“幽灵点”之间点击以在地图上添加新的顶点。
请注意,虽然编辑工具栏允许用户更改地图上的图形,但它没有内置的功能让用户保存他们的更改。当在可编辑要素层上的图形上使用编辑工具栏时,你仍然需要一种方法来告诉要素层保存更改。图形可以在当前会话中编辑,而无需保存更改。
任务
正如我在上一章中提到的,某些应用程序项目需要资源密集型的过程,这些过程最好留给服务器处理。ArcGIS API 将这些操作称为任务。任务种类繁多。一些任务需要服务器进行计算和/或返回数据,例如几何服务和QueryTask。其他任务使用服务器生成自定义文档,如打印任务。还有一些任务不仅执行上述所有操作,还需要自定义扩展,这些扩展最初并非 ArcGIS Server 的一部分。让我们看看什么才是一个好的任务。
任务、参数和结果
正如我们在上一章中讨论的,一个任务由三个部分组成:任务构造函数、任务参数和任务结果。通常,只需要使用 AMD 加载任务构造函数和任务参数。当结果从服务器返回时,任务结果会自动生成。结果格式在 API 文档中显示,以向您展示在成功接收结果时如何访问所需的内容。与任务相关的常见事件顺序如下:
-
构建任务和任务参数。
-
任务参数被填充所需的信息
-
该任务通过任务参数执行服务器调用。
-
当浏览器收到来自服务器的结果时,结果将通过
回调函数或延迟值进行处理。
值得注意的是,当任务执行时,它们返回一个 Dojo 的Deferred对象。这些延迟结果可以被传递到另一个函数,以便在浏览器收到结果时异步运行。
常见任务
在任何 ArcGIS Server 安装中,都提供了许多常见的任务。许多任务依赖于已发布的地图服务或地理处理服务来执行。一些,如GeometryService,是 ArcGIS Server 内置的。让我们看看通过 API 可用的常见任务。
几何服务
GeometryService提供了多种操作几何数据的函数。通常,它作为 ArcGIS Server 的一部分发布。让我们看看可用的几何服务函数。
-
面积和长度: 查找多边形几何形状的面积和周长 -
自动完成: 通过填充它们之间的间隙来帮助创建与其他多边形相邻的多边形 -
缓冲区: 创建一个多边形,其边与源几何形状的距离或距离集 -
凸包: 创建包含所有输入几何形状所需的最小多边形形状 -
裁剪: 沿着辅助多段线将多段线或多边形分割 -
差集: 从一系列几何形状中减去与次要几何形状(可能是一个多边形)重叠的部分 -
距离: 查找几何形状之间的距离 -
简化: 绘制一个看起来相似但点数更少的形状 -
相交: 给定一系列几何形状和另一个几何形状,返回由第一个列表与第二个列表相交定义的几何形状 -
标注点: 在多边形内找到一个最佳位置放置标签的点 -
长度: 对于线和折线,找到从起点到终点的平面或大地距离 -
偏移: 给定一个几何形状和距离,如果距离为正,将在原始几何形状的右侧创建一个定义距离的几何形状,如果距离为负,则在几何形状的左侧创建 -
项目: 基于几何形状和新的空间参考,它返回在新空间参考中的原始几何形状 -
关系: 基于两组几何形状和指定的关系参数,返回一个配对列表,显示两个列表中的项目是如何相关的(例如:list2 中有多少在 list1 内) -
重塑: 给定一条线或一个多边形,以及一个次要线,它根据第二个几何形状改变第一个几何形状的形状,向原始图形添加或丢弃一些部分 -
简化: 给定一个复杂的、边交叉的多边形或线,它返回更简单的多边形,其中交叉部分被取消 -
TrimExtend: 给定一系列折线和一个用于修剪或扩展的折线,它切割列表中与第二个折线交叉的线,并在未达到次要折线的其他折线上进行扩展 -
并集: 给定一系列多边形,创建一个统一的几何形状,其中所有多边形在重叠的边缘上被溶解,形成一个单一的多边形
查询任务
QueryTask 提供了一种从地图服务中的要素收集形状和属性数据的方法。您可以从地图服务的单个图层请求数据,通过几何形状定义您的搜索区域,并通过属性查询限制您的结果。QueryTask 是通过指向地图服务中单个图层的 URL 参数构建的。从成功查询返回的结果数量受 ArcGIS Server 限制,通常为 1,000 或 2,000 个结果。
由于 QueryTask 的结果可能受 ArcGIS Server 限制,QueryTask 对象已扩展了多个方法来收集其搜索结果上的数据。以下是 QueryTask 可通过的一些方法:
-
execute(): 接受QueryTask参数,并根据这些参数请求要素。 -
executeForCount(): 接受相同的参数,并返回与查询匹配的地图服务图层中的结果数量。 -
executeForExtent(): 根据传入的参数,返回包含所有搜索结果的地理范围。请注意,这仅在托管要素服务上www.arcgis.com有效。 -
executeForIds(): 通过传入的参数,结果返回匹配搜索条件的要素的ObjectIds列表。与执行命令不同,在执行命令中结果数量受 ArcGIS Server 限制,而executeForIds()方法返回所有匹配搜索条件的ObjectIds。这意味着,如果有 30,000 个匹配搜索结果,此方法将返回 30,000 个ObjectIds,而不是服务器的 1,000 个限制。
可以使用 Query 模块(esri/tasks/query)创建 QueryTask 参数。如果你有 SQL 经验,你会看到 Query 参数与 SQL Select 语句之间的相似之处,同时也有一些 Query 参数更具体于 ArcGIS Server。以下是一些常见的参数:
-
where(字符串): 查询的合法where子句。 -
geometry(esri/geometry): 一个用于空间选择的形状。 -
returnGeometry(布尔值): 是否想要搜索结果要素的几何形状。将此设置为false仅返回属性数据。 -
outFields(字符串数组): 要检索搜索结果的字段列表。将 ["*"] 分配给outFields参数将返回所有字段,这相当于 SQL 语句SELECT * from。 -
outSpatialReference(esri/SpatialReference): 你想要几何形状绘制的空间参考。通常,地图的空间参考被分配给此值。 -
orderByFields(字符串数组): 按照字段值排序结果的列表。如果想要返回降序排列的值,请在字符串中添加DESC。
搜索结果以 FeatureSet 形式从 QueryTask 返回(esri/tasks/FeatureSet)。FeatureSet 包含关于返回结果的数据,包括结果的几何类型、空间参考、结果字段别名以及匹配搜索结果的要素图形列表。featureSet 甚至返回匹配查询的结果数量是否超过了 ArcGIS Server 能够提供的数据量。featureSet 中特征属性的特征数组可以符号化并直接添加到地图的图形层中以便查看和点击。
IdentifyTask
IdentifyTask 提供了一种快速从地图服务中检索数据以用于弹出窗口的方法。与仅查询地图服务中一个层的 QueryTask 不同,IdentifyTask 可以搜索地图服务中的所有层。虽然 QueryTask 有许多方法可以检索搜索数据,但 IdentifyTask 更为简化,只有一个 execute() 命令来检索服务器结果。
传递给 IdentifyTask 的参数被称为 IdentifyParameters。因为 IdentifyTasks 可以搜索多层,所以 IdentifyParameters 专注于通过几何形状进行搜索。要使用 where 子句进行搜索,还必须在参数中添加一个 layerDefinition 参数。
IdentifyTask 的结果,称为 IdentifyResults,与 FeatureSet 有所不同。首先,IdentifyResults 以包含结果图形的特征参数的对象数组的形式返回,而 FeatureSet 是一个包含其特征参数中图形数组的单个对象。其次,IdentifyResults 不像 FeatureSet 那样包含字段名称和别名列表,因为 IdentifyResult 的特征属性是名称/值对,其中名称键是别名。最后,IdentifyResults 返回它们检索到的层的 ID 和名称。这些属性使得 IdentifyTask 适合快速在点击地图时填充地图的弹出窗口。
FindTask
ArcGIS API 提供了另一个在地图上查找特征的工具,称为 FindTask。FindTask 接受一个文本字符串,并在包含该值的地图服务中搜索特征。在数据库方面,FindTask 会遍历地图服务中的每一层,在允许搜索的字段中搜索带有前后通配符的文本字符串。如果搜索字符串 "Doe",则 FindTask 会返回具有 "John Doe"、"Doe Trail" 或 "Fiddledee doe Studios" 名称值的特征,但不会返回包含 "Do exit" 的特征,因为有空格。
定位器
定位器提供基于街道地址数据的地址的近似位置,称为 地理编码。定位器还可以执行 反向地理编码,根据相同的街道数据为地图上的点提供近似地址。ESRI 在国家和世界范围内提供地理编码服务,但如果你有更准确的街道信息,你可以在 ArcGIS Server 上发布自己的地理编码服务。
对于地址定位,定位器接受一个包含单行输入或包含街道、城市、州和区域(邮编)参数的对象。接受 Locator.execute() 结果的 callback 函数应接受一个名为 AddressCandidates 的对象列表。这些 AddressCandidates 包括可能匹配的街道地址、地址的点位置以及从 0 到 100 的分数,其中 100 是完美匹配。如果没有返回结果,则表示提供的地址没有匹配项。
需要服务器扩展的任务
虽然 ArcGIS Server 包含了很多功能,但 ESRI 还提供了服务器扩展,或可选软件,这些软件通过 ArcGIS Server 执行特定任务。这些软件可以执行许多独特任务,从提供驾驶方向到筛选你所在地区的推文。在尝试使用它们之前,你需要知道你的服务器是否有一个或多个这些扩展。我们将查看其中之一,即路由任务。
路由
路由任务可以提供驾驶方向,计算最有效的路线,并绕过已知的道路障碍。路由任务需要 ArcGIS Server 的网络分析师扩展。对于路由参数,你可以在停止参数中添加一个FeatureSet,并用定义各种必须停车点的图形填充它。你也可以以同样的方式提供障碍物,这些障碍物会阻断某些道路。当路由任务执行时,它根据提供给服务器的街道数据返回最佳路线。
数字
ArcGIS API for JavaScript 还提供了 Dojo 小部件,通常称为小部件。许多这些小部件提供用户界面(UI),其中现有的 API 模块在后台工作。使用这些小部件可以通过提供广泛使用和测试过的 UI 组件来缩短开发时间。小部件文档还显示了用于样式化小部件的 CSS 类,以便开发人员和设计师可以根据页面主题重新设计它们。让我们看看更常用的几个小部件。
测量
测量数字小部件提供了一个工具,可以用来测量地图上位置的距离、面积和周长。它还可以获取地图上点的经纬度。该小部件使用绘图工具栏在地图上绘制形状。从那里,它向GeometryService发送请求。一旦小部件检索到其结果,它要么显示一个点的经纬度,显示在地图上绘制的线的长度,要么显示在地图上绘制的多边形区域的面积和周长。
打印
打印小部件提供了一个下拉控制,允许用户从预定义的打印模板和参数列表中选择。开发者通过将小部件配置为链接到 ArcGIS Server 打印服务以及预定义的打印参数列表来配置小部件。当用户从下拉选项中选择时,打印小部件中内置的printTask会触发一个带有相应打印参数的execute()方法。当printTask从服务器收到成功响应时,小部件提供了一个可以点击的链接来下载打印输出。
如果你需要更多细粒度的打印控制,这个数字小部件不适合你。这个工具不是为了处理所有打印参数的组合。如果你需要支持从 Tabloid ANSI B Landscape .pdf文档到 Letter ANSI A Portrait .gif图像,以及所有介于两者之间的内容,你应该考虑开发自己的工具。
书签
Bookmarks dijit 允许用户在地图上保存自定义区域。您可以在配置文件中预先分配书签区域,用户可以点击并缩放到这些区域。他们还可以将区域添加到列表中,编辑名称,并删除他们想要缩放到地图上的自定义区域。Bookmarks dijit 不会保存地图状态,这意味着之前开启的图层不会自动切换,地图上选定的图形可能不存在。
底图
ArcGIS API 提供了一些 dijit 用于更改您的底图。当数据与当前背景混合或看起来不正确时,这可能很有用。API 提供的第一个工具是 BasemapToggle,它允许您在两个底图之间切换。第二个是 BasemapGallery,它提供了更多选择。两者都提供了缩略图照片和描述底图的标题。
BasemapGallery 可以自定义以显示您自己的底图,或显示来自 ArcGIS Online 的底图。当构建 BasemapGallery 时,可以通过将 showArcGISBasemaps 选项设置为 true 来修改是否包含 ArcGIS Online 底图的选择。然而,请记住关于瓦片地图服务的规则。ArcGIS Online 底图位于 Web Mercator 辅助球面(WKID 102100),这与 Google 和 Bing 地图的投影相同。如果您的数据不在相同的投影中,您可能会遇到精度和/或缺失瓦片层的问题。
弹出窗口和 InfoWindows
弹出窗口和 InfoWindows 提供了一个小窗口来查看地图上特征的数据。有四种 infoWindows 的变体可用:在版本 3.4 中被取代的旧版本,一个较小的旧 infoWindow 版本称为 InfoWindowLite,当前默认的弹出控制,以及当前弹出窗口的轻量级移动版本。
当前弹出窗口暴露了您可以对其进行操作以自定义用户体验的多个元素。弹出窗口可以选择多个特征,并保留所选项目的图形内容在一个特征数组中。它还有一个分页控件,允许用户点击查看其他已选特征。它还通过 selectedIndex 参数跟踪哪个特征被选中。
编辑
编辑 dijit 提供了 UI 组件和工具,您可以使用它们来编辑地图上的特征。这些工具与可编辑的 FeatureLayers 一起工作,允许用户编辑形状、在地图上添加特征、更改属性,并将所有这些更改保存到服务器。让我们看看构成编辑工具的一些组件。
属性检查器
AttributeInspector 允许用户从 HTML 表单中编辑图形属性数据。检查器连接到 FeatureLayer 并显示所选特征之一的数据。检查器还提供了一个上一页和下一页按钮,用于搜索所选特征。AttributeInspector 还提供了一个删除按钮,用于删除所选特征。
检查器随后根据特征的必要条件构建一个表单。日期将会有日历,而域绑定字段将显示一个带有域选择下拉列表的 HTML 选择元素。用户可以输入和编辑数据,并且数据可以在服务器端保存。
模板选择器
TemplatePicker 提供了一个选择器,用于选择你将编辑的特征类型和子类型。当一个 featureLayer 作为可编辑的发布时,它必须通过 REST 服务公开符号和预设样式。然后 TemplatePicker 读取公开的符号和特征设置数据,以创建一个有效的可编辑选择网格。用户点击模板就像点击按钮一样,符号传递给某种类型的绘图工具。
一站式编辑器
ArcGIS API 提供了一个一站式 编辑器 dijit,以满足一些基本的编辑需求。该 dijit 集成了 TemplatePicker 以选择要绘制的特征类型和子类型。它还包含一个工具栏,包含各种工具,与地图上可编辑的特征相关。因此,当你选择一条绿色线时,工具将显示与线相关的绘图工具。
编辑器还在 map.infoWindow 中生成一个 AttributeInspector。你可以从弹出窗口中编辑字段值。如何实现保存更改取决于你。
编辑器还在工具栏中提供了撤销和重做按钮。如果你不小心删除了你想要保存的特征,你可以使用撤销按钮。如果你意识到你确实想要删除那个特征,你可以点击重做按钮。
现在你已经接触到了 ArcGIS API for JavaScript 的主要功能,你更有准备去使用这个 API 编写更多的代码。
摘要
在本章中,我们快速浏览了 ArcGIS API for JavaScript。我们熟悉了网站和 API 的组织结构。我们详细研究了地图对象,查看其所有部分。我们回顾了 API 的其他部分,包括图层、图形、工具栏、任务和 dijits。我们讨论了它们的实现方式以及一些用例。现在我们更了解 API 的各个部分,我们可以在下一章中使用这些组件来创建自定义应用程序和小部件。
第三章. Dojo 小部件系统
Esri 的开发者使用 Dojo 框架创建了 ArcGIS JavaScript API。Dojo 提供了大量工具、库和 UI 控件,这些控件可以在多个浏览器中工作。任何开发者都可以使用 Dojo 和 ArcGIS JavaScript API 创建具有良好协作的 UI 元素的自定义应用程序。此外,Dojo 还提供了开发自己的自定义小部件、库和控件的 AMD 工具。
在前面的章节中,我们已经回顾了 ArcGIS API for JavaScript,并使用该 API 编写了一个小型应用程序。我们甚至将 AMD 的基本原理整合到了一个单页应用程序中。到目前为止我们所做的一切对于一个小型应用程序来说都非常适用。
但是当应用程序变得更大时会发生什么?我们是否要实现一个单独的脚本以加载更大应用程序所需的全部组件?我们将如何扩展我们网站的功能?如果我们更新了库而某些东西出了问题怎么办?我们是否要在单个 JavaScript 文件中的数千行代码中查找需要更改的部分?
在本章中,我们将利用 Dojo 框架为我们的应用程序创建一个自定义小部件。通过这个过程,我们将涵盖以下内容:
-
Dojo 框架的背景
-
dojo、dijit和dojox模块包提供的内容 -
如何创建和使用我们自己的自定义模块
-
如何创建小部件(具有 UI 组件的模块),以及如何扩展它们
Dojo 框架的简要历史
Dojo 框架始于 2004 年,最初在 Informatica 进行工作。Alex Russell、David Schontzler 和 Dylan Schiemann 为该项目贡献了第一行代码。随着代码工作的继续,其他开发者被引入并提供了对框架方向的反馈。项目变得如此之大,以至于创始人创建了 Dojo 基金会来监督代码库及其知识产权。从那时起,超过 60 名开发者为框架做出了贡献,IBM 和 SitePen 等公司至今仍在使用它。更多信息,请访问 dojotoolkit.org/reference-guide/1.10/quickstart/introduction/history.html。
那么,是什么让 Dojo 成为一个框架,而不是一个库呢?当这个问题被提出给 Stack Overflow 的人时 (stackoverflow.com/questions/3057526/framework-vs-toolkit-vs-library),最一致的答案集中在控制反转上。当我们使用库中的工具时,我们的代码控制逻辑和活动的流程。当我们计算一个值时,我们通过我们的代码在需要该值的所有位置更新该值。然而,在框架中,框架控制应用程序的行为。当框架中的值被更新时,框架会在网页上绑定该值的任何位置更新该值。
Dojo 框架提供了一些基于 HTML 的控件,我们可以加载和使用。CSS 外观和 JavaScript 行为的大部分内容都预先集成到控件中。在初始设置之后,我们的代码指示框架在特定控件事件发生时将数据发送到何处。当我们的函数被调用时,我们没有控制权,只有调用之后发生的事情。
注意
如果您想了解更多关于框架和控制反转的信息,Martin Fowler 在他的博客上提供了对这个主题的良好解释,请访问 martinfowler.com/bliki/InversionOfControl.html。
介绍 dojo、dijit 和 dojox
对 Dojo 框架的组织非常用心。当 Dojo 集成 AMD 模块风格时,许多对象、方法和属性都被重新组织到逻辑文件夹和模块中。Dojo 被分解为三个主要包:dojo、dijit 和 dojox。让我们了解这三个包为框架带来了什么。
The dojo package
dojo 包提供了加载、运行和卸载我们应用程序中各种模块所需的大部分基本功能。这些模块提供了跨多个浏览器(包括令人讨厌的旧版 Internet Explorer)工作的函数和工具。例如,开发者不需要通过尝试 addEventListener()(用于 Chrome)和 attachEvent()(用于旧版 IE)来处理事件,因为 dojo/on 在幕后处理这些。有了这个框架,您可以摆脱所有的浏览器黑客技术,专注于创建一个好的应用程序。
dojo 包不包含小部件,但它包含操作小部件内事物所需的工具。您需要处理鼠标点击和触摸事件吗?请使用 dojo/mouse 和 dojo/touch 模块。您需要一个类似 jQuery 的选择器来选择您的 HTML 吗?请加载 dojo/query 模块。dojo 包提供了用于 HTML DOM 操作、数组、AJAX、日期、事件和 i18n 的模块,仅举几例。
The dijit package
dijit 包提供了与 dojo 包良好集成的视觉元素(称为 dijits)。使用 dijit 包创建的元素已在多个浏览器上进行了测试。它们基于库中加载的 CSS 样式表提供了一致的用户界面。
由于 Dojo 框架被许多公司和开发者使用和贡献,因此有大量的用户控件可供选择。无论你是在创建提交表单、事件日历还是执行仪表板,你都可以以适合你需求的方式组合这个包中的 dijits。一些更受欢迎的包包括:
-
dijit/Calendar:这提供了一个在桌面和平板电脑上都能工作的交互式 HTML 日历控件。 -
dijit/form:一个包含按钮、复选框、下拉菜单、文本框和滑块等样式化表单元素的包。这些表单元素在旧版和新版浏览器中具有一致的样式。 -
dijit/layout:一个包含处理布局的控件的包。你可以创建简单的容器、标签容器、手风琴容器,甚至可以控制其他容器位置的容器。 -
dijit/Tree:一个创建可折叠树菜单的模块,例如,可以用来显示文件夹布局。
dijit 包不仅包含用户控件,还提供了创建自定义 dijits 所需的工具。使用 dijit/_WidgetBase 模块和混合,你可以将 HTML 元素和现有的 dijits 集成到自己的自定义 dijit 中。与 dijit 组件一起工作可以为用户提供在整个应用程序中一致的使用体验。
dojox 包
根据关于 Dojo 框架的文档(dojotoolkit.org/reference-guide/1.10/dojox/info.html),dojox 为 Dojo 提供了扩展。这部分库处理更多实验性功能、用户界面和测试功能。由于广泛的使用,库的许多部分已经成熟,而其他部分已被弃用,并且不再处于积极开发中。
dojox 包中的一个有用子包是 dojox/mobile。Dojox/mobile 包提供了可用于移动应用程序的 UI 元素和控制。它们已在各种移动浏览器上进行了测试,其样式甚至可以模仿原生智能手机和平板电脑应用程序的样式。
注意
如需了解 Dojo 框架中 dojo、dijit 和 dojox 包的更多信息,您可以访问教程文档:dojotoolkit.org/documentation/tutorials/1.10/beyond_dojo/。
DojoConfig 包
使用内置的 Dojo 包是很好,但关于创建自己的自定义包怎么办?你可以在本地文件夹中创建自定义包,并通过dojoConfig对象引用它们。在dojoConfig对象中,你可以添加一个包含 JavaScript 对象数组的packages参数。这些包对象应该包含一个name属性,它是您包的引用,以及一个location属性,它是文件夹位置的引用。以下是一个包含对自定义包引用的dojoConfig对象示例:
<script>
dojoConfig = {
async: true,
isDebug: true,
packages: [
{
name: "myapp",
location: location.pathname.replace(/\/[^/]+$/, '') + "/myapp"
}
]
};
</script>
在前面的示例中,引用了myapp包,并且该包的所有文件都被加载到当前页面的myapp文件夹下。因此,如果这个页面在http://www.example.com/testmap/显示,那么myapp包可以在http://www.example.com/testmap/myapp找到。当在您的myapp包中引用Something模块时,您将按如下方式加载模块:
require(["myapp/Something", "dojo/domReady!"], function (Something) { … });
定义您的部件
使用 Dojo 的 AMD 风格,有两种主要方式来使用 AMD 组件。使用require()函数只播放一次脚本,然后完成。但如果你想要创建一个可以重复使用的模块,你将想要define()该模块。define()函数创建一个或多个自定义模块,供应用程序使用。
define()函数接受一个单一参数,它可以是任何 JavaScript 对象。即使是define("Hello World!")也是一个有效的模块定义,尽管不是那么有用。你可以通过传递执行应用程序任务的函数或对象构造函数来创建更有用的模块。请参阅以下示例:
define(function () {
var mysteryNumber = Math.floor((Math.random() * 10) + 1);
return {
guess: function (num) {
if (num === mysteryNumber) {
alert("You guessed the mystery number!");
} else if (num < mysteryNumber) {
alert("Guess higher.");
} else {
alert("Guess lower.");
}
}
};
});
在前面的示例中,该模块从一到十中随机选择一个数字,然后返回一个带有guess()方法的模块,该方法接受一个数值。调用guess()方法时,它会向用户提示猜测是否正确。
声明模块
从更传统的面向对象(OO)语言进入 JavaScript 的开发者可能难以接受这种语言基于原型的继承机制。在大多数面向对象的语言中,类是在编译时定义的,有时会从其他基类继承属性和方法。在 JavaScript 对象中,类类型的对象存储在对象的原型中,这些原型为多个相关对象持有相同的共享属性和方法。
当这些开发者使用dojo/_base/declare模块时,Dojo 可以帮助他们更容易地过渡。该模块创建基于 Dojo 的类,这些类可以被其他应用程序使用。在幕后,它将类似类的 JavaScript 对象转换为基于原型的构造函数。此模块与define()函数很好地配合,用于创建自定义的dojo模块,如下所示:
define(["dojo/_base/declare", …], function (declare, …) {
return declare(null, {…});
});
declare() 函数接受三个参数:类名、父类和类属性对象。类名是一个可选的字符串,可以用来引用类。declare() 函数会将该名称转换为全局变量,以便当你在 data-dojo-type 属性中引用 dijit 包时,dojo/parser 可以将其写入 HTML。如果你不打算使用 dojo/parser 将你的小部件写入 HTML,强烈建议你不要使用第一个参数。
通过 Dojo 进行类继承
declare() 函数的第二个参数指的是父类。定义父类有三种可能的方式。如果你创建的类没有父类,则称其没有继承。在这种情况下,父类参数为 null,如下面的语句所示:
define(["dojo/_base/declare", …], function (declare, …) {
return declare(null, {…});
});
在第二种场景中,你创建的类有一个父类。这个父类可以是另一个 AMD 模块,或者另一个已声明的项。以下示例通过扩展 dijit/form/Button 模块来展示这一点:
define(["dojo/_base/declare", "dijit/form/Button", …],
function (declare, Button, …) {
return declare(Button, {…});
}
);
第三种可能的情况涉及多重继承,其中你的类从多个类继承属性和方法。为此,父类参数也将接受一个对象数组。数组中的第一个项目被认为是基类,它提供了主要的构造参数。列表中的其余项目被称为“混合”。它们不一定提供对象构造功能,但它们添加了属性和方法,这些属性和方法要么补充,要么覆盖现有的基类。以下示例展示了使用我们稍后将要讨论的几个库进行多重继承:
define(["dojo/_base/declare", "dijit/_WidgetBase", "dijit/_OnDijitClickMixin, "dijit/_TemplatedMixin"],
function (declare, _WidgetBase, _OnDijitClickMixin, _TemplatedMixin) {
return declare([_WidgetBase, _OnDijitClickMixin, _TemplatedMixin], {…});
});
处理类和继承
在 declare() 语句的最后一个参数中,开发者定义了类中包含的不同属性和方法。对象属性可以是字符串、数字、列表、对象或函数。请查阅以下代码示例:
declare("myApp/Fibonacci", null, {
list: [],
contructor: function () {
var len = this.list.length;
if (len < 2) {
this.myNumber = 1;
} else {
this.myNumber = this.list[len – 1] + this.list[len-2];
}
this.list.push(this.myNumber);
},
showNumber: function () { alert(this.myNumber); }
});
在使用 declare() 语句创建的对象中,你既有静态属性也有实例属性。静态属性是所有以相同方式创建的对象共享的属性。实例属性是创建的对象独有的属性。在类对象内部定义的属性被认为是静态的,但最初在构造函数或另一个方法中分配的每个属性都被认为是实例属性。
在前面的例子中,showNumber() 方法和 list 属性是静态的,而 myNumber 属性是一个实例。这意味着每个 myapp/Fibonacci 对象将共享 showNumber() 方法和 list 数组,但将具有独特的 myNumber 属性。例如,如果创建了五个 myapp/Fibonacci 对象,每个对象都应该包含列表值 [1, 1, 2, 3, 5]。向列表中添加一个新元素将添加到每个斐波那契列表中。myNumber 属性是在构造函数中创建的,因此每个对象都将具有该属性的独特值。
当从父类创建一个类时,它可以访问其父类的属性和方法。新类也可以通过在其构造函数中拥有同名的新方法来覆盖这些方法。例如,假设一个 HouseCat 对象继承自 Feline 对象,并且它们都有自己的 pounce() 方法版本,如果你调用 HouseCat.pounce(),它将只运行 HouseCat 中描述的方法,而不是 Feline 中的方法。如果你想在 HouseCat.pounce() 调用中运行 Feline.pounce() 方法,你可以在 HouseCat.pounce() 方法中添加 this.inherited(arguments),以显示你想要运行父类方法的时间。
与 Evented 模块一起工作
Evented 模块允许你的小部件发布事件。当你的模块以 Evented 作为父类声明时,它提供了一个 emit() 方法来广播事件已发生,以及一个 on() 方法来监听事件。在 ArcGIS API 中可以找到一个例子,那就是绘图工具栏。它不显示信息,但它有发布事件的必要工具。
emit() 方法接受两个参数。第一个是一个描述事件的字符串名称,例如 map-loaded 或 records-received。第二个是与事件一起创建和传递的对象。该对象可以是你在 JavaScript 中创建的任何内容,但请记住保持返回内容相似,这样监听事件发生的方法就不会错过它。
_WidgetBase 模块的概述
_WidgetBase 模块提供了创建 dijit 模块所需的基类。它本身不是一个小部件,但你可以很容易地在它上面构建一个小部件。使用 _WidgetBase 模块创建的小部件绑定到 HTML DOM 中的一个元素,可以通过其 domNode 属性来引用。
_WidgetBase 模块还引入了小部件的生命周期。生命周期指的是小部件的创建、构建、使用和销毁的方式。此模块加载以下在生命周期中发生的方法和事件:
-
constructor: 这个方法在创建小部件时被调用。无法访问模板,但可以分配值和访问数组。 -
混合到小部件实例中的参数:你传递给对象构造函数的参数,如按钮标签,被添加到小部件中,以覆盖任何现有的值。 -
postMixinProperties:这是渲染小部件 HTML 之前的一个步骤。如果您需要更改或纠正传递给参数的任何值,或者进行其他操作,这将是一个进行这些操作的好时机。 -
buildRendering:如果正在使用模板,这是在现有节点中添加 HTML 模板的地方。如果您正在使用_TemplatedMixin模块,模板字符串将被加载到浏览器中,渲染成 HTML,并插入到现有的domNode位置。绑定在 HTML 数据-* 属性中的事件也在这里分配。 -
Custom setters are called:如果您添加了自定义设置函数,这些函数将在此时被调用。 -
postCreate:现在 HTML 已经渲染,这个函数可以对其进行更多操作。但请注意,小部件的 HTML 可能尚未连接到文档。此外,如果这个小部件包含子小部件,它们可能尚未渲染。 -
startup:这个函数可以在所有解析和子小部件渲染完成后被调用。它通常用于小部件需要通过resize()方法调整大小时。 -
destroy:当这个小部件被拆解并移除时,无论是关闭应用程序还是调用此函数时,都会调用此函数。父类通常自行处理拆解事件,但如果您在销毁小部件之前需要执行任何独特操作,这将是一个扩展函数的好时机。
_WidgetBase 模块提供了专门的获取和设置函数,允许您在设置小部件属性时执行特定任务。您可以使用 get() 函数检索值,并使用 set() 函数设置这些属性。get() 和 set() 的第一个参数是属性的名称,而 set() 方法中的第二个参数是值。因此,如果您想设置小部件的 name,您将调用 widget.set("name", "New Name")。还有一个 watch() 方法,可以在通过 set() 改变值时执行函数。
与其他 _Mixins 一起工作
_WidgetBase 模块可能无法提供您应用程序所需的所有小部件功能。dijit 包为您的提供了混合,或 JavaScript 对象扩展,用于小部件。这些混合可以提供 HTML 模板、点击、触摸、聚焦以及无聚焦的事件处理,例如。使用正确的混合与您的应用程序可以节省大量的行为编码。让我们看看我们可能会使用的一些混合。
添加 _TemplatedMixin
_TemplatedMixin 模块允许模块用字符串模板或来自另一个来源的 HTML 替换其现有的 HTML 内容。小部件的模板字符串通过 templateString 属性分配。这允许小部件跳过可能复杂的 buildRendering 步骤。您可以在以下代码片段中看到一个 _TemplatedMixin 模块的调用示例:
require(["dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin"],
function (declare, _WidgetBase, _TemplatedMixin) {
declare("ShoutingWidget, [_WidgetBase, _TemplatedMixin], {
templateString: "<strong>I AM NOT SHOUTING!</strong>"
});
});
随着 templateString 属性的渲染,属性可以在模板和小部件之间来回传递。在 buildRendering 阶段,可以通过在组件中引用属性名并在其周围使用 ${} 包装器来将小部件属性写入模板。你还可以使用 HTML 数据属性分配节点引用并附加小部件事件。data-dojo-attach-point 属性允许小部件有一个命名属性,该属性连接到模板。data-dojo-attach-event 属性在小部件中附加一个当事件发生时被触发的 callback 方法。你可以查看以下示例:
<div class="${baseClass}">
<span data-dojo-attach-point="messageNode"></span>
<button data-dojo-attach-event="onclick:doSomething">
${label}
</button>
</div>
提示
开发者应该如何布局一个模板小部件?小部件的模板 HTML 应该全部包含在一个单独的 HTML 元素中。你可以使用 <div> 标签、<table> 标签,或者你计划将模板包围在内的元素标签。如果模板在基础层包含多个元素,小部件将无法渲染并抛出错误。
添加 _OnDijitClickMixin
_OnDijitClickMixin 模块允许你的模板中的元素能够“被点击”。这不仅适用于使用鼠标的点击事件,也适用于触摸和键盘事件。除了点击和触摸一个元素外,用户可以通过按制表符键直到元素被突出显示,然后按 Enter 键或空格键来激活它。
如果加载了 _OnDijitClickMixin 模块,开发者可以通过 data-dojo-attach-event 属性向 dijit 模板添加事件处理器。在此数据属性文本值中,添加 ondijitclick: 后跟你的点击事件处理器名称。你必须确保这个事件处理器指向一个有效的函数,否则整个小部件将无法加载。以下是一个使用 clickMe(event) {} 函数的 dijit 模板示例:
<div class="clickable" data-dojo-attach- event="ondijitclick:clickMe"> I like to be clicked.</div>
作为旁注,点击事件处理器中的函数应该准备好接受一个单次点击事件参数,没有其他。
添加 _FocusMixin
_FocusMixin 模块为你的小部件及其组件提供聚焦和失焦事件。例如,如果你有一个在聚焦时占用更多空间的小部件,你可以在对象中添加一个 _onFocus() 事件处理器,如下所示:
_onFocus: function () { domClass.add(this.domNode, "i-am-huge");}
另一方面,当你想让小部件缩小回正常大小时,你将添加一个 _onBlur() 事件处理器:
_onBlur: function() { domClass.remove(this.domNode, "i-am-huge");}
事件系统
JavaScript 中的事件系统是该语言的重要组成部分。JavaScript 被设计为响应浏览器中的事件和用户引起的事件。一个网站可能需要响应用户的点击,或服务器的 AJAX 响应。使用该语言,你可以将称为事件处理器的函数附加到页面和浏览器中某些元素上,以监听事件。
在 Dojo 框架中,使用 dojo/on 模块来监听事件。在分配事件监听器时,模块函数调用返回一个对象,您可以通过调用该对象的 remove() 方法来停止监听。此外,dojo/on 有一个 once() 方法,当事件发生时,它会触发事件一次,然后自动删除该事件。
小贴士
一些较老的 ArcGIS API 示例仍然使用 Dojo 的旧事件处理器,dojo.connect()。事件处理器将通过 dojo.connect() 进行附加,并通过 dojo.disconnect() 移除。目前,Dojo 基金会正在弃用 dojo.connect(),并在 Dojo 升级到 2.0 版本时将其从代码中删除。如果您正在维护旧代码,请开始将所有 dojo.connect() 迁移到 dojo/on。在使用 ArcGIS API 时,请注意事件名称和返回的结果。名称可能从驼峰式命名法变为以破折号分隔,虽然 dojo.connect() 可以在其回调中返回多个项目,但 dojo/on 只返回一个 JavaScript 对象。
事件是通过 emit() 创建的。此函数接受事件字符串名称和一个您希望传递给事件监听器的 JavaScript 对象。emit() 方法适用于使用 dojo/Evented 模块的小部件,以及使用 dijit/_WidgetBase 模块构建的小部件。它也通过 dojo/on 模块提供,但 dojo/on.emit() 在事件名称之前需要一个 HTML DOM。
现在我们已经掌握了 Dojo 框架,让我们用它来构建一些东西。
创建我们自己的小部件
现在我们已经了解了创建自定义小部件的基础,也就是说,为我们的网络应用创建 dijit,让我们将我们的知识付诸实践。在本章的这一部分,我们将把我们在 第一章 中编写的单页应用程序代码,您的第一个映射应用,转换为一个可以与任何地图一起使用的小部件。然后,我们将使用这个小部件在可以扩展以包含其他小部件的地图应用程序中。
从我们上次停止的地方继续…
我们最终收到了 Y2K 社会对我们在 第一章 中创建的人口普查地图的反馈。他们喜欢地图的工作方式,但觉得它需要更多的润色。在与一些小组成员会议后,以下是他们的反馈列表:
-
应用程序应该在顶部和底部有部分来显示标题和关于社会的简要信息
-
颜色应该是温暖和愉快的,例如黄色
-
摆脱人口普查图层数据的普通黑色线条
-
应用程序应该让我们能够做的不仅仅是查看人口普查数据。它应该让我们点击其他按钮来做其他事情
-
人口普查信息需要更加有组织和逻辑地分组在表格中
为了满足他们的要求,我们需要从 第一章 您的第一个映射应用程序 重新组织我们的应用程序。为了解决这个问题,我们需要执行以下操作:
-
我们将创建一个新的全页应用程序,包含页眉和页脚。
-
我们将修改应用程序的样式。
-
我们将使用自定义符号表示人口普查地图服务。
-
我们将在页眉中添加按钮以启动普查弹出窗口。
-
我们将把
infoTemplate数据移动到单独的 HTML 文件中。
我们应用程序的文件结构
为了提供一个不仅能显示人口普查数据的地图,我们将使用 Dojo 框架提供的工具。我们将使用 Dojo 的元素来布局应用程序,并将我们之前的普查地图工具转换为其自己的小部件。而不是将 HTML、CSS 和 JavaScript 混合在同一文件中的单页应用程序,我们将将这些文件分离成独立的组件。
让我们从组织项目文件夹开始。将样式和脚本分别放入不同的文件夹是一个好主意。在我们的项目文件夹中,我们将向项目文件夹添加两个子文件夹。我们将它们命名为 css 和 js,分别用于样式表和脚本文件。在 js 文件夹内,我们将添加另一个文件夹并命名为 templates。这是我们的小部件模板将放置的地方。
在主项目文件夹中,我们将创建一个名为 index.html 的 HTML 页面。接下来,在 css 文件夹中,我们将创建一个名为 y2k.css 的文件,这将是我们的应用程序样式表。在我们的 js 文件夹中,我们将创建两个 JavaScript 文件:一个名为 app.js 的用于应用程序,另一个名为 Census.js 的用于小部件。我们将在 js/templates 文件夹中为普查小部件(Census.html)创建模板文件,以及一些弹出窗口的模板。文件结构应类似于以下:

使用 Dojo 定义布局
为了处理应用程序的布局,我们将使用 dijit/layout 包内的几个模块。这个包包含许多具有样式外观和内置 UI 行为的 UI 容器,我们不需要重新发明。我们将添加布局元素并重新设计它们以满足客户的需求。让我们看看 dijit 包内找到的布局模块。
使用 BorderContainer 框架布局页面
BorderContainer 容器提供了 Dojo 中常用的布局工具。BorderContainer 容器是从 LayoutContainer 模块扩展而来的,它创建了一个填充整个浏览器窗口而不需要滚动的布局。LayoutContainer 模块(以及通过扩展的 BorderContainer 模块)允许使用区域属性放置内容元素。BorderContainer 容器添加了边框、间距和可调整大小的元素,称为分隔符,可以拖动以调整内容项的大小。
可以分配给 BorderContainer 容器内部的内容 dijits 的区域如下:
-
中心:将元素定位在页面中心,相对于其他项目。必须将此值分配给一个元素。
-
顶部:将元素定位在中心元素的上方。
-
底部:将元素定位在中心元素的下方。
-
右侧:将元素定位在中心元素的右侧。
-
左侧:将元素定位在中心元素的左侧。
-
leading:如果页面的
dir属性设置为 "ltr"(从左到右),则 leading 元素放置在中心元素的左侧。如果dir属性为 "rtl"(从右到左),则 leading 元素放置在右侧。 -
尾部:布局与 leading 元素相反,"ltr" 在右侧,"rtl" 在左侧。
BorderContainer 模块及其父模块 LayoutContainer 有一个设计参数会影响围绕中心元素的容器。当 design 参数设置为默认值 headline 时,顶部和底部面板占据容器的整个宽度。右侧、左侧、leading 和 trailing 面板缩小以适应顶部和底部面板之间。或者,您可以将 design 参数设置为 sidebar,侧面板将占据整个高度,而顶部和底部面板则被挤压在它们之间。以下图显示了使用 headline 和 sidebar 设计的示例页面。示例中只更改了 design 参数:

具有带有 'headline'(左侧)和 'sidebar'(右侧)设计属性的 BorderContainer
插入内容面板
ContentPane 面板提供了在大多数桌面 Dojo 页面中使用的通用容器。ContentPane 面板可以调整以适应其分配的位置,并且可以根据用户调整浏览器大小而扩展和收缩。ContentPane 面板可以容纳 HTML 元素,例如其他 Dojo 小部件和控制。它们还有一个 href 属性,当设置时,将加载并渲染另一个 HTML 页面的内容。由于跨域问题,页面通过 XMLHttpRequest (XHR) 加载,因此加载的 HTML 应该在同一域上。
修改我们应用程序的布局
观察需求后,我们将使用 BorderContainer 容器和一些 ContentPane 面板来创建应用程序的标题、页脚和内容。我们将使用标准的 HTML 模板获取必要的库,然后添加 dijit 内容。
在你的应用程序文件夹中,首先创建一个名为index.html的 HTML 文件。我们将将其命名为Y2K Map,并添加必要的元标签和脚本以加载 ArcGIS API for JavaScript。我们还将加载dojo CSS 样式表nihilo.css,以处理应用程序中dojo元素的基本样式。为了将样式应用到我们的应用程序中,我们还将向我们的 HTML 文档的body元素添加一个nihilo类。最后,我们将添加一个链接到我们即将创建的y2k.css文件。你的 HTML 文档应该看起来像以下这样:
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no" />
<title>Y2K Map</title>
<link rel="stylesheet" href="http://js.arcgis.com/3.13/dijit/themes/nihilo/nihilo.css">
<link rel="stylesheet" href="https://js.arcgis.com/3.13/esri/css/esri.css" />
<link rel="stylesheet" href="css/y2k.css" />
<script src="img/"></script>
</head>
<body class="nihilo">
</body>
</html>
接下来,我们将在<body>标签内添加BorderContainer容器和ContentPane瓷砖。这些将基于基本的<div>元素构建。我们将给BorderContainer容器一个id为mainwindow,顶部一个ContentPane瓷砖,其id为header,另一个ContentPane瓷砖,其id为map,我们的地图将放在这里,还有一个ContentPane瓷砖在底部,其id为footer。我们还会在标题和页脚中添加一些内容,使其看起来更美观。以下是一个示例:
<body class="nihilo">
<div id="mainwindow" data-dojo-type="dijit/layout/BorderContainer" data-dojo-props="design:'headline',gutter:false,liveSplitters: true" style="width: 100%; height: 100%; margin: 0;">
<div id="header" data-dojo-type="dijit/layout/ContentPane"
data-dojo-props="region:'top',splitter:true">
<h1>Year 2000 Map</h1>
</div>
<div id="map" data-dojo-type="dijit/layout/ContentPane"
data-dojo-props="region:'center',splitter:true">
</div>
<div id="footer" data-dojo-type="dijit/layout/ContentPane"
data-dojo-props="region:'bottom',splitter:true"
style="height:21px;">
<span>Courtesy of the Y2K Society</span>
</div>
</div>
</body>
由于客户需要一个地方来添加多个功能,包括我们的普查搜索,我们将在右上角添加一个按钮位置。我们将创建一个包含<div>的按钮,并将我们的普查按钮作为dijit/form/Button模块插入。使用dijit按钮将确保该部分的样式将与小部件的样式保持一致。以下是一个示例:
<div id="header"
data-dojo-type="dijit/layout/ContentPane"
data-dojo-props="region:'top',splitter:true">
<h1>Year 2000 Map</h1>
<div id="buttonbar">
<button data-dojo-type="dijit/form/Button"
id="census-btn" >Census</button>
</div>
</div>
为了使文件正常工作,我们需要添加一个链接到我们将为应用程序运行的 main 脚本文件。我们将把app.js文件作为<body>标签内的最后一个元素插入,这样加载文件就不会阻止浏览器下载其他内容。在以下代码中,你可以看到我们插入的位置:
<span>Courtesy of the Y2K Society</span>
</div>
</div>
<script type="text/javascript" src="img/app.js"></script>
</body>
在app.js文件中,我们将只做足够的事情来获得视觉效果。我们将随着工作的进行向此文件添加内容。我们将从正常的require()语句开始,加载BorderContainer、ContentPane和Button元素的模块。我们还将引入一个名为dojo/parser的模块,该模块将解析 HTML 中的 Dojo 数据标记并将其转换为应用程序小部件。代码如下:
require([
"dojo/parser",
"dijit/layout/ContentPane",
"dijit/layout/BorderContainer",
"dijit/form/Button",
"dojo/domReady!"
], function(
parser
) {
parser.parse();
});
经过所有这些工作后,我们的 HTML 应该看起来像以下这样:
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no" />
<title>Y2K Map</title>
<link rel="stylesheet" href="http://js.arcgis.com/3.13/dijit/themes/nihilo/nihilo.css">
<link rel="stylesheet" href="https://js.arcgis.com/3.13/esri/css/esri.css" />
<link rel="stylesheet" href="css/y2k.css" />
<script src="img/"></script>
</head>
<body class="nihilo">
<div id="mainwindow" data-dojo-type="dijit/layout/BorderContainer"
data-dojo-props="design:'headline',gutter:false, liveSplitters: true"
style="width: 100%; height: 100%; margin: 0;">
<div id="header"
data-dojo-type="dijit/layout/ContentPane"
data-dojo-props="region:'top',splitter:true">
<h1>Year 2000 Map</h1>
<div id="buttonbar">
<button id="census-btn" data-dojo-type="dijit/form/Button">Census</button>
</div>
</div>
<div id="map" data-dojo-type="dijit/layout/ContentPane" data-dojo-props="region:'center',splitter:true">
<div id="census-widget"></div>
</div>
<div id="footer"
data-dojo-type="dijit/layout/ContentPane"
data-dojo-props="region:'bottom',splitter:true"
style="height:21px;">
<span>Courtesy of the Y2K Society</span>
</div>
</div>
<script type="text/javascript" src="img/app.js"></script>
</body>
</html>
应用程序样式
如果我们现在运行应用程序,它看起来可能不会像预期的那样。事实上,它看起来就像一个空白屏幕。这是因为我们还没有为我们的页面分配大小。在这种情况下,ContentPane瓷砖的样式创建出定位外观的面板,绝对定位将内容从计算页面流程中移除。由于没有其他内容来填充body的高度,它就塌陷到零高度。
解决这个问题的快速方法是更新 HTML 和<body>标签的样式。在你的文本编辑器中打开y2k.css,并添加以下 CSS 行:
html, body {
border: 0;
margin: 0;
padding: 0;
height: 100%;
width: 100%;
font-size: 14px;
}
我们应用的这个 CSS 使页面填充了浏览器窗口的 100%。添加边框、边距和填充移除了不同浏览器可能插入页面的任何可能的格式化。我们添加了字体大小,因为在 body 级别设置像素字体大小是一种良好的实践。进一步的字体大小分配可以使用em单位相对于这个值进行。
如果你现在播放你的页面,你应该看到以下输出:

这无疑是进步,但我们还需要满足应用的其它要求。首先,如果我们将#buttonbar元素精确地定位在右上角,并为人口普查按钮留出一些垂直居中的空间,它会看起来更好。接下来,我们将在不同的面板上添加一些黄色色调,并使页眉和页脚的角落变得圆润。我们的应用最终将具有以下风格化的外壳:

要实现这一点,这是我们将添加到y2k.css中的 CSS:
h1 {
margin: 2px 8px;
display: inline-block;
*display: inline; /* IE 7 compatible */
zoom: 1; /* IE 7 compatible */
}
#buttonbar {
position: absolute;
top: 10px;
right: 15px;
width: auto;
height: auto;
}
#mainwindow,
#mainwindow .dijitSplitter {
background: #fffaa9; /* paler yellow */
}
#header, #footer {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
border: 1px solid #6f6222;
}
#header { background: #ffec50; /* bold yellow */ }
#footer { background: #d0a921; /* darker yellow */ }
添加我们的自定义包
在我们能够使用自定义包之前,我们需要告诉 Dojo 它在哪。为此,我们将在加载 ArcGIS JavaScript API 之前添加一个dojoConfig对象。在dojoConfig对象中,我们将添加一个包数组对象,并告诉它y2k模块位于js文件夹中。脚本看起来可能像这样:
<link rel="stylesheet" href="css/y2k.css" />
<script type="text/javascript">
dojoConfig = {
packages: [
{
name: 'y2k',
location: location.pathname.replace(/\/[^\/]*$/, '') +
'/js'
}
]
};
</script>
<script src="img/"></script>
设置我们的应用
现在我们需要设置我们的应用脚本并开始移动。我们将打开我们的app.js文件,并添加地图和即将到来的Census小部件的功能。我们将像在第一章中做的那样添加对esri/map模块和我们的y2k/Census小部件的引用。我们将初始化地图,就像我们在第一章中做的那样,并创建一个新的Census小部件。遵循许多 dojo 小部件的模式,Census小部件构造函数将接受一个选项对象作为第一个参数,以及一个指向<div>地图内 HTML 节点的字符串引用作为第二个参数。我们将在稍后填写选项:
require([
"dojo/parser",
"esri/map", "y2k/Census",
"dijit/layout/ContentPane",
…
], function(
parser, Map, Census
) {
parser.parse();
var map = new Map("map", {
basemap: "national-geographic",
center: [-95, 45],
zoom: 3
});
var census = new Census({ }, "census-widget");
});
注意
可能你会想知道为什么我们代码中的一些模块以大写字母开头,而另一些则不是。常见的 JavaScript 编码约定指出,对象构造函数应以大写字母开头。Map和Census模块创建地图和人口普查小部件,因此它们应该大写。为什么对esri/map模块的引用没有大写仍然是个谜,如果你弄错了,这可能会成为错误的一个来源。
编写我们的小部件
我们需要开始组装我们的人口普查小部件。为此,我们将使用本章前面学到的创建自定义小部件的知识。在Census.js文件中,我们将使用define()、declare()和_WidgetBase模块创建一个外壳小部件。它应该看起来像以下代码:
define([
"dojo/_base/declare",
"dijit/_WidgetBase"
], function ( declare, _WidgetBase ) {
return declare([_WidgetBase], { });
});
对于我们的小部件,我们希望有一个模板来指导用户如何使用工具。也许让用户关闭工具也是一个好主意,因为我们不希望地图被多个工具所杂乱:
define([
"dojo/_base/declare",
"dijit/_WidgetBase", "dijit/_TemplatedMixin",
"dijit/_OnDijitClickMixin"],
function (declare, _WidgetBase, _TemplatedMixin,
_OnDijitClickMixin) {
return declare([_WidgetBase, _TemplatedMixin, _OnDijitClickMixin], { });
});
在这一点上,我们需要将我们的模板加载到我们的小部件中。为了做到这一点,我们将实现另一个名为dojo/text的 dojo 模块。
添加一些dojo/text!
dojo/text模块允许模块以字符串的形式下载任何类型的文本文件。内容可以是 HTML、文本、CSV 或任何相关的基于文本的文件。当使用 AMD 加载文件时,格式如下:
require([…, "dojo/text!path/filename.extension", …],
function (…, textString, …) {…});
在前面的例子中,filename.extension描述了文件名,例如report.txt。路径显示了文件相对于脚本的定位。因此,路径./templates/file.txt意味着该文件位于templates文件夹中,它是包含此小部件脚本文件夹的子文件夹。
我们声明中的感叹号表示该模块有一个插件属性,可以在加载时自动调用。否则,我们不得不等待并在我们的脚本中模块加载后调用它。我们看到这种情况的另一个模块是dojo/domReady。感叹号激活了该模块,暂停我们的应用程序,直到 HTML DOM 也准备好。
回到我们的应用程序,现在是加载我们的 dijit 模板的时候了。_TemplatedMixin模块提供了一个名为templateString的属性,它从中读取以构建dijit包的 HTML 部分。我们将使用dojo/text模块从我们的Census.html模板中加载 HTML,然后将从该字符串创建的字符串插入到templateString属性中。它看起来可能像这样:
define([
…
"dijit/_OnDijitClickMixin",
"dojo/text!./templates/Census.html"
], function (…, _OnDijitClickMixin, dijitTemplate) {
return declare([…], {
templateString: dijitTemplate
});
});
我们的小部件模板
对于我们的模板,我们将利用 Dojo 的一些酷技巧。第一个技巧是我们可以通过与我们在第一章中学习的相同替换模板,将属性值混合到我们的小部件中,您的第一个映射应用程序。其次,我们将利用_OnDijitClickMixin模块来处理点击事件。
对于我们的模板,我们正在考虑创建一个带有标题(例如“人口普查”),一些说明和一个close按钮的东西。我们将通过data-dojo-attach-event属性使用ondijitclick分配一个关闭按钮的事件处理程序。我们还将从小部件分配一个baseClass属性到小部件中的 CSS 类。如果你还没有在你的template文件夹中创建Census.html文件,现在就创建它。然后,输入以下 HTML 内容:
<div class="${baseClass}" style="display: none;">
<span class="${baseClass}-close"
data-dojo-attach-event="ondijitclick:hide">X</span>
<b>Census Data</b><br />
<p>
Click on a location in the United States to view the census
data for that region.
</p>
</div>
小贴士
模板中可能存在哪些错误会导致小部件无法加载?如果你通过data-dojo-attach-event参数合并事件,请确保模板中的回调函数与你的 dijit 中的回调函数名称匹配。否则,dijit 将无法加载。
在我们的代码中,我们将分配一个 baseClass 属性,以及 hide() 和 show() 函数。许多 dijit 使用这些函数来控制它们的可见性。通过这些函数,我们将显示样式属性设置为 none 或 block,如下面的代码所示:
define([…, "dojo/dom-style"],
function (…, domStyle) { …
{
baseClass: "y2k-census",
show: function () {
domStyle.set(this.domNode, "display", "block");
},
hide: function () {
domStyle.set(this.domNode, "display", "none");
}
});
使用 dijit 构造函数
我们需要扩展 dijit 构造函数功能,但首先我们需要考虑这个 dijit 需要什么。我们最初创建 dijit 的目的是让用户点击地图并识别人口普查地点。因此,我们需要一个地图,由于我们要识别某些东西,我们需要向 IdentifyTask 对象的构造函数提供地图服务 URL。
跟随大多数 dijit 构造函数的趋势,我们的 dijit 构造函数将接受一个选项对象,以及一个 HTML DOM 节点或该节点的字符串 id。在选项对象中,我们将寻找一个映射和一个 mapService URL。构造函数中的第二个参数将被分配为 dijit 的 domNode,如果它是一个字符串,则根据该字符串找到相应的节点。
对于我们的构造函数,我们将地图集成到小部件中,并将 mapService 转换为 IdentifyTask。我们还将添加 dojo/dom 模块,以提供对常见 JavaScript 操作 document.getElementById() 的快捷方式,并使用它将字符串 id 转换为 DOM 元素:
define([…, "dojo/dom", "esri/tasks/IdentifyTask", …],
function (…, dom, IdentifyTask, …) {
…
constructor: function (options, srcRefNode) {
if (typeof srcRefNode === "string") {
srcRefNode = dom.byId(srcRefNode)
}
This.identifyTask = new IdentifyTask(options.mapService);
this.map = options.map || null;
this.domNode = srcRefNode;
},
…
});
重复使用我们的旧代码
回顾 第一章,您的第一个映射应用程序,我们能够查看地图,点击它,并获得一些结果。如果我们可以重用一些代码会怎么样?好吧,通过一些修改,我们可以重用大部分的代码。
我们需要解决的第一件事是分配我们的地图点击事件。由于我们并不总是有机会这样做,因此最合理的时间是在我们的 dijit 可见时分配地图点击事件。我们将修改我们的显示和隐藏函数,以便它们分别分配和移除点击处理程序。我们将命名地图点击事件处理程序为 _onMapClick()。我们还将加载模块 dojo/_base/lang,它帮助我们处理对象。我们将使用 lang.hitch() 函数重新分配函数的 this 语句:
define([…, "dojo/on", "dojo/_base/lang", …],
function (…, dojoOn, lang, …) {
…
show: function () {
domStyle.set(this.domNode, "display", "block");
this._mapClickHandler = this.map.on("click", lang.hitch(this, this._onMapClick));
},
hide: function () {
domStyle.set(this.domNode, "display", "none");
if (this._mapClickHandler && this._mapClickHandler.remove) {
this._mapClickHandler.remove();
}
},
_mapOnClick: function () {}
…
作为旁注,虽然 JavaScript 不支持私有变量,但 JavaScript 对象的典型命名约定表明,如果一个属性或方法以下划线 (_) 字符开头,它被认为是开发者认为的私有。
在我们的_onMapClick()方法中,我们将重用第一章中“您的第一个映射应用程序”的点击事件代码,但有几点值得注意。现在请记住,我们不是将地图作为变量来引用,而是将其作为小部件的属性。对于IdentifyTask以及我们可能在这个 dijit 中调用的任何其他方法也是如此。要引用方法内的属性和方法,变量前面必须加上this。在这种情况下,this将引用小部件,我们在调用_onMapClick()时使用了dojo/_base/lang库来确保这一点。如果你的应用程序在地图点击事件失败,可能是因为你没有正确地分配带有正确this上下文的变量:
define([…, "esri/tasks/IdentifyParameters", …],
function (…,IdentifyParameters, …) {
…
_onMapClick: function (event) {
var params = new IdentifyParameters(),
defResults;
params.geometry = event.mapPoint;
params.layerOption = IdentifyParameters.LAYER_OPTION_ALL;
params.mapExtent = this.map.extent;
params.returnGeometry = true;
params.width = this.map.width;
params.height= this.map.height;
params.spatialReference = this.map.spatialReference;
params.tolerance = 3;
this.map.graphics.clear();
defResults = this.identifyTask.execute(params).addCallback (lang.hitch(this, this._onIdentifyComplete));
this.map.infoWindow.setFeatures([defResults]);
this.map.infoWindow.show(event.mapPoint);
},
…
加载更多模板
Y2K 协会要求我们以更有组织和逻辑的方式显示弹出窗口。在我们的网络开发者心中,没有什么比表格更能体现组织良好的数据了。我们可以为我们的模板设置和组织表格,但创建那些用于组织长列表的长字符串会令人望而却步。在我们的 JavaScript 代码中包含 HTML 字符串将会很痛苦,因为大多数 IDE 和语法高亮文本编辑器都不会在我们的 JavaScript 中高亮显示 HTML。
但然后我们记得我们可以使用dojo/text模块将 HTML 加载到我们的 JavaScript 中。如果我们使用小的 HTML 片段定义我们的InfoTemplates内容,并使用dojo/text加载它们,这个过程将会更加流畅:
define([…,
"dojo/_base/array",
"esri/InfoTemplate",
"dojo/text!./templates/StateCensus.html",
"dojo/text!./templates/CountyCensus.html",
"dojo/text!./templates/BlockGroupCensus.html",
"dojo/text!./templates/BlockCensus.html"
],
function (…,
arrayUtils, InfoTemplate, StateTemplate, CountyTemplate, BlockGroupTemplate, BlockTemplate) {
_onIdentifyComplete: function (results) {
return arrayUtils.map(results, function (result) {
var feature = result.feature,
title = result.layerName,
content;
switch(title) {
case "Census Block Points":
content = BlockTemplate;
break;
case "Census Block Group":
content = BlockGroupTemplate;
break;
case "Detailed Counties":
content = CountyTemplate;
break;
case "states":
content = StateTemplate;
break;
default:
content = "${*}";
}
feature.infoTemplate = new InfoTemplate(title, content);
return feature;
});
}
});
});
我会将 HTML infoTemplates的内容留给你作为练习。
回到我们的 app.js
我们没有忘记我们的app.js文件。我们为加载地图和Census dijit 编写了代码框架,但没有分配任何其他内容。我们需要为Census按钮分配一个点击事件处理程序来切换Census小部件的可见性。为此,我们将使用dijit/registry模块从其id加载dijit按钮。我们将加载地图和Census dijit 的mapService,并添加一个事件监听器到Census按钮的点击事件,这将显示Census小部件。我们将再次使用dojo/_base/lang模块来确保Census.show()函数被正确地应用于Census dijit:
require([
…
"dojo/_base/lang",
"dijit/registry",
…
], function(
… lang, registry, …
) {
…
var census = new Census({
map: map,
mapService: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/"
}, "census-widget");
var censusBtn = registry.byId("census-btn");
censusBtn.on("click", lang.hitch(census, census.show));
});
当我们运行我们的代码时,它应该看起来像以下这样:

摘要
在本章的整个过程中,我们学习了 ArcGIS API for JavaScript 中包含的 Dojo 框架提供的许多功能。我们了解了 Dojo 的三个主要包,以及如何使用它们来创建应用程序。我们学习了如何创建自己的模块,并且修改了一个现有的应用程序来创建一个可以被任何应用程序导入的模块。
在下一章中,我们将学习 ArcGIS API for JavaScript 如何通过 REST 服务与 ArcGIS Server 通信。
第四章。在 REST 中寻找平静
我们已经花费了几章讨论和构建使用 ArcGIS API for JavaScript 的应用程序。我们使用不同的 API 工具与 ArcGIS Server 通信,关于其地图服务。但 API 是如何与 ArcGIS Server 通信的呢?
在本章中,我们将专注于 ArcGIS Server。我们将探讨它是如何实现 REST 接口的。我们将回顾 ArcGIS REST API,该 API 在 resources.arcgis.com/en/help/arcgis-rest-api/#/The_ArcGIS_REST_API/02r300000054000000/ 中概述。这描述了服务器和浏览器之间传递的文件结构和数据格式。最后,我们将通过更改弹出高亮符号来扩展前一章的应用程序。
什么是 REST?REST 代表 表征状态转移。它是一种软件架构,它侧重于使用超媒体环境在服务器和客户端之间的接口。它限制了客户端和服务器之间可以执行的操作,但提供了足够的信息和文档,使用户能够在数据和状态之间导航。
在本章关于 ArcGIS Server 和 REST 的讨论中,我们将涵盖以下主题:
-
处理 REST 端点和数据格式
-
通过浏览器看到的 ESRI REST 服务层次结构
-
常见的 JSON 格式 REST 数据
-
如何使用 ArcGIS REST 服务和 JSON,如 REST 服务 REST API 要求中定义的那样
REST 是一种基于网页的方法论。一个网站展示一个状态(URL),数据传输(HTML 页面、CSS 和 JavaScript),以及一种良好的文档化方式在各个状态之间导航(链接)。虽然理解一个网站是 RESTful 的很好,但是什么让 ArcGIS Server 如此 RESTful 呢?
为了使一个网络服务被认为是 RESTful 的,它必须满足以下要求:
-
客户端-服务器:客户端和服务器的作用被明确定义。客户端不关心服务器包含一条或一百万条记录,服务器也不依赖于客户端的特定 UI。只要客户端和服务器之间的接口保持不变,客户端和服务器代码可以独立更改。
-
无状态:客户端处理应用程序的状态,而服务器不需要跟上它。客户端拥有进行请求所需的一切,包括必要的参数和安全令牌。
-
可缓存:有时客户端应用程序为了提高性能而缓存数据,因为万维网以异步方式交付数据。服务器需要告诉客户端哪些请求可以被缓存以及缓存多长时间。
-
分层系统:服务器端应用程序可以放置在负载均衡器、安全系统或代理后面,而对客户端应用程序没有明显的影响。
-
按需代码(可选):服务器可以为客户端提供运行代码。例如包括 Java 小程序或 JavaScript 脚本。并非所有 REST 服务都这样做。
-
统一接口:使用 REST 服务,服务器通过一个统一的接口提供客户端与数据交互的方式。这个统一接口可以进一步分解为四个原则。
-
包含资源标识的信息请求。这包括从数据源到输出类型的一切。
-
客户端从请求中获取足够的信息来操纵或删除数据。
-
服务器发送的消息包含有关如何使用它们的说明。
-
状态是由客户端使用超媒体(网页、查询参数和会话状态)处理的。
-
如果您查看 ArcGIS 服务器实现,会发现它符合这些标准。因此,它被认为是 RESTful 的。
查看地图服务器
ArcGIS 服务器提供对其地图服务内容的网络访问。要访问内容,您需要知道 ArcGIS 服务器名称和站点名称。默认情况下,ArcGIS 服务器通过端口 6080 访达。如果它已配置并授权在端口 80 上提供网络内容,也可以通过该端口访问。REST 服务端点可以通过以下地址通过浏览器访问:
http://<GIS Server Name>:6080/<site name>/rest/services
其中 GIS 服务器名称 指的是 ArcGIS 服务器机器,而 站点名称 指的是 ArcGIS 服务器实例,默认情况下是 arcgis,如果 ArcGIS 服务器已经设置为在端口 80 上传输流量,则端口号是可选的。这是互联网流量的默认端口。
当有多个 GIS 服务器时,通常处理大量的流量或复杂的服务,可能会安装一个网络适配器。网络适配器根据服务请求、负载均衡和其他相关问题将流量路由到多个 ArcGIS 服务器。网络适配器还提供一层安全性,其中 ArcGIS 服务器机器名称不会直接暴露给外界。要通过网络适配器访问 REST 服务,请使用以下 URL。
http://<web server name>/<web adaptor name>/rest/services
只要 ArcGIS 服务器可以从我们的计算机访问,我们就可以在网页浏览器中访问信息。默认情况下,服务数据以 HTML 格式呈现。从那里我们可以看到 REST 服务的属性,并跟随链接到服务器上的其他服务和操作。这使得开发者可以在不创建任何应用程序的情况下审查和测试地图服务。
ArcGIS REST 服务提供了一种很好的方式,使用 HTML 查看和交互服务数据,这对于演示很有用。我们的大多数应用程序将通过服务器请求与 REST 服务交互。因此,ArcGIS 服务器可以通过另一种称为 JSON 的数据格式通过 REST 进行通信。
与 JSON 一起工作
JavaScript 对象表示法(JSON)为松散定义的数据结构提供了一种结构化数据格式。一个 JSON 对象由其他 JSON 对象构建而成,包括字符串、数字、其他对象和数组。任何数据都是允许的,只要所有内容都是自包含的,并且没有因为缺少括号和花括号而出现格式上的混乱。
有许多方法可以测试有效的 JSON。访问jsonlint.com,在那里你可以复制并粘贴你的 JSON,并提交它进行验证。它将指出缺失或格式错误的问题,以及如何解决它们。
随着你阅读这本书中的示例,你会发现 JSON 的格式并不总是相同的,尤其是在 JSON 对象键字段(或属性名)中。JSON 验证器要求所有字符串项都必须用引号括起来。单引号或双引号都行,只要你在字符串的末尾使用与开头相同的标记。这包括 JSON 对象键字段。浏览器中的 JavaScript 解释器更灵活,键字段不需要用引号括起来。这完全取决于你如何测试 JSON。
在 JSON 开发之前,数据是以一种称为可扩展标记语言(XML)的格式从服务器传递到客户端的。XML 是一种文档标记语言,它以人类和机器都能读取的格式显示数据。XML 格式可以被多种编程语言读取和解析。
与 XML 相比,JSON 是 Web 应用程序首选的数据格式有两个主要原因。首先,JSON 数据可以立即被 JavaScript 应用程序消费。XML 需要额外的步骤将数据解析成可用的对象。其次,JSON 数据占用的空间更少。让我们通过比较两个数据片段来探讨这一点。以下片段是用 XML 编写的:
<mountain>
<name>Mount Everest</name>
<elevation>29029</elevation>
<elevationUnit>ft</elevationUnit>
<mountainRange>Himalaya</mountainRange>
<dateOfFirstAscent>May 29, 1953</dateOfFirstAscent>
<ascendedBy>
<person>
<firstName>Tenzing</firstName>
<lastName>Norgay</lastName>
</person>
<person>
<firstName>Edmund</firstName>
<lastName>Hillary</lastName>
</person>
</ascendedBy>
</mountain>
现在,这是用 JSON 编写的相同数据:
{
"type": "mountain",
"name": "Mount Everest",
"elevation": 29029,
"elevationUnit": "ft",
"mountainRange": "Himilaya",
"dateOfFirstAscent": "May 29, 1953",
"ascendedBy": [
{
"type": "person",
"firstName": "Tenzing",
"lastName": "Norgay"
},
{
"type": "person",
"firstName": "Edmund",
"lastName": "Hillary"
}
]
}
在 JSON 中,相同的数据比 XML 少 62 个字符。如果我们去掉换行和额外空格,或者最小化数据,JSON 数据比最小化的 XML 数据短 93 个字符。考虑到带宽宝贵,尤其是在移动浏览器中,你可以看到为什么 JSON 是数据传输的首选格式。
JSON 和 PJSON 格式
JSON 有两种风味。默认的 JSON 是经过最小化的,所有额外的空格和换行都已被移除。漂亮的 JSON,或简称 PJSON,包含换行和空格以显示数据的结构和层次。之前的珠穆朗玛峰示例显示了 PJSON 的样子。虽然 PJSON 更容易阅读,因此更容易调试错误,但最小化的 JSON 要小得多。在示例中,PJSON 有 397 个字符,而最小化版本只有 277 个字符,大小减少了 30%。
当查看 ArcGIS REST 服务数据时,你可以通过在 REST 服务 URL 中添加一个f查询参数来更改数据的格式。它应该看起来像以下 URL:
http://<GIS web service>/arcgis/rest/services/?f=<format>
在这里,您可以设置 f=JSON 以接收原始 JSON 数据,或 f=PJSON 以接收人类可读的格式化 JSON(如果您更喜欢,也可以是填充的 JSON)。一些浏览器,如 Google Chrome 和 Mozilla Firefox,提供第三方扩展,可以将原始 JSON 数据重新格式化为 PJSON,而无需发出请求。
服务级别
让我们从查看示例 ArcGIS Server 服务开始,sampleserver6.arcgisonline.com/arcgis/rest/services。当我们以 HTML 的形式请求页面时,我们注意到一些事情。首先,显示了 ArcGIS Server 的版本(在撰写本文时为版本 10.21)。版本号很重要,因为许多功能和信息可能不会出现在旧版本中。其次,我们看到指向文件夹的链接列表。这些是按照发布者选择的任何方式组合的地图服务。我们还在文件夹列表下方看到地图服务链接列表。最后,在页面底部,我们看到支持的接口。在这个网站上,我们可以看到我们熟悉的 REST 接口。本书不会涵盖其他接口。以下是服务的图片:

如果我们将浏览器中 REST 服务请求的格式更改为 Pretty JSON,通过在 URL 末尾添加 ?f=pjson,我们可以大致了解 ArcGIS JavaScript API 会如何查看此位置:

在这里,返回的 JSON 对象包括数字 currentVersion、文件夹名称的数组以及服务对象的数组。服务 JSON 对象包含一个名称和一个类型属性,这告诉您正在处理什么类型的服务,并提供了构建指向这些服务的 URL 链接所需组件。此格式如下:
http://<server>/arcgis/rest/services/<service.name>/<service.type>
如果我们跟随链接到我们的人口普查地图服务,我们可以看到更多详细信息。
地图服务
地图服务使应用程序能够访问使用 ArcGIS Server 发布的地图数据。它包含有关地图布局、格式、内容和其他必要项的信息,以正确使用各种 ArcGIS API 渲染地图。地图服务 URL 的格式如下:
http://<ArcGIS Server REST Services>/<mapName>/MapServer
or
http://<ArcGIS Server REST Services>/<folder>/<mapName>/MapServer
当您使用浏览器导航到地图服务时,您会看到有关地图服务的大量信息。HTML 提供了查看不同应用程序中数据的链接,包括 ArcGIS JavaScript API 和 ArcMap。如果地图服务已发布以提供该格式的数据,则还可以使用 Google Earth。地图服务的 HTML 还提供了大量元数据,以帮助您了解其提供的内容。这些属性包括描述、服务描述、版权文本和文档信息。
一些地图服务属性在没有上下文的情况下可能难以理解。我们将回顾一些重要的属性。请记住,此列表中的属性显示了它们在 HTML 中的列出方式。当以 JSON 显示时,这些项是驼峰式命名的(第一个字母小写,没有空格,并且每个新单词的首字母大写,第一个单词除外)。
-
空间参考:地图布局与真实世界之间的比较,我们稍后会讨论这一点。
-
单个融合地图缓存:让您知道地图数据是否已被缓存,或者是否是动态的。您可以使用
ArcGISTiledMapServiceLayer或ArcGISDynamicMapServiceLayer分别加载图层。 -
初始范围/完整范围:当您首次使用 ArcGIS JavaScript API 加载地图时,初始范围描述了您第一次看到的区域的边界框。完整范围是地图服务的预期完整区域,可能比所有数据都要宽得多。
-
支持的图像格式类型:当 ArcGIS 服务器将地图层绘制为瓦片时,这些是可以返回的图像格式。如果您的数据有很多半透明度和颜色,则推荐使用
PNG32,而PNG8与非常简单的符号配合良好。 -
支持动态层:如果为真,则开发人员可以在显示地图服务时更改符号和层定义。
-
最大记录数:在提交查询、标识或其他搜索时,这是地图服务可以返回的最大结果数。此信息只能通过服务器端对地图服务的更改来更改。
最后,地图服务 HTML 提供了到许多相关 REST 服务端点的链接。大多数这些链接扩展了现有的 URL,并提供了有关地图服务的更多信息。至少应该包含以下内容:
-
图例:显示地图服务中层的符号。
-
导出地图:此功能允许您下载一个图像,显示地图中适合特定边界框的区域。您可以指定参数。
-
标识:这允许您根据传入的几何形状在地图服务的所有层中识别功能。此功能由
IdentifyTask使用。 -
查找:这允许用户根据传递给它的文本行的存在来搜索功能。此功能通过
FindTask实现。
地图服务层
在探索地图服务的层时,了解要寻找的内容很有帮助。地图服务在其层属性中列出了其层的基内容,以对象数组的形式。
所有图层对象具有相同的格式,具有相同的属性。每个图层对象都有一个表示图层在列表中零基位置的数字 id 属性。图层对象还有一个 name 属性,它来自图层在地图服务中的命名方式。这些图层也有 minScale 和 maxScale 属性,显示图层可见的范围(0 值表示没有 minScale 或 maxScale 限制)。在确定可见性时,图层对象还包含一个布尔 defaultVisibility 属性,描述了图层在地图服务加载时是否最初可见。
地图服务图层对象还包含有关其图层层次结构的信息。每个地图图层对象包含一个 parentLayerId 和一个 subLayerIds 属性。parentLayerId 是一个数字,它引用了特定图层父组图层的索引。父层 id 为 -1 表示所讨论的图层没有父层。subLayerIds 是一个整数数组,其中包含可以找到特定父层子图层的索引。如果一个图层没有子图层,subLayerIds 将是一个 null 值,而不是空列表。你可以在以下代码中看到一个地图服务图层的示例:
layers: [
{
"id" : 0,
"name" : "Pet Lovers",
"parentLayerId" : -1,
"defaultVisibility" : true,
"subLayerIds" : [1, 2],
"minScale" : 16000
"maxScale" : 0
},
{
"id" : 1,
"name" : "Dog Lovers",
"parentLayerId" : 0,
"defaultVisibility" : true,
"subLayerIds" : null,
"minScale" : 16000
"maxScale" : 0
},
{
"id" : 2,
"name" : "Cat Lovers",
"parentLayerId" : 0,
"defaultVisibility" : true,
"subLayerIds" : null,
"minScale" : 16000
"maxScale" : 0
}
],
…
在前面的代码片段中,地图服务有三个图层。Pet Lovers 图层实际上是一个 parentLayer,对应于在 ArcMap .mxd 文件中分配的组图层。parentLayer 中有两个图层:Dog Lovers 和 Cat Lovers。所有图层默认都是可见的,并且根据 minScale,图层不会显示,直到地图的比例小于 1:16,000。maxScale 属性设置为零,意味着没有最大比例,图层会再次关闭。
特征服务
特征服务类似于地图服务,但提供了更多的功能。如果数据库和地图设置支持这些操作,它们的内容可以进行编辑。它们不需要图例服务即可显示其特征符号。它们的符号也可以通过更改其渲染器在客户端进行修改。特征服务的 URL 与地图服务类似,但以 FeatureServer 结尾,如下所示:
http://<GIS-web- server>/arcgis/rest/services/<folder>/<mapname>/FeatureServer
特征服务首先和最重要的是其功能。除了允许你查询数据外,特征服务功能允许用户创建、更新和/或删除记录。熟悉 CRUD 操作的人会认出这些词作为 CRUD 中的 C、U 和 D(R 代表读取,即当你查询结果时发生的情况)。功能包括如果允许创建、更新或删除,则包括编辑。此外,如果特征服务支持将文件附件(如照片)附加到数据,则功能将包括“上传”一词。
还有其他特征服务属性可能有助于你了解该服务。它们包括以下内容:
-
具有版本化数据:让您知道地理数据库已启用版本控制,这允许撤销/重做编辑。
-
支持断开连接编辑:数据可以在没有互联网连接的环境中签出并进行编辑。当应用程序再次连接到互联网时,数据可以被签回。
-
同步启用:如果这是真的,则要素数据可以在数据来源的地理数据库和另一个地理数据库(另一个书籍的主题)之间同步。
-
允许几何更新:如果允许编辑,这会让 API 知道要素几何是否可以编辑。由于某些权限,应用程序可能只允许更新要素属性,而几何形状保持不变。
-
启用 Z 默认值:如果数据包含高度数据(
z),则在地图服务中分配默认值。
层级
地图服务和要素服务由图层组成。这些图层将具有相同几何类型和相同属性集的地理要素组合在一起。图层在列表中通过其数值索引进行引用。图层索引从 0 开始,对于底层图层,每增加一个图层就向上增加一个。地图服务中第一层的 URL 可能看起来像这样:
http://<GIS-web- server>/arcgis/rest/services/<folder>/<mapname>/MapServer/0
地图图层提供了一整套数据,帮助您理解您正在查看的内容。图层的 name 属性要么来自 .mxd 文件中的名称,要么来自未保存的文件中的图层内容表。地图图层还提供描述和版权数据。显示字段属性告诉地图服务在开启标签时使用什么来标记要素。
地图图层也提供了您可以在应用程序中使用的重要数据。type 参数告诉您图层的几何形状,是点、线还是多边形。默认可见性让您知道图层在地图服务开始时是否原本是可见的。最小比例尺和最大比例尺会影响可见性,这取决于您的缩放级别。地图服务还会告诉您图层是否有附件,是否可以使用不同的渲染器进行修改,以及查询可以返回多少结果。
字段
地图服务图层通过字段属性提供有关其属性的信息。字段属性是一系列具有类似格式的字段对象。所有字段都有一个类型、一个名称和一个别名属性。类型指的是字段的 数据类型,无论是字符串还是整数,还是其他类型。支持的类型列表可以在 resources.arcgis.com/en/help/arcgis-rest-api/#/field/02r300000051000000/ 找到。名称属性是属性在地理数据库中的字段名称,字段名称不包含空格或特殊字符。
alias字段是一个字符串,用于显示用于展示目的的字段name。与field名称不同,alias可以包含空格或其他特殊字符。如果在地理数据库或地图服务中没有分配alias,则alias字段与字段name相同。例如,当使用 ArcMap 创建地图服务时,你可能有一些具有字段名称NUMB_HSES的区块数据。如果你想在一个图表中显示这个属性的值,字段名称可能看起来粗糙且有些令人困惑。你可以通过将其称为Number of Houses来为NUMB_HSES字段添加一个别名。这个alias为字段提供了一个更好的描述:
{
"type": "esriFieldTypeInteger",
"name" "NUMB_HSES",
"alias": "Number of Houses"
}
域
字段对象也可以分配域属性。域是在地理数据库级别强加的字段值限制。域在地理数据库中唯一创建,可以分配给要素类和表字段。域通过限制可以输入的内容,使输入正确的值变得更容易。例如,在报告服务中,你可能会提供一个包含所有正确输入的街道名称的域字段,而不是允许用户在报告中误输入街道名称。然后用户可以从列表中选择,而不是猜测如何拼写街道名称。
ArcGIS REST API 支持两种域类型:范围和编码值。范围,正如其名,为特征属性设置最小和最大数值。一个范围示例可能是一家中餐馆的平均用户评分。餐馆可能得到一星到五星之间的评分,所以你不想餐馆意外得到6或小于1的值。你可以在以下代码片段中看到一个具有该范围域的评分字段示例:
{
"type": "esriFieldTypeInteger",
"name": "RATING",
"alias": "Rating",
"domain": {
"type": "range",
"name": "Star Rating",
"range": [1, 5]
}
}
编码值域提供了一组代码和值对的列表,用作合法的属性值。编码值列表包含具有名称和代码的项目。代码是存储在地理数据库中的值。名称是编码值的文本表示。它们很有用,因为用户被迫选择一个有效值,而不是输入一个正确的值。
在以下示例中,我们可以看到一个具有编码值域的字段。该字段包含州缩写,但域允许用户看到完整的州名:
{
"type": "esriFieldTypeString",
"name": "STATE",
"alias": "State",
"length": 2,
"domain": {
"type": "codedValue",
"name": "State Abbreviation Codes",
"codedValues": [
{"name": "Alabama", "code": "AL"},
{"name": "Alaska", "code": "AK"},
{"name": "Wisconsin", "code": "WI"},
{"name": "Wyoming", "code": "WY"}
]
}
}
在前面的示例中,州名以两位字母代码形式存储。域提供了一个完整的州名参考表,包含各州的完整名称。如果你使用此字段发送查询以获取特征,你会使用代码值。查询所有STATE = 'Alaska'的特征将不会产生结果,而一个STATE = 'AK'的查询可能会给你结果。
注意
注意,代码和值不必是同一类型。你可以为水线部件编号使用数字代码,并使用编码值来显示它们的描述性名称。
相关表
非地理数据的表可以发布在地图服务中。这些表可能提供与地图特征相关的数据,例如露营地点的评论或财产的销售历史。这些表可以像特征一样进行搜索和查询。地图层和表之间的关系也可以发布和搜索。
图层和表可以通过使用地理数据库关系类或 ArcMap 中的临时关系分配进行连接。当使用 ArcMap 发布时,这些关系在 ArcGIS Server 中得以保留。相关特征和表之间的连接存储在图层和表的relationships属性中。开发者可以根据父特征类中的选择查询相关数据。
关联对象具有相同的一般格式。每个关联对象都包含一个数值id和relatedTableId。relatedTableId与RelationshipQuery对象相关联,用于查询相关结果。角色描述了当前层或表是关系的起点还是终点。基数描述了单个起点对象是否有一个或多个与之相关的终点。
小贴士
在查询结果时,如果从起点开始并使用RelationshipQuery在目标表上进行操作,结果返回得更快。从目标表开始可能需要更长的时间。
常见 JSON 对象
ArcGIS REST API 定义了系统常用 JSON 数据对象的格式。ArcGIS JavaScript API 使用此格式与服务器通信。当你在浏览器开发者控制台中查看网络流量时,你会在请求和响应中看到这些常见的 JSON 对象。让我们更详细地看看这些 JSON 对象定义中的两个:几何形状和符号。
几何形状
几何 JSON 是 ArcGIS Server 过程中常用的数据格式。这是有道理的,因为寻找地理位置依赖于几何形状。几何形状和大小多种多样。
空间参考
如前几章所述,空间参考指的是在地图上表示地球的计算,可以是球体(几乎是一个球体),也可以是地表的二维表示。而不是记录所有可能影响计算空间参考的因素,遵循一个通用格式,要么使用分配的已知 ID(WKID),要么使用已知文本(WKT)。
对于具有已知 id 的空间参考,空间参考 JSON 由一个必需参数(WKID)和三个可选参数组成。WKID 是空间参考的数值参考,可以在www.spatialreference.org进行搜索。常见的值包括wkid 4326,代表 WGS84,通常用于纬度和经度。可选的空间参考对象参数包括最新的 WKID 或latestWkid,垂直坐标系统 WKID 或vcs Wkid,以及最新的垂直坐标系统 WKID 或latest vcs Wkid。最后两个用于具有三维点的要素,包括高度。
空间参考也可以使用字符串定义,在已知文本的情况下。已知文本详细说明了用于在数学上简化地图所需的必要项。
我们不会使用带有空间参考的计算。在我们的应用中,我们将它们用于数据比较或作为服务请求的参数。假设我们需要查询结果的纬度和经度坐标,但数据不是以十进制度数纬度/经度空间参考形式存在。我们可以查询数据,但将query.outSpatialReference设置为wkid 4326,然后 ArcGIS Server 执行必要的计算,以我们需要的格式给出结果。
点
点是最简单的几何形状,因此也是最容易解释的。它们有三个必需的参数,一个x值,一个y值和一个spatialReference值。x值指的是经度,或者根据坐标系统是东经值。y值指的是纬度或北纬值。点还可以有一个可选的z值来描述高度,以及一个可选的m值来描述坡度。您可以在以下示例中看到一个点的 REST 对象示例:
// a two-dimensional point
{x: -95.25, y: 38.09, spatialReference: {wkid: 4326}}
// a three-dimensional point
{x: -38.93, y: -45.08, z: 28.9, spatialReference: {wkid: 4326}}
在前面的例子中,点具有x和y值,以及一个空间参考。正如我们在上一节中学到的,已知的id为4326意味着x和y值是经度和纬度。在第二个例子中,添加了一个z值,表示高度。
多点
如您可能从前面的章节中回忆起来,多点定义了一组点。这些点共享相同的空间参考(spatialReference)。多点通过使用points属性定义点,这是一个点的二维数组。最低的数组包含x、y值,以及可能的z(高度)和m(坡度)值。如果点包含z和/或m值,多点的 JSON 也将对其hasZ和hasM属性有 true 值:
// a two-dimensional multipoint
{
"points": [[12.831, 48.132], [19.813, 49.908], [-90.10, 83.132]],
"spatialReference": {wkid: 4326}
}
此示例显示了三个点作为一个多点组合在一起,使用纬度和经度坐标(wkid 4326)定义位置。
多点
折线可以是单条线,也可以是描述地图上相同线性特征的线条组。折线的 JSON 由一个空间参考和其paths属性中的三维点数组组成。折线路径数组的最低级别包含线上一点的x、y、可能还有z和m坐标。如果存在z和m值,折线 JSON 对象将hasZ和hasM值分别设置为 true。
一条折线可能包含多于一条线。例如,如果铁路线的一部分被废弃,铁路仍然拥有该部分两侧的线路。两个好的部分仍然被视为一条折线。这就是为什么折线包含三个级别的数组。前面的代码用一系列经纬度坐标表示了一条线:
// a two dimensional path.
{
"paths": [[[43.234,-28.093], [44.234,-32.232], [43.239,-33.298], [49.802,-35.099]]],
"spatialReference": {wkid: 4326}
}
多边形
多边形为区域提供了一个实心形状。多边形 JSON 由一个空间参考和一个rings参数组成。rings参数包含一个三重数组,其中包含一个或多个点的列表,其中最后一个点与第一个点相同。这样,多边形就封闭了自己,并提供了内部和外部。多边形环内的点可以包含高度z值。在这种情况下,多边形将可选的hasZ属性设置为 true。
多边形 JSON 可以包含多个闭合的点环,这些多边形被称为多部分多边形。一个例子就是一块土地中间有一条道路穿过。如果道路不再属于所有者,所有者仍然保留由道路分割的两部分财产的所有权。由于土地曾经是一个整体,它仍然被视为一个单位,但有两部分。下面的 JSON 代码展示了多部分多边形。该多边形有两个环,每个环有三个点:
// a two dimensional polygon with two rings.
{
"rings": [[[-85.032,18.098], [-85.352,18.423], [-85.243,18.438], [-85.032,18.098]], [[85.042,18.098], [84.995,18.008], [85.123,18.900], [85.042,18.098]]],
spatialReference: {wkid: 4326}
}
信封
信封,也称为范围,是表示地图上项目最小和最大x和y值的边界框。以下示例显示了经度42.902度和53.923度之间,纬度-23.180度和-18.45度之间的区域:
{
"xmin": 42.902, "xmax": 53.923, "ymin": -23.180, "ymax": -18.45,
"spatialReference": {wkid: 4326}
}
符号
符号 JSON 包含描述符号属性所需的最小数据量。正如我们在前面的章节中学到的,图形符号描述了图形的外观,影响颜色、线宽和透明度等特性。不同的几何类型有相应的符号类型来展示其特性。我们将探讨 ArcGIS REST API 如何描述不同的符号类型,以及如何在我们的应用程序中使用它们。
颜色
在 ArcGIS REST 服务中,颜色定义为包含三个到四个元素的整数数组,取值范围从 0 到 255。前三个值分别代表红色、绿色和蓝色颜色值。数值越高表示颜色越浅,数值越低表示颜色越深,[0, 0, 0]代表黑色。
第四个值指的是 alpha 值,或颜色的不透明度。255 的值是完全不透明的,而 0 的值是完全透明的,介于两者之间的值则有一定的透视性。如果省略了第四个值,则默认为完全不透明(255)。这种描述与esri/Color对不透明度的描述不同,后者将值定义为介于0.0和1.0之间的十进制值。
简单线符号
至于符号,我们首先从简单的线符号开始,因为它被所有其他符号所使用。线由四个基本属性组成:一个type,一个style,一个width和一个color。对于简单的线符号,type,esriSLS始终相同。width描述了线的像素宽度,而color描述了在前面颜色部分中描述的数值 RGB 值。style是几个以esriSLS为前缀的字符串常量之一。选项包括Solid,Dash,Dot,DashDot,DashDashDot和Null。您可以在以下代码片段中看到一个 JSON 简单线符号定义的示例:
{
type: "esriSLS",
style: "esriSLSDash",
width: 3,
color: [123,98,74,255]
}
简单标记符号
简单标记符号描述了简单点的样式,由以下属性组成:一个type,style,color,size,angle,xoffset,yoffset和outline。对于这些符号,type始终是esriSMS。样式以类似于简单线符号的方式定义,除了所有样式都以esriSMS为前缀。样式可以从Circle,Cross,Diamond,Square,X或Triangle中选择。Angle定义了符号的旋转,而xoffset和yoffset描述了绘制的点与实际点之间的距离。Size是数值,衡量图形的像素大小。最后,outline接受一个简单的线符号定义来描述点图形的边框:
{
type: "esriSMS",
style: "esriSMSCircle",
color: [255,255,255,50],
size: 10,
angle: 0,
xoffset: 0,
yoffset: 0,
outline: {
width: 1,
color: [0,0,0]
}
}
简单填充符号
简单填充符号可以用以下属性描述:type,style,color和outline。Type始终是esriSFS,所有样式都以相同的方式开始。样式包括Solid,Null以及描述图形内绘制的线的各种其他方式。Outline,就像简单的标记符号一样,使用简单的线符号来描述多边形的边框。查看以下代码以获取 JSON 描述的示例:
{
type: "esriSFS",
style: "esriSFSSolid",
color: [123, 99, 212, 120],
outline: {
type: "esriSLS",
style: "esriSLSDashDot",
width: 3,
color: [123, 99, 212]
}
}
图片标记符号
图片标记符号描述了一个带有图片图形的点符号。该符号的 JSON 包括独特的属性,如url,imageData,contentType,width和height,以及与简单标记符号共有的属性(type,angle,xoffset和yoffset)。URL 链接到您想要查看的图像。Width和height描述了您想要图像在像素中有多大。ContentType指的是文件扩展名,例如.png文件的image/png。最后,imageData由可以转换为图像文件的base64字符串填充。请参考以下示例:
{
type: "esriPMS",
url: "images/Cranston.jpg",
imageData: "iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAAAA…",
contentType: "image/jpg",
width: 24,
height: 24,
angle: 0,
xoffset: 0,
yoffset: 0
}
图片填充符号
图片填充符号可以添加到多边形中,以显示形状内部镶嵌的图像。图片填充符号具有与图片标记符号许多相同的参数,包括url、imageData、contentType、width、height、angle、xoffset和yoffset。它需要线符号 JSON 来定义轮廓。它接受自己的数值xscale和yscale参数,这些参数可以缩小或拉伸特征内显示的图像。您可以在以下位置看到图片填充符号 JSON 的示例:
{
type: "esriPFS",
url: "images/foliage.png",
imageData: "iVBORwoadfLKJFDSFKJLKEWIUnjnKUHWkunUWNkJNiuN…",
contentType: "image/png",
outline: {
type: "esriSLS",
style: "esriSLSSolid",
width: 3,
color: [16, 243, 53]
},
width: 32,
height: 32,
angle: 0,
xoffset: 0,
yoffset: 0,
xscale: 1,
yscale: 1
}
文本符号
文本符号 JSON 描述了在地图上创建自定义文本标签的格式。文本符号 JSON 与其他符号看起来非常不同,因为它还涉及字体、粗体和斜体等因素。然而,文本符号确实与其他符号有一些共同属性,例如color、angle、xoffset和yoffset。
文本符号有一些独特的属性需要处理。垂直和水平对齐位置将文本定位到点附近,因为文本符号通常放置在标签点周围。光环大小和颜色,自 ArcGIS Server 10.1 引入,描述了围绕文本的颜色轮廓,使其在繁忙的航空背景中更易于阅读。字体参数数量众多,需要自己的对象来描述标签的 CSS 样式。您可以在以下位置看到文本符号的示例:
{
"type": "esriTS",
"color": [158,38,12,255],
"backgroundColor": [0,0,0,0],
"borderLineSize": 1,
"borderLineColor": [158,38,12,255],
"haloSize": 2,
"haloColor": [255,255,128,255],
"verticalAlignment": "bottom",
"horizontalAlignment": "center",
"rightToLeft": false,
"angle": 0,
"xoffset": 0,
"yoffset": 0,
"kerning": true,
"font": {
"family": "Georgia",
"size": 14,
"style": "normal",
"weight": "bold",
"decoration": "none"
}
}
使用
虽然你很少在服务器和客户端之间看到传递的符号 JSON,但 ArcGIS JavaScript API 中的所有符号构造函数都可以接受 JSON 对象来定义符号。因此,开发者可以将符号样式存储在配置文件中,直到需要时再使用。开发者还可以在将 JSON 对象分配为符号之前,使用不同的调色板和定义小部件来构建 JSON 对象。
使用 JSON 分配符号比之前定义简单符号的方法要简单得多。旧的方法包括包含长列表参数的符号构造函数。一些构造函数可能包含嵌套构造函数。另一种构建符号的方法是创建一个基本符号,然后使用赋值方法分配必要的属性。让我们看看旧方法的例子:
require(["esri/symbols/SimpleFillSymbol", "esri/symbols/SimpleLineSymbol","esri/Color", …],
function (SimpleFillSymbol, SimpleLineSymbol, esriColor, …) {
…
var lineSymbol = new SimpleLineSymbol()
.setColor(new esriColor([255,128, 128, 0.8]))
.setWidth(3)
.setStyle(SimpleLineSymbol.STYLE_DASHDOTDOT);
var fillSymbol = new SimpleFillSymbol()
.setOutline(lineSymbol)
.setColor(new esriColor([64,255,64,0.4]));
…
});
在这里,构建了一个基本符号,并调用其他方法来分配颜色、线宽和线型。将此与使用 JSON 进行相同符号声明的相同符号进行比较:
require(["esri/symbols/SimpleFillSymbol", …],
function (SimpleFillSymbol, …) {
…
var fillSymbol = new SimpleFillSymbol({
style: "esriSFSSolid",
color: [64,255,64,102],
outline: {
color: [255,128,128,204],
width: 3,
style: "esriSLSDashDotDot"
}
});
…
});
虽然 JSON 代码占用额外的代码行,但它更紧凑且易于阅读。此外,此符号分配不需要客户端加载两个额外的模块来分配颜色和线型。所有这些都在单个构造调用中包含。
小贴士
当使用 JSON 构造符号时,您不需要添加符号中定义的每个 JSON 属性。当符号被构造时,JSON 对象的属性会混合到默认符号中。这意味着,如果您使用以下代码构造线符号,您将创建一个 1 像素宽的绿色线,而不是 1 像素宽的黑线。
Var symbol = new SimpleLineSymbol({color: [0,255,0]});
回到我们的应用
现在我们已经了解了如何处理 JSON 数据,我们可以改进我们的 2000 年人口普查地图。我们在应用样式方面已经取得了很大的进步,但地图的默认高亮颜色与网站的其他部分不协调。
那个 InfoWindow 怎么样
Y2K 社会的客户刚刚对网站给出了反馈。他们喜欢页面的调色板。不幸的是,他们抱怨说,当他们点击地图时,地图弹出窗口上的青色高亮符号与网站不协调。甚至有用户声称长时间盯着它会让他头疼。这个问题是可以解决的,我们可以做一些事情让客户满意。
让我们看看我们在上一章中编写的应用程序。如果您还没有看到第三章中的应用程序,这本书中包含了代码示例。我们使用 Dojo 框架创建了一个小部件,以与地图交互并揭示 2000 年的人口普查数据。我们将修改这个小部件,使其能够为我们修改地图属性。
在您最喜欢的文本编辑器中打开 Census.js 文件。如果您仔细查看,您会看到我们构建了人口普查小部件,然后使用 show() 和 hide() 函数切换地图的可点击性。我们希望修改地图 infowindow 的符号,但我们从哪里开始呢?
我们可以在构造函数中更改地图的 infowindow 属性,但这可能会导致问题。如果您还记得,在第二章中,我们讨论了在地图完成加载之前更改地图设置会导致错误,并可能停止应用程序。在我们对 infowindow 进行任何更改之前,我们必须检查地图是否已加载。如果没有加载,我们将为地图的加载事件分配一个事件监听器。如果已经加载,我们将继续运行事件监听器。我们将调用 _onMapLoad() 事件监听器,如下面的代码所示:
…
this.map = options.map || null;
this.domNode = srcRefNode;
if (this.map.loaded) {
this._onMapLoad();
} else {
this.map.on("load", lang.hitch(this, this._onMapLoad));
}
…
在 Census dijit constructor() 方法下方,我们将添加新的 _onMapLoad() 方法。我们知道地图已经加载,这意味着 infoWindow 对象也应该准备好了。我们将使用我们自己的符号修改 infoWindow 对象的 markerSymbol 和 fillSymbol 属性。现在,我们还没有添加 SimpleMarkerSymbol 和 SimpleFillSymbol 的模块,所以我们将在我们定义语句中添加对这些模块的引用:
define([
…
"esri/InfoTemplate",
"esri/symbols/SimpleMarkerSymbol",
"esri/symbols/SimpleFillSymbol"
], function (
InfoTemplate, MarkerSymbol, FillSymbol
) {
…
_onMapLoad: function () {
// change the infoWindow symbol
this.map.infoWindow.markerSymbol = new MarkerSymbol({});
this.map.infoWindow.fillSymbol = new FillSymbol({});
},
});
因此,我们知道符号构造函数可以接受 JSON 对象来创建符号。让我们填写我们将传递给符号的对象中的属性。我们将根据应用程序的颜色调色板选择一些颜色。我们还将调整填充符号的内部透明度,以便用户可以看到下面的内容。记住,在定义颜色时,四个数字代表红色、绿色、蓝色和不透明度值,范围从全 255 到空 0。另外,如果你不介意它使用默认值,你不必传递每个单独的符号 JSON 属性。
this.map.infoWindow.markerSymbol = new MarkerSymbol({
style: "esriSMSDiamond",
color: [255,200,30],
size: 12,
outline: {
width: 1,
color: [111,98,34]
}
});
this.map.infoWindow.fillSymbol = new FillSymbol({
style: "esriSFSSolid",
color: [255,250,169,128],
outline: {
width: 2,
color: [111,98,34]
}
});
然后,我们可以在浏览器中保存并重新加载我们的应用程序。如果我们输入的内容都是正确的,地图应该会无错误地加载。当我们点击人口普查按钮时,浮动小部件应该加载在角落,当我们放大并点击一个区域时,我们应该在选定点时看到新的黄色钻石,并且以与应用程序类似的颜色突出显示的多边形。我们希望客户会对我们的颜色选择感到满意。

前面的图像显示了带有金色杆状钻石的人口普查区块点。每个选定的点都会显示相同的图形。以下你可以看到其中一个州的样子。请注意,填充颜色是半透明的,这使用户能够阅读下面的内容:

摘要
我们已经探讨了 ArcGIS Server REST 服务端点的不同部分,这是我们的基于 ArcGIS JavaScript API 的应用程序的主要数据源。我们学习了服务 RESTful 的含义以及它如何应用于 ArcGIS Server。我们探讨了 ArcGIS Server 地图服务的组织结构。我们了解了在网页浏览器中查看 REST 服务时可以获取哪些信息,以及使用 JSON 请求时的情况。我们学习了 ArcGIS REST API,了解了数据是如何为请求格式化的,以及它如何被 ArcGIS Server 和 ArcGIS JavaScript API 消费。最后,我们将一些知识应用到改进我们的应用程序中。
通过你对客户端和服务器端数据处理的了解,你将能够实现 ArcGIS Server 为基于 Web 的应用程序提供的许多强大功能。在下一章中,我们将探讨这些强大功能之一:编辑地图数据。
第五章. 编辑地图数据
数据必须来自某个地方。在价值数十亿美元的地理空间行业中,收集数据是昂贵的。从航空摄影中可见的特征需要绘制,而在工作站上不太明显的特征需要在现场收集它们的 GPS 坐标。你的时间是宝贵的,数据收集不会自行发生。
但如果你能让别人为你工作会怎样?如果你能创建一个让其他人收集信息的网站会怎样?经过培训的工人可以记录公用事业线路,或者关心市民可以报告城镇中的问题地点。通过使用志愿者数据收集,你可以快速收集所需的数据。
ArcGIS Server 不仅提供地图上的数据可视化,还提供编辑功能。可以创建服务,并围绕它们构建应用程序,使用户能够向地图添加项目、更改它们的形状、编辑它们的属性,甚至删除它们。ArcGIS Server 还允许服务的创建者控制哪些数据更改是被允许的。
在本章中,我们将做以下事情:
-
查看 ArcGIS Server 通过 ArcGIS JavaScript API 提供的地图数据编辑功能
-
了解 API 中的编辑控制以及它们如何创建一个有用的编辑体验
-
创建一个使用 ArcGIS JavaScript API 工具的编辑应用程序
网络地图编辑的使用案例
GIS 专业人士不必独自编辑所有地图数据。经过培训的员工和热心的志愿者可以协助他们感兴趣的数据收集和地图编辑项目。作为开发者,你需要提供他们收集和更新数据的工具。以下是一些使用网络地图编辑的应用程序示例,你可以创建:
-
现场工作人员更新公用事业数据
-
公共服务请求和事件报告
-
分析后的地块分类重新分配
-
志愿者地理信息数据收集
地图编辑需求
使用 ArcGIS JavaScript API 编辑地理数据需要在 ArcGIS Server 上进行一些设置。必须在 ArcGIS Server 上发布可编辑的要素服务,这需要 ArcSDE 地理数据库。文件地理数据库、个人地理数据库和 shapefile 不能用于存储可编辑数据。ArcGIS Online 允许你将可编辑数据上传到 ESRI 的云服务,但数据上传和编辑过程有一些要求,这些要求在第十一章 ArcGIS 开发的未来 中有所介绍。
使用 ArcGIS Server 和其 JavaScript API 设置可编辑地图应用程序有一些要求。存储数据的地理数据库应该是版本化的,如果您想在提交到默认数据库之前审查数据。版本化数据还支持撤销和重做操作。您可能希望与可编辑特征服务一起发布只读地图服务。最后,一些编辑操作需要几何服务来处理几何变化,例如切割、合并和修剪特征。
特征服务
特征服务提供了一个在服务器上存储的数据与用于使用它的浏览器应用程序之间的网络接口。它们可以通过类似于地图服务的 URL 端点访问。然而,它们产生非常不同的结果。它们可以像动态或瓦片服务一样在地图上加载和查询,但不仅如此。特征服务返回图形而不是瓦片。这些图形可以被查询,如果服务允许,甚至可以编辑。
特征模板
在 ArcGIS Server 10.1 中,可以发布带有特征模板功能的服务。特征模板为用户提供预先配置的特征,以便添加到地图中。特征模板在 ArcMap 中创建,并定义了符号和预定义属性。这些模板使得编辑服务数据变得更加容易。
特征模板的一个例子可以在动物观测地图上找到。地图上的点表示动物观测发生的位置。可以创建特征模板来显示每种主要动物(猫、狗、鸟、兔子、鹿等)的图片。某些字段中的值可以在事先定义。例如,可以说所有猫都是温血的。
作为开发者,您如何利用特征模板?除了演示每个符号的含义之外,ArcGIS JavaScript API 的工具中还有模板选择器,不仅显示特征模板,还允许您点击它们并将它们添加到您的地图中。
特征层
特征层提供了访问特征类中图形的途径。因此,用户可以查询和编辑图形的形状和属性。我们在第四章在 REST 中寻找平静中回顾了它们的 REST 服务配置文件。我们以加载动态和瓦片服务相同的方式加载特征层。然而,由于内容可编辑的特性,它们的选项通常需要更多的参数。
特征服务模式
当从特征服务初始化特征层时,您可以选择如何加载数据。您是想一次性加载所有数据?您是想加载所有可见的特征?您只想加载您已选择的特征,而不显示其余的?在接下来的章节中,我们将回顾用于将数据下载到客户端浏览器的三种特征服务模式。
快照模式
有时,如果没有太多数据,一次性下载所有数据会更好。这就是快照模式的作用。快照模式根据时间定义和定义表达式下载要素数据,但受最大下载限制的限制。数据的可见性随后由时间范围确定。
如果没有太多数据要下载,或者在使用过程中可能存在连接问题,快照模式很有帮助。用户可以一次性下载所有要素数据,处理它,然后在连接变得有利时再次保存更改。
按需模式
有时,您可能只对下载眼前的数据感兴趣。在这种情况下,按需模式是最佳选择。按需模式仅下载地图范围内的要素。它们也受时间定义和定义表达式的影响。与快照模式不同,每次地图范围改变时都会进行数据请求。对于任何FeatureLayer,按需模式是默认模式。
在特征层中数据量很大,但用户只需查看其中一小部分时,通常会使用按需模式。这对于专注的编辑任务非常适用。对于包含大量地图导航和连接问题的移动应用来说,它并不那么好,因为一些图形可能无法加载。
选择模式
通过选择加载要素更为限制性,因为它只显示已选择的功能。要素选择是通过使用特征层的selectFeatures()方法来处理的,其方式类似于从地图服务层进行查询。在这种情况下,返回的图形被认为是“已选择”。选择方法包括在地图上点击并发送带有特定参数的查询。如果有很多要素,而您只想下载特定的要素,无论是通过区域还是属性,这种方法非常有帮助。
编辑工具
ArcGIS JavaScript API 附带了一套专为编辑设计的控件和模块。使用编辑控件,用户可以向地图添加要素,更改它们的形状,编辑它们的属性,如果服务允许,甚至可以删除它们。让我们看看 API 中可用的工具。
小贴士
为什么您应用程序中的编辑工具不起作用?可能是 CSS 的问题。编辑小部件是用 Dojo 用户控件或 dijits 创建的。这些控件需要 Dojo 样式表,例如claro.css或nihilo.css。如果没有它们,按钮将停止工作,并可能出现其他意外的行为。
编辑工具栏
加载了esri/toolbars/edit模块的编辑工具栏允许用户更改地图上图形的形状、方向、比例和位置。我们曾在第二章 深入 API 中讨论过它,与其他工具栏相关。需要单独的控件来保存使用编辑工具栏所做的更改。您可以在以下位置看到用于编辑工具栏的三角形图像:

编辑工具栏在其构造函数中需要一个地图。构造函数还需要一些可选参数来修改其外观和行为。许多选项取决于正在操作的数据的几何类型。截至 API 版本 3.13,以下是编辑工具栏的一些可用选项:
-
allowAddVertices(布尔值):如果为真,你可以向折线或多边形添加顶点。 -
allowDeleteVertices(布尔值):如果为真,你可以从折线或多边形中删除顶点。 -
ghostLineSymbol(线符号):当移动线或多边形边缘时,这是显示新线/边缘将去往何处的符号。 -
ghostVertexSymbol(标记符号):如果你被允许添加顶点,这是显示点击以插入顶点的符号。 -
textSymbolEditorHolder(字符串或HTML DOM 节点):当你想要添加文本符号编辑小部件时网页的位置。 -
uniformScaling(布尔值):当为真时,调整折线或多边形的大小时保持原始的宽高比。 -
vertexSymbol(标记符号):在编辑折线和多边形时,这是每个顶点的符号。
你可以在以下代码片段中看到加载编辑工具栏的示例:
require([…, "esri/toolbars/edit",
"esri/symbols/SimpleMarkerSymbol",
"esri/symbols/SimpleLineSymbol", ],
function ( …, EditToolbar, MarkerSymbol, Linesymbol, …) {
var editTB = new EditToolbar(map,… {
allowAddVertices: true,
allowDeleteVertices: true,
ghostLineSymbol: new LineSymbol(…),
ghostMarkerSymbol: new MarkerSymbol(…),
uniformScaling: false,
vertexSymbol: new MarkerSymbol(…)
});
});
当你想使用编辑工具栏来编辑一个特性时,你调用activate()方法。activate()方法需要两个参数,并有一个可选的第三个参数。首先,该方法需要一个工具,这个工具是通过将编辑工具栏常量与管道|符号组合而成的。常量包括EDIT_TEXT、EDIT_VERTICES、MOVE、ROTATE和SCALE。其次,activate()方法需要一个要编辑的图形。最后一个可选参数是一个类似于创建编辑工具栏的对象。在下面的代码片段中,我们有一个添加到地图中的图形,并且给它分配了一个点击事件,当双击该图形时激活编辑工具栏来编辑该图形:
var editTB = new EditToolbar(…);
…
map.graphics.add(myGraphic);
dojoOn(myGraphic, "dblclick", function () {
editTB.activate(EditToolbar.EDIT_VERTICES | EditToolbar.MOVE | EditToolbar.ROTATE | EditToolbar.SCALE, myGraphic);
dojoOn.once(myGraphic, "dblclick", function () {
editTB.deactivate();
});
});
属性检查器
有时候,你不在乎事物在哪里,你只关心内容。这就是属性检查器发挥作用的地方。属性检查器提供了一个带有可编辑字段列表和适当空白来编辑它们的表单。属性检查器绑定到一个要素层,并显示所选层的可编辑值。属性检查器中的字段响应于属性的字段类型。日期字段在编辑时显示日历。具有编码值域的字段显示一个下拉列表而不是文本空白。下面,你可以看到一个属性检查器加载到弹出窗口中的示例,尽管它也可以添加到单独的 HTML 元素中。

当初始化属性检查器时,您需要定义检查器将如何处理图形属性中的不同字段。属性检查器构造函数接受一个 options 对象,以及一个 HTML 元素或元素的 id 字符串引用。options 对象有一个名为 layerInfos 的参数,它接受一个 layerInfo 对象数组。每个 layerInfo 对象包含以下参数之一或多个:
-
featureLayer(必需): 要编辑的要素层。 -
userId(字符串,可选): 如果编辑需要令牌身份验证,则连接到服务的 ArcGIS Server 用户 ID。如果您已使用身份管理器来处理登录,则不需要此信息。 -
showObjectID(布尔值,可选): 当要素被选中时,您是否想看到要素的对象 ID。默认情况下,此值是false。 -
showGlobalID(布尔值,可选): 当要素被选中时,您是否想看到要素的全局 ID。默认情况下,此值是false。 -
showDeleteButton(布尔值,可选): 默认情况下,属性检查器显示一个删除按钮,允许您删除选定的要素。将此设置为 false 将移除它。 -
showAttachments(布尔值,可选): 当设置为 true,并且要素层有附件时,这将在属性检查器中显示一个附件编辑表单,允许您查看和上传附加到要素的文件。 -
isEditable(布尔值,可选): 允许您控制要素是否可编辑。这不会覆盖要素在服务器端是否可编辑。它只是阻止没有适当凭证的人编辑数据的额外方式。 -
fieldInfos(对象数组 [],可选): 允许开发者对可编辑的字段及其方式进行细粒度控制。这不允许用户编辑根据要素层的发布方法不允许编辑的字段。如果未设置此值,属性检查器将列出所有可编辑字段。FieldInfo对象包含以下内容:-
fieldname(字符串): 要编辑的字段名称。 -
format(对象,可选): 一个对象,允许您在编辑日期时编辑时间。当设置时,添加以下对象:{time: true}。 -
isEditable(布尔值,可选): 当设置为 false 时,这将禁用用户更改该字段值的权限 -
stringFieldOption(字符串,可选): 当设置时,用户可以在单行文本框、多行文本区域或包含额外格式的富文本字段中编辑字符串值。 -
label(字符串,可选): 当设置时,这允许您覆盖要素服务中的字段别名名称。 -
tooltip(字符串,可选): 当设置时,在用户开始编辑属性时显示文本工具提示。
-
-
您可以在以下位置看到属性检查器加载单个要素层的示例:
var layerInfos = [{ 'featureLayer': bananaStandFL, 'showAttachments': false, 'isEditable': true, 'format': {'time': true }, 'fieldInfos': [ {'fieldName': 'address', 'isEditable':true, 'tooltip': 'Where is it?', 'label':'Address:'}, {'fieldName': 'time_open', 'isEditable':true, 'tooltip': 'Time the Banana Stand opens.', 'label':'Open:'}, {'fieldName': 'time_closed', 'isEditable':true, 'tooltip': 'Time the Banana Stand closes.', 'label':'Closed:'}, {'fieldName': 'is_money_here', 'isEditable':false, 'label':'Is Money Here:', 'tooltip': 'There\'s money in the Banana Stand.'} ] }]; var attInspector = new AttributeInspector({ layerInfos: layerInfos }, "edit-attributes-here"); attInspector.startup();
虽然属性检查器允许您编辑地图上图形的属性,但它不提供立即保存编辑的方法。何时将属性更改保存到服务器由开发者决定。开发者可以添加一个保存按钮,或者每当功能不再被选中时自动保存。
模板选择器
模板选择器允许用户从一系列功能模板中选择,以向地图添加功能。它显示来自连接的功能层的功能模板网格。这些模板包括功能名称、几何类型和预设样式。用户可以点击任何模板按钮,然后在地图上绘制它们。您可以加载多个功能层,并且可以轻松地在它们之间切换。您可以在下面的屏幕截图中看到一个示例:

模板选择器,像大多数 dijit 一样,需要参数对象以及一个 HTML 元素或元素的 id 的字符串引用,以便加载。在选项中,模板选择器接受一个featureLayers数组。它还接受它将创建的rows或columns的数量。如果您不使用具有自己的功能模板的featureLayers,您可以使用items数组中的配置项来定义自己的模板。您还可以直接设置 CSS 样式。最后,您可以控制当您悬停在符号上时是否显示工具提示。在下面的代码片段中,您可以看到一个初始化的模板选择器示例:
var widget = new TemplatePicker({
featureLayers: layers,
rows: "auto",
columns: 9,
showTooltip: true,
style: "height: 100%; width: 900px;"
}, "templatePickerDiv");
上述代码显示了一个具有九列和显示其featureLayers属性中加载的layers数据的工具提示的模板选择器。宽度为 900 像素,高度根据需要调整。
附件编辑器
有句老话说,一张图胜千言。有时,你需要那张图来解释你提交的数据。附件编辑器可以帮到您。附件编辑器允许应用程序上传文件,通常是图像,并将其连接到地图上的功能。您可以查看其他附件,如果权限允许,还可以编辑或删除它们。附件编辑器可以作为属性检查器的一部分加载,通过在构建编辑器时将属性编辑器选项中的showAttachments属性设置为true:

编辑器 dijit
编辑器 dijit提供了一个一站式编辑工具,用于创建、更新和删除地图功能。编辑器 dijit 包括模板选择器、属性检查器和带有众多工具的编辑工具栏。它允许您在地图上绘制新功能、编辑现有功能,还可以删除功能。
编辑器 dijit 提供的工具如下:
-
功能选择工具,用于添加新选择、进一步添加到现有选择或从现有选择中删除
-
功能绘制工具,包括从地图中删除功能的功能
-
允许您剪切、合并和裁剪折线和多边形部分的工具
-
撤销和重做操作(需要版本化地图服务)
捕捉管理器
当创建编辑工具时,你可能会收到一个常见的请求:“我想有一个工具,让我可以根据另一个特征的线条来编辑这个特征”。你可以尝试编写自己的工具来选择一个特征并遍历特征的每个阶段。或者,通过一些额外的设置,你可以实现地图的捕捉管理器。
捕捉管理器在浏览器中模仿 ArcMap 的捕捉控制。当你的鼠标指针接近图形特征的角落或边缘时,例如在一个GraphicsLayer或一个FeatureLayer中,一个新的指针会移动到特征的点上。这显示了如果你在地图上点击,你会在哪里添加一个点。你可以沿着一系列的点、线顶点或多边形角落点击,使用这个工具绘制与现有特征完美对齐的东西。
在加载捕捉管理器时,有一些重要的选项需要设置。每个捕捉管理器都需要一个地图来捕捉。它还需要加载一个图形层或特征层,以及有关其捕捉行为的信息。它应该知道是否要捕捉到线或多边形的边缘或顶点,以及是否要捕捉到点特征类的点。所有这些信息都添加在其构造函数选项中的layerInfo数组中,或者可以通过使用setLayerInfos()方法稍后添加。
在捕捉管理器中还有其他可选的可配置项。你可以告诉捕捉管理器始终捕捉到图形,或者你希望在点击时按住键盘上的某个键来控制捕捉。你还可以配置哪个键盘键是snapKey,通过加载适当的dojo/keys常量来设置该属性。最后,捕捉管理器的tolerance指的是指针在捕捉到特征之前应该与特征的最大像素数。
在以下代码中,你可以看到一个在 JavaScript API 中加载的捕捉管理器的示例:
require([…, "esri/SnappingManager", "dojo/keys", …],
function (…, SnappingManager, dojoKeys …) {
var propertyLayer = new FeatureLayer({…});
var sm = new SnappingManager({
alwaysSnap: false, // default: false
map: map,
snapKey: dojoKeys.CTRL, // default: dojoKeys.copyKey
tolerance: 10, // default: 15
layerInfo: [{
layer: propertyLayer, // this is a featureLayer,
snapToEdge: false, // default: true
snapToVertex: true //default: true
}]
});
…
});
上述示例显示了一个在用户在 PC 上按下Ctrl键(在 Mac 上为Command键)时开启的捕捉管理器。它只捕捉到propertyLayer特征层中线条或多边形的角落。捕捉的tolerance设置为 10 像素。
保护编辑服务
如果你打算公开你的数据供公众编辑,你需要准备好应对麻烦。从不良数据输入到恶意攻击,作为开发者,你需要考虑到可能出现的问题。幸运的是,ArcGIS Server 和 ArcGIS API for JavaScript 可以帮助你。
限制用户输入
我记得有一个项目,我们需要让用户根据另一个系统提供的列表来搜索地址。那个系统对用户可以输入的内容没有任何限制。结果,地址列表非常不正常。在一条街道上,可能会有十五种不同的方式来列出街道名称。有的全部大写,而有的用 "Rd" 代替 "Road"。有的拼写错误,一个 m 而不是两个,还有一些在街道名称和后缀之间有太多的空格。不用说,数据构建得很差,没有标准化。
ArcGIS Server 提供了一些工具来帮助您限制用户输入。在地理数据库中实现编码值域和范围可以帮助减少不良输入。属性检查器尊重字段属性,如长度和数据类型。您可以在要素服务要素模板中设置默认值,以限制额外的用户输入。
您还可以将验证和其他控件结合起来,以确保用户不会意外地做一些像将电话号码添加到日期列之类的事情。Dojo 自带用户控件,如验证文本框,可以限制不良输入。
密码保护服务
当涉及到保护您的编辑服务时,ArcGIS Server 也提供了一个更好的选项。如果您想限制对编辑数据的访问,您可以为地图服务要求基于令牌的认证。令牌是一个包含用户名、过期日期和用于验证目的的额外信息的加密字符串。您需要从 http://myserver/arcgis/tokens 请求令牌,其中 myServer 指的是您的 ArcGIS Server 网络端点或网络适配器。在令牌作为 cookie 添加到您的浏览器之前,您需要提交必要的用户名和密码。令牌只在有限的时间内有效,这可以通过在 ArcGIS Server 中的可配置设置进行调整。
这些基于令牌的安全措施与地图服务和可编辑要素服务都兼容。没有令牌,您无法在浏览器中看到受保护地图服务。有了它,您可以探索受保护的服务,查询它们,甚至编辑其中的数据。
身份管理器
身份管理器(esri/IdentityManager)用于处理登录以及 ArcGIS Server 和 ArcGIS Online 服务的安全性。当您尝试在浏览器中加载受令牌保护的服务时,身份管理器会显示用户名和密码提示。其用户界面使用 Dojo UI 控件,因此需要加载适当的 Dojo 样式表,以便身份管理器正常工作。
现在我们已经回顾了 ArcGIS Server 提供的部分编辑功能,让我们将这些知识应用到应用程序中。
新的地图应用程序
那么,回到我们的故事和我们的地图应用程序。我们仍在等待 Y2K 协会关于人口普查地图的消息,但我们有一个新的应用程序,我们被要求去工作。看起来加利福尼亚州的霍利斯特市要求我们为他们制作一个应用程序。让我们看看他们想要什么。
霍利斯特市希望创建一个应用程序,让市民能够报告城市中的问题。他们希望市民能够在地图上报告涂鸦、人行道、路缘石和街道问题、损坏的财产、下水道问题和树木问题,并提供额外的信息。如果可能的话,他们希望有问题的照片,以便工作人员知道要寻找什么。
文件设置
由于我们将使用现成的 ArcGIS JavaScript API 编辑工具,我们不需要创建任何自定义包。相反,我们将创建一个简单的文件设置,包含一个css和一个js文件夹。我们将在css文件夹中添加我们的自定义style.css样式表,在js文件夹中添加我们的app.js文件。我们还将添加一个名为proxy的文件夹来处理我们的代理服务。文件结构应类似于以下内容:

首页
让我们从 HTML 文档开始。我们将使用我们的基本 cookie-cutter 网站。这次,我们将添加 Dojo 的claro.css样式表。我们不需要任何自定义包,因此我们可以将这些包从dojoConfig文件中排除。我们希望有一个沿着页面侧边的长列,以及一个放置页面布局标题的页眉部分。我们将加载具有侧边栏设计的BorderContainer,其中较长的侧边栏。我们将为页眉添加三个ContentPanes,一个用于按钮的引导列,以及一个用于地图的中心区域。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no" />
<title>Incident Reporting App</title>
<meta name="description" content="">
<meta name="author" content="Ken Doman">
<link rel="stylesheet" href="http://js.arcgis.com/3.13/dijit/themes/claro/claro.css">
<link rel="stylesheet" href="https://js.arcgis.com/3.13/esri/css/esri.css" />
<link rel="stylesheet" href="./css/style.css" />
<script type="text/javascript">
dojoConfig = {
async: true,
isDebug: true
};
</script>
<script src="img/"></script>
</head>
<body class="claro">
<div id="mainwindow"
data-dojo-type="dijit/layout/BorderContainer"
data-dojo-props="design:'sidebar', gutter:false"
style="width: 100%; height: 100%; margin: 0;">
<div data-dojo-type="dijit/layout/ContentPane" data-dojo-props="region:'top'">
<h1>Incident Reporting App</h1>
</div>
<div id="map" data-dojo-type="dijit/layout/ContentPane" data-dojo-props="region:'center'"></div>
<div id="editpane" style="width: 130px" data-dojo-type="dijit/layout/ContentPane" data-dojo-props="region:'leading'">
<div id="editordiv"></div>
</div>
</div>
<script type="text/javascript" src="img/app.js"></script>
</body>
</html>
我们将在style.css页面为 HTML 和 body 添加一些基本的样式。让我们添加以下样式:
html, body {
width: 100%;
height: 100%;
border: 0;
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Helvetica, Arial, sans-serif;
}
*, *:before, *:after { box-sizing: inherit;}
我们已将 HTML 和body的width和height设置为100%,没有边框、边距或填充。我们还更改了字体为常见的sans-serif字体,如Helvetica、Arial或简单的sans-serif。最后,我们设置页面上的元素使用border-box的box-sizing,这使得在页面上处理尺寸框更容易。
加载地图
我们将从页面设置开始编写我们应用程序的代码。我们有一个焦点区域,即霍利斯特市。为了方便起见,我们将添加城市边界作为范围:
require([
"dojo/parser", "esri/map", "esri/graphic",
"esri/geometry/Extent", "esri/dijit/editing/Editor",
"esri/dijit/editing/TemplatePicker", "esri/tasks/query",
"dijit/layout/BorderContainer", "dijit/layout/ContentPane",
"dojo/domReady!"
], function (parser, Map, Graphic, Extent,Editor, TemplatePicker, Query) {
var maxExtent = new Extent({
"xmin":-13519092.335425414,
"ymin":4413224.664902497,
"xmax":-13507741.43672508,
"ymax":4421766.502813354,
"spatialReference":{"wkid":102100}
}),
map, selected, updateFeature, attInspector;
parser.parse();
map = new Map("map", {
basemap: "osm",
extent: maxExtent
});
});
在前面的代码中,我们已加载必要的模块并使用dojo/parser解析它们。我们已添加基于 OpenStreetMap 的基本地图,并创建了一个maxExtent来模拟城市边界。
添加地图图层
现在我们有了地图,我们需要向地图添加图层。为了这个练习,我们将使用 ESRI 提供的旧金山 311 特征服务。我们将以选择模式加载特征层,这样我们只影响我们点击的特征。我们还将添加互补的动态地图服务,因为没有它我们无法看到特征。我们还将使用简单的标记符号设置特征层选择符号,以着色我们点击的特征:
require([
…,
"esri/layers/FeatureLayer",
"esri/layers/ArcGISDynamicMapServiceLayer",
"esri/symbols/SimpleMarkerSymbol",
…
], function (…,
FeatureLayer, ArcGISDynamicMapServiceLayer,
MarkerSymbol, …
) {
var maxExtent = …,
map, incidentLayer, visibleIncidentLayer;
…
incidentLayer = new FeatureLayer("http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/SanFrancisco/311Incidents/FeatureServer/0", {
mode: FeatureLayer.MODE_SELECTION,
outFields: ["req_type","req_date","req_time","address","district", "status"],
id: "incidentLayer"
});
visibleIncidentLayer = new ArcGISDynamicMapServiceLayer( "http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/SanFrancisco/311Incidents/MapServer");
…
map.addLayers([visibleIncidentLayer, incidentLayer]);
当地图图层被添加后,我们最终可以与之交互,无论是作为用户还是作为开发者。我们将向地图的 layers-add-result 事件添加一个名为 startEditing() 的事件监听器。我们将在那里设置要素层的编辑事件。我们将添加一个地图点击事件,如果已从页面侧边的菜单中选择了一些内容,则会绘制一个要素。务必在定义图层之后、将它们添加到地图之前添加此事件。
var …, incidentLayer, visibleIncidentLayer, selected;
visibleIncidentLayer = …;
function startEditing () {
var incidentLayer = map.getLayer("incidentLayer");
// add map click event to create the new editable feature
map.on("click", function(evt) {
// if a feature template has been selected.
if (selected) {
var currentDate = new Date();
var incidentAttributes = {
req_type: selected.template.name,
req_date:(currentDate.getMonth() + 1) + "/" + currenDate.getDate() + "/" + currentDate.getFullYear(),
req_time: currentDate.toLocaleTimeString(),
address: "",
district: "",
status: 1
};
var incidentGraphic = new Graphic(evt.mapPoint, selected.symbol, incidentAttributes);
incidentLayer.applyEdits([incidentGraphic],null,null)
}
});
incidentLayer.setSelectionSymbol(
new MarkerSymbol({color:[255,0,0]})
);
map.infoWindow.on("hide", function() {
incidentLayer.clearSelection();
});
}
incidentLayer.on("edits-complete", function() {
visibleIncidentLayer.refresh();
});
map.on("layers-add-result", startEditing);
map.addLayers([visibleIncidentLayer, incidentLayer]);
在前面的代码中,我们创建了一个名为 startEditing() 的 callback 函数。这会导致每当地图被点击时,应用程序都会向可编辑要素层添加一个新的图形。可编辑要素应用了默认属性和符号。每当弹出窗口被隐藏时,可编辑要素层会清除其选择。此外,当编辑完成后,可见层会使用新数据刷新。当一组图层被添加时,startEditing() 方法会被分配执行,这会导致图层被添加到地图中。
使用代理
如果您现在尝试加载地图,可能会出现错误。如果您现在没有出现错误,您可能在尝试在地图上保存更改时出现错误。原因是这些编辑操作通常需要一个代理应用程序来处理数据,这些数据太大,无法适应大多数浏览器 GET 请求的大约 2,048 个字符的限制。
注意
您可以遵循 ESRI 提供的说明来设置代理服务,请参阅 developers.arcgis.com/javascript/jshelp/ags_proxy.html。
代理根据您的应用程序环境有三种类型。ESRI 提供了基于 PHP、Java 和 .Net 的代理服务。我们将在应用程序中添加对代理的引用。以下示例展示了如何使用基于 .Net 的代理来完成此操作:
require([
…,
"esri/config",
"esri/layers/FeatureLayer",
"esri/layers/ArcGISDynamicMapServiceLayer",
"esri/symbols/SimpleMarkerSymbol",
…
], function (…, esriConfig, …) {
…
// set up proxy for the featureLayer
esriConfig.defaults.io.proxyUrl = "./proxy/proxy.ashx";
incidentLayer = …
查找用户的位置
我们的客户要求应用程序能够使用户在地图上找到他们,无论他们是在使用移动设备还是 Wi-Fi 上的笔记本电脑。我们可以通过添加一个名为 LocateButton 的 ArcGIS dijit 来提供该功能。我们在应用程序中加载该模块,在地图准备好时初始化它,然后就可以使用了。加载它的代码可能看起来像这样:
require([…, "esri/dijit/LocateButton", …
], function (…, LocateButton, …) {
…
function startEditing() {
// add the Locate button
var locator = new LocateButton({map: map}, "locatebutton");
}
…
});
如果我们在地图 ContentPane 内插入一个具有 id 为 locatebutton 的 <div>,并在我们的浏览器中查看页面,我们会看到定位按钮位于地图上方,将地图向下推。我们更愿意将它定位在其他的缩放按钮附近。我们将添加以下样式到 style.css 表格中以达到这个目的:
.LocateButton {
position: absolute;
left: 29px;
top: 120px;
z-index: 500;
}
模板选择器
对于我们的应用程序,我们将使用 ArcGIS JavaScript API 的模板选择器来选择要添加到地图中的事件点类型。我们将在页面侧边栏中加载它们,并将它们设置为单列宽以添加要素。当选择要素模板时,我们将该要素模板传递给 selected 变量。最后,当地图和要素层都加载完成后,我们将加载所有这些内容:
function generateTemplatePicker(layer) {
console.log("layer", layer);
var widget = new TemplatePicker({
featureLayers: [ layer ],
rows: layer.types.length,
columns: 1,
grouping: false,
style: "width:98%;"
}, "editordiv");
widget.startup();
widget.on("selection-change", function() {
selected = widget.getSelected();
console.log("selected", selected);
});
}
…
function startEditing () {
var incidentLayer = map.getLayer("incidentLayer");
generateTemplatePicker(incidentLayer);
…
属性检查器
现在我们能够向地图添加新功能,我们需要一种方法来编辑这些功能的内容。为此,我们将添加属性检查器。我们将初始化属性检查器并将其绑定到地图的infoWindow。
require([…,
"dojo/dom-construct",
"esri/dijit/AttributeInspector",
"dijit/form/Button",
…
], function (…, domConstruct, AttributeInspector, Button, …) {
var maxExtent = …,
map, incidentLayer, visibleIncidentLayer, selected,
attInspector, updateFeature;
…
function generateAttributeInspector(layer) {
var layerInfos = [{
featureLayer: layer,
showAttachments: true,
isEditable: true,
}];
attInspector = new AttributeInspector({
layerInfos: layerInfos
}, domConstruct.create("div", null, document.body));
attInspector.startup();
//add a save button next to the delete button
var saveButton = new Button({ label: "Save", "class": "saveButton"});
domConstruct.place(saveButton.domNode, attInspector.deleteBtn.domNode, "after");
saveButton.on("click", function(){
updateFeature.getLayer().applyEdits(
null, [updateFeature], null
);
});
attInspector.on("attribute-change", function(evt) {
//store the updates to apply when the save button is clicked
updateFeature.attributes[evt.fieldName] = evt.fieldValue;
});
attInspector.on("next", function(evt) {
updateFeature = evt.feature;
console.log("Next " + updateFeature.attributes.objectid);
});
attInspector.on("delete", function(evt){
evt.feature.getLayer().applyEdits(
null, null, [updateFeature]
);
map.infoWindow.hide();
});
if (attInspector.domNode) {
map.infoWindow.setContent(attInspector.domNode);
map.infoWindow.resize(350, 240);
}
}
…
function startEditing () {
var incidentLayer = map.getLayer("incidentLayer");
generateTemplatePicker(incidentLayer);
generateAttributeInspector(incidentLayer);
…
我们需要在属性检查器的保存按钮上添加一点定位样式。我们将把这个条目添加到style.css样式表中,以便它不会覆盖删除按钮。
.saveButton {
margin: 0 0 0 48px;
}
现在属性检查器已加载,我们可以将其集成到地图层和事件层的点击事件中。我们将创建一个showInspector()函数,该函数接受一个地图点击事件。它将查询incidentLayer以获取该位置上的任何功能,并弹出一个包含属性检查器的地图infoWindow。它还将选定的图形(如果有)分配给:
…
function showInspector(evt) {
var selectQuery = new Query(),
point = evt.mapPoint,
mapScale = map.getScale();
selectQuery.geometry = evt.mapPoint;
incidentLayer.selectFeatures(selectQuery, FeatureLayer.SELECTION_NEW, function (features) {
if (!features.length) {
map.infoWindow.hide();
return;
}
updateFeature = features[0];
map.infoWindow.setTitle(updateFeature.getLayer().name);
map.infoWindow.show(evt.screenPoint, map.getInfoWindowAnchor(evt.screenPoint));
});
}
…
function startEditing() {
…
map.on("click", function (evt) {
…
if (selected) {
…
incidentLayer.applyEdits([incidentGraphic],null,null)
.then(function () {
showInspector(evt);
});
} else {
showInspector(evt);
}
…
});
…
incidentLayer.on("click", showInspector);
前面代码的结果显示在下述屏幕截图:

保护应用程序
现在我们已经有一个工作状态的事件报告应用程序,是时候考虑如何保护应用程序了。这是一个公开的应用程序,因此公众应该被允许提交问题。然而,我们不希望数据不符合我们的数据模式,或者我们的代表边界。
我们可以确保应用程序免受不良输入影响的一种方法是对我们接受更改的位置进行限制。我们不希望员工浪费时间调查记录在市、州甚至国家之外的系统投诉。我们可以通过使用应用程序开始时提供的城市范围来实现这一点。我们可以在点击事件中测试点击的点是否位于城市范围内,如果不在,就通知客户端。这应该看起来像下面这样:
…
map.on("click", function (evt) {
// if the clicked point isn't inside the maxExtent
if (!maxExtent.contains(evt.mapPoint)) {
alert("Sorry, that point is outside our area of interest.");
return; // disregard the click
}
…
谈到与范围一起工作,我们还可以在导航出城市区域时锁定选择按钮。这将提醒用户我们不接受城市区域外的投诉。当然,我们应该通知用户他们为何被锁定。
我们将从在 HTML 中添加阻塞内容和通知开始。我们将在页面上添加两个div,一个具有outsidemessage ID 的div位于地图中,另一个div与编辑div相邻,具有blockerdiv ID。我们将通过添加内联样式display: none将它们默认设置为隐藏。它应该看起来像下面这样:
…
<div id="map" data-dojo-type="dijit/layout/ContentPane" data-dojo-props="region:'center'" >
<div id="locatebutton"></div>
<div id="outsidemessage" style="display:none;">
<p>Sorry, but you have navigated outside our city. Click on this message to get back to the city.</p>
</div>
</div>
<div id="editpane" style="width: 130px"
data-dojo-type="dijit/layout/ContentPane"
data-dojo-props="region:'leading'">
<div id="editordiv"></div>
<div id="blockerdiv" style="display:none;"></div>
</div>
我们将在style.css文件中添加以下样式来美化这些项目。外部消息将以灰色块状浮动在地图的中间部分,文字足够大以便阅读,并且具有圆角(因为很多人喜欢圆角)。阻塞的div将直接位于模板选择按钮的上方。当可见时,阻塞的div将是浅灰色,半透明,并覆盖整个模板选择器。
#blockerdiv {
width: 100%;
background: rgba(188, 188, 188, 0.6);
position: absolute;
top: 0;
left: 0;
bottom: 0;
z-index: 500;
}
#outsidemessage {
position: absolute;
bottom: 40px;
left: 50%;
width: 200px;
height: auto;
margin: 0 0 0 -100px;
background: rgba(255,255,255,0.8);
padding: 8px;
z-index: 500;
border-radius: 8px;
text-align: center;
font-weight: bold;
}
我们将在app.js文件中添加一些代码来处理这两个节点的可见性。我们可以监听地图范围的更改。当地图的范围超出城市范围,并且它们不再相交时,消息 div 和阻塞性 div 都将变为可见(display: block;)。如果用户可以看到查看区域的一部分,div对象将再次隐藏(display: none;)。它应该看起来像这样:
require([…, "dojo/dom-style", …
], function (…, domStyle, …) {
function onMapExtentChange (response) {
if (!response.extent.intersects(maxExtent)) {
// show these blocks if the map extent is outside the
// city extent
domStyle.set("blockerdiv", "display", "block");
domStyle.set("outsidemessage", "display", "block");
} else {
// hide these blocks if the max Extent is visible within
// the view.
domStyle.set("blockerdiv", "display", "none");
domStyle.set("outsidemessage", "display", "none");
}
}
map.on("extent-change", onMapExtentChange);
…
我们还将向外部消息div添加一个事件处理程序,允许用户点击返回地图的起始位置。我们将加载dojo/on事件来处理click事件:
require([…, "dojo/dom", "dojo/on", …
], function (…, dojoDom, dojoOn, …) {
…
dojoOn(dojoDom.byId("outsidemessage"), "click", function () {
map.setExtent(maxExtent);
})
…
});
现在,当我们加载我们的应用程序并平移到城市界限之外时,应该出现以下消息:

限制数据输入
除了阻止来自点击的不良用户输入外,我们还应该考虑阻止来自文本编辑的不良用户输入。我们可以通过修改分配给属性检查器的layerInfos数组来实现这一点。我们将首先移除删除按钮,因为我们不希望市民删除其他人的投诉。我们还将修改fieldInfos列表,并将一些字段设置为仅在属性检查器中编辑时显示。在这种情况下,我们将保留req_type、地址和区域标签页以供编辑。
var layerInfos = [{
featureLayer: layer,
showAttachments: true,
isEditable: true,
showDeleteButton: false,
fieldInfos: [
{'fieldName': 'req_type', 'isEditable':true, 'tooltip': 'What\'s wrong?', 'label':'Status:'},
{'fieldName': 'req_date', 'isEditable':false, 'tooltip': 'Date incident was reported.', 'label':'Date:'},
{'fieldName': 'req_time', 'isEditable':false,'label':'Time:'},
{'fieldName': 'address', 'isEditable':true, 'label':'Address:'},
{'fieldName': 'district', 'isEditable':true, 'label':'District:'},
{'fieldName': 'status', 'isEditable':false, 'label':'Status:'}
]
}];
这些是我们可以做的简单事情之一,以帮助保护我们的应用程序免受不受欢迎的结果的影响,同时仍然使应用程序对公众用户友好。
摘要
在本章中,我们探讨了 ArcGIS Server 和 ArcGIS API for JavaScript 提供的工具和流程,以使网络编辑成为可能。我们研究了可编辑要素服务中包含的内容。我们还探讨了 ArcGIS JavaScript API 附带的各种小部件,用于添加新要素、编辑几何形状和编辑属性属性。我们通过创建一个使用编辑工具创建事件报告应用程序的应用程序来结束,用户可以使用该应用程序在地图上报告城市中的问题。
在下一章中,我们将对现有数据进行图形上的调整。
第六章. 绘制进度图表
在地理上显示数据为用户提供位置意识,但有些人想看到的不仅仅是地图上的点和一些数字。他们想看到每个位置的数据如何进行比较,无论是在地图上还是在位置内部。其他显示数据的方法,如图表和图形,可以提供更多信息。
图表和图形是商业的大事。公司花费数百万美元创建执行仪表板,这些仪表板是图表和图形的组合,与公司数据和指标相连。它们之所以有效,是因为人类在处理大量抽象数字方面不如计算机,但在处理视觉数据方面做得更好。好的图表和图形可以一目了然地提供可比数据,任何人都能理解。
在本章中,我们将学习:
-
如何使用 ArcGIS JavaScript API 和 Dojo 框架提供的工具创建图表和图形
-
如何使用
D3.js实现相同的图表和图形 -
如何添加外部库如
D3.js作为 AMD 模块
将图形混合到我们的地图中
正如我们之前所学的,ArcGIS JavaScript API 不仅包含创建地图和文本的工具。建立在 Dojo 框架之上,ArcGIS API 附带了许多用户控件和小部件,以帮助您展示数据。我们可以创建动态表格、图表、图形和其他数据可视化。
但您不仅限于 API 提供的图表和图形工具。使用 Dojo 的 AMD 风格,您可以将框架之外的库集成到小部件的构建中,并在需要时加载它们。如果您与更熟悉像D3.js这样的库的团队成员合作,您可以将库异步加载到您的组件中,并让其他人开发图形。
在本章中,我们将探讨内部和外部图形库,以将图形添加到我们的数据中。我们将使用 ArcGIS JavaScript API 中打包的dojox/charting (dojotoolkit.org/reference-guide/1.10/dojox/charting.html)模块,我们还将使用D3.js (d3js.org)实现图形,这是一个流行的数据可视化库。
我们的故事还在继续
Y2K 协会的客户又提出了另一个请求。他们不喜欢我们添加到人口弹出窗口中的表格。所有的大数字让一些用户感到不知所措。他们更愿意看到数据以图形的形式表示,这样用户就可以看到数据之间的关系。
Y2K 协会特别要求为种族和性别数据提供饼图。我们可以为种族数据使用任何颜色,但对于性别,他们提出了具体的颜色要求。他们想使用水平条形图来表示年龄数据,因为他们看到人口数据以这种方式展示,并且喜欢这种外观。他们希望其他一些数据也能以图表和图形的形式展示,但他们愿意将如何展示的决定权交给我们。
在这一轮中,我们将尝试两种不同的方法,看看客户更喜欢哪一种。我们将使用这两个库以相同的数据创建图表,并将它们添加到人口普查数据弹出窗口中,看看客户更喜欢哪一个。
使用 dojox 图表
我们首先应该查看 ArcGIS API for JavaScript,看看它有什么可以提供的。我们可以使用dojox/charting访问一系列图表资源。这些模块允许您在浏览器中绘制线形图、饼图、柱状图以及大量其他图表。它包含许多预定义的主题来展示您的数据,并且可以扩展以使用您自定义的主题。图表库可以在可缩放矢量图形(SVG)、矢量标记语言(VML)、Silverlight 和 Canvas 上渲染,这使得它们既适用于较新的浏览器,也适用于像 IE7 这样的旧浏览器。
与大多数 Dojo 组件一样,dojox/charting可以在 HTML 中声明性地渲染图表,也可以通过 JavaScript 编程方式渲染。声明性图表利用了data-dojo属性。在接下来的练习中,我们将探索编程示例,因为它们更动态,当出现问题时更容易调试。
在 JavaScript 中创建图表
dojox/charting没有单一的模块可以处理您所有的绘图需求。dojox/charting中的这四个主要模块类可以加载以创建独特的图表或图形,并添加新的外观和功能:
-
图表对象
-
图表样式
-
图表主题
-
图表动作和效果
以下是对四种模块类型按顺序加载的示例:
require([
"dojox/charting/Chart", // object
"dojox/charting/plot2d/Pie", // style
"dojox/charting/themes/MiamiNice", // theme
"dojox/charting/action2d/Highlight", // action
"dojox/charting/action2d/Tooltip", // action
"dojo/ready"],
function(Chart, Pie, MiamiNice, Highlight, Tooltip, ready){
ready(function(){
var myChart = new Chart("myChart ");
myChart.setTheme(MiamiNice)
.addPlot("default", {
type: Pie,
font: "normal normal 11pt Tahoma",
fontColor: "black",
labelOffset: -30,
radius: 80
}).addSeries("Series A", [
{y: 3, text: "Red", stroke: "black", tooltip: "Red Alert"},
{y: 5, text: "Green", stroke: "black", tooltip: "Green Day"},
{y: 8, text: "Blue", stroke: "black", tooltip: "I'm Blue!"},
{y: 13, text: "Other", stroke: "black", tooltip: "A bit different"}
]);
var anim_a = new Highlight(myChart, "default");
var anim_b = new Tooltip(myChart, "default");
myChart.render();
});
});
图表对象
图表对象,加载了dojox/charting/Chart模块,是您创建和修改图表的主要对象。几乎所有的自定义设置都将通过此对象执行。图表对象通过页面上的 HTML 元素创建,可以是节点或与节点 id 匹配的字符串。在下面的代码中,您可以看到一个创建简单图表的示例:
require(["dojox/charting/Chart", "dojox/charting/axis2d/Default", "dojox/charting/plot2d/Lines", "dojo/ready"],
function(Chart, Default, Lines, ready){
ready(function(){
var chart1 = new Chart("fibonacci");
chart1.addPlot("default", {type: Lines});
chart1.addAxis("x");
chart1.addAxis("y", {vertical: true});
chart1.addSeries("Series 1", [1, 1, 2, 3, 5, 8, 13, 21]);
chart1.render();
});
});
在前面的代码中,生成了一条线形图,其中一系列的值从1增加到21。
图表对象的构建也可以接受一个选项对象。在这些选项中,您可以添加地图标题和控制元素,例如标题文本、位置、字体、颜色以及标题和图表之间的间隙。
dojox/charting库还包括一个名为dojox/charting/Chart3D的 3D 图表库。该对象可以渲染三维图表和图形,可以旋转和缩放以获得对数据的良好视角。在下面的代码中,您可以看到一个 3D 柱状图的示例:
require(["dojox/charting/Chart3D", "dojox/charting/plot3d/Bars", "dojox/gfx3d/matrix", "dojo/ready"
], function(Chart3D, Bars, m, ready){
ready(function(){
var chart3d = new Chart3D("chart3d", {
lights: [{direction: {x: 6, y: 6, z: -6}, color: "white"}],
ambient: {color:"white", intensity: 3},
specular: "white"
},
[m.cameraRotateXg(10), m.cameraRotateYg(-10), m.scale(0.8), m.cameraTranslate(-50, -50, 0)]),
bars3d_a = new Bars(500, 500, {gap: 8, material: "red"}),
bars3d_b = new Bars(500, 500, {gap: 8, material: "#0F0"}),
bars3d_c = new Bars(500, 500, {gap: 8, material: "blue"});
bars3d_a.setData([3, 5, 2, 4, 6, 3, 2, 1]);
chart3d.addPlot(bars3d_a);
bars3d_b.setData([5, 6, 4, 2, 3, 1, 5, 4]);
chart3d.addPlot(bars3d_b);
bars3d_c.setData([4, 2, 5, 1, 2, 4, 6, 3]);
chart3d.addPlot(bars3d_c);
chart3d.generate().render();
});
});
在前面的代码中,已经生成了一个 3D 柱状图,有三组数据分别用红色、绿色和蓝色着色。然后通过一个稍微旋转的相机来查看这些值,以增加图像的透视感。
图表样式
图表样式描述了我们正在创建的图表类型。它定义了我们是将数据加载为折线图还是柱状图、饼图还是散点图。对于二维图表,你可以在dojox/charting/plot2d文件夹中找到这些样式。图表样式可以分为五大主要类别,如下所示:
-
线条:这些是典型的折线图,可能显示或不显示个别数据点。
-
堆叠线:与折线图类似,但高度是堆叠在一起的。这些允许用户比较随时间变化的绘图数据的综合效果以及比例的变化。
-
柱状图:通过图表上的行宽比较值。
-
柱状图:通过相关柱的高度比较数量。
-
杂项:当其他图表无法像之前那样按类别分组时,它们就会归入这个类别。这个组包括饼图、散点图和气泡图。
如果你正在使用 3D 图表,这些图表的样式可以在dojox/charting/plot3d文件夹中找到。为了充分利用 3D 样式,最好加载dojox/gfx3d/matrix模块以实现 3D 图形效果。matrix模块允许你旋转 3D 图形,以便获得 3D 图表的良好视角。
图表主题
图表主题描述了图表和图形中文本元素的颜色、阴影和文本格式。Dojo 框架附带了许多预定义的主题,你可以从中选择,在dojox/charting/themes中。
注意
你可以通过访问archive.dojotoolkit.org/nightly/checkout/dojox/charting/tests/test_themes.html来查看不同的主题外观。
以下示例是加载具有MiamiNice主题的图表的代码。在这个例子中,我们加载了一个带有x轴和y轴的折线图。我们使用setTheme()方法将主题设置为MiamiNice。然后,我们添加了要绘制和渲染的数字系列:
require(["dojox/charting/Chart", "dojox/charting/axis2d/Default", "dojox/charting/plot2d/Lines", "dojox/charting/themes/MiamiNice", "dojo/ready"],
function(Chart, Default, Lines, MiamiNice, ready){
ready(function(){
var chart1 = new Chart("simplechart");
chart1.addPlot("default", {type: Lines});
chart1.addAxis("x");
chart1.addAxis("y", {vertical: true});
chart1.setTheme(MiamiNice);
chart1.addSeries("Fibonacci", [1, 1, 2, 3, 5, 8, 13, 21]);
chart1.render();
});
});
如果你找不到适合你的主题,或者如果你需要在应用程序设计中遵循特定的颜色和样式,你可以使用SimpleTheme对象来帮助定义你的自定义主题。SimpleTheme基于GreySkies主题,但可以扩展为其他颜色和任何你选择的格式。你不需要定义主题的每个属性,因为SimpleTheme应用了所有未用自定义样式覆盖的默认值。你可以在以下位置查看实现SimpleTheme的代码示例:
var BaseballBlues = new SimpleTheme({
colors: [ "#0040C0", "#4080e0", "#c0e0f0", "#4060a0", "#c0c0e0"]
});
myChart.setTheme(BaseballBlues);
提示
主题通常在其调色板中使用不超过五种颜色。如果你需要为数据集添加更多颜色,将颜色十六进制字符串push()到主题的.color数组中,但必须在设置图表主题之前这样做。
图表动作和效果
创建吸引人的图表和图形可能对你来说很有趣,但现代网络时代的用户期望与数据进行交互。他们期望当鼠标悬停在图表元素上时,图表元素会发光、生长并改变颜色。他们期望在点击饼图时发生某些事情。
dojox/charting/action2d 包含使图表更具教育性和交互性的图表动作和效果。你不必过度使用动作,让你的图表做所有的事情。你可以简单地应用你需要的事件来让用户感受到效果。以下是一个基本动作和效果的列表,以及相应的描述:
-
Highlight:这会在你选择的图表或图表元素上添加高亮。 -
Magnify:这允许你放大图表或图形的一部分以便更容易查看。 -
MouseIndicator:你可以将鼠标拖动到图上的特征上以显示更多数据。 -
MouseZoomAndPan:这允许你使用鼠标缩放和平移图表。滚动轮缩放和缩小,而点击和拖动则允许你在图表周围平移。 -
MoveSlice:当使用饼图时,点击一个切片可以将其从图表的其余部分移出。 -
Shake:这会在图表上的一个元素上创建震动动作。 -
Tooltip:将鼠标光标悬停在图表元素上会显示更多信息。 -
TouchIndicator:这提供了在图表上显示数据的触摸动作。 -
TouchZoomAndPan:这提供了使用触摸手势进行缩放和平移的能力。
与图表样式和主题不同,你将图表组件附加到图表对象上,图表动作是单独调用的。图表动作构造函数将新图表作为第一个参数加载,并将可选参数作为第二个参数。请注意,动作是在图表渲染之前创建的。你可以在以下代码中看到一个示例:
require(["dojox/charting/Chart",
…,
"dojox/charting/action2d/Tooltip"],
function(Chart, …, Tooltip){
var chart = new Chart("test");
…
new Tooltip(chart, "default", {
text: function(o){
return "Population: "+o.y;
}
});
chart.render();
});
在前面的示例中,创建了一个图表,并添加了一个工具提示,显示当你悬停在图表特征上时的人口数据。
在弹出窗口中使用 Dojox 图表
将 dojox/charting 模块与 ArcGIS API for JavaScript 结合使用提供了许多显示数据的方式。通过地图的 infoWindow 传递特征数据的一种方式是通过图表。信息窗口使用 HTML 模板作为其内容,这可以提供我们需要的钩子来附加我们的图表。
当将图表添加到信息窗口时,一个问题是在何时绘制图表。幸运的是,有一个事件可以处理这个问题。地图的 infoWindow 在所选特征图形改变时触发 selection-changed 事件,无论是通过点击另一个图形,还是通过点击下一个和上一个按钮。我们可以为该事件分配事件监听器,查看所选图形,如果它包含我们所需的数据,我们就可以绘制图表。
在我们的应用程序中使用 Dojo 图表
在上一章中,我们的普查应用程序在呈现数据时可能需要一些视觉吸引力。我们将使用 dojox/charting 库尝试添加图表和图形。每当用户点击普查区块组、县或州时,我们将应用图形到地图弹出窗口。普查区块没有足够的信息供我们进行图形化。
加载模块
由于我们的图表目前仅限于我们的普查应用程序,我们需要更新自定义 y2k/Census 模块定义中的模块:
-
我们将首先添加
dojo/on来处理地图弹出事件。 -
我们将添加默认图表对象以及饼图和柱状图模块。
-
我们将添加
PrimaryColors主题和SimpleTheme来创建我们自己的自定义颜色模板。 -
最后,我们将添加高亮和工具提示操作,以便用户在悬停在图表的部分时阅读结果。
-
它应该看起来有点像以下内容:
define([… "dojo/on", "dojox/charting/Chart", "dojox/charting/plot2d/Pie", "dojox/charting/plot2d/Bars", "dojox/charting/action2d/Highlight", "dojox/charting/action2d/Tooltip", "dojox/charting/themes/PrimaryColors", "dojox/charting/SimpleTheme",. ], function (…, dojoOn, Chart, Pie, Bars, Highlight, Tooltip, PrimaryColors, SimpleTheme, …) { … });
准备弹出窗口
作为我们计划的一部分,我们希望当特征被点击时,图表和图形将在地图的 infowindow 中渲染。我们只对显示当前选中特征的图表和图形感兴趣,因此我们将添加一个事件处理程序,每次 infoWindow 对象的 selection-change 事件触发时都会运行。我们将称之为 _onInfoWindowSelect()。在我们为 Census.js 模块编写该函数的存根后,我们将在 _onMapLoad() 方法中添加事件处理程序。这样我们就知道地图及其弹出窗口是可用的。它应该类似于以下代码:
_onMapLoad: function () {
…
dojoOn(this.map.infoWindow, "selection-change", lang.hitch(this,
this._onInfoWindowSelect));
},
…
_onInfoWindowSelect: function () {
//content goes here
}
当特征被添加到或从选择中删除时,infoWindow 对象的 selection-change 事件会触发。当我们检查 infoWindow 对象的选中特征时,我们必须测试以确定它是否包含特征。如果存在一个,我们可以处理该特征的属性并将相关的图形添加到弹出窗口。infoWindow 函数应类似于以下内容:
_onInfoWindowSelect: function () {
var graphic = this.map.infoWindow.getSelectedFeature(),
ethnicData, genderData, ageData;
if (graphic && graphic.attributes) {
// load and render the ethnic data
ethnicData = this.ethnicData(graphic.attributes);
this.ethnicGraph(ethnicData);
// load and render the gender data
genderData = this.genderData(graphic.attributes);
this.genderGraph(genderData);
// load and render the age data
ageData = this.ageData(graphic.attributes);
this.ageGraph(ageData);
}
},
…
ethnicData: function (attributes) { },
ethnicGraph: function (data) { },
genderData: function (attributes) { },
genderGraph: function (data) { },
ageData: function (attributes) { },
ageGraph: function (data) { }
更新 HTML 模板
为了将图形添加到我们的弹出窗口中,我们需要更新 HTML 模板以包含元素 ID。JavaScript 代码将寻找渲染图形的位置,我们可以指示它在添加了 id 的元素中渲染。打开 CensusBlockGroup.html 来查看弹出窗口模板。找到 种族群体 部分,并删除其下的整个表格。您可以在测试目的下将其注释掉,但当我们把此应用程序投入生产时,我们不希望每个人都下载所有这些浪费的内容。用具有 id 等于 ethnicgraph 的 div 替换表格。它应该看起来像以下内容:
…
<b>Ethnic Groups</b>
<div id="ethnicgraph"></div>
…
在 Males/Females 和 Ages 部分下重复相同的操作,分别用 div 元素替换那些表格,这些元素分别标识为 gendergraph 和 agegraph。如果您选择显示其他图形,请遵循相同的指南。同样,对 CountyCensus.html 和 StateCensus.html 模板也进行重复操作。
处理数据
如果你回顾一下其他 dojox/charting 操作的示例,你会注意到数据是以数组的形式添加到图表中的。然而,我们从地图服务中获得的数据并不是这种格式。我们需要将属性数据处理成 dojox/charting 模块可以使用的格式。
当将数据对象传递给 dojox/charting 图表和图形时,图表期望数据具有可绘制的 x 和 y 属性。由于我们不是比较价值随时间变化或某些其他独立变量的变化,我们将数值人口添加到我们的因变量 y 中。工具提示文本的值可以分配给 JSON 工具提示数据属性。你可以在以下代码中看到生成的函数:
…
formatAttributesForGraph: function (attributes, fieldLabels) {
var data = [], field;
for (field in fieldLabels) {
data.push({
tooltip: fieldLabels[field],
y: +attributes[field]
});
}
return data;
},
…
注意
在人口对象中的属性前面的 + 符号是一个快捷方式,用于将值转换为数字,如果它还不是数字的话。你可以使用 parseInt() 或 parseFloat() 方法得到相同的效果。
现在我们能够将我们的数据转换成可用于我们的图形小部件的格式,我们可以调用我们的 ethnicData()、genderData() 和 ageData() 方法。我们将从特征属性中提取所需的数据,并将其放入数组格式,以便由 chart 模块使用。
解析种族数据
我们对提取人口普查区域人口的种族构成感兴趣。我们感兴趣的是州、县和街区组特征类中存在的 WHITE、BLACK、AMER_ES、ASIAN、HAWN_PI、HISPANIC、OTHER 和 MULT_RACE 字段。由于我们有很多字段可能存在于特征类中,或者可能不存在,我们将以相同的方式添加它们,因此我们将创建一个字段名称和我们要添加的相应标签的数组。请参见以下代码:
…
ethnicData: function (attributes) {
var data = [],
fields = ["WHITE", "BLACK", "AMERI_ES", "ASIAN", "HAWN_PI", "HISPANIC", "OTHER", "MULT_RACE"],
labels = ["Caucasian", "African-American", "Native American /<br> Alaskan Native", "Asian", "Hawaiian /<br> Pacific Islander", "Hispanic", "Other", "Multiracial"];
},
…
现在我们有了字段和标签,让我们将所需的信息添加到数据数组中。dojox/charting 库期望图形数据为数值列表或具有特定格式的 JSON 对象。由于我们想在饼图中添加标签到我们的数据,我们将创建复杂对象:
…
ethnicData: function (attributes) {
var fieldLabels = {
"WHITE": "Caucasian",
"BLACK": "African-American",
"AMERI_ES":"Native American /<br> Alaskan Native",
"ASIAN": "Asian",
"HAWN_PI":"Hawaiian /<br> Pacific Islander",
"HISPANIC": "Hispanic", "OTHER": "Other",
"MULT_RACE": "Multi-racial"
}
return this.formatAttributesForGraph(attributes, fieldLabels);
},
…
解析性别数据
我们将以类似的方式计算性别数据。我们只对特征属性中的 MALES 和 FEMALES 字段感兴趣。我们将它们添加到与前面代码中相同的格式的 JSON 对象列表中。它应该看起来像以下这样:
genderData: function (attributes) {
var fieldLabels = {
"MALES": "Males", "FEMALES", "Females"
}
return this.formatAttributesForGraph(attributes, fieldLabels);
},
…
解析年龄数据
我们将对 ageData() 方法执行与 ethnicData() 方法相同风格的数据处理。如果年龄小于 5、5-17、18-21、22-29、30-39、40-49、50-64 和 65 岁及以上有可用的人口普查数据,我们将收集这些数据。然后,我们将添加适当的工具提示标签,并返回格式化的数据数组。它应该看起来如下:
ageData: function (attributes) {
var fieldLabels = {
"AGE_UNDER5": "< 5", "AGE_5_17": "5-17", "AGE_18_21": "18-21",
"AGE_22_29": "22-29", "AGE_30_39": "30-39", "AGE_40_49": "40-49",
"AGE_50_64": "50-64", "AGE_65_UP": "65+"
};
return this.formatAttributesForGraph(attributes, fieldLabels);
},
显示结果
现在我们已经以我们可以用于图表的格式获得了结果,我们可以将它们加载到我们的图表中。我们的民族和性别图都是饼图,而年龄图是水平条形图。让我们看看构建每个图表需要什么。您可以在自己的时间里使用剩余的数据创建任何额外的图表。
民族图
我们希望一个饼图能够适应民族图的弹出窗口。90 像素的半径应该可以很好地适应弹出窗口。我们将使用PrimaryColors,这是dojox/charting中的默认主题之一,来设置图表的主题。我们还将向图表添加饼图功能,并在用户悬停数据时添加工具提示和突出显示动画。最后,我们将渲染民族饼图:
ethnicGraph: function (data) {
var ethnicChart = new Chart("ethnicgraph");
ethnicChart.setTheme(PrimaryColors)
.addPlot("default", {
type: Pie,
font: "normal normal 11pt Tahoma",
fontColor: "black",
radius: 90
}).addSeries("Series A", data);
var anim_a = new Tooltip(ethnicChart, "default");
var anim_b = new Highlight(ethnicChart, "default");
ethnicChart.render();
},
当应用程序绘制民族图时,它应该看起来像以下图像:

民族群体
性别图
对于性别图,我们将设置一个与民族图相似的饼图。但在我们这样做之前,我们将加载一个新的主题来使用。我们将从SimpleTheme构造函数创建一个genderTheme构造函数,并为女性添加浅粉色,为男性添加浅蓝色。然后我们将创建图表,添加新的主题,并添加与民族图相同的一切。您可以在以下代码中看到这一点:
genderGraph: function (data) {
var genderTheme = new SimpleTheme({
colors: ["#8888ff", "#ff8888"]
}),
genderChart = new Chart("gendergraph");
genderChart.setTheme(genderTheme)
.addPlot("default", {
type: Pie,
font: "normal normal 11pt Tahoma",
fontColor: "black",
radius: 90
}).addSeries("Series A", data);
var anim_a = new Tooltip(genderChart, "default");
var anim_b = new Highlight(genderChart, "default");
genderChart.render();
},
当应用程序绘制性别图时,它应该看起来像以下图像:

年龄图
我们将为年龄图创建一个条形图来显示年龄人口统计。与饼图不同,条形图不关心半径,而是更喜欢知道条形可以增长多长(maxBarSize),以及它们应该相隔多远(间隙)。我们将继续使用PrimaryColors主题来创建这个对象:
ageGraph: function (data) {
var ageChart = new Chart("agegraph");
ageChart.setTheme(PrimaryColors)
.addPlot("default", {
type: Bars,
font: "normal normal 11pt Tahoma",
fontColor: "black",
gap: 2,
maxBarSize: 220
}).addSeries("Series A", data);
var anim_a = new Tooltip(ageChart, "default");
var anim_b = new Highlight(ageChart, "default");
ageChart.render();
}
当您绘制ageChart时,它应该看起来像以下图像:

介绍 D3.js
如果您想要创建令人惊叹的图形,可以超越 ArcGIS JavaScript API 和 Dojo。一个流行的 JavaScript 库,您可以使用它来创建图表、图形和其他数据驱动的可视化,是D3.js。D3.js是由纽约时报的 Mike Bostock 创建的,用于使用 HTML、SVG、CSS 和 JavaScript 创建交互式数据驱动的图形。它从 HTML 读取数据,并以您决定的方式渲染。
D3.js自首次公开发布以来,其发展势头迅猛。这个库非常灵活,因为它不仅渲染图表和图形。它提供了创建图表、图形和其他可以像任何 HTML 元素一样移动和样式的交互式图形的构建块。甚至可以使用D3.js和一种称为 GeoJSON 的文件格式在网页上显示不同投影的 GIS 地图。
对于有 jQuery 经验的任何人,使用D3.js编写的脚本的行为方式相同。您可以使用d3.select()或d3.selectAll()方法选择 HTML 元素,这些方法类似于 jQuery 基本方法。D3 命令可以一个接一个地链接,这也是许多 jQuery 开发人员喜欢的功能之一。在以下示例中,我们使用 D3 通过select()方法查找具有addflair类的元素。然后我们向这些元素添加相关的文本内容:
d3.select(".addflair").append("span").text("I've got flair!");
使用 Dojo 的 AMD 添加 D3.js 库
假设您想将D3.js添加到您的地图应用中。您找到d3库的链接,并将其像这样复制并粘贴到您的应用中:
…
<link rel="stylesheet"
href="https://js.arcgis.com/3.13/esri/css/esri.css" />
<script src="img/"></script>
<script src=https://d3js.org/d3.v3.js"></script>
</head>
…
您复制并粘贴一个示例来测试它是否可行。您打开浏览器,加载您的页面。您耐心地等待一切加载,然后它崩溃了。发生了什么?
结果表明,在 ArcGIS JavaScript API 之后加载的额外库干扰了 AMD 库引用。让我们看看一些将外部库加载到基于 AMD 的应用程序中的解决方案。
在 AMD 模块外部加载另一个库
如果您打算在 AMD 模块之外与 JavaScript 库一起工作,最好在加载 ArcGIS JavaScript API 之前加载该库。如果您是在在另一个框架中先前编写的现有应用上添加地图,您将使用此方法。
在 AMD 模块内加载另一个库
在您的 AMD 应用中处理 D3 和其他外部库的另一种方法是将其作为 AMD 模块加载。您可以像对待任何其他基于 Dojo 的模块一样对待它们,并且仅在必要时将它们加载到内存中。这对于您偶尔使用且不需要在启动时使用的库来说效果很好。它也适用于将库的所有功能加载到单个 JavaScript 对象中的库,例如D3.js或 jQuery。
要将外部库作为 AMD 模块加载,您必须首先在dojoConfig中将其作为包引用,就像您在第三章 《Dojo 小部件系统》 中对自定义Dojo模块所做的那样。将您的外部库添加到包中会告诉 Dojo 的require()和define()函数在哪里查找库。记住,当在包中列出库的位置时,您引用的是 JavaScript 库的文件夹,而不是库本身。对于 D3,dojoConfig脚本可能看起来像以下这样:
dojoConfig = {
async: true,
packages: [
{
name: "d3",
location: "http://cdnjs.cloudflare.com/ajax/libs/d3/3.4.12/"
}
]
};
一旦将库文件夹引用添加到您的dojoConfig变量中,您就可以将其添加到任何require()或define()语句中。将库加载到 AMD require()语句中的样子如下:
require([…, "d3/d3", …], function (…, d3, …) { … });
在我们的应用中使用 D3.js
在我们的应用中,我们将探索使用 D3 向我们的应用添加图表。我们将用它来替换dojox/charting代码中添加到地图弹出窗口的部分。许多步骤将是相似的,但也有一些不同。
将 D3.js 添加到配置中
由于我们的应用程序严重依赖于 Dojo 框架,我们将使用 AMD 添加我们的D3.js库。我们将在dojoConfig.packages列表中添加 D3 的引用。新的dojoConfig脚本应如下所示:
dojoConfig = {
async: true,
packages: [
{
name: 'y2k',
location: location.pathname.replace(/\/[^\/]*$/, '') +'/js'
},
{
name: "d3",
location: "http://cdnjs.cloudflare.com/ajax/libs/d3/3.4.12/"
}
]
};
现在 AMD 代码知道在哪里查找D3库,我们可以在我们的普查应用程序中添加对它的引用。然后D3库将可用于我们的普查小部件,但它不会干扰可能有自己的d3变量的其他应用程序。我们的Census.js代码应如下所示:
define([
…,
"esri/config", "d3/d3"
], function (
…,
esriConfig, d3
) {
…
});
准备弹出窗口
D3.js现在应该在我们的小部件中可用,我们可以准备弹出窗口以加载数据。我们将以与加载dojox/charting模块相同的方式设置我们的代码。我们将把相同的事件附加到地图的infoWindow对象的selection-change事件上,然后在该事件上运行函数来操作和渲染我们的数据。
注意
请参考章节中 dojox/charting 部分的准备弹出窗口部分以获取代码。
至于块组、县和州的 HTML 弹出模板,我们可以对dojox/charting示例中进行的相同更改。遵循互联网上的最佳实践,我们将用具有相同名称的类标签替换绘图div元素上的id标签(例如,种族群体获得class="ethnicgraph")。这将减少 HTML id冲突的可能性。此外,虽然 Dojo 小部件需要 HTML 元素或id字符串,但D3.js图表可以添加到任何 CSS 选择器找到的元素中。
处理我们的数据
当我们收集dojox/Charting模块的属性数据时,我们必须将属性数据排列成数组,以便它们可以被绘图模块消费。对于D3.js也是如此。我们将格式化属性为列表,以便图表可以读取。
与dojox/charting库不同,D3.js对绘图部分使用的属性没有名称限制。你可以给属性起更合理的名字。D3.js中的函数将被添加来计算图表值。由于我们的大部分种族、性别和年龄数据基于人口并按名称排序,因此将那些属性分别命名为人口和名称是有意义的:
…
formatAttributesForGraph: function (attributes, fieldLabels) {
var data = [], field;
for (field in fieldLabels) {
data.push({
name: fieldLabels[field],
population: +attributes[field]
});
}
return data;
},
…
我们在formatAttributesForGraph()方法中将属性名称和人口值添加到列表中。该列表将在稍后时间进行绘图。我们不需要更改任何代码,因为我们使用相同的函数在ethnicData()、genderData()和ageData()函数中处理属性数据。
显示结果
现在我们已经创建了数据列表,我们可以在弹出窗口的图表中显示它们。
显示种族图表
对于我们的种族图表,我们将创建一个饼图:
-
我们将调整其大小以适应弹出窗口中的
240像素乘以210像素的区域。 -
我们将使用 CSS 颜色列表添加自己的颜色比例。
-
我们将寻找我们想要放置图表的 HTML DOM 元素(
class="ethnicgraph"),然后附加饼图图形。 -
我们将应用颜色,使用我们的人口数据调整大小,然后用种族群体的名称标记它:
ethnicGraph: function (data) { var width = 240, height = 210, radius = Math.min(width, height) / 2; var color = d3.scale.ordinal() .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00", "#c7d223"]); var arc = d3.svg.arc() .outerRadius(radius - 10) .innerRadius(0); var pie = d3.layout.pie() .sort(null) .value(function(d) { return d.population; }); var svg = d3.select(".censusethnic").append("svg") .attr("width", width) .attr("height", height) .append("g") .attr("transform", "translate(" + width/2 + "," + height/2 + ")"); if (!data || !data.length) { return; } var g = svg.selectAll(".arc") .data(pie(data)) .enter().append("g") .attr("class", "arc"); g.append("path") .attr("d", arc) .style("fill", function(d) { return color(d.data.name); }); g.append("text") .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; }) .attr("dy", ".35em") .style("text-anchor", "middle") .text(function(d) { return d.data.name; }); },
当应用程序绘制图表时,它应该看起来像以下图表:

显示性别图表
对于性别图表,我们将从种族图表复制并粘贴代码。代码类似,除了两个小的变化:
-
对于我们的第一个更改,我们将为男性和女性人口添加自定义颜色。查找颜色变量分配的地方,并将两个十六进制颜色数字插入到颜色范围内:
genderGraph: function (data) { … var color = d3.scale.ordinal().range(["#8888ff", "#ff8888"]); … }, -
接下来,我们希望标签显示所讨论的性别和实际人口。为了制作两行标签,我们需要添加另一个
tspan来填写人口数据。我们还需要移动该标签,使其位于其他标签下方,并且不与之交叉:g.append("text") .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; }) .attr("dy", ".35em") .style("text-anchor", "middle") .text(function(d) { return d.data.name; }) .append("tspan") .text(function(d) { return d.data.population;}) .attr("x", "0").attr("dy", '15'); -
一旦运行应用程序并使用一些数据测试它,图表应该看起来像以下图像,待数据:
![显示性别图表]()
显示年龄图表
年龄图表从简单的 html div 元素创建条形图。它会根据我们提供的数据调整大小。我们需要计算数据的最大值,以便将数据值调整到最大宽度内。从那里,我们可以使用提供的数据绘制和标记我们的图表:
ageGraph: function (data) {
// calculate max data value
var maxData = d3.max(arrayUtils.map(data, function (item) {return item.population;}));
// create a scale to convert data value to bar width
var x = d3.scale.linear()
.domain([0, maxData])
.range([0, 240]);
// draw bar graph and label it.
d3.select(".censusages")
.selectAll("div")
.data(data)
.enter().append("div")
.style("width", function(d) { return x(d.population) + "px"; })
.text(function(d) { return d.age + ": " + d.population; });
}
使用 CSS 样式,我们可以根据需要转换数据的外观。在这个例子中,我们决定采用交替颜色主题,使用 CSS3 的 nth-child(even) 伪类选择器。您也可以添加自己的 CSS 悬停效果,以匹配我们与 dojox/charting 所做的:
.censusages > div {
background: #12af12;
color: white;
font-size: 0.9em;
line-height: 1.5;
}
.censusages > div:nth-child(even) {
background: #64ff64;
color: #222;
margin: 1px 0;
}
使用 CSS 和我们的数据,我们能够创建以下图表:

注意
如果您想了解更多关于 D3.js 库的信息,有大量的信息可供参考。官方 D3.js 网站是 d3js.org/。您可以去那里找到示例、教程和其他令人惊叹的图形。您还可以查看 Swizec Tellor 的 Data Visualization with d3.js、Nick Qi Zhu 的 Data Visualization with D3.js Cookbook 和 Pablo Navarro Castillo 的 Mastering D3.js。
摘要
在我们的网络地图应用中,dojox/charting 和 D3.js 都有其优点和缺点。dojox/charting 库与 ArcGIS JavaScript API 一起提供,并且易于与现有应用程序集成。它提供了许多可以快速添加的主题。另一方面,D3.js 与 HTML 元素和 CSS 样式一起工作,以创建令人惊叹的效果。它提供了比 dojox/charting 更多的数据可视化技术,并使用 CSS 样式提供了可定制的外观。您的最终选择可能取决于您对这些工具的舒适程度和您的想象力。
在本章中,我们学习了如何在我们的 ArcGIS JavaScript API 应用程序中集成图表和图形。我们使用了 Dojo 框架提供的图形库,这些库基于地图要素的数据创建图形。我们还使用了 D3.js 在我们的应用程序中渲染图表和图形。在这个过程中,我们学习了如何在基于 Dojo 的 AMD 应用程序中加载和访问其他库。
在下一章中,我们将探讨如何将我们的 ArcGIS JavaScript API 应用程序与其他流行的 JavaScript 框架混合使用。
第七章:与其他库和框架兼容
近年来,JavaScript 开发已经成熟。曾经只是用来编写一些动画和用户交互的简单语言,JavaScript 应用现在可以支持几年前只有桌面应用才能完成的全面任务。有众多文档齐全的库和框架,通过网页浏览器使复杂应用变得可管理。
没有任何 JavaScript 库或框架能够完美地完成所有事情。大多数库专注于提高应用开发的一些关键需求,例如 DOM 操作、数据绑定、动画或图形。有些库侧重于速度;而有些则侧重于跨浏览器兼容性。
成功的 JavaScript 开发者不必从头开始使用单个库或原生 JavaScript 来构建应用以使其工作。通过添加当前兼容性良好的库,开发者可以加快开发速度,并处理多种浏览器类型,而无需担心它们。
在本章中,我们将涵盖以下内容:
-
将 jQuery 集成到应用中
-
将 Backbone.js 集成到应用中
-
将 Knockout.js 集成到应用中
-
讨论在地图应用中使用 AngularJS
与其他库和框架的兼容性
开发者描述 JavaScript 库可用性的一个方式是它们与其他库的兼容性。你的库与越多库兼容,就越有可能在项目中使用。当库与其他库和框架不兼容时,可能会导致意外的代码错误和未预料到的行为。由于开发者无法控制项目中使用的其他库,因此如果库与其他库兼容会更好。
库之间可能存在几种冲突方式。过去更常见的一种方式是通过命名空间冲突。如果两个库在全局命名空间中创建了相同的 cut() 函数,你无法确定哪个会被调用。另一种更微妙的是通过操作标准 JavaScript 类型的原型。一个库可能会期望对数组上的 map() 方法执行特定任务,但如果另一个库覆盖了 Array.prototype.map(),结果可能不会如预期,甚至可能破坏应用。通常,只有当你修补旧浏览器的支持时,才礼貌地操作基础 JavaScript 类型的原型。
JavaScript 库之间冲突的另一种较新的方式是它们实现模块的方式。在 ECMAScript 6 发布模块标准之前,有两种模块开发流派。一种是通过使用 CommonJS 来定义模块,另一种是 AMD。通常,一种模块可以从另一种中加载,因为它们的调用和构建方式不同。大多数未明确定义的小型库都可以通过一些工作加载到其中。
即使在 AMD 中,也可能存在一些冲突。一些库和框架使用 Require.JS 来加载它们的模块。虽然语法上相似,但 Dojo 的老版本与 Require.JS 模块混合时会产生错误。当你在应用程序中混合和匹配 JavaScript 库时,这是一件需要注意的事情。
常用的库来玩耍
现在我们已经整体研究了库的兼容性,是时候检查 ArcGIS API for JavaScript 如何与其他 JavaScript 库和框架协同工作了。我们将查看一些开发者使用的流行库,并尝试将它们与 ArcGIS API for JavaScript 一起加载。我们将使用 jQuery、Backbone.js、Knockout.js 和 AngularJS 创建相同的应用程序,并比较它们。最后,我们将探讨与任何框架和 ArcGIS API 相关的几项“应该做”和“不应该做”的事项。
我们的故事还在继续
因此,我们的普查数据应用程序在 Y2K 社会中非常受欢迎。他们喜欢图形的外观,并希望我们扩展它。小部件的功能很棒,但他们提出了新的要求。似乎有一位成员在昨晚的深夜喜剧节目中得知,有些人无法在地图上找到他们的州或县。
在与客户讨论可能的解决方案后,我们决定添加一些下拉控件,让用户可以选择一个州、县,甚至是一个人口普查区块组。选择一个州后,地图会缩放到该州,并在弹出窗口中显示数据。此外,县下拉列表会填充该州的所有县名。当选择县时,县会通过弹出窗口被选中,并为该县和州填充区块组名称。最后,如果选择了区块组,区块组会被缩放到,并在地图上显示数据。
在某种程度上相关的新闻中,我们招聘了一名实习生。他是一位热衷于跟随最新 JavaScript 动态的年轻小伙子。他希望为这个项目做出贡献,而这个任务看起来是一个让他锻炼的好机会。
问题在于,我们的实习生想要使用另一个库或框架来编写代码,而不仅仅是简单的库。他希望引入包括 jQuery、Backbone、Knockout、Node.js 和 Angular 在内的多个流行库。虽然我们欣赏他学习的愿望(以及充实简历),但我们不得不与他严肃地讨论选择适合项目的库。
在说服实习生放弃 Node.js 项目(因为这个是一个客户端应用程序,目前不需要服务器端工作)之后,我们向他解释了他在列表上其他库的一般用法。我们决定给实习生一个学习项目,让他构建四个应用程序的副本,每个应用程序都包含 jQuery、Backbone、Knockout 或 Angular 之一。他可以评估每个应用程序,并告诉我们哪个与该应用程序配合得最好。我们认为他做四倍的工作是可以接受的,因为他不是付费实习生。
我们应该继续设置四个项目文件夹,并复制我们上一章工作中的D3.js应用程序的副本。在每个项目中,我们需要修改/js/template文件夹中的Census.html模板,以显示下拉菜单。我们将添加三个带有粗体标签的下拉菜单,这些菜单将由四个库稍后填充。它应该看起来像以下这样:
<div class="${baseClass}" style="display: none;">
<span class="${baseClass}-close" data-dojo-attach-event="ondijitclick:hide">X</span>
<b>Census Data</b><br />
<p>
Click on a location in the United States to view the census data for that region. You can also use the dropdown tools to select a State, County, or Blockgroup.
</p>
<div >
<b>State Selector: </b>
<select></select>
</div>
<div>
<b>County Selector: </b>
<select></select>
</div>
<div>
<b>Block Group Selector: </b>
<select></select>
</div>
</div>
当新应用程序加载时,新的小部件应该看起来像以下这样:

jQuery 概述
在过去几年中,最受欢迎的库之一无疑是 jQuery。jQuery 最初由 John Resig 创建,作为一个用于处理 DOM 操作和事件的库。跨浏览器兼容性是该库的一个强大卖点,因为它在从最新版本的 Chrome 到非常古老的 Internet Explorer 版本上都能工作,以至于大多数开发者都会感到不适。其直观的命令和功能使得新手开发者容易上手。该库非常受欢迎,据 jQuery 博客(blog.jquery.com/2014/01/13/the-state-of-jquery-2014/)报道,截至 2013 年底,它被估计用于世界上排名前 100,000 的网站中的 61%。
如果你理解 jQuery 的工作原理,你可以跳过下一节,直接进入在我们的应用程序中添加 jQuery。如果不理解,这里简要介绍一下 jQuery 的工作原理。
jQuery 的工作原理
jQuery 库在网页上选择和操作 DOM 元素方面非常有用。可以使用jQuery()函数或其常用快捷方法$()来选择 DOM 元素。它接受一个 DOM 元素或一个包含 CSS 选择器的字符串。要选择一个具有id的元素,你需要在 ID 名称前加上一个#。要选择一个 CSS 类,你需要在类名前插入一个.。要选择具有特定标签名的所有元素,只需使用该标签名。实际上,jQuery 几乎支持任何合法的 CSS select语句。以下是一些代码示例:
jQuery("#main"); // selects a node with an id="main"
$("#main"); // A common shortcut for jQuery; same as above.
$(".classy"); // selects all elements with class="classy"
$("p"); // selects all paragraphs
$(".article p"); // selects all paragraphs in nodes of
// class="article"
$("input[type='text']"); // selects all text inputs
一旦 jQuery 对象选择了节点,你可以在它们上执行多个函数。你可以使用show()和hide()方法使它们显示或消失。你可以使用css()改变它们的样式,使用attr()改变它们的属性,使用html()或text()方法改变它们的内容。可以使用val()方法检索和设置选定的表单元素值。你也可以使用find()在选定的元素内查找元素,并使用each()方法遍历每个项目。事件也可以使用on()方法分配,并使用off()方法移除。这些只是最常见的 jQuery 方法。
jQuery 的另一个强大功能是名为 链式 的属性。jQuery 函数返回 jQuery 对象。您可以直接在返回的内容上调用另一个函数。这些链式命令从左到右进行评估。您可以使用此代码对您的 HTML 执行一些强大的转换。例如,如果您想将每个无序列表项变成一只绿色的青蛙,每个有序列表项变成一只大棕色的蟾蜍,您可以这样做:
$("ul > li").css("color", "green").text("Frog");
$("ol > li").css({"color": "green", "font-size": "72px"})
.text("Toad"); // totally legal to do this.
只是要警告一下,当您在 jQuery 中使用复杂的链式函数时,有时函数的目标会改变。像 each() 和 find() 这样的函数会改变传递给下一个函数的选择。在这个例子中,开发者想要显示一个表单,将其宽度设置为 400px,清除文本框的值,并将标签变为蓝色。当他们运行它时,他们会发现标签没有像预期的那样变蓝:
$("form").show().css("width", "400px")
.find('input').val('')
.find('label').css('color', '#0f0');
当开发者调用 find('input') 时,它将选定的表单元素更改为输入元素。然后,当调用 find('label') 时,jQuery 在输入标签内搜索标签。没有找到任何标签,所以没有东西变蓝。幸运的是,开发者记得 jQuery 还提供了一个 end() 函数,该函数可以返回到原始选择。以下代码片段工作得更好:
$("form").show().css("width", "400px")
.find('input').val('').end()
.find('label').css('color', '#0f0');
现在我们已经对 jQuery 进行了快速学习,让我们尝试在我们的应用中使用它。
将 jQuery 添加到您的应用中
因此,我们的实习生开始使用基于 jQuery 的应用时,遇到了他的第一个问题。他如何让 jQuery 和 Dojo 一起良好地工作?为了回答这个问题,我们可以参考 第六章,绘制您的进度,当我们向应用中添加 D3.js 时的一些经验教训。
加载我们 D3.js 应用的副本,我们首先将在 dojoConfig 包中添加对 jQuery 库的引用。记住,这是在加载 ArcGIS JavaScript API 之前加载的 JavaScript 配置对象。我们将添加一个具有名称和文件位置的包,但我们还会添加一个 main 属性,这将使应用加载 jQuery 库。您可以从网上下载 jQuery 库并将其放置在应用中的一个可访问文件夹中,或者您可以将引用指向一个托管库的外部 内容分发网络(或 CDN)。使用由 Google 托管的 jQuery 的外部引用,它应该看起来像以下代码:
dojoConfig = {
async: true,
packages: [
{
name: 'y2k',
location: location.pathname.replace(/\/[^\/]*$/, '') + '/js'
}, {
name: "d3",
location: "http://cdnjs.cloudflare.com/ajax/libs/d3/3.4.12/"
}, {
name: "jquery",
location:"http://ajax.googleapis.com/ajax/libs/jquery/1.11.2",
main: "jquery"
}
]
};
现在我们可以通过 AMD 访问 jQuery,让我们打开 Census.js 文件,并将其作为模块添加到我们的文件中。我们将在 require 语句中添加对其的引用,并将其分配给模块 $,正如在大多数 jQuery 示例中常见的那样。它应该看起来像以下代码:
require([…, "d3/d3", "jquery"], function (…, d3, $) {
…
});
接下来,我们需要更新Census小部件模板,以便 jQuery 有东西可以搜索。当我们在js/templates/文件夹中更新Census.html时,我们将为每个select菜单添加一个stateselect、countyselect和blockgroupselect类。虽然添加 ID 会使选择元素更快,但添加类名将确保与应用程序中的其他小部件没有 ID 名称冲突。模板将看起来像以下示例:
…
<p>
Click on a location in the United States to view the census data for that region. You can also use the dropdown tools to select a State, County, or Blockgroup.
</p>
<div >
<b>State Selector: </b>
<select class="stateselect"></select>
</div>
<div>
<b>County Selector: </b>
<select class="countyselect"></select>
</div>
<div>
<b>Block Group Selector: </b>
<select class="blockgroupselect"></select>
</div>
…
由于我们有东西让 jQuery 选择,我们需要在我们的代码中让 jQuery 选择它。向我们的Census小部件构造函数中添加一些 jQuery 选择器,以获取具有stateselect、countyselect和blockgroupselect类的节点,并将它们按顺序分配给小部件的stateSelect、countySelect和blockGroupSelect属性。这被称为缓存我们的选择,这对于 jQuery 应用程序来说是一个好习惯,因为重复的 DOM 搜索可能需要很长时间,尤其是在一个较大的应用程序中:
constructor: function (options, srcNodeRef) {
…
this.map = options.map || null;
this.domNode = srcRefNode;
this.stateSelect = $(".stateselect");
this.countySelect = $(".countyselect");
this.blockGroupSelect = $(".blockgroupselect");
…
},
如果我们现在运行应用程序,我们会发现stateSelect、countySelect和blockGroupSelect属性中没有任何内容。为什么?如果你还记得,在第三章《Dojo 小部件系统》中,我们讨论了Dojo小部件的生命周期。我们讨论了这样一个事实,即在构造函数运行时,模板还没有加载。实际上,它将不会可用,直到小部件运行postCreate()方法。我们需要在我们的应用程序中添加postCreate(),添加一行引用从WidgetBase类继承的postCreate函数的代码,然后将我们之前编写的 jQuery 代码剪切并粘贴到这个函数中。它应该看起来像以下代码:
constructor: function (options, srcNodeRef) {
…
this.domNode = srcRefNode;
/* stateSelect, countySelect, and blockGroupSelect removed */
…
},
postCreate: function () {
this.inherited(arguments);
this.stateSelect = $(".stateselect");
this.countySelect = $(".countyselect");
this.blockGroupSelect = $(".blockgroupselect");
},
现在,当postCreate方法被调用时,stateSelect、countySelect和blockGroupSelect将填充适当的对象。
填充下拉列表
我们需要数据来填充我们的选择下拉列表。为此,我们需要从地图服务中获取州、县和街区组名称,并将它们填充到下拉列表中。对于五十多个州来说,这很简单,但县和街区组怎么办?仅德克萨斯州就有 250 多个县,街区组更多。我们需要一种系统化的方法来填充下拉列表。
如果每次我们选择一个州,县下拉列表就填充该州的所有县,会怎么样?此外,如果街区组列表直到用户选择了感兴趣的县才填充,会怎么样?如果我们监听select元素的change事件,我们可以实现这一点。当事件触发时,我们将从上一级查询的新列表中获得新的选择结果。
我们将首先在Census模块中添加两个新的方法,分别命名为queryDropdownData()和populateDropdown()。而不是添加我们尚未知的参数列表,我们将向queryDropdownData()方法添加一个名为args的单个参数。对于populateDropdown()方法,我们将添加args参数,以及一个来自查询地图数据的featureSet参数:
…
getDropDownData: function (args) { },
populateDropdown: function (args, featureSet) { },
…
添加查询任务
在queryDropdownData()方法内部,我们将查询地图服务以获取一个州中的县列表或县中的街区组列表。为此,我们需要添加 ArcGIS JavaScript API 的QueryTask引用。如果你还记得第二章中的内容,深入 API,QueryTask允许我们从地图服务中的单个图层中拉取类似 SQL 的查询。我们需要添加对QueryTask及其关联的任务参数构建器Query的引用。我们将继续在我们的getDropdown()方法中构建这些:
require([…, "esri/tasks/Query", "esri/tasks/QueryTask", …],
function (…, Query, QueryTask, …) {
…
queryDropdownData: function (args) {
var queryTask = new QueryTask(/* url */),
query = new Query();
},
…
});
现在我们需要定义QueryTask和查询的参数。为此,我们将使用通过args对象传递的参数。我们可以定义一个args.url来向QueryTask发送 URL 字符串。我们还可以使用args.field来设置查询对象中我们想要返回的数据的字段名,以及args.where来提供一个where子句以过滤结果。queryDropdownData()现在应该看起来如下:
queryDropdownData: function (args) {
var queryTask = new QueryTask(args.url),
query = new Query();
query.returnGeometry = false;
query.outFields = args.fields;
query.orderByFields = args.fields;
query.where = args.where;
query.returnDistinctValues = true;
return queryTask.execute(query);
},
对于我们的populateDropdown()方法,我们将使用基于 jQuery 的选择器,并添加来自queryDropdownData()方法的featureSet功能。记住,featureSet包含一个features参数,它包含一系列图形结果。我们只对返回的图形的属性感兴趣。它应该看起来如下。我们将使用 jQuery 的each()函数遍历功能列表,为每个结果创建一个选项,将其附加到选择器上,并用值填充它。它应该看起来像以下这样:
_populateDropdown: function (args, featureSet) {
args.selector.empty();
$.each(featureSet.features, function () {
args.selector.append($("<option />")
.val(this.attributes[args.fieldName])
.text(this.attributes[args.fieldName])
.attr("data-fips", this.attributes[args.fips]));
});
},
其他辅助函数
通过添加QueryTask,我们现在可以直接查询从下拉菜单中选择的州、县或街区组。我们需要定义一个函数,从服务器收集这些信息。我们可以在我们的小部件中调用queryShapeAndData()方法,它将接受一个包含所需数据的单个args参数:
queryShapeAndData: function (args) {
var queryTask = new QueryTask(args.url),
query = new Query();
query.returnGeometry = true;
query.outFields = ["*"];
query.outSpatialReference = this.map.spatialReference;
query.where = args.where;
return queryTask.execute(query);
},
虽然直接将这些图形添加到地图上是可能的,但我们应该格式化数据,使其在点击时能够弹出 infoWindow,就像我们亲自点击一样。为此,我们将添加一个 _onQueryComplete() 方法。它将接受来自 QueryTask 的 featureSet,并返回一个具有适当弹出模板的特征列表,就像我们在通过点击进行识别时分配的模板一样。不幸的是,featureSets 并不返回与 IdentifyResults 相同的描述信息,因此我们将必须手动添加特征的标题,以便它能够准确地选择正确的 InfoTemplate:
_onQueryComplete: function (title, featureSet) {
featureSet.features = arrayUtils.map(
featureSet.features,
lang.hitch(this, function (feature) {
return this._processFeature(feature, title);
}));
},
回到 queryShapeAndData() 方法,我们可以在 execute 语句中添加 callback 函数,并使其每次返回一个处理过的结果。queryShapeAndData() 的最后部分将看起来像以下这样:
…
return queryTask.execute(query)
.addCallback(lang.hitch(
this, this._onQueryComplete, args.title)
);
},
最后,我们需要一种方法来在地图上显示查询到的图形。我们将创建一个名为 _updateInfoWindowFromQuery() 的方法,它接受一个 featureSet,将其特征缩放到合适的大小,并添加 infoWindow。我们将使用 esri/graphicsUtils 模块来收集图形的整体范围,以便我们可以将其缩放。一旦异步缩放完成,我们将在 infoWindow 上设置图形并显示它。您可以在以下代码中看到执行所有这些操作的代码:
_updateInfoWindowFromQuery: function (results) {
var resultExtent = graphicsUtils.graphicsExtent(results.features);
this.map.setExtent(resultExtent)
.then(lang.hitch(this, function () {
this.map.infoWindow.setFeatures(results.features);
this.map.infoWindow.show(resultExtent.getCenter());
}));
},
处理事件
现在,我们将向 stateSelect、countySelect 和 blockGroupSelect 项添加事件监听器。我们将使用我们在前面的章节中开发的辅助函数来填充数据,并使用我们对 dojo/Deferred 的了解来异步连接它们。让我们从州开始。
当您从下拉菜单中选择一个状态时,select 元素将触发一个 change 事件。我们不会收集这个事件的数据。相反,我们将直接从下拉选择器获取数据,并使用这些数据生成我们需要的查询。
让我们继续在 Census 小部件中创建 _stateSelectChanged() 方法。它不接受任何参数。我们将以相同的方式处理 _countySelectChanged() 和 _blockGroupSelectChanged() 方法。然后,使用 jQuery 的 .on() 方法,我们将监听 stateSelect、countySelect 和 blockGroupSelect 控件中的 change 事件。我们将使用 dojo 的 lang.hitch() 方法确保当我们说 this 时,我们指的是 this 小部件。它应该看起来像以下这样:
postCreate: function () {
this.stateSelect = $(".stateselect")
.on("change", lang.hitch(this, this._stateSelectChanged));
this.countySelect = $(".countyselect")
.on("change", lang.hitch(this, this._countySelectChanged));
this.blockGroupSelect = $(".blockgroupselect")
.on("change",lang.hitch(this, this._blockGroupSelectChanged));
},
…
_stateSelectChanged: function () {},
_countySelectChanged: function () {},
_blockGroupSelectChanged: function () {},
…
处理状态
在 _stateSelectChanged() 方法中,我们将首先收集在 stateSelect 中选择的状态的名称。如果有值,我们将开始查询该州上的图形数据。我们将使用 queryShapeAndData() 方法来查询形状数据并处理它。当这完成时,我们可以将其传递给 _updateInfoWindowFromQuery() 方法:
_stateSelectChanged: function () {
var stateName = this.stateSelect.val();
if (value && value.length > 0) {
this.queryShapeAndData({
title: "states",
url: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3",
where: "STATE_NAME = '" + stateName + "'"
}).then(lang.hitch(this, this._updateInfoWindowFromQuery),
function (err) { console.log(err); });
}
},
…
现在我们正在显示图形,是时候填写县名了。我们将使用queryDropdownData()方法查询县列表,并使用.then()异步地将结果传递给我们的_populateDropdown()方法。我们将首先在查询值列表中分配县名,因为我们希望它们按字母顺序排列。我们将在_stateSelectChanged()方法的末尾添加它,它应该看起来像以下内容:
…
// search for counties in that state.
this.queryDropdownData({
url: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/2",
fields: ["NAME", "CNTY_FIPS"],
where: "STATE_NAME = '" + stateName + "'",
}).then(lang.hitch(this, this._populateDropdown, {
selector: this.countySelect,
fieldName: "NAME",
fips: "CNTY_FIPS"
}));
…
各县之间的差异
县应该以类似州的方式加载。主要问题是我们必须查询具有州和县 FIP 代码的县下的基础区块组。区块组人口普查数据中没有州或县名称。
由于我们将 FIP 代码分配给了下拉菜单的data-fips属性,应该有一种方法可以获取它们,对吧?是的,我们可以,但我们必须利用 jQuery 的链式方法。例如,从stateSelect,我们可以使用jQuery.find(":selected")方法在select中找到选定的选项。从那里,我们可以调用 jQuery 的attr()方法来获取data-fips属性。对于州和县名称以及 FIP 代码,它应该看起来像以下内容:
_countySelectChanged: function () {
var stateName = this.stateSelect.val(),
stateFIPS = this.stateSelect.find(":selected")
.attr("data-fips"),
countyName = this.countySelect.val(),
countyFIPS = this.countySelect.find(":selected")
.attr("data-fips");
…
从这里,我们可以使用stateName和countyName来查询要显示的正确县,使用stateFIPS和countyFIPS来获取区块组的列表。它们将使用与_stateSelectChanged()方法相同的函数,但具有不同的地图服务和where子句。
最后,区块组
_blockGroupSelectChanged() 方法编写起来要容易得多,因为我们只对显示区块组感兴趣。关键在于收集从各自的下拉菜单中选择的州 FIP 代码、县 FIP 代码以及blockgroup值。然后我们将拼接查询中的where子句,并请求地图的图形。该方法应类似于以下代码:
_blockGroupSelectChanged: function () {
var stateFIPS = this.stateSelect.find(":selected").attr("data- fips"),
countyFIPS = this.countySelect.find(":selected").attr("data- fips"),
blockGroup = this.blockGroupSelect.val();
this.queryShapeAndData({
title: "Census Block Group",
url: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/1",
where: "STATE_FIPS = '" + stateFIPS + "' AND CNTY_FIPS = '" + countyFIPS + "' AND BLKGRP = '" + blockGroup + "'"
}).then(
lang.hitch(this, this._updateInfoWindowFromQuery),
function (err) { console.log(err); });
},
填充州信息
现在应用程序已经连接好,我们终于可以从地图服务中加载数据了。我们将首先查询州层以获取州名和州 FIP 代码。为了获取所有州,我们将使用一个小巧的 SQL 技巧。SQL 将返回where子句为真的所有行,因此如果你想要所有行,你必须返回一些总是为真的东西。在这种情况下,我们将1=1分配给queryDropdownData()的where子句,因为1=1总是为真。
一旦我们从服务器收到查询结果,我们将它们传递给我们的_populateDropdown()方法。我们将选项分配给stateSelect下拉菜单,在每个选项上显示州名,并将 FIP 代码也存储在选项中。我们将在postCreate()方法的末尾添加以下片段:
this.queryDropdownData({
url: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3",
fields: ["STATE_NAME", "STATE_FIPS"],
where: "1=1",
}).then(lang.hitch(this, this._populateDropdown, {
selector: this.stateSelect,
fieldName: "STATE_NAME",
fips: "STATE_FIPS"
}));
因此,如果我们已经正确连接了一切,我们就可以在浏览器中查看网站并打开Census小部件。然后我们应该看到已经加载到下拉菜单中的各州。在县份填充之前,您必须选择一个州,并为区块组选择一个县。
将此项目放在手边。我们将以此作为起点复制此项目以用于其他项目。现在让我们尝试构建一个 Backbone 应用程序。
Backbone.js 概述
在像我们用 jQuery 编写的简单应用程序中,逻辑可以迅速变成意大利面代码。我们需要实施良好的实践来组织我们的代码。我们实施哪些实践?第一个试图回答这个问题的库之一是 Backbone.js。
Backbone.js 是早期实现模型-视图-控制器(MVC)架构的 JavaScript 库之一。MVC 通过将数据和企业逻辑(Model)与输出(View)分离,并通过一个单独的组件(Controller)更新两者来组织代码。使用 MVC,您不需要编写一个复杂的 JavaScript 函数,该函数从一些文本框中获取输入,将内容相加,并保存结果。这三个动作可以写入三个不同的函数,分别根据它们的功能以及它们如何适合Model、View和Controller分类。
Backbone.js 需要几个其他库才能正常工作。Backbone 使用 jQuery 来处理 DOM 操作,包括在页面上显示和隐藏元素。Backbone 还需要另一个名为 Underscore.js 的库。这个库提供了一系列处理 JavaScript 对象、数组等的函数和快捷方式。Underscore.js 提供了在dojo/_base/lang和dojo/_base/array中找到的方法,以及可以帮助您从特征图形中提取相关数据的其他方法。
Backbone 的组件
与大多数流行的库和框架相比,Backbone.js 相当简单。它将您的代码组织成五个类别:Model、View、Collection、Event和Router。这些类别共同展示您的数据并对用户输入做出反应。除了事件之外,所有这些类别都是通过 Backbone 的extend()方法创建的,该方法包含一个定义Model、View、Collection或Router的 JavaScript 对象。另一方面,事件是通过创建其他项目来定义的。让我们逐个回顾每个项目。
模型
Backbone.js 的 Model 提供了将要使用的数据的描述。你可以将默认属性、方法和事件分配给模型,这些模型将由其他类别中创建的应用程序功能调用。模型通过 Backbone.Model.extend() 构造函数创建。使用此方法创建的模型成为应用程序中模型数据的构造函数。通过模型创建的数据具有不同的 get() 和 set() 数据的方法,通过 has() 测试数据的存在,甚至可以通过 isChanged() 或 changedAttributes() 检测数据的变化。
这里是一个玩牌模型示例,以及使用该模型创建的卡片。CardModel 变量包括 rank 和 suit 属性,以及一个用于将卡片描述为单个字符串的函数:
var CardModel = Backbone.Model.extend({
defaults: {
"rank": "2",
"suit": "heart"
},
description: function () {
return this.rank + " of " + this.suit + "s";
}
});
var myCard = new CardModel({rank: "9", suit: "spade"});
myCard.description(); // returns "9 of spades";
视图
Backbone 的 View 设置了数据在应用程序中的展示方式。View 通过一系列在 Backbone.View.extend() 构造函数中定义的参数来定义 HTML 输出。你可以在 HTML 中的特定 DOM 元素上创建一个视图,通过在 extend() 构造函数方法中分配 .el 属性来实现。你还可以分配一个 tagName 属性来定义创建的 HTML 元素,一个 template 属性如果内容比单个元素更复杂,甚至可以分配一个 className 属性来将 CSS 类分配给主元素。
视图大量使用了 jQuery 和 Underscore.js。例如,虽然视图的元素由视图的 .el 属性定义,但可以通过引用视图的 $el 属性获得 jQuery 版本。此外,HTML 内容可以通过视图的模板定义,该模板是通过 Underscore 的 .template() 创建器创建的。
当视图首次创建时,它从一个名为 initialize() 的方法开始,该方法由你定义。在 initialize() 方法中,你可以将事件监听器分配给视图的其他部分,包括模型和集合。你还可以告诉视图 render(),或者写出 HTML 代码。你定义的 render() 方法用于在 View 元素内添加自定义 HTML。你还可以在 View 内渲染其他视图。
在以下代码中,你可以找到一个展示卡片的示例 View:
var CardView = Backbone.View.extend({
tagName: "li"
className: "card"
template: _.template("<%= description %> <button>Discard</button>"),
initialize: function () {
this.render();
}
render: function () {
this.$el.html(
this.template(this.model.toJSON())
);
}
});
集合
如果一个 Backbone 模型描述了一个数据项,那么如何描述一个数据项的列表呢?这就是 Collection 的用武之地。一个 Collection 代表了特定模型的数据项列表。可能不会让人意外,Collection 构造函数可以通过 Backbone.Collection.extend() 方法创建。
集合提供了一系列管理列表内容的方法。你可以从集合列表中 .add() 或 .remove() 模型定义的项目,以及 .reset() 整个列表到传递的任何参数。你可以定义一个 url 参数作为 JSON 数据源,并 .fetch() 数据。
在以下代码中,你可以看到如何使用一副扑克牌创建一个 Collection。它基于在模型中定义的 CardModel:
Var CardCollection = Backbone.Collection.extend({
model: CardModel,
url: "http://cardjson.com", // made up for this example
deal: function () {
return this.shift();
}
});
var deck = new CardCollection();
deck.fetch(); //load cards from website if it existed.
实现路由器
Backbone 路由器帮助通过 URL 定义应用程序的状态。它们响应 URL 哈希的变化,或者 URL 中 # 符号后面的文本。哈希最初是在网页中创建的,以便你可以点击锚点标签向下移动页面到相关内容,而无需重新加载整个页面。当启用 Backbone 路由器时,你可以更改哈希,比如通过按钮或锚点点击,它将运行一些 JavaScript 代码来响应内容。所有这些都在发生,页面不会重新加载。
当路由器更改时没有页面重新加载,这使得单页应用程序成为可能。而不是加载新页面,Backbone 可以简单地根据路由器显示不同的视图。这给页面带来了更快的响应。
处理事件
事件在 Backbone 创建的其他项中定义。通过 on() 方法将事件监听器附加到 Model、View、Collection 或 Router 的元素上。on() 方法接受三个参数,一个包含事件名称的字符串,当事件发生时要调用的函数,以及定义 this 的上下文。
由视图创建的 HTML 元素中的事件以不同的方式定义。Backbone.View.extend() 构造函数包含一个 events 参数,它由一个格式不寻常的 JSON 对象定义。事件名称和元素的 jQuery 选择器用作键,一个包含在视图中调用的函数名称的字符串组成键值。示例事件可能如下所示:
…
events: {
"keypress input": "stopOnEnter",
"click #mybutton": "itsMine",
"focus .tinything": "bigScare",
"blur .tinything": "makeSmallAgain"
}
…
在你的应用程序中加入一些 Backbone
由于 Backbone 底层使用 jQuery 与 DOM 交互,我们可以重用 jQuery 应用程序中的大部分代码。我们将使用相同的 ArcGIS JavaScript API 模块与地图服务交互。我们只需更改下拉选项的渲染方式和处理这些下拉框上的更改事件。所以,让我们先复制我们的 jQuery 应用程序,并将文件夹命名为 Backbone。
接下来,我们需要在我们的 dojoConfig 中添加对 Backbone 和 Underscore 库的引用,以便它们可以通过 AMD 可用。对于这个应用程序,我们将从 CDN 源加载它们,尽管你可以将它们下载到自己的应用程序文件夹中:
dojoConfig = {
async: true,
packages: [
…
{
name: "jquery",
location: "http://ajax.googleapis.com/ajax/libs/jquery/1.11.2",
main: "jquery"
}, {
name: "underscore",
location: "http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0",
main: "underscore"
}, {
name: "backbone",
location: "http://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2",
main: "backbone"
}
]
};
之后,我们将在 Census.js 文件中的 define 语句中引用 jQuery、Underscore 和 Backbone 库。文件应按如下方式加载:
define([…, "d3/d3", "jquery", "underscore", "backbone"],
function (…, d3, $, _, Backbone), {
…
});
定义模型
现在,我们有定义我们将要工作的数据模型的机会。如果你还记得 jQuery 练习,我们主要对普查地点的名称和 FIP 代码感兴趣。在普查小部件的 postCreate() 方法中,我们将使用这些字段的默认值来定义我们的模型:
postCreate: function () {
…
// Backbone Models
var State = Backbone.Model.extend({
defaults: {
"STATE_NAME": "",
"STATE_FIPS": ""
}
});
var County = Backbone.Model.extend({
defaults: {
"STATE_NAME": "",
"STATE_FIPS": "",
"NAME": "",
"CNTY_FIPS": ""
}
});
var BlockGroup = Backbone.Model.extend({
defaults: {
"BLKGRP": "0"
}
});
},
定义集合
对于州、县和街区组集合,我们将简单地根据之前定义的相应模型来定义它们。然后我们将创建与 Dojo dijit关联的集合对象。它应该看起来像以下这样:
postCreate: function () {
// Backbone Collections
var StateCollection = Backbone.Collection.extend({
model: State
});
var CountyCollection = Backbone.Collection.extend({
model: County
});
var BlockGroupCollection = Backbone.Collection.extend({
model: BlockGroup
});
this.stateCollection = new StateCollection([]);
this.countyCollection = new CountyCollection([]);
this.blockGroupCollection = new BlockGroupCollection([]);
},
定义视图
现在我们已经定义了我们的模型和集合,是时候为它们创建视图了。我们的视图应该创建出我们在应用程序中需要的数据选项。由于在创建视图时分配模板会导致 Backbone 版本出现错误,因此我们需要为每个下拉菜单创建一个单独的视图。
让我们从StateView变量开始。StateView变量将通过Backbone.View.extend创建。在StateView变量中,我们想要定义一个模板、一个initialize()方法和一个render()方法。initialize()方法将监听集合的reset事件,并使其再次render()。模板是通过 Underscore 的_.template函数定义的,该函数调用由 jQuery 拉取的 HTML 字符串。jQuery 选择器将查找具有stateitemtemplate ID 的元素:
// Backbone Views
var StateView = Backbone.View.extend({
initialize: function () {
this.collection.on("reset", this.render, this);
},
template: _.template( $("#stateitemtemplate").html()),
render: function () {
// compile the template using underscore
var template = this.template,
el = this.$el.empty();
// load the compiled HTML into the Backbone "el"ement
_.each(this.collection.models, function (item) {
el.append(template(item.toJSON()));
});
}
});
在前面的视图中,render函数做了两件事。首先,它将视图模板和空的 jQuery 选择对象加载到变量中。接下来,它使用 Underscore 的each()方法遍历每个集合模型,用模型中的 JSON 内容填充模板,并将其追加到select元素内部。一些其他的 Backbone 示例会在单独的视图中停止选项创建,将选项追加到select元素中,但这种方法是为了简洁而选择的。
现在已经定义了StateView,你可以复制并粘贴代码,并对其进行调整以创建单独的CountyView和BlockGroupView构造函数。在这些构造函数中,你需要更改的唯一事情是模板 jQuery 选择器,分别更改为#countyitemtemplate和#blkgrpitemtemplate。保持initialize()和render()方法不变:
…
var CountyView = Backbone.View.extend({
…
template: _.template( $("#countyitemtemplate").html()),
…
});
var BlockGroupView = Backbone.View.extend({
…
template: _.template( $("#blkgrpitemtemplate").html()),
…
});
最后,我们将实际的视图属性分配给这些View构造函数。对于每个视图,我们将分配它们将要渲染的元素,即我们的小部件中的select元素以及它们将要使用的集合:
this.stateView = new StateView({
el: $(".stateselect"),
collection: this.stateCollection
});
this.countyView = new CountyView({
el: $(".countyselect"),
collection: this.countyCollection
});
this.blockGroupView = new BlockGroupView({
el: $(".blockgroupselect"),
collection: this.blockGroupCollection
});
创建模板
回想一下 jQuery 应用程序,我们用包含特征名称和可用于查询下一级 FIP 代码的选项标签填充了选择下拉菜单。我们需要在 HTML 模板中创建相同的 HTML 元素,而不是通过代码将它们拼接在一起。我们将通过向我们的主页添加 HTML 模板来完成此操作。
我们如何创建页面上不可见的 HTML 模板?我们可以通过将它们插入到脚本标签中来实现。我们将给每个模板脚本标签一个 text/template 类型。这让我们知道脚本的内容实际上是 HTML 代码。浏览器会查看类型,不知道如何处理 text/template 类型,因此简单地忽略它。
因此,让我们为州选项、县选项和区块组选项创建模板。我们将分配模板 ID 为 stateitemtemplate、countyitemtemplate 和 blkgrpitemtemplate,正如我们在代码中所添加的那样。在每个模板中,我们将值、文本和 data-fips 值分配给模型中的适当项。请看以下模板:
<script type="text/template" id="stateitemtemplate">
<option value="<%= STATE_NAME %>" data-fips="<%= STATE_FIPS %>">
<%= STATE_NAME %>
</option>
</script>
<script type="text/template" id="countyitemtemplate">
<option value="<%= NAME %>" data-fips="<%= CNTY_FIPS %>">
<%= NAME %>
</option>
</script>
<script type="text/template" id="blkgrpitemtemplate">
<option value="<%= BLKGRP %>" data-fips="<%= BLKGRP %>">
<%= BLKGRP %>
</option>
</script>
根据你使用的模板库,不同的库有不同的赋值方式。Underscore 的 template() 方法将属性名称包裹在 <%= %> 标签中。你可以使用其他模板库,如 Handlebars.js,但由于需要 Underscore,为什么不使用我们已有的呢?
连接事件
现在让我们让事情发生。我们将重用为 jQuery 练习创建的事件监听器,并使它们适用于 Backbone。我们将从查看 _stateSelectChanged() 方法开始。
首先改变的是我们如何收集 stateName 和 stateFIPS。我们不再引用之前定义的作为 jQuery 对象的 stateSelect,而是通过 stateView.$el 属性访问 select。记住,在 View 中,el 属性暴露 DOM 元素,而 $el 属性暴露 jQuery 元素。对于其他选择更改监听器,我们可以找到并替换 countySelect 和 blockGroupSelect 为 countyView.$el 和 blockGroupView.$el。
需要更改的唯一其他部分是在从地图服务查询后如何填充新的下拉数据。我们可以用简单的匿名函数替换 _populateDropdown() 方法。在匿名函数中,我们将使用 Underscore 的 pluck() 方法从 featureSet 创建一个属性列表。它逐项遍历数组,获取你描述的项的属性,并将其放入一个新的列表中。接下来,我们将通过其 reset() 方法将这个列表分配给 countyCollection。这就是更新县列表所需的所有内容。同样的过程也可以应用于 _countySelectChanged() 方法,以重新填充区块组。你的代码更改应如下所示:
_stateSelectChanged: function () {
var stateName = this.stateView.$el.val();
…
This.queryDropdownData({
…
}).then(lang.hitch(this, function (featureSet) {
this.countyCollection.reset(
_.pluck(featureSet.features, "attributes")
);
});
},
_countySelectChanged: function () {
var stateValue = this.stateView.$el.val(),
stateFIPS = this.stateView.$el.find(":selected")
.attr("data-fips"),
countyValue = this.countyView.$el.val(),
countyFIPS = this.countyView.$el.find(":selected")
.attr("data-fips");
…
This.queryDropdownData({
…
}).then(lang.hitch(this, function (featureSet) {
this.blockGroupCollection.reset(
_.pluck(featureSet.features, "attributes")
);
});
},
_blockGroupSelectChanged: function () {
var stateFIPS = this.stateView.$el.find(":selected")
.attr("data-fips"),
countyFIPS = this.countyView.$el.find(":selected")
.attr("data-fips"),
blockGroup = this.blockGroupView.$el.val();
…
},
让 Backbone 跳舞
最后,我们需要填充 stateView 的初始值。我们将在 jQuery 的 postCreate() 方法调用的末尾使用 queryDropdownData() 方法。如果我们对这个调用做出与事件监听器相同的更改,我们应该能够填充状态下拉菜单。从那里,我们应该能够通过分配给 View 元素的事件监听器填充其他菜单:
postCreate: function () {
…
this.queryDropdownData({
url: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3",
fields: ["STATE_NAME", "STATE_FIPS"],
where: "1=1",
}).then(lang.hitch(this, function (featureSet) {
this.stateCollection.reset(
_.pluck(featureSet.features, "attributes")
);
}));
注意
如果你想了解更多关于使用 Backbone.js 和 Marionette 实现 ArcGIS JavaScript API 的信息,你可以查看 Dave Bouwman 关于此问题的博客文章,网址为blog.davebouwman.com/2013/02/20/part-1-app-design-and-page-layout/。有关 Backbone.js 的更多信息,你可以阅读 Swarnendu De 的《Backbone.js Patterns and Best Practices》。
Knockout.js 概述
另一个可以用来创建交互式单页应用的 JavaScript 库是 Knockout.js。Knockout.js 是由微软的 Steve Sanderson 开发的,尽管它不被视为微软的产品。它基于 Windows Presentation Framework,因为它使用模型-视图-ViewModel (MVVM)架构,并允许在观察属性上进行双向绑定。双向绑定意味着数据不仅被写入 HTML 元素,还可以更新,就像表单中的文本输入字段一样,数据将已经在应用中反映出来。
Knockout 和 MVVM
MVVM 架构在本质上与 MVC 架构相似。两者都使用模型来获取数据,并使用View来显示数据。然而,与一个主动的控制器指导模型和视图不同,ViewModel位于 UI 层之下,并将模型的功能和数据暴露给View。ViewModel通常对其所工作的View一无所知。它只是根据 HTML View的请求存储和提供信息。
在 Knockout 中,ViewModel的创建方式与任何正常的 JavaScript 对象一样,只是 Knockout 增加了一些功能。用于双向绑定的ViewModel属性使用 Knockout 的observable()和observableArray()构造函数创建。这允许这些属性被View访问,并更新,而不需要像 jQuery 那样更新 DOM。构造函数看起来如下所示:
// note that ko is the global library object for knockout.js
function PersonViewModel (firstName, lastName) {
this.firstName = ko.observable(firstName);
this.lastName = ko.observable(lastName);
}
HTML 文档充当View,可以通过基于 HTML5 的data-*属性(更具体地说,是data-bind属性)绑定到ViewModel。当浏览器加载包含ViewModel的 HTML 视图和脚本时,Knockout 将绑定data-bind标签中的属性到ViewModel中的相应字段。对于前面创建的ViewModel,你可能会看到一些如下所示的 HTML:
<div>
<label for='fninput'>First Name:</label>
<input type='text' id='fninput' data-bind='value:firstName' />
<br />
<label for='lninput'>Last Name:</label>
<input type='text' id='lninput' data-bind='value:lastName'/>
<br />
<p>
Hello,
<span data-bind='text: firstName'></span>
<span data-bind='text: lastName></span>!
</p>
</div>
在ViewModel中,可以添加基于ViewModel中其他可观察值的计算属性。这些属性使用ko.computed()构造函数创建。例如,我们可以从前面代码中的PersonViewModel添加一个计算属性fullName,当名字或姓氏改变时,它会自动更新:
function PersonViewModel (firstName, lastName) {
this.firstName = ko.observable(firstName);
this.lastName = ko.observable(lastName);
this.fullName = ko.computed(function () {
return this.firstName() + " " + this.lastName();
}, this);
}
Knockout 没有许多其他库和框架所拥有的额外功能,例如路由器和 AJAX 请求。它通常依赖于其他库,如Sammy.js用于路由和 jQuery 用于 AJAX。然而,它所提供的是即使在旧浏览器上也能工作的双向绑定。
在我们的应用程序中使用 Knockout
让我们复制我们的 jQuery 应用程序并命名文件夹为 Knockout。我们不需要 jQuery 来处理这个应用程序,因为我们可以使用 Knockout 和 ArcGIS JavaScript API 来处理这些功能。我们将首先将所有对 jQuery 的引用替换为 Knockout 引用。文档开头的 dojoConfig 脚本应该看起来像以下这样:
dojoConfig = {
async: true,
packages: [
{
name: 'y2k',
location: location.pathname.replace(/\/[^\/]*$/, '') + '/js'
}, {
name: "d3",
location: "http://cdnjs.cloudflare.com/ajax/libs/d3/3.4.12/"
}, {
name: "knockout",
location: "http://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0",
main: "knockout-min"
}
]
};
接下来,我们将在 Census.js 文件中添加对 Knockout 的引用。我们将保留其他 AMD 模块和代码,并在进行过程中替换它们。Census.js 文件顶部的 define() 语句应该看起来有点像以下这样:
define([…, "d3/d3", "knockout"], function (…, d3, ko) {
…
});
定义 ViewModel
我们知道我们正在处理什么类型的数据模型,但我们需要的是一个 ViewModel 来组织它。我们可以在我们的 Census dijit 中创建 ViewModel 构造函数,并公开它供我们的小部件使用。
我们这个小部件的 ViewModel 只需要六个项目。我们需要维护州、县和街区组的列表,以填充 select 元素,以及选定的值。对于 ViewModel 的 stateList、countyList 和 blockGroupList 属性,我们将为每个构建 Knockout 的 observableArrays。selectedState、selectedCounty 和 selectedBlockGroup 将分别使用 Knockout 可观察对象创建。您可以在以下示例中看到我们如何在小部件中构建 ViewModel:
…
SelectorViewModel: function () {
this.stateList = ko.observableArray([]);
this.selectedState = ko.observable();
this.countyList = ko.observableArray([]);
this.selectedCounty = ko.observable();
this.blockGroupList = ko.observableArray([]);
this.selectedBlockGroup = ko.observable();
},
…
添加自定义绑定处理程序
在我们的 jQuery 和 Backbone 应用程序中,我们向 select 元素附加了事件监听器,以便在它们发生变化时,我们可以查询人口普查数据并填充下一个下拉列表。在 Knockout 中,我们可以使用自定义绑定处理程序做同样的事情。绑定处理程序有两个可选方法:init() 和 update()。init() 方法在绑定首次发生时运行,而 update() 方法在绑定值每次更改时运行。init() 和 update() 都有五个参数,如下所示:
-
element: 参与绑定的 HTML DOM 元素。 -
valueAccessor: 一个函数,它提供了对绑定到元素的观察属性访问权限。要获取此属性的值,请在ValueAccessor()返回的值上调用ko.unwrap()方法。 -
allBindings: 一个用于获取元素特定绑定的对象,如文本、值或名称。可以使用allBindings.get()获取绑定属性。 -
viewModel: 这是获取整个ViewModel的旧方法,在 3.x 版本之前。 -
bindingContext: 这是获取所有绑定的方式。bindingContext至少可能包含以下内容:-
$data: 当前分配给此元素的ViewModel -
$rawData: 直接访问ViewModel中持有的值 -
$parent: 访问可能分配给此ViewModel的父ViewModel -
$parents: 一个数组对象,提供对连接到此ViewModel的每个层的ViewModel的访问 -
$root: 这提供了对基础ViewModel的直接访问,最初绑定到整个页面
-
在将绑定应用到页面之前,我们需要创建绑定处理程序。虽然我们可以在更早的时候创建绑定处理程序,但我们打算在部件的postCreate()方法中分配它们,因为这是我们其他应用程序中应用更改的地方。我们将首先为stateUpdate、countyUpdate和blockGroupUpdate创建一些空的绑定处理程序。我们只对update()方法感兴趣,所以我们将省略init()方法。以下代码是stateUpdate的空版本:
…
postCreate: function () {
ko.bindingHandlers.stateUpdate = {
update: function (
element, valueAccessor, allBindings, viewModel, bindingAccessor
) {
// content will come here shortly.
}
};
},
…
在绑定处理程序和随后的对 ArcGIS JavaScript API 模块的调用中,部件作用域将会丢失。在postCreate()方法中,我们将创建一个名为self的变量,并将对部件的引用分配给它,如下所示:
postCreate: function () {
var self = this;
ko.bindingHandlers.stateUpdate = {…};
…
},
设置了绑定处理程序后,我们将从我们之前的事件处理程序中迁移我们的代码。从_stateSelectChanged()复制代码并做出我们的更改。首先,我们将更改函数收集州名称和人口普查 FIP 代码以供查询的方式。一旦我们有了名称,获取视觉数据将会很容易。最后,我们将更改一旦完成对州内县的查询后如何更新ViewModel。stateUpdate绑定处理程序应如下所示:
…
// within the postCreate() method.
var self = this;
ko.bindingHandlers.stateUpdate = {
update: function (
element, valueAccessor, allBindings, viewModel,
bindingAccessor
) {
// retrieve the selected state data.
var stateData = ko.unwrap(valueAccessor()),
stateName, stateFIPS;
// if stateData is valid and has a state name…
if (stateData && stateData.STATE_NAME) {
// assign state name and FIPS code.
stateName = stateData.STATE_NAME;
stateFIPS = stateData.STATE_FIPS;
// query for shape and attributes to show on the map.
// Replace all "this" references with "self" variable.
self.queryShapeAndData({
url: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3",
where: "STATE_NAME = '" + stateName + "'"
}).then(
lang.hitch(self, self._updateInfoWindowFromQuery),
function (err) { console.log(err); });
// search for counties in that state.
self.queryDropdownData({
url: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/2",
fields: ["NAME","STATE_NAME","CNTY_FIPS","STATE_FIPS"],
where: "STATE_NAME = '" + stateName + "'",
}).then(function (featureSet) {
// create an array of county data attributes
var theCounties = arrayUtils.map(
featureSet.features,
function (feature) {
return feature.attributes;
});
// insert a blank value in the beginning of the array.
theCounties.splice(0, 0, {
NAME:"",STATE_NAME:"",CNTY_FIPS:"",STATE_FIPS:""});
// assign the list to the countyList in the ViewModel
bindingContext.$data.countyList(theCounties);
});
}
}
};
对于县和区块组,我们将在countyUpdate和blockGroupUpdate中遵循类似的模式。记住以下对这个应用程序的更改:
-
将
this引用替换为变量self -
从
ko.unwrap(valueAccessor())获取县和区块组特征属性数据 -
使用
Dojo模块方法arrayUtils.map()收集下拉列表的特征属性列表 -
在特征属性列表的第一个特征上添加一个值为空的属性
-
将新列表添加到
bindingContext.$data数组中
定义视图
现在我们已经完成了设置ViewModel及其相关功能的所有艰苦工作,让我们来处理一些 HTML。在js/template/文件夹中打开Census.html模板。这是我们将要应用View的地方。首先,移除三个select元素上的类分配,并用文本data-bind=""替换它们。
接下来,我们将在每个data-bind上下文中分配四个属性:options、optionsText、value以及我们在上一节中创建的适当绑定处理程序。选项将被分配到适当的observableArray列表中。optionsText将是我们要在选项中看到的字段名称。值和我们创建的绑定处理程序都将绑定到该类型的选定可观察对象。Census.html文件应如下所示:
…
<div >
<b>State Selector: </b>
<select data-bind="options: stateList,
optionsText: 'STATE_NAME',
value: selectedState,
stateUpdate: selectedState"></select>
</div>
<div>
<b>County Selector: </b>
<select data-bind="options: countyList,
optionsText: 'NAME',
value: selectedCounty,
countyUpdate: selectedCounty"></select>
</div>
<div>
<b>Block Group Selector: </b>
<select data-bind="options: blockGroupList,
optionsText: 'BLKGRP',
value: selectedBlockGroup,
blockGroupUpdate: selectedBlockGroup"></select>
</div>
…
应用 ViewModel
现在我们已经有了工作的View和ViewModel,以及将它们与Model连接的代码,是时候将它们全部组合在一起了。一旦页面在postCreate()方法中加载,并且所有我们的绑定处理程序都已分配,就可以安全地调用ko.applyBindings()到ViewModel上。在您在网上看到的多数示例中,ViewModel的绑定方式如下:
ko.applyBindings(new ViewModel());
如果能就此结束并称之为一天,那将是极好的,但我们没有状态数据来初始化SelectViewModel。我们将以某种方式分配它。我们将从 jQuery 和 Backbone 代码中取一些代码来分配状态下拉框的初始值。然后我们将修改它以适应我们为分配其他下拉框所建立的模式。我们将查询州列表,并将列表添加到我们的工作ViewModel中,如下所示:
…
var viewModel = new this.SelectorViewModel();
ko.applyBindings(viewModel);
this.queryDropdownData({
url: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3",
fields: ["STATE_NAME", "STATE_FIPS"],
where: "1=1",
}).then(function (featureSet) {
// make a list of the feature attributes of the states
var theStates = arrayUtils.map(
featureSet.features,
function (feature) { return feature.attributes; });
// add in a blank value to the beginning of the list
theStates.splice(0, 0, {STATE_NAME: "", STATE_FIPS: ""});
// apply the list of states to our viewModel
viewModel.stateList(theStates);
});
注意
如需了解更多关于 KnockoutJS 的信息,您可以访问knockoutjs.com或learn.knockoutjs.com获取交互式教程。关于该库的书籍,您可以查阅 Eric M. Barnard 所著的KnockoutJS Starter,或 Timothy Moran 所著的Mastering KnockoutJS。
AngularJS 简要概述
我们可以在 JavaScript 应用程序旁边使用的较新的流行框架之一是 AngularJS。Angular 最初在 Google 创建,作为一种帮助设计师编码的语言。它很快成为 Google 和国外首选的 JavaScript 框架。核心 Angular 开发团队确保 Angular 在数据绑定和可测试性方面表现出色。
Angular 通过解析 HTML 页面中的特定元素属性来工作。这些属性向 Angular 提供了关于如何将页面上的输入和输出元素绑定到内存中的 JavaScript 变量的指令。绑定到 HTML 的数据可以是代码中定义的,也可以是从 JSON 源请求的。绑定是双向的,类似于 Knockout,但这个框架已经扩展,提供了其他库中更受欢迎的功能。
Angular 和 MV*
虽然 Angular 的设计初衷是 MVC 模式,但开发者们争论说它并不完全遵循 MVC 模式。他们说控制器方面并不真正像控制器那样行为,更像演示者(在 MVP 中)或ViewModel(在 MVVM 中)。这导致一些 Angular 开发者创造了术语模型视图任意(MVW或简称MV)。无论它使用哪种架构风格,它都引起了众多开发者的注意。它目前是最受欢迎的框架之一,甚至比 jQuery 更受大多数 JavaScript 开发者的青睐。
Angular 词汇表
使用 AngularJS 的一个障碍是它引入的新词汇。许多术语是由 Google 的计算机科学家创造的,所以它们可能听起来非常学术。然而,当我们把它们放在我们熟悉的语言环境中时,我们可以更好地理解这些概念。我们将讨论以下 Angular 术语,以及它们如何与我们的映射应用程序相关联:
-
控制器
-
服务
-
指令
应用程序控制器
在 Angular 中,控制器是一个通过 Angular 标签与 HTML 文档进行双向绑定的 JavaScript 对象。控制器范围内的值的变化将改变 HTML 文档上显示的值。Angular 控制器与 Knockout 中的ViewModel类似。对于使用 Angular 编写的基于地图的应用程序,地图通常被定义为主要控制器的一部分。
应用程序服务
在 Angular 中,服务是一个用于与服务器和其他数据源通信的 JavaScript 对象。服务创建一次,并在浏览器应用程序的生命周期内持续运行。ArcGIS JavaScript API 中的等效项可能是QueryTask或GeometryService之类的任务。实际上,如果你使用 ArcGIS JavaScript API 编写 Angular 应用程序,你可以将QueryTask包装在 Angular 服务中,并通过这种方式提供数据。
应用程序指令
在 Angular 中,指令是一个具有在代码中定义的自身行为的自定义元素。指令加载一个模板 HTML 文件,应用自己的绑定,并在网页上显示。在 ArcGIS JavaScript API 中,与指令相对应的是我们在第三章“Dojo 小部件系统”中了解到的 Dojo dijit。dijit定义了一个 HTML 模板以及与之绑定的 JavaScript 行为。指令和dijit之间有一个区别,那就是 Angular 指令允许给 HTML 元素一个自定义名称,例如<overviewmap></overviewmap>,而dijits必须建立在现有的 HTML 元素之上。此外,Angular 事件和描述是用ng-* HTML 参数而不是data-dojo-*参数来描述的。
使 Web 地图应用更加 Angular
由于 AngularJS 是一个框架,它被期望处理页面布局、事件处理等工作。然而,Dojo 也是一个框架,并且对如何处理这些页面事件有自己的看法。这两个框架能否和平共处?
在 AngularJS 和 ArcGIS JavaScript API 并行的使用中,关键是要让 Angular 处理大部分页面布局,但使用来自 ArcGIS JavaScript API 的地图、小部件和任务与 ArcGIS Server 进行通信。
使我们的应用程序在 AngularJS 下运行所需的大部分工作需要太长时间来解释,并值得拥有自己的章节。相反,我提供了一些你可以查阅的资源,以将 Angular 集成到自己的应用程序中。
注意
关于将 ArcGIS JavaScript API 与 Angular.JS 集成的更多信息,你可以阅读 Rene Rubalcava 在 odoe.net/blog/using-angularjs-with-arcgis-api-for-javascript/ 的博客文章,或者阅读他由 Manning Press 出版的书籍 ArcGIS Web Development。你还可以查看 ESRI 在 github.com/Esri/angular-esri-map 上的代码。最后,关于 Angular.JS 的更多信息,你可以阅读 Dan Menard 的 Instant AngularJS Starter,或者 Pawel Kozlowski 和 Peter Bacon Darwin 的 Mastering Web Application Development with AngularJS。
其他框架的整体结果
本章中我们审查的所有框架都与 ArcGIS API for JavaScript 一起工作。其中一些,如 jQuery,可以无缝使用,可以直接使用。而其他一些,如 Backbone 和 Angular,则需要重写应用程序。这些库在浏览器下载网站时都会占用大量的带宽。这些库和框架必须带来非常重要的东西,才能使等待变得值得。
jQuery
jQuery 库执行了在 Dojo 中找到的许多功能。两者在广泛的浏览器中都能很好地工作。虽然 jQuery 的功能和风格使其更容易与 HTML DOM 一起工作,但其事件处理并不直接与 Dojo 的事件处理程序耦合。换句话说,$(map).on("click", …) 并不与 map.on("click", …) 做同样的事情。
如果你正在创建一个以 jQuery 为核心的应用程序,并且想添加一个基于 ArcGIS 的地图,将这两个库混合在一起是完全可以的。你可以轻松地使用 Dojo 和其他表单操作通过 jQuery 定义地图操作。然而,jQuery 并没有添加任何 ArcGIS JavaScript 库中无法通过导入几个额外的 Dojo 模块(这也有助于节省下载带宽)实现的功能。
Backbone.js
Backbone 代码组织良好,但需要大量的代码才能使其工作。所有模型、视图和其他功能都必须单独定义,并且相互关联。还有一些扩展库,如 Marionette.js,可以使 Backbone 更容易编写。使用 Backbone 可以非常有益于围绕地图构建应用程序,但对于这个简单的任务来说,它有点过度了。
Knockout 和 Angular
Knockout 和 Angular 都是优秀的 CRUD(创建、读取、更新和删除)应用框架,但它们并没有为网络地图应用带来任何新颖和强大的功能。如果在一个 HTML 文档中应用大量的双向绑定,它们可能会使应用程序变慢。此外,由于 Angular 是一个框架,它被编写来处理许多后台的用户交互。如果将 Angular 添加到现有的 ArcGIS JavaScript API 应用程序中,大部分代码都需要重写。
总之,我们本可以使用 Dojo 和 ArcGIS JavaScript API 轻松实现我们的目标。通过使用 ArcGIS JavaScript API 编写这些部分,我们可以在智能手机或平板电脑等小型设备上节省时间和带宽。然而,了解如何将这些框架中编写的现有应用程序与 ArcGIS 地图集成是有帮助的。
摘要
在本章中,您已经学习了如何使用 ArcGIS JavaScript API 以及其他 JavaScript 库。我们使用 jQuery、Backbone.js、Knockout.js 和 Angular.js 构建了应用程序。我们比较了这些库和框架的不同用途,以及它们如何与 ArcGIS API 和 Dojo 一起工作。
在下一章中,我们将探讨一些 JavaScript 开发者可能会感到恐惧的主题……样式。
第八章。样式化你的地图
这是让开发者感到恐惧的一个词:样式。他们认为这是一个以右脑为主的艺术家领域。他们将色彩理论、排版和空白视为外语。他们渴望将这些任务推给网页设计师,并专注于算法和 API 调用。
许多小型公司、政府机构和部门没有在他们的员工中配备网页设计师的奢侈,甚至没有可随时调用的人。这些小型组织往往由一或多个因技术分析和技能而被雇佣的人组成,而设计技能则被当作次要考虑。也许你现在就在为这样的组织工作。
虽然这一章可能无法让你立刻成为一名网页设计师,但它将帮助你有效地使用 CSS 来布局你的网络地图应用程序。
在本章中,我们将涵盖以下主题:
-
CSS 如何应用于 HTML 文档
-
在页面上定位地图的不同方式
-
如何使用 Dojo 的
dijit/layout模块来样式化你的页面 -
如何将 Bootstrap 添加到页面的布局中
CSS 的内部工作原理
正如我们在第一章中提到的,你的第一个地图应用程序,层叠样式表(CSS)告诉浏览器如何渲染 HTML 页面。当浏览器扫描 HTML 时,它会扫描所有适用的 CSS 样式,包括 CSS 文件中的样式以及 HTML 中的任何覆盖样式,以确定如何渲染元素。CSS 描述,如颜色和字体大小,通常从父元素级联到其子元素,除非有特定的覆盖。例如,应用于div标签的样式也将应用于其内部的p标签,如下面的代码所示:
<div>
<p>I'm not red.</p>
<div style="color:red;">
<p>You caught me red-handed.</p>
<p>Me too.</p>
</div>
<p>I'm not red</p>
</div>
CSS 与大多数编程语言的工作方式略有不同。在 JavaScript 中,当你程序的一部分有错误时,你可以在浏览器中逐步执行代码,直到找到出错的部分。然而,在 CSS 中,定义元素外观的规则可以跨越多个样式表,甚至可以写在元素内部。元素的外观也可能受到内部和外部元素的影响。
选择器特定性
浏览器通过比较用于定义样式的选择器类型来决定元素应该如何被样式化。它根据选择器的类型和数量应用权重。浏览器有五个基本的 CSS 选择器等级,根据它们的特定性来划分。技术上,当你使用*选择页面上的所有元素时,有一个零级选择器,但与其他选择器相比,它没有价值。选择器等级如下:
-
通过元素(例如
h1、div或p) -
通过元素类、属性和伪选择器。一些例子包括:
-
.email或.form(类) -
input[type='text'](属性) -
a:hover或p:first-child(伪选择器)
-
-
通过 ID(例如
#main或#map-div) -
通过内联样式(
<p style=""></p>) -
通过标记
!important的样式
记录特定性的一个常见方法是对每个类别的选择器数量进行计数,并用逗号分隔它们。一个p选择器得到特定性1,而p > a得到特定性2。然而,p.special > a得到特定性1,2,因为类属于一个单独的、更高等级的分类。一个#main选择器的特定性为1,0,0,而p标签的内联样式得到特定性1,0,0,0。强大的!important子句是唯一可以覆盖内联选择器的因素,它获得特定性1,0,0,0,0。
当比较选择器特定性时,一个高等级选择器胜过任何数量低等级选择器。在出现平局的情况下,比较下一个最低等级的选择器。例如,让我们看看以下 HTML 片段:
<style>
.addr { background: red; }
.start.blank { background: orange; }
.help.blank.addr { background: yellow; }
#citytxt { background: green; }
input[type='text'] { background: blue; }
input { background: purple; }
</style>
…
<input type="text" id="citytxt" class="addr start blank help" />
你认为背景颜色会是什么?正确答案是绿色。#citytxt规则是一个第三等级的选择器,因为它指向页面上的单个元素。如果我们查看选择器及其特定性等级,它们看起来如下:
<style>
.addr { background: red; } /* 0,1,0 */
.start.blank { background: orange; } /* 0,2,0 */
.help.blank.addr { background: yellow; } /* 0,3,0 */
#citytxt { background: green; } /* 1,0,0 */
input[type='text'] { background: blue; } /* 0,1,1 */
input { background: purple; } /* 0,0,1 */
</style>
那么,当其他一切都相等时会发生什么?
等级选择器特定性
当两个或更多规则具有相同的选择器特定性时,最后列出的规则获胜。这是 CSS 级联效果的另一个特性。我们总是在我们的应用程序中将自定义 CSS 放在 Dojo 和 ArcGIS JavaScript API 样式表之后。我们做出的任何样式更改都不会被其他样式表覆盖。
使用“最后者胜出”规则,我们可以撤销应用于我们的小部件的 CSS 规则可能产生的任何意外副作用。我们不必总是求助于使用!important标签或编写丢失在代码审查中的内联样式。只要我们将它放在旧规则之后,我们就可以使用相同的选择器特定性强度,并得到我们想要的结果。
样式技巧和窍门
现在我们已经了解了 CSS 的工作原理,我们可以在此基础上构建一些适用于我们应用程序的工作样式:
-
我们将首先研究一些我们需要避免的不良模式。我们将看看它们如何对样式和进一步的开发产生负面影响。
-
接下来,我们将探讨一些良好的实践,例如使用响应式设计和标准化样式表。
-
我们将探讨如何组织你的样式表,使它们更容易扩展和调试。
-
我们将介绍如何将地图定位在应用程序需要的任何位置。
样式禁忌
在我告诉你该做什么之前,让我们先了解一下你应该避免的一些事情。这些不良的设计习惯通常是在完成初学者教程和从互联网上复制粘贴单页应用程序时形成的:
-
内联样式化元素:试图逐个更改 20 个或更多段落的样式是一件痛苦的事情。
-
使一切变得重要:
important子句允许你覆盖其他小部件和导入的样式表施加的样式,但不要过分沉迷。 -
对单个 ID 的引用过多:一些元素 ID 引用是可以接受的,但如果你想在其他页面或项目中重用你的 CSS 文件,你希望它们尽可能通用。并不是每个人都会使用你的
#pink_and_purple_striped_address2_input元素。 -
在页面底部添加新的 CSS 文件更改:我们都知道“后到先得”,但如果每次都将新的更新直接添加到页面底部,文件就会变成一个杂乱无章的 CSS 规则抽屉。
就像任何硬性规则一样,有时打破它们是合适的。但,在这些规则的范围内工作,会使你自己和他人更容易维护你的应用程序。
响应式设计
响应式设计运动在网站开发中占据了稳固的地位。响应式设计围绕的理念是,网站应该在各种屏幕尺寸上可用,从大屏幕到手机屏幕。这减少了为桌面和移动用户维护多个网站的成本。
表面上看,响应式设计涉及分配百分比宽度和高度而不是固定大小,但还有更多。流体网格布局在较宽的屏幕上支持多列,而在较窄的屏幕上则折叠成单列。可以为平板电脑和具有视网膜显示的屏幕提供不同分辨率的图像,以获得更清晰的图像。CSS 媒体查询可以根据不同大小或不同媒体更改元素的显示方式。
使用 ArcGIS JavaScript API 创建的地图与响应式设计布局配合良好。地图会跟踪其 HTML 容器元素的尺寸和尺寸变化,同时更新其内容。当地图比例保持不变时,范围会重新计算,并为之前未存储在内存中的位置请求新的地图瓦片。这些更改可以通过在桌面浏览器中调整浏览器窗口大小或是在移动浏览器中将平板电脑侧转来触发。
Normalize.css
在浏览器中使你的应用程序看起来良好可能会令人沮丧。引入更多浏览器会加剧这个问题。许多浏览器在不同的设备上都有独特的渲染引擎,这使得相同的 HTML 元素在每个设备上看起来都不同。难道他们不能就 HTML 元素的样式达成一致吗?
开发者和设计师经常使用一个名为 normalize.css 的 CSS 文件(necolas.github.io/normalize.css/)。这个样式表为 HTML 元素设置样式,使它们在不同浏览器和设备上看起来相似。当你关心页面外观时,这减少了猜测的工作量。
normalize.css 文件样式通常作为 HTML 文档头部的第一个样式表插入。你对网站样式所做的任何更改都将在此应用 normalize 规则之后进行,并且不太可能被覆盖。一些 CSS 框架,如 Twitter Bootstrap,已经在其样式表中集成了normalize.css。
组织你的 CSS
如前所述,在“不要用你的样式做这些事情”的列表中,最大的违规行为包括使用比所需更高的选择器特定性,以及将样式表当作一个杂物抽屉。通过正确组织你的样式表,你可以减少这两种违规行为,并使你的应用程序更容易进行样式化和维护。让我们来探讨一些最佳实践。
按选择器特定性组织
当前网页设计趋势要求 CSS 选择器从最低的选择器特定性组织到最高的。你所有的div、h1和p标签可能都放在样式表的最顶部。在定义 HTML 元素的外观之后,你可以添加各种类、选择器和伪选择器来描述它们如何改变元素的外观。最后,你可以通过引用其id来分配单个元素的外观。
按模块分组
使用 ArcGIS JavaScript API 编写的应用程序可以很容易地按小部件组织,那么为什么不按小部件组织样式表呢?你可以在定义页面样式之后,在应用程序中定义单个小部件的样式。你可以使用/* comments */在模块和小部件样式之间分隔你的 CSS 为逻辑部分。
一切皆可分类
在组织选择器特定性代码时,一个常见的做法是尽可能多地分配 CSS 类。你的地图小部件可能有一个.open类来设置width和height。同一个小部件可能有一个.closed类来隐藏小部件。使用dojo/dom-class模块,你可以按任何方式添加、删除和切换你定义的类:
.open {
width: 30%;
height: 100%;
display: block;
}
.closed {
width: 0%;
display: none;
}
使用描述性类可以使你更容易看到你的应用程序正在做什么,尤其是在查看页面源代码时。描述性类在你的样式表中更容易引用。
媒体查询
媒体查询提供了在不同屏幕上创建自定义外观和响应式网格的有效方法。你可以根据媒体类型(屏幕、打印、投影仪等)、屏幕宽度和甚至像素深度(视网膜显示屏与标准桌面屏幕)来改变你网站的外观。
在组织代码时,有一件事需要考虑的是,媒体查询应该放在正常选择器之后。这样,你可以利用“后入先得”的原则,当屏幕尺寸变化时,使用相同的选择器显示不同的结果。我曾经犯过没有注意我的媒体查询放置位置的错误,浪费了时间排查为什么我的过渡效果没有发生。后来我才发现在 CSS 的混乱中,我在媒体查询之后应用了一条规则,这取消了效果。
定位你的地图
我们不能总是依赖其他框架和样式表来正确定位我们的地图。有时候,我们必须自己动手用 CSS 来做。我们将讨论一些地图的样式场景,并查看我们需要应用到地图元素上的 CSS 规则以正确定位它。所有示例都假设你正在创建一个 ID 为"map"的div元素上的地图。
固定宽度的地图
默认情况下,地图会创建一个特定的宽度和高度。宽度和高度可以是任何非负数,从全屏到狭窄的列。如果没有指定高度,地图元素将分配一个默认的height为400px。你可以在以下位置看到地图的简单、非响应式 CSS 样式:
#map {
width: 600px;
height: 400px;
}
将地图拉伸到全屏
有时候,你的地图不仅仅是页面上的一个重要部分。有时候,地图需要占据整个页面。这就是全屏尺寸所代表的含义。假设 HTML 和 body 标签的宽度和高度也是100%,这种样式是可行的。这种全屏样式也可以分配给应该填充另一个元素100%区域的地图:
#map {
width: 100%;
height: 100%;
}
将地图浮动到侧面
有时候你不需要全图。有时候你只需要在屏幕的一侧显示一个小地图,显示与它共享页面的内容的地理位置。然后你可以将内容浮动到一边。将元素浮动到右边或左边可以让其他内容填充在其周围。这种技术通常用于照片和文本,其中文本围绕照片流动。这也适用于地图:
#map {
width: 30%;
height: 30%;
float: right;
}
定位地图顶部和居中
有时候你需要将地图在你的布局中居中。你有一些文本,你只想让地图在中间整齐排列。使用这个居中技巧,你可以水平居中页面上的任何块级元素。在这里,你设置位置为相对,并分配一个自动的左右边距。浏览器会自动将相同的数值分配给左右边距,从而实现居中。但请记住,这必须在具有相对定位的块级元素(如地图)上执行。移除这些条件中的任何一个,这个技巧就不起作用,如下面的代码所示:
#map {
/* just in case something sets the display to something else */
display: block;
position: relative;
margin: 0 auto;
}
使用地图覆盖页面的大部分区域
如果你需要一个几乎满页的效果,需要为标题栏或左侧或右侧的列留出空间,你可以使用绝对定位来拉伸地图。绝对定位将元素移出正常布局,并允许你将其定位在任何你想要的位置。
一旦你为地图分配了绝对定位,你可以使用 top、bottom、left 和 right 值来拉伸地图。将 bottom 的值设置为 0,告诉浏览器将元素的底部边缘设置为页面底部。将 top 的值设置为 40px,告诉浏览器将地图元素的顶部设置为页面顶部下方 40 像素处。通过分配左右两个值,你将地图元素在两个方向上拉伸。
作为注意事项,请记住,绝对定位的元素会超出其位置范围,将位于整个页面内,或者位于具有相对定位的第一个父元素内:
#map {
position: absolute;
top: 40px; /* for a 40px tall titlebar */
bottom: 0;
left: 0;
right: 0;
}
居中已知宽度和高度的地图
有时,你需要将地图放在页面中心。你想要创建一个模态效果,地图在垂直和水平方向上居中,有点像模态对话框。如果你知道地图的宽度和高度,你可以轻松实现这一点。你将绝对定位分配给地图,并将地图的 top 和 left 边缘设置为页面宽度的 50%。一开始这看起来可能不太对,直到你分配了边距。技巧是分配负的 top 和 left 边距,其值分别为地图元素高度和宽度的一半。你得到的是一个垂直和水平居中的地图,同时在旧浏览器中也有效:
#map {
width: 640px;
height: 480px;
position: absolute;
top: 50%;
left: 50%;
margin: -240px 0 0 -320px;
}
居中未知宽度和高度的地图
如果你将百分比或其他单位应用于地图的样式,你可能不知道在任何时候地图有多宽。我们可以使用绝对定位将元素的左上角放置在页面中间,但如何将元素移动,使其位于页面中间?当宽度和高度可变时,有一个简单的方法来垂直和水平居中地图。这需要 CSS3 变换。
我们可以通过使用 CSS3 变换来在任何方向上翻译或移动元素。第一个值将其移动到右侧或左侧,而第二个值将其移动到上方或下方。负值表示向左和向上移动。我们可以用像素为单位应用宽度和高度,或者我们可以应用元素宽度的百分比来居中元素:
#map {
position: absolute;
top: 50%;
left: 50%;
width: 60%; /* any width is okay */
height: 60%; /* any height is okay */
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
/* transform shifts it halfway back*/
}
小贴士
CSS3 变换在大多数现代浏览器中可用,一些稍微旧一点的浏览器需要供应商前缀。Internet Explorer 8 及更早版本不支持这些变换。有关更多详细信息,请参阅 caniuse.com/#feat=transforms2d。
故障排除
随着浏览器端 Web 开发工具的日益流行,您的浏览器是排查页面样式问题的最佳工具。Mozilla Firefox 最初拥有最先进的检查工具,这些工具是通过一个名为Firebug的免费第三方插件实现的。后来,Chrome 发布了自己的开发工具,而 Firefox 和 Internet Explorer 最终也构建并改进了自己的工具。现在所有现代桌面浏览器都为桌面和移动设备提供了高级 JavaScript 和 CSS 信息。
大多数浏览器开发者工具都可以使用相同的键盘快捷键打开。Internet Explorer 从版本 8 开始就响应F12键。Chrome 和 Firefox 也响应F12,键盘组合为Ctrl + Shift + I(Mac 上的Cmd + Opt + I)。
响应式调整大小
所有桌面浏览器在最大化或缩小它们时都会缩小和增长。然而,许多现代浏览器都有额外的功能和插件,可以帮助您测试应用程序,就像它们是移动浏览器一样。Firefox 开发者版的最新版本有一个响应式设计视图,可以根据移动设备调整浏览器的大小。当用户旋转手机时,它可以旋转屏幕,甚至触发触摸事件。Google Chrome 有一个设备模式,允许您从流行的智能手机和平板电脑中选择,可以模拟较慢的网络速度,并通过更改请求中发送的用户代理来模拟移动浏览器。最新版本的 Internet Explorer 在其开发者工具中也提供了这些功能。
现在我们已经回顾了可以用来测试布局的工具,让我们看看 ArcGIS JavaScript API 为我们提供布局应用程序的工具。
Dojo 布局
Dojo 使用自己的框架来控制应用程序的布局。Dojo 的布局可以在dijit/layout模块中找到。这些模块可以通过使用 Dojo 框架来实现具有所有功能的完整页面应用程序。
使用dijit/layout模块创建的 Dijits 可以直接编码在 HTML 中。这些编码使用data-dojo-type属性。包括样式和行为在内的属性编码在元素的data-dojo-props属性中。这些 Dijits 可以通过使用dojo/parser模块解析 HTML 来从 HTML 中加载。
小贴士
在通过 HTML 加载dijit/layout元素的应用中,如果dojo/parser无法访问layout模块,通常会出错。请确保所有用于 HTML 中的布局元素模块都已加载在调用dojo/parser模块的parse()方法的require()或define()语句中。检查模块加载器或 HTML 中的data-dojo-type属性中的拼写错误。
使用dijit/layout模块创建的布局可以分为两类:容器和面板。面板元素通常位于容器内部。
容器
容器是父元素,用于控制其中分配的子窗格的位置和可见性。容器有多种形状和功能。有些可以同时显示多个窗格,而有些则一次显示一个或几个。在 JavaScript 中,如果您可以访问dijit容器,您可以通过调用容器的getChildren()方法来访问其内部的窗格元素。
让我们看看一些常见的容器。
LayoutContainer
LayoutContainer允许其他窗格围绕中心窗格进行定位。在LayoutContainer中的中心窗格被分配了一个中心区域属性。围绕它的窗格被分配了top、bottom、right、left、leading或trailing区域值来定义它们相对于中心窗格的位置。多个窗格可以具有相同的区域属性,例如两个或三个左侧窗格。这些窗格将并排堆叠在中心窗格的左侧。
BorderContainer
BorderContainer是从LayoutContainer派生出来的。正如其名称所暗示的,它为应用程序中的不同窗格添加了边框以分隔它们。BorderContainers还可以提供livesplitters,这些是可拖动的元素,允许用户根据需要调整窗格的大小。
AccordionContainer
AccordionContainer使用手风琴效果来排列和切换窗格。在这种安排中,窗格标题堆叠在一起,任何给定时间只有一个窗格可见。其他窗格的内容通过手风琴效果被隐藏。当用户在AccordionContainer中选择另一个窗格时,窗格将以滑动动画的方式切换,隐藏当前窗格并显示所选窗格。
TabContainer
TabContainer提供了一个基于标签的内容组织。包含ContentPane标题的标签描述了内容,点击这些标签将移除内容的可见性。效果类似于旋转文件柜或文件夹,您可以通过翻动标签来查看所需的内容。
窗格
dijit/layout模块中的窗格为您的应用程序提供了一个容器,用于放置用户控件和小部件。您可以编写 HTML 内容或添加其他 dijit。窗格可以用 HTML 编码,或用 JavaScript 创建并附加到其父容器。让我们看看使用 ArcGIS JavaScript API 在 Dojo 中可用的几个窗格。
ContentPane
ContentPane瓦片是在容器中插入的最常见的窗格。它可以作为除AccordionContainer之外所有其他容器中的窗格插入。表面上,它们看起来像是被赋予了跟踪大小和与其他 dijit 之间关系的div元素。但ContentPane瓦片还可以从其他网页下载并显示内容。设置ContentPane瓦片的href属性将在您的应用程序的单个窗格中下载并显示另一个网页的 HTML 内容。
AccordionPane
在AccordionContainer内添加一个或多个AccordionPane面板,以以可折叠的格式显示其内容。AccordionPane标题堆叠在一起,当你点击标题时,内容滑入视图,覆盖之前打开的AccordionPane。否则,AccordionPane表现出与ContentPane相同的职能行为。
现在我们已经回顾了 Dojo 框架如何处理应用程序布局,让我们看看如何使用替代样式框架。
Bootstrap
如果你正在寻找 Dojo 布局方式的替代方案,你可能需要考虑 Bootstrap。Bootstrap 是一个由 Twitter 的开发者最初创建的流行样式框架。据说,开发者需要一种快速发布网站的方法,因此他们制定了一套样式表作为项目起点。这些样式模板因其易用性和满足大多数网络开发者的需求而非常受欢迎。最初命名为 Twitter Blueprint 的模板后来于 2011 年 8 月作为 Bootstrap 发布。
Bootstrap 为开发者提供了适用于桌面和移动浏览器的响应式设计样式。响应式网格可以根据需要调整,以提供多列布局,在较小的浏览器窗口中折叠成更小的尺寸。Bootstrap 提供了看起来很时尚的表单元素和足够大的按钮,以便在手机浏览器上方便操作。该框架提供了易于理解的 CSS 类,文档和样式表提供了如何使用框架的指导。从可以跨越语言障碍理解的照片图标,到创建模态对话框、标签页、轮播图和其他我们在网站上习惯使用的元素,JavaScript 插件都可以实现。可以使用 Bootstrap 创建整个应用程序。
虽然 Bootstrap 样式不需要 JavaScript 库,但所有 JavaScript 插件都需要 jQuery 才能运行。这对那些使用 Dojo 的人来说并不太有帮助,但我们确实有一个替代方案。
ESRI-Bootstrap
结合 Bootstrap 的易用性和与 ArcGIS JavaScript API 的兼容性,ESRI 创建了 ESRI-Bootstrap 库(github.com/Esri/bootstrap-map-js)。该样式框架可以像 Bootstrap 一样调整地图和其他元素的大小,同时许多元素保留了 Bootstrap 网站相同的视觉和感觉。对话框不会跑出屏幕,元素对鼠标点击和触摸都有响应。最后,ESRI Bootstrap 可以与 Dojo 和 jQuery 的组合或仅使用 Dojo 一起使用。
我们将在我们的 Y2K 应用程序上添加 ESRI-Bootstrap。你可以使用我们在第七章中编写的Dojo/jQuery应用程序,与其他人相处融洽。我们将使用与 jQuery 并行编写的纯 Dojo 应用程序来展示如何在不使用 jQuery 的情况下将 ESRI-Bootstrap 添加到应用程序中。
重新设计我们的应用程序
我们最近让我们的实习生使用其他框架创建了我们的应用程序的多个副本。当他们忙于做这件事的时候,我们决定只用 ArcGIS JavaScript API 编写更新。Y2K 协会检查了我们的应用程序并批准了我们的更改,但这并不是他们要说的全部。
当我们与 Y2K 协会董事会会面时,我们发现董事会的新成员中有一位批评者。他们认为看起来还可以,但需要更现代的外观。当被要求详细说明时,他们向我们展示了他们认为看起来不错的网站。我们看到的所有网站都有一个共同点,它们都是使用 Bootstrap 构建的。他设法说服董事会上的其他人,我们的应用程序需要采用这种新风格。
回到原点,我们研究了 ESRI 能提供什么。我们决定将 ESRI-Bootstrap 整合到我们的应用程序中。它提供了现代感。
将 ESRI-Bootstrap 添加到我们的应用程序中
让我们先在我们的应用程序中添加对 ESRI-Bootstrap 的引用。ESRI-Bootstrap 的入门页面告诉我们下载 GitHub 上发布的最新版本的 ZIP 文件。点击 GitHub 页面右侧的下载 ZIP按钮。下载完成后,解压文件。
我们主要对src/js/和src/css/文件夹中的bootstrapmap.js和bootstrapmap.css文件感兴趣。其余的文件包含模板和示例,您可以查看它们如何使用 Bootstrap 与地图结合。将这些文件复制到js和css文件夹中。
接下来,我们将在index.html文件的头部标签中添加必要的引用。由于我们不再使用 Dojo 小部件来布局我们的应用程序,我们可以移除nihilo.css外部样式表,并添加以下内容:
…
<link href="http://netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet" media="screen" />
<link rel="stylesheet" href="https://js.arcgis.com/3.13/esri/css/esri.css" />
<link rel="stylesheet" type="text/css" href="css/bootstrapmap.css" >
<link rel="stylesheet" href="css/y2k.css" />
…
现在我们已经添加了样式表,我们需要在dojoConfig变量中添加对 Dojo 版 Bootstrap 的引用。覆盖通常由 jQuery 处理的特性的 Dojo Bootstrap 库可以在github.com/xsokev/Dojo-Bootstrap找到,但我们将从rawgit.com拉取它,它直接提供 GitHub 代码。它应该看起来像这样:
dojoConfig = {
async: true,
packages: [
…
{
name: "d3",
location: "http://cdnjs.cloudflare.com/ajax/libs/d3/3.4.12/"
},
{
name: "bootstrap",
location: "http://rawgit.com/xsokev/Dojo-Bootstrap/master"
}
]
};
现在头部已经处理好了,是时候重新设计主体了。
启动我们的 HTML
由于 ESRI-Bootstrap 提供了大量的 CSS 样式和 JavaScript,大部分工作涉及重新设计我们的 HTML。我们可以从移除主 HTML 页面主体中的 Dojo 布局小部件引用开始。您可以移除body元素的class="nihilo",#mainwindow div中的dijit/layout/BorderContainer,以及所有dijit/layout/ContentPane引用。
用导航栏替换标题
我们的第一项任务是替换应用程序中的头部。你可以用很多种方式开始这样的页面,但我们将使用 Bootstrap NavBar 替换头部,因为页面的初始布局是一个没有滚动的全页应用程序。你可以在 getbootstrap.com 看到如何实现 HTML 中的 navbar 的几个示例。
我们将在头部中使用具有 navbar 和 navbar-default 类的 nav 元素替换 header div。我们将在其中添加一个具有 container-fluid 类的 div,因为其内容的大小和外观会随着不同的浏览器而变化。我们将在 container-fluid div 元素中添加两个 div 元素。第一个将具有 navbar-header 类,第二个将具有折叠和 navbar-collapse 类。
我们将在 navbar-header 中添加一个按钮,该按钮仅在浏览器足够窄时才会出现。该按钮将具有 navbar-toggle 和 collapsed 类,以及 data-toggle 属性为折叠。当可见时,该按钮将浮动到最右侧。地图的标题将放在与切换按钮相邻的 span 中。这个 span 将被赋予 navbar-brand 类。
我们将把显示我们的 Census 小部件的按钮添加到 navbar-collapse div 元素中。我们将添加 btn 类(使其成为一个按钮),btn-default(使其具有默认颜色),以及 navbar-btn(使按钮样式适合 navbar)。完成之后,HTML 应该看起来如下所示:
…
<body>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<span class="navbar-brand" >Year 2000 Map</span>
</div>
<div class="collapse navbar-collapse"
id="bs-example-navbar-collapse-1">
<button id="census-btn"
class="btn btn-default navbar-btn">Census</button>
</div>
</div>
</nav>
…
重新排列地图
现在头部已经重新设计,是时候看看页面的其余部分了。我们将想要将人口普查小部件的位置从地图中提取出来,以便包含一个 Bootstrap 模态对话框。我们可以将页脚移动到地图内部以填补之前版本中丢失的空间。我们可以在头部下方定义地图和相关项目,如下所示:
…
<div class="modal fade" id="census-widget"></div>
<div id="map">
<span id="footer">Courtesy of the Y2K Society</span>
</div>
<script type="text/javascript" src="img/app.js"></script>
…
重新设计我们的应用程序
由于我们正在移除 Dojo 布局系统的所有痕迹,我们需要手动重新设计页面。我们可以移除大部分样式引用,因为它们将与 Bootstrap 样式冲突。我们将保留 HTML 和 body 标签的基本样式,因为它们使应用程序成为一个全页应用程序。我们还将保留 D3.js 图表的样式,但可以删除其余的样式。
我们需要将地图从上到下、从左到右拉伸。如果不这样做,地图的宽度和高度将受到限制。我们可以使用绝对定位将地图拉伸到页面。我们将使用之前提到的几乎全页样式。由于页面上的工具栏高度为 50 像素(当你尝试应用程序时你会看到),我们将地图的顶部设置为从顶部 50px。底部、右侧和左侧可以定位在屏幕的边缘:
#map {
position: absolute;
top: 50px; /* 50 pixels from the top */
bottom: 0; /* all the way to the bottom */
left: 0; /* left side 0 units from the left edge */
right: 0; /* right side 0 units from the right edge */
}
我们还需要重新设计另一个元素,即页脚元素。我们可以使用与地图相同的技巧将其定位在页面底部。我们还可以使背景半透明,以达到很好的效果:
#footer {
position: absolute;
bottom: 5px; /* 5 pixels from the bottom */
left: 5px; /* left side 5 pixels from the left edge */
padding: 3px 5px;
background: rgba(255,255,255,0.5); /* white, semitransparent */
}
一旦应用了这些样式,我们就可以看到我们地图的工作示例。您可以在以下图片中看到地图的示例:

使我们的 Census dijit 模态化
现在我们已经将页面转换成了 Bootstrap 应用,我们需要将相同的样式应用到我们的Census dijit 上。我们需要利用 Bootstrap 的模态小部件来模仿浮动对话框的效果。
在js/templates/文件夹中打开Census.html文件。在基础div元素中,添加modal和fade类。modal类告诉 Bootstrap 这将是一个模态对话框,而fade描述了元素如何隐藏和显示。我们还将向元素添加一个data-backdrop属性并将其设置为static。这将创建一个通用的模态对话框,在打开时阻止点击页面的其余部分。在这种情况下,我们将放弃关闭小部件将关闭地图点击事件的想法:
<div class="${baseClass} modal fade"
style="display: none;" data-backdrop="static">
…
</div>
我们将在基础div中添加几个更多的div元素来定义模态和标题。在我们的模态类内部的一个级别,我们将添加一个具有modal-dialog和modal-sm类的div元素。modal-dialog类定义了模态的样式,而modal-sm使模态更小。移除modal-sm将创建一个跨越更大屏幕的对话框。
我们将在具有modal-dialog类的div中创建一个具有modal-content类的div,并在其中添加两个div元素,分别具有modal-header和modal-body类。我们将把我们的关闭按钮和标题添加到modal-header div 中。我们将把剩余的dijit文本和选择下拉列表添加到modal-body div 中:
<div class="${baseClass} modal fade"
style="display: none;" data-backdrop="static">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
…
</div>
<div class="modal-body">
…
</div>
</div>
</div>
</div>
我们将在modal-header div 中将dijit关闭事件替换为 Bootstrap 的模态关闭事件。我们将向按钮添加一个close类,并添加一个模态的data-dismiss属性。大多数 Bootstrap 示例还包含 ARIA 属性来处理屏幕阅读器和其他辅助工具,因此我们将添加一个aria-hidden值为true,这样屏幕阅读器就不会大声朗读我们放置在那里的X。对于标题,我们将用具有modal-title类的span包围Census数据。它应该看起来像以下代码:
…
<div class="modal-header">
<button type="button" class="close"
data-dismiss="modal" aria-hidden="true">x</button>
<span class="modal-title">Census Data</span>
</div>
…
我们将把我们的描述段落添加到modal-body div 中,并格式化选择元素,使它们看起来像表单元素。我们将向围绕我们的选择下拉列表的div元素添加form-group类。这将使内容对齐并添加适当的间距和格式。我们将用label标签替换b标签,并添加control-label类。我们将向选择元素添加form-control类。这将使select下拉列表扩展到对话框的宽度。我们的 HTML 应该如下所示:
…
<div class="modal-body">
<p>
Click on a location in the United States to view
the census data for that region. You can also use
the dropdown tools to select a State, County, or
Blockgroup.
</p>
<div class="form-group" >
<label class="control-label">State Selector: </label>
<select class="form-control"
data-dojo-attach-point='stateSelect'
data-dojo-attach-event='change:_stateSelectChanged'>
</select>
</div>
<div class="form-group">
<label class="control-label">County Selector: </label>
<select class="form-control"
data-dojo-attach-point='countySelect'
data-dojo-attach-event='change:_countySelectChanged'>
</select>
</div>
<div class="form-group">
<label class="control-label">
Block Group Selector: </label>
<select class="form-control"
data-dojo-attach-point='blockGroupSelect'
data-dojo-attach-event='change:_blockGroupSelectChanged'>
</select>
</div>
</div>
…
我们将在modal-body div 中添加一个带有modal-footer类的div。在这里,我们可以添加另一个按钮来关闭对话框,以防用户没有注意到上角的小x。我们将通过添加btn和btn-default类来格式化关闭按钮,这将影响形状和颜色,顺序如下。我们还将添加data-dismiss属性并将其设置为modal。它应该看起来如下:
…
<div class="modal-footer">
<button id="btnDismiss" type="button"
class="btn btn-default" data-dismiss="modal">
Dismiss
</button>
</div>
</div>
…
一旦正确格式化小部件 HTML,应用程序应该看起来像以下图像。注意下拉菜单和宽间距的按钮,这使得在较小的移动设备上点击它们更容易:

现在应用程序功能已经完善,我们可以修改其部分以创建我们自己的外观和感觉。记住,Bootstrap 旨在作为创建网站的起点。它不一定是最终结果。我们仍然可以更改颜色和其他功能,使应用程序成为我们自己的。
摘要
在本章中,我们讨论了您可以使用不同方式来设置您的 ArcGIS JavaScript API 应用程序的样式。我们探讨了 CSS 的工作原理,规则如何相互影响,以及浏览器如何决定遵循哪些 CSS 规则。我们探讨了 Dojo 布局模块,以及如何使用这些模块来处理应用程序的外观和功能。我们还探讨了 ESRI-Bootstrap,这是一个可以与 ArcGIS JavaScript API 一起运行的 Bootstrap 版本。最后,我们将 ESRI-Bootstrap 的外观添加到我们的应用程序中。
在下一章中,我们将转向移动端。我们将创建一个适用于大多数平板电脑和手机浏览器的移动应用程序。
第九章:移动开发
移动网页开发在过去的几年中引起了很大的轰动。当苹果公司推出 iPhone 时,它不支持第三方插件,如 Flash 和 Silverlight。这挑战了网页开发者仅使用 HTML、CSS 和 JavaScript 在移动平台上提供有价值的网页体验。如 HTML5、CSS3、ECMAScript 5 和 6 等提议的功能增强,以及更强大的浏览器,都改善了移动浏览体验。
公司和组织在提供移动网页体验方面采取了不同的方法。一些组织将移动浏览器重定向到仅提供移动内容的网站(使用如 mobile.example.com 或 m.example.com 这样的 URL,而不是 www.example.com)。其他人则使用了响应式设计和以移动为先的策略,为手机、平板电脑和桌面屏幕提供格式不同的相同内容。还有一些公司,如 Facebook,已经放弃了移动网页开发,专注于移动应用,使用原生应用或混合网页应用。
在本章中,我们将探讨以下内容:
-
移动设备开发与桌面网站开发的不同之处
-
如何使用 ArcGIS 紧凑型构建
-
如何使用
dojox/mobile模块控制移动用户体验
拥抱移动
移动网页开发是一个快速发展的市场。在 2012 年的印度,从移动设备提供互联网内容的百分比超过了桌面电脑。在美国,报告显示,2014 年来自移动设备的网络流量占 10%,并且这个比例正在增加。
人们在使用移动互联网时都在做什么?有些人正在检查他们的电子邮件。其他人正在搜索信息,玩游戏,或者与他人保持联系。人们希望保持连接,享受娱乐,并获取信息,他们希望这些信息在他们需要的时候可用。
移动与众不同
使网站在移动设备上工作与在桌面机器上工作大相径庭。在桌面浏览器上曾是特性的许多事物,现在在移动设备上却成为了障碍。让我们看看是什么让移动与众不同,无论是好是坏。
好的方面
即使有所有这些负面因素,移动应用开发仍然是一个令人兴奋的领域。移动网页应用有很多好的特性。现代智能手机提供了在桌面电脑上显得奇怪但却是移动应用至关重要的传感器和工具。它们为用户带来了一个信息的世界,并允许用户在需要的时候记录和分享事物,而不是在启动他们的桌面电脑的那天晚上。让我们更详细地看看这些特性。
访问手机传感器
移动设备内置了许多传感器。这些传感器可以检测设备的方向、加速度,甚至位置。位置可以通过内置的 GPS 设备、手机信号三角测量或基于 Wi-Fi 信号的位置来收集。手机中的新传感器可以传递电池强度。此外,一些第三方混合工具提供了访问更多手机功能的方法,例如内存、联系人和相机。
即时访问应用程序
用户不再需要写下 URL 在家中使用。现在,他们可以将 URL 说给浏览器听,或者拍下二维码来访问网站。这种即时访问让更多的人立即使用您的应用程序,减少了忘记应用程序位置的风险。
通过即时访问应用程序,用户可以在他们所在的位置收集数据,而不是回家输入信息。用户可以拍摄损坏的消防栓的照片,登录社区问题应用程序,并向适当的当局报告问题。现场工作人员可以在现场收集特征数据并检查它们。志愿者地理数据收集可用于公民科学、市政问题跟踪以及其他众多应用。
即时访问用户
与可能被图书馆电脑亭的任何人使用的桌面应用程序不同,手机更有可能只有一个用户。应用程序的使用可以与用户资料相结合,为您提供一个更全面的了解,了解您的应用程序如何被不同的群体所使用。可能性是无限的。
坏处
你花费数小时搭建了一个看起来完美的网站,在您的显示器上看起来无懈可击。你在三四个不同的浏览器上测试了您的网站,并且对看到的结果感到满意。它看起来很棒;它运行得很好。它注定会成功。
然后,你遇到了一个朋友,想向他们展示你的惊人网站,但你只有智能手机。你认为没问题,因为你正在将网站的 URL 输入到手机的浏览器中。出现的是可用性噩梦。屏幕的一部分被切掉了。控件不像你计划的那样工作。你不能正确地浏览菜单。总的来说,一团糟。
现在,让我们看看移动网页开发的一些痛点。
屏幕尺寸如此之多
在互联网的旧日子里,你只需要担心几个显示器尺寸。只要你在 1024x768 的显示器上制作了一个看起来不错的网站,你就没问题了。有些人有钱买更大的显示器,而有些人有更小的显示器,但差别不大。
现在,智能手机、平板电脑和其他设备的屏幕尺寸从四英寸的角到角到平板电视大小不等,由于屏幕技术的巨大进步,较小的屏幕的像素密度是标准桌面显示器的 1.5 倍、2 倍甚至 3 倍。在桌面电脑上容易阅读的网站在智能手机上变得拥挤且更小。
随着屏幕分辨率的增加,您需要在网站上进行的测试数量也在增加。网站在 3 英寸宽的手机上看起来和高清电视上一样好吗?它是否能够很好地缩放?
指尖精度
当从桌面应用程序迁移到移动应用程序时,丢失的另一个功能是鼠标的高精度输入。无论您使用鼠标、笔记本电脑触摸板还是电脑的触控笔,您都有一个鼠标指针,可以提供对您内容的精细操作。在移动设备上,您必须考虑到较大的手指可能会点击到您精心设计的多个按钮。此外,您为网站创建的小型关闭按钮可能对于一些大型指纹来说太难关闭。
除了失去鼠标指针的精度外,您还会失去鼠标悬停事件。从简单的工具提示到由 CSS 驱动的可折叠菜单,在移动浏览器中都不会按预期工作,因为没有悬停事件可供监听和处理。您五到十年前的旧代码在移动网络时代将不会按相同的方式工作。
电池寿命
处理电池寿命可能又是移动开发的另一个障碍。重复访问位置数据和持续监控广告可能会耗尽移动设备的电量。虽然这些信息很有用,但它是有代价的。请记住,并不是每个人的手机都有满电,而且并不是每个电池都能持续数小时。
更多设备
我们之前提到了屏幕尺寸的多样性,但这只是开始。市面上有各种各样的设备,运行着 Android、iOS、Windows Phone 和其他操作系统。每个设备都有多个网络浏览器的选择,甚至这些网络浏览器可能处于不同的版本号。因此,对于最新的和最好的网络功能的支持可能会有所不同,这取决于具体的功能。您将不得不决定您愿意支持什么,以及您愿意购买哪些设备进行测试。
现在我们已经探讨了为什么我们应该构建移动应用程序,让我们看看通过 ArcGIS JavaScript API 我们可以使用哪些工具。
ArcGIS 紧凑型构建
ArcGIS JavaScript API 可以作为更紧凑的库加载到移动浏览器中。所谓的紧凑型构建,包含了在移动浏览器中查看地图应用程序所需的最小库。其他模块可以通过 Dojo 的require()语句下载,但许多模块不会与初始库一起预加载。
紧凑型构建中包含的模块
ArcGIS 紧凑型构建包含构建网络地图应用程序所需的所有模块。它加载的方式与常规的 ArcGIS API for JavaScript 相同,使用require()和define()语句。它还包含最常用的模块,如esri/Map和esri/layers/ArcGISDynamicMapServiceLayer,以便在尽可能少使用带宽的情况下快速加载您的地图。
未包含的内容
虽然 ArcGIS JavaScript 紧凑版构建提供了所有这些功能,但您可能会认为它们必须牺牲一些东西。紧凑版构建首先放弃的是重量。在 3.13 版本中,该库的重量为 179.54 KB,而其更庞大的同侪为 107.26 KB。常规构建附带了一些预加载的库,而紧凑版构建使用 require() 或 define() 语句单独请求这些模块。这样,您可以更好地控制发送给用户的库部分。
在 ArcGIS JavaScript API 紧凑版构建中牺牲的另一项是依赖于 dijit 命名空间。你首先会注意到弹出窗口被替换为更简化的版本。此外,如果你喜欢使用渐变缩放滑块来缩放地图,在紧凑版构建中你可能要放弃了。它只支持 + 和 – 按钮来缩放地图。如果你有依赖于 dijit/_Widgetbase 库的小部件,可以通过 require() 和 define() 语句单独下载。
这对你意味着什么?
ArcGIS JavaScript 紧凑版构建提供了与常规构建相同的大部分功能。在一些控件上存在一些差异,但它们都呈现相同的地图和信息。较小的库大小非常适合将地图嵌入现有应用程序中,或者使用其他库,如 Angular、Knockout 或 jQuery,来处理其他组件交互。如果你不依赖于紧凑版构建中丢失的少数功能,尝试一下是值得的。
注意
关于 ArcGIS JavaScript API 紧凑版构建的更多信息,请参阅 developers.arcgis.com/javascript/jshelp/inside_compactbuild.html。
ESRI Leaflet
Leaflet.js 库为 ArcGIS JavaScript API 提供了另一种选择。这是一个轻量级的库,可以在广泛的浏览器上显示地图。Leaflet 与任何瓦片地图服务都兼容,可以通过 geojson(一种流行的开源地理数据 JSON 格式)添加点、线和多边形。该库可以通过插件支持不同的工具和数据源。Leaflet 库拥有丰富的插件生态系统,每天都有更多工具和数据源插件被开发。
ESRI 发布了 ESRI Leaflet 插件,以便 Leaflet 地图可以使用 ArcGIS 服务器地图服务。根据 ESRI Leaflet GitHub 页面,它支持以下地图服务层:
-
ESRI 底图服务
-
特征服务
-
瓦片地图服务
-
动态地图服务
-
ImageServer 地图服务
注意
关于 Leaflet.js 库的更多信息,您可以访问 leafletjs.com/。关于该库的书籍,您可以查阅 Paul Crickard III 的 Leaflet.js Essentials 或 Jonathan Derrough 的 Interactive Map Designs with Leaflet JavaScript Library How-to。
Dojox 移动
你不希望创建一个模仿移动设备风格的应用程序,同时看起来像原生应用吗?这正是 Dojo 框架的一些贡献者所想的,这导致了 dojox/mobile 中的模块。这些模块提供了与原生移动应用中的许多 UI 元素相匹配的控件,在形式和功能上模仿它们。在这个库的部件中,按钮和滑块看起来像 Safari 上的 iPhone 按钮和滑块,而在基于 Android 的浏览器上则表现为原生 Android 按钮和滑块。
dojox/mobile 模块提供了一种模仿原生移动应用的视觉交互框架。与它们的 dijit 表单对应物不同,dojox/mobile 用户控件不使用那么多 HTML 元素,从而提高了下载速度和内存使用。UI 元素与其他 dojox/mobile 控件配合良好,从占据整个屏幕的 dojox/mobile/View 到最后的 dojox/mobile/Button。让我们看看其中的一些。
Dojox 移动视图
dojox/mobile/View 模块在应用程序的部分之间提供了视觉分隔。视图是完整的页面容器,可以通过滑动或按钮点击进行导航。这些在组织内容方面与 dijit/layout/ContentPane 有一定的相似性。
与 dojox/mobile/View 相关,dojox/mobile/ScrollableView 以移动用户期望的方式提供了额外的滚动功能。在许多移动设备上,当用户滑动屏幕向下滚动页面时,该用户期望页面将继续滚动,直到减速停止。"ScrollableView" 实现了页面上的惯性滚动。
注意
由于 ScrollableView 滚动事件会干扰触摸屏界面上地图的平移,因此您不应将交互式地图添加到该视图中。"ScrollableView" 更适合于表单和可能超出屏幕高度的内容。
与触摸一起工作
如果您习惯于在 JavaScript 中使用鼠标事件,触摸事件可能会有些令人困惑。touchstart 和 touchend 事件看起来与 mousedown 和 mouseup 事件相当。
在当前版本的 ArcGIS JavaScript API 中,许多触摸事件已经被 API 模块处理。当与地图一起工作时,您不需要在 map.on("click") 事件监听器之上分配 map.on("touchstart") 事件监听器。map.on("click") 事件监听器为您处理它。对于任何 Dojo 小部件和控制也是如此。这又少了一件您需要做的事情来使您的应用程序移动化。
说到地图,有一些触摸事件可用,使得一些导航工具变得过时。您可以在屏幕上捏合或展开手指来分别放大和缩小。通过在地图上拖动手指可以控制平移。这些操作消除了放大、缩小和平移按钮的需求,从而可以释放宝贵的屏幕空间。
手势
JavaScript 处理复杂的移动手势并没有像在原生应用程序中那样顺畅。例如,原生应用程序被开发出来以区分点击和长按点击。默认情况下,许多基于 JavaScript 的应用程序将它们都视为点击。
ArcGIS JavaScript API 中的 Dojo 框架提供了一些用于处理手势的实验性库,即 dojox/gesture 模块。这些模块允许你使用 dojo/on 来分配事件,如下面的代码片段所示:
require(["dojo/on", "dojox/gesture/tap"], function (dojoOn, tap) {
dojoOn(node, tap, function (e) {…});
});
对于简单的手势定义,dojox/gesture 模块允许你使用 dojox/gesture/tap 和 dojox/gesture/swipe 分别定义点击和滑动事件。对于点击事件,你可以定义单次点击、双击和长按点击事件。对于滑动事件,你可以定义滑动事件开始和结束的事件。在下面的代码片段中,你可以看到实现它的示例:
require(["dojo/on", "dojox/gesture/swipe"], function(on, swipe){
on(node, swipe, function(e){ … });
on(node, swipe.end, function(e){ alert("That was easy.");});
});
你找不到你想要的操作手势吗?使用 dojox/gesture/Base 模块,你可以定义你自己的自定义手势。到目前为止,你必须定义自己的方法来处理旋转、捏合和手指展开等手势。在某个时候,将会有对这些手势的更通用支持,但不是在撰写本文时。
注意
如果你想要了解更多关于在 Dojo 应用程序中处理触摸和手势的信息,你可以访问 dojotoolkit.org/reference-guide/1.10/dojox/gesture.html。
我们的应用程序
随着我们的故事继续,我们收到了来自加利福尼亚州霍利斯特市的电话,关于他们的事故报告应用程序。他们喜欢这个应用程序,并且它对处理这些问题的电话接待员来说效果很好。现在,他们想要一个更适合移动设备的版本,并且他们来找我们寻求帮助。是我们利用我们对移动应用程序的知识来创建一个他们可以在现场使用的智能手机工具的时候了。
原始的事故报告应用程序是使用典型的 dijit/layout 元素构建的,其中每个面板都在屏幕上有一个位置。现在,我们必须考虑在较小的屏幕上可能没有足够的空间来容纳所有内容。相反,我们需要将每个面板组织成它自己的独立视图。我们需要控制如何在这些视图之间导航,并使用适合移动设备的适当控件。
我们将使用 ArcGIS JavaScript API 紧凑构建,以及 dojox/mobile 模块,来创建一个适合移动设备的网络应用程序。我们将地图放在一个视图中,事故选择器放在第二个视图中,更详细的报告表单放在第三个视图中。对于所有这些,我们将使用 dojox/mobile 用户界面组件,以及 ArcGIS JavaScript API 编辑小部件,不仅创建一个适合移动设备的,而且创建一个用户友好的报告体验。
更改布局
我们将通过将 ArcGIS 紧凑构建加载到 index.html 文件中来开始创建移动应用程序。在 HTML 文档的头部,我们将更改指向 ArcGIS JavaScript API 的链接以加载紧凑构建。我们将保留 esri.css 文件和我们的样式表引用,但我们可以移除 claro.css 样式表引用,因为我们的应用程序不需要它。我们的 index.html 文件应该看起来像以下这样:
<head>
…
<!-- note that the claro.css stylesheet is removed -->
<link rel="stylesheet"
href="https://js.arcgis.com/3.13/esri/css/esri.css" />
<link rel="stylesheet" href="./css/style.css" />
<script type="text/javascript">
dojoConfig = { async: true, isDebug: true };
</script>
<script src="img/"></script>
</head>
…
我们的应用程序主体有三个可操作的部分。有一个地图,我们在其中放置事件。还有一个面板,我们可以从中选择地图上的事件。最后,还有一个表单,我们可以填写有关事件的更多信息。我们将这些内容布局到不同的视图中:mapview、incidentview 和 attributeview。在每个视图中,我们将添加我们应用程序所需的标题和控制项。它应该看起来像以下这样:
<body>
<div id="mapview" data-dojo-type="dojox/mobile/View">
<h1 data-dojo-type="dojox/mobile/Heading">
Incident Reporting App
</h1>
<div id="map" >
<div id="locatebutton"></div>
</div>
</div>
<div id="incidentview" data-dojo-type="dojox/mobile/View">
<h2 data-dojo-type="dojox/mobile/Heading"
data-dojo-props="back:'Map',moveTo:'mapview'">
Incident
</h2>
<div id="pickerdiv"></div>
</div>
<div id="attributeview" data-dojo-type="dojox/mobile/View">
<h2 data-dojo-type="dojox/mobile/Heading"
data-dojo-props="back:'Incident',moveTo:'incidentview'">
Description
</h2>
<div id="attributediv"></div>
</div>
…
</body>
在前面的代码中,我们添加了熟悉的 data-dojo-type 属性来在页面上创建 dojox/mobile/View 模块。在每个视图中,我们都有一个 dojox/mobile/Heading 模块元素,用于在页面顶部显示标题。标题还充当一种按钮栏,我们可以将其中的返回按钮放入其中。在标题的 data-dojo-props 属性中,我们定义了一个带有 back 属性(定义按钮文本)和 moveTo 属性(定义它切换到的视图)的返回按钮。
修改 JavaScript
在我们的 app.js 文件中,我们需要修改 require 语句以加载移动库的适当模块。而不是加载用于设置布局的 dijit/layout 模块,我们需要添加 dojox/mobile 的等效模块。在 app.js 文件的 require() 语句中,修改代码以添加以下内容:
require([
"dojox/mobile/parser",
"dojo/dom",
"dojo/on",
"esri/config",
"esri/map",
"esri/graphic",
"esri/layers/FeatureLayer",
"esri/layers/ArcGISDynamicMapServiceLayer",
"esri/symbols/SimpleMarkerSymbol",
"esri/geometry/Extent",
"esri/dijit/editing/Editor",
"esri/dijit/editing/TemplatePicker",
"esri/dijit/editing/AttributeInspector",
"esri/dijit/LocateButton",
"esri/tasks/query",
"dijit/registry",
"dojox/mobile/Button",
"dojox/mobile",
"dojox/mobile/deviceTheme",
"dojox/mobile/compat",
"dojox/mobile/View",
"dojox/mobile/Heading",
"dojox/mobile/ToolBarButton",
"dojo/domReady!"
], function (
parser, dojoDom, dojoOn,
esriConfig, Map, Graphic,
FeatureLayer, ArcGISDynamicMapServiceLayer,
MarkerSymbol, Extent,
Editor, TemplatePicker, AttributeInspector,
LocateButton, Query, registry, Button
) {
如您所见,我们用 dojox/mobile/parser 的等效项替换了正常的 dojo/parser。我们添加了 dojox/mobile 基础类,dojox/mobile/deviceTheme 根据您的浏览器加载适当的主题,以及 dojox/mobile/compat 以确保网站也可以在较旧的桌面浏览器(如 Internet Explorer)上查看。对于我们想要从 dojox/mobile 库中看到的元素,我们加载了 View、用于查看标题数据的 Heading 和用于向标题添加按钮的 ToolBarButton。
在移动设备上处理地图
让我们专注于使地图正常工作。在我们的移动应用程序中,我们在 mapview div 中添加了地图。我们在 style.css 文件中将地图的宽度和高度设置为 100%。地图应该正常加载,对吧?
当我们以这种方式加载地图时,尤其是从较大的浏览器中加载,我们发现地图并没有延伸到最底部。使用我们最喜欢的检查 DOM 元素的工具,我们发现地图 div 的高度已被设置为内联的 400px。我们以前在哪里见过这种情况?
在检查地图周围特征 DOM 元素后,我们发现视图的高度尚未设置。默认情况下,mapview div 的高度取决于其内容的高度。由于其高度未定义,地图将其高度设置为400px。为了修复这个问题,我们需要在style.css文件中手动定义mapview div 的高度。我们还将通过将overflow-y设置为hidden来停止mapview div 的滚动。这将移除我们地图上的任何不美观的滚动条,这可能会干扰地图导航:
#mapview {
height: 100%;
overflow-y: hidden;
}
#map {
width: 100%;
height: 100%;
}
修复 LocateButton
使用浏览器的 GPS 功能将地图中心定位到我们位置的LocateButton已经移动了位置。看起来是添加了dojox/mobile/Heading和用于缩放和平移的移动按钮,导致我们的LocateButton偏移。我们可以使用我们喜欢的浏览器 DOM 浏览器来重新定位 Locate 按钮到合适的位置,然后将其包含在style.css文件中以使其更加持久。LocateButton的样式应该看起来像以下这样:
.LocateButton {
position: absolute;
left: 21px;
top: 130px;
z-index: 500;
}
当你完成时,你应该有一个看起来像以下的地图:

使用编辑小部件
现在我们使用 ArcGIS 紧凑型构建,我们无法访问与附件编辑器一起使用的弹出 dijit。我们也没有许多其他基于 dijit 的模块。这个应用程序可能需要更多的工作才能使其适合移动设备。
模板选择器
为了从事件列表中选择,我们选择了esri/dijit/editing/TemplatePicker模块来创建选择事件的按钮。现在,我们将继续使用它,但我们将以不同的视角来看待它。原始的一个在页面的一侧呈现了垂直的按钮列表来展示事件。现在,我们将移除这些设置,并正常定义模板选择器。初始化模板选择器的代码应该看起来像以下这样:
function startEditing () {
…
picker = new TemplatePicker({
featureLayers: [ layer ],
style: "width:100%;height:auto;",
grouping: false
}, "pickerdiv");
picker.startup();
…
要访问事件选择器或编辑当前选定的特征,我们需要调用showInspector()函数。如果我们查看现有的函数,它尝试根据我们点击的点周围的位置在特征服务中选择特征。它使用地图的infoWindow来显示属性编辑器。现在,由于我们使用另一个位置来编辑特征属性,我们需要修改showInspector()代码以处理我们的新功能。
我们使showInspector()函数在移动设备上工作的第一步是调整与在地图上点击相比所选的表面积。目前,它在我们点击点周围创建了一个两像素宽的范围。我们可以将其扩展到 10 像素,因为我们的手指比鼠标指针宽。此外,我们还需要在查询成功后修改callback函数。如果在地图上点击的位置没有特征,我们将显示模板选择器。如果没有选择,我们将告诉它转到属性检查器:
function showInspector(evt) {
var selectQuery = new Query(),
point = evt.mapPoint,
mapScale = map.getScale();
selectQuery.geometry = new Extent({
xmin: point.x - mapScale * 5 / 96,
xmax: point.x + mapScale * 5 / 96,
ymin: point.y - mapScale * 5 / 96,
ymax: point.y + mapScale * 5 / 96,
spatialReference: map.spatialReference
});
incidentLayer.selectFeatures(selectQuery, FeatureLayer.SELECTION_NEW, function (features) {
if (!features.length) {
goToPicker(point);
} else {
goToAttributeInspector('mapview', features[0]);
}
});
}
在我们的goToPicker()函数中,我们将从mapview切换到incidentview。我们将通过使用dojox/mobile/View提供的performTransition()方法来实现这一点。它接受最多五个参数:一个用于查看另一个视图的id、基于数字的方向(1或-1)、过渡样式以及定义this的用于完成第五个参数的callback函数的对象。我们将告诉mapview过渡到incidentview,从右侧使用slide动画,并在过程完成后添加一个callback函数:
function goToPicker(point) {
registry.byId('mapview').performTransition('incidentview', 1, 'slide', null, function() {
…
});
}
当我们尝试以当前形式运行goToPicker函数时,它会跳转到incidentview,但我们看不到任何事件。这是由于一些 dojo 小部件的一个有趣特性。当不可见时,小部件将它们的宽度和高度设置为0,从而变得不可见。我们需要刷新TemplatePicker内部网格并清除模板选择。
当用户从列表中选择一个特征时,我们需要某种方式将我们的选择传达给属性检查器。我们还将使用dojo/on模块中的once()方法添加一个单一的事件,并将其附加到TemplatePicker小部件的selection-change事件。从那里,我们将收集所选属性、当前日期以及一些其他属性,并将它们传递给一个将事件添加到地图的函数。该函数应如下所示:
registry.byId('mapview').performTransition('incidentview', 1, 'slide', null, function() {
//refresh the grid used by the templatePicker.
picker.grid.render();
picker.clearSelection();
// on picker selection change…
dojoOn.once(picker, 'selection-change', function () {
var selected = picker.getSelected();
if (selected) {
// log the date and time
var currentDate = new Date();
var incidentAttributes = {
req_type: selected.template.name,
req_date:(currentDate.getMonth() + 1) + "/" + currentDate.getDate() + "/" + currentDate.getFullYear(),
req_time: currentDate.toLocaleTimeString(),
address: "",
district: "",
status: 1
};
addIncident(point, selected.symbol, incidentAttributes);
}
});
});
}
对于addIncident()函数,我们将我们的点位置、符号和属性添加到一个图形中。然后,我们将图形特征添加到可编辑的incidentLayer。一旦完成,我们将尝试使用incidentLayer层的selectFeatures()方法再次选择它,然后将结果发送到goToAttributeInspector()函数。我们将传递当前视图的名称(incidentview),以及所选的第一个特征。它应如下所示:
function addIncident(point, symbol, attributes) {
var incident = new Graphic(point, symbol, attributes);
// add incident to the map so it can be selected and passed to the
// attribute inspector.
incidentLayer.applyEdits([incident],null,null).then(function () {
var selectQuery = new Query(),
mapScale = map.getScale();
selectQuery.geometry = new Extent({
xmin: point.x - mapScale / 96,
xmax: point.x + mapScale / 96,
ymin: point.y - mapScale / 96,
ymax: point.y + mapScale / 96,
spatialReference: map.spatialReference
});
// must select features before going to attributeInspector
incidentLayer.selectFeatures(selectQuery, FeatureLayer.SELECTION_NEW, function (features) {
if (features.length) {
// fill in the items
goToAttributeInspector(features[0], "incidentview");
}
});
});
}
如果一切正常,你应该能够使用incidentview访问你的事件选择器,并且它应该如下所示:

属性检查器
现在地图和模板选择器已经为移动应用程序正确更新,是时候查看我们事件报告应用程序的第三阶段了。使用我们之前的代码,我们将通过FeatureLayer选择来选择事件。然后我们将加载属性检查器并编辑数据。最后,我们将特征数据(包括任何图像)保存到特征服务中。
在之前的桌面应用程序中,我们在地图弹出窗口中加载了属性检查器。然而,在 ArcGIS 紧凑型构建中,我们没有之前拥有的相同的弹出窗口小部件。在地图屏幕上,它将不会那么可编辑。然而,在属性视图中,我们有大量的屏幕空间,所以我们将在这里加载检查器。此外,请注意,我们将删除任何地图弹出窗口加载或更改属性检查器的相关事件。
首先,我们需要在属性视图中为属性检查器在页面上创建一个位置。在index.html页面中,在attributeview div 元素内,我们将添加一个具有id为attinspector的 div 元素。当我们的应用程序加载时,它将在该位置创建一个属性检查器。它应该看起来像以下这样:
<div id="attributeview" data-dojo-type="dojox/mobile/View">
<div id="attinspector"></div>
</div>
在我们的app.js文件中,我们仍然会使用当地图加载时由startEditing()函数调用的generateAttributeInspector()函数。然而,generateAttributeInspector()函数需要一些更改才能与它的更永久的环境一起工作。我们需要做以下几件事:
-
在
attinspectordiv 元素所在的位置初始化和启动属性检查器 -
移除对地图的
infoWindow属性的任何引用 -
当更改应用到
generateAttributeInspector()函数时,它应该看起来像以下这样:function generateAttributeInspector(layer) { layerInfos = [{ featureLayer: layer, showAttachments: true, isEditable: true, showDeleteButton: false, fieldInfos: [ {'fieldName': 'req_type', 'isEditable':true, 'tooltip': 'What\'s wrong?', 'label':'Status:'}, {'fieldName': 'req_date', 'isEditable':false, 'tooltip': 'Date incident was reported.', 'label':'Date:'}, {'fieldName': 'req_time', 'isEditable':false,'label':'Time:'}, {'fieldName': 'address', 'isEditable':true, 'label':'Address:'}, {'fieldName': 'district', 'isEditable':true, 'label':'District:'}, {'fieldName': 'status', 'isEditable':false, 'label':'Status:'} ] }]; //"","req_date","req_time","address","district","status" attInspector = new AttributeInspector({ layerInfos: layerInfos }, "attinspector"); attInspector.startup(); //add a save button next to the delete button var saveButton = new Button({ label: "Save", "class": "saveButton"}); domConstruct.place(saveButton.domNode, attInspector.deleteBtn.domNode, "after"); saveButton.on("click", function(){ updateFeature.getLayer() .applyEdits(null, [updateFeature], null); }); attInspector.on("attribute-change", function(evt) { //store the updates to apply when the save button is clicked updateFeature.attributes[evt.fieldName] = evt.fieldValue; }); attInspector.on("next", function(evt) { updateFeature = evt.feature; console.log("Next " + updateFeature.attributes.objectid); }); attInspector.on("delete", function(evt){ evt.feature.getLayer() .applyEdits(null,null,[updateFeature]); map.infoWindow.hide(); }); // content after this was deleted. }
一旦我们做出了更改,我们就可以在浏览器中运行应用程序并检查属性检查器。在点击我们地图上的一个麻烦位置,并使用TemplatePicker识别事件后,我们应该能够使用属性检查器查看和编辑事件属性。
应用程序中的问题
哎呀,我们在应用程序中遇到了一点问题。我们通过在地图上轻触一个位置来测试应用程序,以报告一个事件。我们从模板选择器中选择了事件类型,并进行了选择。几秒钟后,它切换到了属性检查器,我们得到了以下内容:

属性检查器表单非常难看,并且不像桌面网页应用程序那样表现。用于编辑特征属性的控件工作得不是很好。这怎么可能发生呢?
属性检查器的问题实际上回到了我们在应用程序开始时所做的某件事。我们在网页头部移除了claro.css文件,以及与之相关的任何其他dijit引用。这个行为为我们应用程序节省了大量的带宽,但我们也失去了属性检查器中用户控件的风格和功能。现在,它将无法按照我们的预期工作。
重建属性检查器
然而,还有另一种方法。我们可以创建自己的表单来更新特征属性。我们可以使用dojox/mobile模块中的表单元素来创建自己的表单,而不是使用属性检查器。此外,经过仔细检查,属性检查器的附件编辑部分工作得很好。我们可以在自定义表单之后加载附件编辑器,并使用它来保存图像到特征中。
创建表单
为了创建自定义表单,我们需要加载几个dojox/mobile模块进行解析。在我们的app.js文件的require()列表中,我们将添加dojox/mobile/RoundRect模块以创建表单的圆角主体。我们还将使用dojox/mobile/TextBox进行文本输入,以及dijit/form/DataList和dojox/mobile/ComboBox的组合来创建移动下拉菜单。我们还将使用dojox/mobile/Button来保存我们的更改。我们的require语句应如下所示:
require([…, "dojox/mobile/RoundRect", "dojox/mobile/TextBox",
"dijit/form/DataList", "dojox/mobile/ComboBox",
"dojox/mobile/Button", "dojo/domReady!"], function ( … ) {
…
});
接下来,我们将修改属性视图以制作编辑事件属性的表单。我们将在index.html中使用DataList和ComboBox作为事件类型的选取工具。这样,如果选择了错误类型,用户将能够进行更正。接下来,我们将使用Textbox记录Address和District属性。此时日期、时间和状态是只读的,因为我们不希望报告者更改事件日期和时间以及事件是否已打开或关闭。最后,我们将在表单中添加一个Save按钮以保存结果。一旦我们将这些添加到index.html中,文件应如下所示:
<div id="attributeview" data-dojo-type="dojox/mobile/View">
<h2 data-dojo-type="dojox/mobile/Heading"
data-dojo-props="back:'Incident',moveTo:'incidentview'">
Description
</h2>
<div data-dojo-type="dojox/mobile/RoundRect">
<label>Request Type:</label>
<select data-dojo-type="dijit/form/DataList"
data-dojo-props='id:"incidentDataList"'>
<option>Graffiti Complaint</option>
<option>Blocked Street or Sidewalk</option>
<option>Damaged Property</option>
<option>Sewer Issues</option>
<option>Tree Maintenance or Damage</option>
<option>Sidewalk and Curb Issues</option>
</select>
<input type="text" data-dojo-type="dojox/mobile/ComboBox"
data-dojo-props='list:"incidentDataList",
id:"incidentSelector"' />
<br />
<label>Date:</label>
<span id="incidentDate"></span>
<br />
<label>Time:</label>
<span id="incidentTime"></span>
<br />
<label>Address:</label>
<input type="text" data-dojo-type="dojox/mobile/TextBox"
data-dojo-props='id:"incidentAddress"' />
<br />
<label>District:</label>
<input type="text" data-dojo-type="dojox/mobile/TextBox"
data-dojo-props='id:"incidentDistrict"' />
<br />
<label>Status: </label>
<span id="incidentStatus"></span>
<br />
<button data-dojo-type="dojox/mobile/Button"
data-dojo-props="id:'saveButton'">Save</button>
…
</div>
最后,我们需要修改我们的 JavaScript 以处理表单的输入和输出。我们将创建两个函数,setupIncident()和saveEdits(),以从事件详情表单中加载数据和保存数据。setupIncident()函数将接受要修改的要素作为参数。由于setupIncident()可以在地图上点击现有事件时调用,或者在TemplatePicker中选择事件类型后调用,我们将传递视图名称以及要素数据,以便它可以移动到事件详情视图:
function setupIncident(feature, view) {
var attr = feature.attributes;
updateFeature = feature;
registry.byId("incidentSelector").set("value", attr.req_type);
dojoDom.byId("incidentDate").innerHTML = attr.req_date;
dojoDom.byId("incidentTime").innerHTML = attr.req_time;
registry.byId("incidentAddress").set("value", attr.address);
registry.byId("incidentDistrict").set("value", attr.district);
dojoDom.byId("incidentStatus").innerHTML = attr.status;
attInspector.showAttachments(feature, incidentLayer);
registry.byId(view).performTransition('attributeview', 1, 'slide');
}
saveEdits()函数将收集表单中的值,将这些值作为要素属性添加,并将要素保存回地理数据库:
function setupIncident() {
…
}
function saveEdits(){
// add updated values
updateFeature.attributes.req_type = registry.byId("incidentSelector").get("value");
updateFeature.attributes.address = registry.byId("incidentAddress").get("value");
updateFeature.attributes.district = registry.byId("incidentDistrict").get("value");
// update the feature layer
updateFeature.getLayer().applyEdits(null, [updateFeature], null);
// move back to the map view
registry.byId("attributeview").performTransition("mapview", -1, 'slide');
}
附件编辑器
我们将要实现的功能是向事件报告中添加照片。之前的版本使用了属性检查器中的附件编辑器。现在,随着我们实现自己的输入表单,我们需要单独包含附件编辑器。
在我们的应用程序中添加附件编辑器的第一步是在app.js文件的require语句中添加模块引用。根据 API 文档,要使用的模块位于esri/dijit/editing/AttachmentEditor。我们将在以下代码中添加引用:
require([…
"esri/dijit/editing/TemplatePicker",
"esri/dijit/editing/AttachmentEditor",
…
], function (…
TemplatePicker, AttachmentEditor,
…
) { });
我们将在startEditing()函数中的TemplatePicker之后初始化附件编辑器,并将其分配给变量attachmentEditor。我们需要attachmentEditor的作用域适用于整个应用程序,因为我们将在其他函数中将它与要素数据连接。您可以在以下代码中看到突出显示的添加内容:
require([…
"esri/dijit/editing/TemplatePicker",
"esri/dijit/editing/AttachmentEditor",
…
], function (
TemplatePicker, AttachmentEditor,
…
) {
var attachmentEditor;
…
function startEditing () {
// add the Locate button
var locator = new LocateButton({map: map}, "locatebutton");
var incidentLayer = map.getLayer("incidentLayer");
picker = new TemplatePicker({
featureLayers: [ layer ],
style: "width:100%;height:auto;",
grouping: false
}, "pickerdiv");
picker.startup();
attachmentEditor = new AttachmentEditor({}, "attributediv");
attachmentEditor.startup();
…
}
…
});
当我们将我们的编辑表单与setupIncident()函数中的数据连接时,我们还需要将attachmentEditor与其数据连接。在用功能值更新编辑表单后,我们将调用attachmentEditor的showAttachments()方法。此方法接受功能以及要编辑的图层。附件编辑器将处理如何显示现有附件以及如何添加新附件。代码更改应如下所示:
function setupIncident(feature, view) {
var attr = feature.attributes;
updateFeature = feature;
registry.byId("incidentSelector").set("value", attr.req_type);
dojoDom.byId("incidentDate").innerHTML = attr.req_date;
dojoDom.byId("incidentTime").innerHTML = attr.req_time;
registry.byId("incidentAddress").set("value", attr.address);
registry.byId("incidentDistrict").set("value", attr.district);
dojoDom.byId("incidentStatus").innerHTML = attr.status;
attachmentEditor.showAttachments(feature, incidentLayer);
registry.byId(view).performTransition('attributeview', 1, 'slide');
}
最后,我们需要向index.html提供一个元素,我们将在此处附加附件编辑器小部件。在attributeview元素的底部,在编辑表单下方,我们将添加一个具有id为attributediv的div元素。我们的index.html页面的一部分应如下所示:
<div id="attributeview" data-dojo-type="dojox/mobile/View">
<h2 data-dojo-type="dojox/mobile/Heading"
data-dojo-props="back:'Incident',moveTo:'incidentview'">
Description
</h2>
<div data-dojo-type="dojox/mobile/RoundRect">
…
<button data-dojo-type="dojox/mobile/Button"
data-dojo-props="id:'saveButton'">Save</button>
</div>
<div id="attributediv"></div>
</div>
当你运行应用程序并开始报告事件时,你应该最终看到一个如下所示的形式:

最终结果
在修改我们的应用程序布局和行为后,我们现在有一个有用的移动报告工具。市民可以在他们的浏览器中加载此页面并报告他们发现的问题。城市工作人员也可以使用此工具报告事件。你的应用程序应该看起来像这样:

可以做更多的工作来改进应用。目前,它需要持续的网络连接来编辑。如果你的移动设备处于网络覆盖较差的区域,任何编辑都将丢失。
摘要
在本章中,我们探讨了使网站移动化的因素。我们研究了移动应用程序的需求,包括屏幕空间、功能带宽。我们研究了用于移动应用程序的 ArcGIS 紧凑型构建。我们还修改了一个以前为桌面使用格式化的现有应用程序,并使其适用于移动设备。
在下一章中,我们将探讨如何使用测试驱动开发编写可测试的代码。
第十章。测试
当你将网站或网络应用程序发布给公众时,用户期望它能正常工作。链接应该占据它们的位置,按钮应该执行它们的功能。他们可能没有足够的技能去检查浏览器控制台或观察网络流量。他们甚至有充分的理由在浏览器中禁用 JavaScript。
事实上,有时网站无法正常工作是因为硬件、软件或用户错误,但有时原因来自开发者的有缺陷的代码。也许代码是从代码样本和 Stack Overflow 答案的剪切和粘贴拼贴画中拼凑起来的。然后开发者匆忙测试应用程序一次或两次,使用他们每次编写组件时都使用的相同工作流程。
然而,也许有些事情超出了开发者的控制范围。也许曾经包含数字值的功能属性现在包含字符串。也许 API 已更新,应用程序发生了根本性的变化。也许 REST 数据服务宕机了,或者也许有些无聊的孩子正在尝试输入随机数据或恶意 SQL 注入代码来使网站崩溃。
通过代码进行测试
这些都是为你的应用程序创建测试的良好理由。测试是确保你的代码按预期执行的一种方法。它定义了明确的用例,具有明确定义的预期结果。测试还充当一种项目文档。它告诉其他与项目合作的开发者代码应该如何表现。
现在你已经了解了为什么测试很重要,让我们来看看多年来其他开发者所使用的测试方法。
单元测试
单元测试是指将单个组件或代码模块加载到测试框架中并单独进行测试。测试被开发出来,结果与预期值进行比较。对于面向对象的编程,可以加载并测试单个对象,而无需等待其他组件加载。
JavaScript 对象和模块非常适合单元测试。开发者可以编写脚本来单独加载模块,将值传递给函数和对象,并查看输出结果。通过测试确保 JavaScript 模块和对象将按照预期的测试案例执行。
然而,单元测试并不能防止所有错误。虽然它们测试了对象和函数的输入和输出,但它们并没有测试这些对象是如何交互的。当两个或更多内存密集型模块在同一浏览器中运行时会发生什么?当从模块 B 期望的输入超时时,模块 A 会发生什么?此外,单元测试也没有解决用户体验问题。很难为地图编写单元测试,因为它的缩放速度比我预期的要慢。显然,单元测试不能成为你测试工具箱中的唯一工具。
功能测试
功能测试处理单个流程的测试,而不是单个单元的测试。它执行工作流程中预期的许多更改,并检查结果是否符合预期。虽然一个或多个单元测试可能覆盖功能测试中测试的组件,但功能测试将它们连接起来以模拟现实世界的例子。
一个例子可能是计算欧洲国家在线购买中的增值税(VAT)。你可能有一些单元测试覆盖以下内容:
-
意大利是否有正确的增值税百分比
-
购买物品列表的小计成本是多少
-
应用增值税后购买的总成本是多少
使用功能测试,你可以将它们全部串联起来。你可以问,如果意大利人购买了这些物品,增值税后的总成本是否符合预期。
端到端测试
如果单元测试检查应用程序的各个组件,而功能测试测试单个工作流程,那么端到端测试则从开始到结束检查整个流程。整个应用程序被加载到一个专门为处理测试而设计的平台上。一个用户脚本被加载,包含预定义的操作,如要输入的文本或要点击的按钮。任何错误都会由系统记录以供审查。一些测试框架甚至记录测试过程的截图或视频以供审查。
端到端测试旨在捕获组件之间交互产生的错误。它们还倾向于捕获组件的不当实现(即人为错误)。然而,端到端测试在计算机资源和时间方面可能很昂贵。你使用端到端测试测试的用例越多,得到结果所需的时间就越长。端到端测试通常用于代码发布或夜间构建过程,而单元测试则预期覆盖发布之间的较小代码更改。
网上有很多端到端测试的付费服务。对于一些框架,例如 Angular,有如Protractor.js 这样的测试框架可用于端到端测试,但如果你想要既免费又与框架无关的端到端测试,你也有选择。开源库如 Phantom.js、Casper.js 以及像 Selenium 这样的网络浏览器自动化工具可以帮助你从开始到结束测试你的应用程序,只需一点设置。
编码时的测试
许多开发者发现,测试代码的最佳时机是在编写代码时。当你花费一整天编写一个模块,结果却失败,却不知道为什么,这是一种糟糕的感觉。通过在开发过程中创建单元测试,开发者可以快速收到有关其代码部分成功或失败的反馈。现在,让我们看看开发者如何在编码时编写测试的一些方法。
测试驱动开发
测试驱动开发(TDD)是一个编写单元测试和端到端测试,然后编写代码以通过这些测试的过程。每当代码重构时,它都会运行相同的测试以确保结果符合预期。
使用 TDD 的开发者通常会按照以下步骤进行编码:
-
根据需求编写一个测试。
-
运行所有测试并查看新测试失败。这消除了不必要的测试并测试了整体测试框架。
-
编写一些代码以通过测试。
-
再次运行测试。
-
根据需要重构,以清理丑陋的代码、名称和功能。
-
重复步骤 1 到 5,直到完成。
通过在编写代码之前编写测试,开发者保持对模块目标的关注,并且不会编写不必要的代码。测试还增强了开发者对代码的信心,并导致代码开发速度更快。
行为驱动开发
行为驱动开发(BDD)是 TDD 的一种形式,它回答了“我应该测试多少?”的问题。在 BDD 中,为代码模块和对象编写的测试带有以“它应该做 ______”开始的描述。然后编写代码测试以测试这些特性和这些特性。
BDD 测试有助于定义验收标准和记录代码。通过查看测试描述,其他开发者将更好地了解代码应该做什么。通过提前描述验收标准,开发者不需要为代码永远不会遇到的使用情况编写额外的代码。
测试语句
每个 TDD 和 BDD 设置的核心是测试语句。测试语句是以可读的格式编写的代码行,用于测试结果的性质和值。测试语句在单元测试或功能测试中定义了一个简单的测试。
根据你选择的生成测试语句的库,它们可能遵循 BDD 的 should 模式、TDD 的 expect 模式或 TDD 的 assert 模式。一些库可能允许你为测试使用这些模式中的一个或多个。例如,使用 Chai.js 库(我们将在本章稍后讨论),我们可以查看使用这三种模式中的任何一种的示例。使用 should 模式的测试可能看起来像以下这样:
chai.should();
country.should.be.a('string');
country.should.equal('Greece');
country.should.have.length(6);
texas.should.have.property('counties')
.with.length(253);
expect 模式可能看起来像以下这样:
var expect = chai.expect;
expect(country).to.be.a('string');
expect(country).to.equal('Greece');
expect(country).to.have.length(6);
expect(texas).to.have.property('counties')
.with.length(253);
最后,assert 模式应该看起来像以下这样:
var assert = chai.assert;
assert.typeOf(country, 'string');
assert.equal(country, 'Greece');
assert.lengthOf(country, 6)
assert.property(texas, 'counties');
assert.lengthOf(texas.counties, 253);
这三种格式测试相同的内容。它们之间的唯一区别是它们用来获得相同结果的语法。根据你的团队喜欢使用的测试格式,许多测试库都将提供相应的功能。
现在我们已经回顾了软件测试背后的基本概念,让我们看看我们可用的工具。在过去的几年里,JavaScript 社区已经产生了一系列测试框架。有些是特定于某个框架的,例如用于 Angular 的 Protractor,而其他则几乎与任何库或框架一起工作,例如 Mocha、Jasmine 或 Intern。在本章中,我们将探讨其中的两个:Intern 和 Jasmine。
Intern 测试框架
Intern (theintern.github.io/) 是一个用于测试网站和应用的测试框架。其网站宣称,它不仅可以测试纯 JavaScript 应用,还可以测试用 Ruby 和 PHP 构建的服务器端网站,以及 iOS、Android 和 Firefox 应用。Intern 支持 AMD 和异步测试。
如果你使用 Node.js 作为你的开发环境,Intern 可能很好地集成。如果你使用 Grunt,Intern 自带 Grunt 任务,可以轻松集成到现有的工作流程中。Intern 还与持续集成服务如 Jenkins 和 Travis CI 兼容。
Intern 在大多数现代浏览器中工作,包括 Android Browser、Chrome、Firefox、Safari、Opera 和 Internet Explorer。对于 Internet Explorer,Intern 与 9 及更高版本兼容。如果你需要测试 Internet Explorer 的旧版本,可以通过npm获取一个名为intern-geezer的模块。测试将适用于不支持 EcmaScript 5 的 Internet Explorer 6-8 版本,而常规的 Intern 需要 EcmaScript 5。
设置你的测试环境
如果你使用 Node.js,安装可以像npm install intern一样简单。根据网站上的说明,Intern 为你的项目推荐了一种文件夹结构,如下面的截图所示:

Intern 的制作者建议你将源代码和可分发代码保存在与测试不同的文件夹中。这样,测试就不会意外地与代码一起发布。你可能不希望人们找到你的测试,这可能会暴露你的安全服务、私有 API 密钥和敏感密码。
ArcGIS JavaScript API 的特殊要求
根据几位用户的报告,使用 Intern 与 ESRI 提供的 ArcGIS API for JavaScript 的托管链接不建议。因为它可能导致测试在不应失败的地方失败,因为模块及其依赖加载得太慢。推荐的解决方案是将 API 和 Dojo 框架下载到与你的项目文件并列的文件夹中。你可以使用 Grunt,这是一个 Node.js 任务运行器。
为 Intern 编写测试
考虑到 Dojo 的 AMD 风格,为 Intern 编写测试是可能的。一旦分配了模块路径,这些模块就可以加载到测试中并运行。让我们看看如何做到这一点。
使用 Intern 进行单元测试
为 Intern 编写单元测试与编写Dojo模块非常相似,但有几点例外。每个单元测试由一个define()函数组成。define()函数可以接受一个带有单个require参数的函数,或者是一个字符串引用模块的列表,以及一个在所有这些模块加载完成后要运行的函数。在 Node.js 模块中,通过单个require()函数单独加载模块是一种常见的做法。
你需要在单元测试函数中加载一个测试接口和一个断言库。测试接口提供了一种注册测试套件的方式。当将接口作为函数调用时,你提供一个 JavaScript 对象,其中包含描述性的键字符串和包含运行测试或分类子测试的函数的值。以下是一个单元测试的示例:
define(function (require) {
var registerSuite = require('intern!object');
var assert = require('intern/chai!assert');
registerSuite({
'One plus one equals two': function () {
Var result = 1 + 1;
assert.equal(result, 2, "1 + 1 should add up to 2");
},
'Two plus two equals four': function () {
Var result = 2 + 2;
assert.equal(result, 4, "2 + 2 should add up to 4");
}
});
});
在前面的代码中,registerSuite变量接受 Intern 创建测试的对象接口,而assert接受 Intern 加载的chai.assert库。调用registerSuite()函数,传入一个包含两个测试的对象。第一个测试检查一加一是否等于二,而第二个测试检查两个二相加是否等于四。assert.equal()函数测试结果变量是否与预期结果匹配,如果测试失败,将抛出一个带有文本消息的错误。
使用 Intern 的测试生命周期
在测试套件中运行测试之前,或者在每个测试之前和之后,你可能需要做一些事情。这是测试生命周期。Intern 在测试定义中提供了关键字来定义测试生命周期中应该发生什么。所有这些生命周期函数都是可选的,但它们将帮助你创建可用的测试。以下是测试的一般生命周期的概述:
-
setup: 这个函数在所有测试运行之前执行,可能是为了设置测试对象。例如,你可以在测试小部件之前在这里创建一个地图。 -
在每个测试期间,以下事件会发生:
-
beforeEach: 这个函数将在每个测试之前运行。例如,你可能需要加载你小部件的新副本。 -
测试运行。
-
afterEach: 这个函数将在每个测试之后运行。在这里,你可能需要销毁在beforeEach函数中创建的小部件,或者你可能需要重置在测试过程中可能改变的变量值。
-
-
teardown: 这个函数在套件中的所有测试运行之后执行。在这里,你可能需要销毁在setup阶段创建的任何地图或对象。
你可以在以下代码中看到一个使用生命周期的单元测试示例。这个示例模拟了加载地图并向地图添加小部件:
define(["intern!object", "intern/chai!expect", "esri/map", "app/widget"],
function (registerSuite, expect, Map, Widget) {
var map, widget;
registerSuite({
setup: function () {
map = new Map("mapdiv", {});
},
beforeEach: function () {
widget = new Widget({map: map}, "widget");
},
afterEach: function () {
widget.destroy();
},
teardown: function () {
map.destroy();
},
'My first test': function () {
// test content goes here.
}
});
});
在前面的代码中,define语句看起来与我们熟悉的语句非常相似,可以一次性加载多个对象。请注意,map是在setup阶段创建并在teardown阶段销毁的,而widget是在每个测试之前新创建的,并在每个测试之后销毁。
Jasmine 测试框架
如果你正在寻找一个简单的 BDD 框架来测试你的应用程序,Jasmine 可能正是你所需要的。Jasmine 是一个框架无关的 BDD 库,可以用来在 JavaScript 应用程序上运行测试。它可以通过 Node.js 安装,或者将库下载并复制到测试项目文件夹中。
Jasmine 将其测试加载到一个名为 SpecRunner 的 HTML 文件中(简称规范运行器)。这个页面加载了主要的 Jasmine 库,以及所有用户库和单元测试。一旦加载,Jasmine 库就会在浏览器中运行测试并显示结果。
为 Jasmine 编写测试
一旦我们设置了 SpecRunner,就到了编写测试的时候了。测试使用常规的 JavaScript 逻辑和 Jasmine 提供的一些测试对象和方法编写。让我们看看一些不同的部分。
编写套件、规范和测试
Jasmine 测试是由套件、规范和测试组成的。套件构成了顶层,并描述了被测试的单元。实际上,Jasmine 测试使用 describe() 函数来编写套件,该函数描述了要测试的功能。describe() 函数的参数包括一个描述功能的字符串和一个运行测试的函数。
可以使用 describe 套件中的 it() 函数编写功能的规范。it() 函数与 describe() 函数类似,包含一个描述测试的字符串和一个执行测试的函数。在 it() 函数中发生的任何错误都会导致测试失败,而成功运行的 it() 函数会显示行为的通过测试。以下是一个 describe() 和 it() 语句的示例:
describe("A widget listing the attributes of the U.N.",
function () {
it("has its headquarters located in New York City.",
function () {
//tests go here
}
);
}
);
在每个规范中都有一个或多个测试。在 Jasmine 中,测试是以 expect 格式编写的。根据传递给 expect 对象的对象,你可以对其进行多种测试,例如是否等于特定值,或者它是否返回一个“真”的 JavaScript 语句:
describe("A widget listing the attributes of the U.N.",
function () {
// unitedNations.countries is a string array of country names.
it("contains more than 10 countries.", function () {
expect(unitedNations.countries.length).toBeGreaterThan(10);
});
it("should contain France as a member country.", function () {
expect(unitedNations.countries).toContain("France");
});
});
设置和清理
有时候你需要加载模块或创建 JavaScript 对象来测试。你可能还希望在测试之间重置它们,并在测试后拆解它们以节省内存。为了设置和清理要测试的项目,你会使用 beforeEach 和 afterEach 函数。每个函数都允许你在测试之间设置和清理值,以便每个测试都有新鲜且可重复的值。这些值在套件中加载,并应在需要它们的任何规范之前调用:
describe("A widget listing the attributes of the U.N.",
function () {
var unitedNations;
beforeEach(function () {
unitedNations = new UnitedNations();
});
afterEach(function () {
unitedNations.destroy();
});
it("should do something important", function () {
//…
});
}
);
忽略测试
如果你的模块有一个已过时的功能呢?如果你期望你的模块有一天会有一个功能,但现在你不想编写一个会失败的测试。你可以删除或注释掉代码,但这样做你会在某种程度上失去你模块的一些历史。Jasmine 有一种方法可以禁用套件并将规范标记为挂起,而不会丢失代码。通过将你的describe语句替换为xdescribe,以及将你的it语句替换为xit,你可以编写现在不会失败的测试,但会被标记为“挂起”。你可以在以下代码示例中找到一个使用xit的例子:
xit("Elbownia will join the United Nations next year.",
function () {
expect(unitedNations.countries).toContain("Elbownia");
}
);
红灯绿灯
许多编写 TDD 和 BDD 测试的开发者会练习红绿测试,这在 Jasmine 中得到支持。红代表失败的测试,而绿代表通过的测试。在红绿测试中,开发者会做以下操作:
-
编写一个他们知道会失败的必要单元测试(红)。
-
编写代码以使对象或模块通过测试(绿)。
-
根据需要重构代码,确保它仍然通过测试。
-
重复步骤 1 到 3,直到代码和测试都令人满意。
使用红绿测试方法,开发者在开发模块功能时始终牢记测试。通过测试会给予开发者对代码的信心,并允许更快地开发。
在 Jasmine 中,SpecRunner HTML 页面上的测试结果使用红色和绿色显示。当测试失败时显示红色,而当所有测试通过时显示绿色。SpecRunner页面将显示哪个规范失败(如果有),以及失败时抛出的错误。有了这些信息,开发者可以修复问题并继续开发过程中的下一步。
Jasmine 和 ArcGIS JavaScript API
为使用 ArcGIS JavaScript API 的应用程序设置SpecRunner会有点更具挑战性。Dojo 框架的 AMD 特性使得加载模块和测试变得有点更具挑战性,但这是可以做到的。
第一步是将 Jasmine 和 ArcGIS JavaScript API 加载到应用程序中。由于 Jasmine 需要在我们的应用程序外部处理所有部分,以及因为 AMD 模块在发生错误时往往不会显示,我们需要在 ArcGIS JavaScript API 之前加载 Jasmine:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Jasmine Spec Runner</title>
<link rel="stylesheet" href="https://js.arcgis.com/3.13/esri/css/esri.css" />
<link rel="stylesheet" type="text/css" href="tests/jasmine/jasmine.css" />
<script type="text/javascript" src="img/jasmine.js">
</script>
<script type="text/javascript" src="img/jasmine-html.js" ></script>
<script type="text/javascript">var dojoConfig={};</script>
<script type="text/javascript" src="img/" ></script>
</head>
你将能够加载你模块的位置,以及那些模块上的测试,在dojoConfig包中。我们将使用 AMD 加载器来加载测试套件作为模块。在以下示例中,AMD 模块位于js子文件夹中,而规范测试位于tests子文件夹中。dojoConfig包应该看起来像以下这样:
Var basePath = location.pathname.replace(/\/[^\/]*$/, '');
var dojoConfig = {
async: true,
packages: [
{
name: 'app',
location: basePath + '/js/'
}, {
name: 'spec',
location: basePath + "/tests/"
}
]
};
由于你的许多自定义小部件将需要加载其他模块,包括其他自定义模块,因此你需要确保你的自定义模块的路径与你的应用程序中的路径相同。
在 HTML 文档的主体中,添加一个脚本通过 Dojo 的require()函数调用规范。代码将通过 AMD 从测试子文件夹中加载规范。当模块加载完毕并且调用dojo/ready函数时,我们将加载 Jasmine 的HtmlReporter并执行 Jasmine 测试:
<body>
<script type="text/javascript">
require([
"dojo/ready",
"spec/Widget1",
"spec/Widget2",
"spec/Widget3"
], function (ready) {
ready(function () {
// Set up the HTML reporter - this is responsible for
// aggregating the results reported by Jasmine as the
// tests and suites are executed.
jasmine.getEnv().addReporter(
new jasmine.HtmlReporter()
);
// Run all the loaded test specs.
jasmine.getEnv().execute();
});
});
</script>
</body>
当测试加载并运行时,它们将在浏览器中的这个 HTML 页面上渲染。
在规范中
通常,Jasmine 规范可以作为脚本运行或封装在自运行的 JavaScript 函数中。然而,为了与 Dojo 一起工作,它们需要被封装在模块定义中。与过去我们制作的自定义模块不同,我们不会使用dojo/_base/declare模块来创建自定义模块。我们将简单地使用define()语句来加载必要的模块,并在其中运行测试。
对于每个套件,使用define()语句来帮助加载测试。你也可以加载任何可能需要的其他 ArcGIS JavaScript API 模块或小部件来帮助测试模块。根据主应用的要求,Widget1规范代码可能如下所示:
define(["app/Widget1", "esri/geometry/Point"],
function (Widget1, Point) {
});
在define语句中开始编写 Jasmine 规范。当套件模块加载时,它们将自动运行其中的任何测试并显示在浏览器中的SpecRunner页面上的结果。你可以使用beforeEach来加载一个全新的模块进行测试,如果需要的话,使用afterEach来销毁它。你可以在套件和规范中执行任何与 ArcGIS JavaScript API 相关的自定义任务:
define(["app/Widget1", "esri/geometry/Point"],
function (Widget1, Point) {
describe("My widget does some incredible tasks.", function () {
var widget, startingPoint;
// before each test, make a new widget from a staring point.
beforeEach(function () {
startingPoint = new Point([-43.5, 87.3]);
widget = new Widget({start: startingPoint});
});
// after each test, destroy the widget.
afterEach(function () {
widget.destroy();
});
it("starts at the starting point", function () {
expect(widget.start.X).toEqual(startingPoint.X);
expect(widget.start.Y).toEqual(startingPoint.Y);
});
//…
});
});
一旦你定义了测试,你可以在浏览器中查看SpecRunner页面并检查结果。如果你已经设置好了一切,你应该有正在运行的测试并显示结果。查看以下截图以获取示例:

Jasmine 和 ArcGIS JavaScript API 的使用案例
Jasmine 为你的 JavaScript 小部件提供了一个易于编写的测试环境。如果你是单元测试和 BDD 的新手,并且不习惯在测试之上设置 Node.js 环境,那么 Jasmine 是一个很好的起点。你需要的所有库都可以从 GitHub 下载,你几乎可以立即开始编写测试。
注意
你可以从其 GitHub 仓库github.com/jasmine/jasmine下载 Jasmine。Jasmine 的文档可以在jasmine.github.io/找到。如果你对这个框架感兴趣,可以阅读 Paulo Ragonha 的《Jasmine JavaScript Testing, Second Edition》或 Munish Sethi 的《Jasmine Cookbook》。
我们的应用
对于我们的应用程序,我们将使用 Intern 为我们的 Y2K 地图应用程序设置单元测试。与我们的其他应用程序不同,它们只需要浏览器,我们将使用 Node.js 进行此设置。如果我们遵循真正的 TDD/BDD 实践,我们会在开发应用程序时编写这些测试。然而,这将为您练习为之前编写的任何遗留应用程序编写测试提供机会。
对于这个示例,我们将遵循 David Spriggs 和 Tom Wayson 通过 GitHub 提供的示例(github.com/DavidSpriggs/intern-tutorial-esri-jsapi)。我们将对 ArcGIS JavaScript API 和与测试相关的模块的最新更新进行一些修改。
添加测试文件
在我们的 Y2K 应用程序中,我们将添加几个文件以与所需的 Node.js 模块一起工作。我们将添加一个 package.json 文件,它告诉 Node 包管理器 (NPM) 我们需要哪些模块以及需要哪些版本号。接下来,我们将添加一个 Grunt.js 文件,因为我们将使用 Grunt 在本地加载 ArcGIS JavaScript API。我们还将添加一个 bower.json 文件,以便 Bower 在本地加载正确的 Dojo 库,并添加一个 .bowerc 文件以在运行时配置 Bower。最后,我们将在 tests 文件夹中添加一个 intern.js 文件以配置 Intern 测试。
我们的新文件夹结构应该如下所示:

package.json 文件
在我们的 package.json 文件中,我们将添加我们 Node.js 模块所需的文件依赖项。当指定某些模块的版本号时,你会注意到一些模块前面有一个波浪号 (~) 符号,而其他模块则有一个 caret 符号 (^)。波浪号匹配主版本号和次版本号(三位版本代码中的前两个数字),而 caret 符号将拉取最新的匹配主版本号。将以下内容复制到您的 package.json 文件中:
{
"name": "intern-tutorial-esri-jsapi",
"repository": {
"type": "git",
"url": "https://github.com/DavidSpriggs/intern-tutorial-esri-jsapi.git"
},
"version": "0.2.0",
"devDependencies": {
"dojo": "¹.10",
"esrislurp": "¹.1.0",
"grunt": "⁰.4",
"grunt-contrib-watch": "~0",
"grunt-esri-slurp": "¹.4.7",
"intern": "².0.3",
"selenium-server": "2.38.0"
}
}
Grunt 设置
Grunt 是在 Node.js 应用程序中使用的流行任务运行器。它用于自动化代码构建步骤,例如压缩 JavaScript、从 LESS 或 SASS 等预处理器创建 CSS 文件,或者在这种情况下,测试代码。Grunt 读取 Grunt.js 文件,该文件告诉它在哪里查找文件以及如何处理它们。在我们的 Grunt.js 文件中,我们将添加以下代码:
module.exports = function(grunt) {
grunt.initConfig({
intern: {
dev: {
options: {
runType: 'runner',
config: 'tests/intern'
}
}
},
esri_slurp: {
options: {
version: '3.13'
},
dev: {
options: {
beautify: false
},
dest: 'esri'
}
},
esri_slurp_modules:{
options: {
version: '3.13',
src: './',
dest: './modules'
}
},
watch: {
all: {
options: { livereload: true },
files: ['src/js/*.js']
}
}
});
// Loading using a local copy
grunt.loadNpmTasks('intern');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-esri-slurp');
// download Esri JSAPI
grunt.registerTask('slurp', ['esri_slurp']);
grunt.registerTask('create_modules', ['esri_slurp_modules']);
// Register a test task
grunt.registerTask('test', ['intern']);
// By default we just test
grunt.registerTask('default', ['test']);
};
在前面的代码中,Node.js 预期我们将把我们的模块定义传递给 module.exports 变量。我们在 grunt.initConfig 中告诉 Grunt 在哪里找到我们的 intern 模块以进行测试,以及 esri_slurp 以下载 ArcGIS JavaScript API。我们还告诉它监视 src/js/ 文件夹中的任何 JavaScript 文件更改,这将触发测试再次运行。
一旦配置加载完成,我们告诉 Grunt 加载 intern、grunt-contrib-watch 和 grunt-esri-slurp 模块,从 npm 中加载模块。然后注册任务,并设置好运行测试的环境。
设置 Bower
Bower 是一个 Node.js 模块,用于自动下载项目所需的所有 JavaScript 库。我们将使用 Bower 下载运行测试所需的库和 Dojo 模块。大多数常见的库和框架都可以通过 Bower 在本地下载。在这个例子中,我们的例外是 ArcGIS JavaScript API,它目前将通过 esri-slurp 处理。我们需要填写一个 bower.json 文件来告诉 Bower 我们需要哪些库,以及它们的版本应该是什么。
在我们的 bower.json 文件中,我们需要创建一个 JSON 对象来列出我们的项目名称、版本号、依赖项以及任何开发依赖项。我们可以将其命名为任何我们想要的名称,所以我们将命名为 y2k-map-app。我们将为测试分配版本号 0.0.1,并且我们确保在依赖项或库版本号需要更新时增加它。在这种情况下,我们没有开发依赖项,但我们需要列出 Dojo 框架的各个部分,以及 D3.js,在这个应用程序中。
我们不仅需要列出测试所需的库和模块,还需要列出版本号。我们可以参考 ArcGIS JavaScript API 的 What's New 页面 developers.arcgis.com/javascript/jshelp/whats_new.html 来查找。根据页面,我们需要 Dojo 框架的版本 1.10.4,包括来自 dojo/util 的 dojo、dijit、dojox 和 util 模块。我们还需要添加版本为 0.3.16 的 dgrid 模块、版本为 0.3.6 的 put-selector 和版本为 0.1.3 的 xstyle。回顾我们 index.html 页面上的 dojoConfig 包,我们看到 d3.js 使用的是版本 3.5.6。我们将在 JSON 对象中列出依赖项,版本号前加一个 # 符号。我们的 bower.json 文件的内容应如下所示:
{
"name": "y2k-map-app",
"version": "0.0.1",
"dependencies": {
"dijit": "#1.10.4",
"dojo": "#1.10.4",
"dojox": "#1.10.4",
"util": "dojo-util#1.10.4",
"dgrid": "#0.3.16",
"put-selector": "#0.3.6",
"xstyle": "#0.1.3",
"d3": "#3.5.6"
},
"devDependencies": {}
}
对于 .bowerc 文件,我们将添加一个简单的配置对象。在配置对象的 JSON 中,我们将添加一个目录字段并分配一个值为 .。这将告诉 Bower 加载此文件夹中请求的文件。.bowerc 文件的内容应如下所示:
{"directories": "."}
Intern.js 配置
现在我们需要在 tests 文件夹中设置我们的 Intern.js 配置文件以运行测试。我们将从在页面上添加一些默认代码开始。有关此配置文件的更多信息,请参阅 github.com/theintern/intern/wiki/Configuring-Intern。首先,复制以下内容:
// Learn more about configuring this file at <https://github.com/theintern/intern/wiki/Configuring-Intern>.
// These default settings work OK for most people. The options that *must* be changed below are the
// packages, suites, excludeInstrumentation, and (if you want functional tests) functionalSuites.
define({
// The port on which the instrumenting proxy will listen
proxyPort: 9000,
// A fully qualified URL to the Intern proxy
proxyUrl: 'http://localhost:9000/',
// Default desired capabilities for all environments. Individual capabilities can be overridden by any of the
// specified browser environments in the `environments` array below as well. See
// https://code.google.com/p/selenium/wiki/DesiredCapabilities for standard Selenium capabilities and
// https://saucelabs.com/docs/additional-config#desired-capabilities for Sauce Labs capabilities.
// Note that the `build` capability will be filled in with the current commit ID from the Travis CI environment
// automatically
capabilities: {
'selenium-version': '2.38.0'
},
// Browsers to run integration testing against. Note that version numbers must be strings if used with Sauce
// OnDemand. Options that will be permutated are browserName, version, platform, and platformVersion; any other
// capabilities options specified for an environment will be copied as-is
environments: [{
browserName: 'internet explorer',
version: '10',
platform: 'Windows 8'
}, {
browserName: 'internet explorer',
version: '9',
platform: 'Windows 7'
}, {
browserName: 'firefox',
version: '23',
platform: ['Linux', 'Windows 7']
}, {
browserName: 'firefox',
version: '21',
platform: 'Mac 10.6'
}, {
browserName: 'chrome',
platform: ['Linux', 'Mac 10.8', 'Windows 7']
}, {
browserName: 'safari',
version: '6',
platform: 'Mac 10.8'
}],
// Maximum number of simultaneous integration tests that should be executed on the remote WebDriver service
maxConcurrency: 3,
// Name of the tunnel class to use for WebDriver tests
tunnel: 'SauceLabsTunnel',
// Connection information for the remote WebDriver service. If using Sauce Labs, keep your username and password
// in the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables unless you are sure you will NEVER be
// publishing this configuration file somewhere
webdriver: {
host: 'localhost',
port: 4444
},
// The desired AMD loader to use when running unit tests (client.html/client.js). Omit to use the default Dojo
// loader
useLoader: {
'host-node': 'dojo/dojo',
'host-browser': 'node_modules/dojo/dojo.js'
},
// Configuration options for the module loader; any AMD configuration options supported by the Dojo loader can be
// used here
loader: {
// Packages that should be registered with the loader in each testing environment
packages:[]
},
// Non-functional test suite(s) to run in each browser
suites: [],
// A regular expression matching URLs to files that should not be included in code coverage analysis
excludeInstrumentation: /^tests\//
});
在 loader.packages 列表中,我们将添加我们预期拥有的所有文件的文件夹位置。这包括我们现在拥有的文件,以及我们预期通过 Bower 下载的文件。请注意,我们的 app 文件夹是通过存储我们的 Census 小部件的文件夹引用的。此外,我们正在加载所有 ESRI 和 Dojo 文件,以及用于图形的 D3.js。您的包部分应如下所示:
packages: [{
name: 'tests',
location: 'tests'
}, {
name: 'app',
location: 'src/js'
}, {
name: 'gis',
location: 'gis'
}, {
name: 'esri',
location: 'esri'
}, {
name: 'dgrid',
location: 'dgrid'
}, {
name: 'put-selector',
location: 'put-selector'
}, {
name: 'xstyle',
location: 'xstyle'
}, {
name: 'dojo',
location: 'dojo'
}, {
name: 'dojox',
location: 'dojox'
}, {
name: 'dijit',
location: 'dijit'
}, {
name: 'd3',
location: 'd3'
}
],
…
我们需要将我们的测试套件名称添加到套件列表中。目前,我们将添加以下内容:
…
// Non-functional test suite(s) to run in each browser
suites: [
'tests/Census'
],
…
在这个文件中,我们应该有运行 Census 小部件的一些成功测试所需的一切。随着我们添加更多模块,我们可以通过更多测试来扩展这一点。
到目前为止,我们应该准备好通过 Node.js 加载所需的文件。如果您还没有安装 Node.js 或此应用程序的其他节点依赖项,请参阅下一节。如果您已经安装了,您仍然可以查看下一节,或者如果您已经熟悉 Grunt 和 Bower,可以跳过。
如果您还没有安装 Node.js
在本节中,我们将介绍设置 Node.js 和必要依赖项的必要步骤。在这些示例中,我们将使用 NPM、Grunt、Grunt-CLI 和 Bower。如果您已经在您的开发环境中设置了这些,您可以跳到下一节。
如果您还没有安装 Node.js,您可以从 nodejs.org 下载一份。在主页上,您会找到一个链接,指向适合您电脑的安装程序。按照安装向导中的说明来安装软件。
Node.js 通过命令行界面工作。所有命令都将在 Linux 或 OSX 终端或 Windows 命令提示符 (cmd.exe) 中输入。某些部分可能需要您具有 root 或管理员权限。此外,如果您使用的是 Windows 机器,可能需要更新 PATH 变量以包括 node.js 文件夹及其 node_modules 子文件夹,其中将加载其他命令和实用程序。
接下来,如果您还没有安装 grunt、grunt-cli 和 bower 模块,我们将使用 npm 安装它们。NPM 是 Node.js 的内置组件,并提供对大量插件、库和应用程序的访问,以帮助您更快地生成应用程序。从您的命令提示符(或终端)输入以下内容以下载我们需要的模块:
npm install –g grunt
npm install –g grunt-cli
npm install –g bower
-g 标志告诉 npm 全局安装模块,这样它们就可以在任何可以调用 Node.js 和 npm 的地方访问。如果没有 -g 标志,模块将在当前目录的 node_modules 子目录中加载。
本地加载所有库
现在是加载所有必要的 Node.js 应用程序以进行测试的时候了。我们将执行以下操作:
-
使用
npm安装我们所有的模块。 -
使用 Bower 本地加载大多数外部 JavaScript 库。
-
使用 Grunt Slurp 下载 ArcGIS JavaScript API。
我们将首先通过npm加载所有模块。您需要使用命令行工具,如 Windows 的命令提示符(cmd.exe)或 Mac 或 Linux 的terminal来运行代码。我们的package.json文件定义了我们需要运行的模块。我们可以在命令行中键入以下内容来安装模块:
npm install
如果我们需要测试旧浏览器中的功能,例如 Internet Explorer 8 或更早版本,我们需要安装一个名为intern-geezer的 Intern 扩展。它允许测试在可能没有标准 Intern 使用的功能的旧浏览器上运行。如果您需要旧版支持,请在命令行中输入以下内容:
npm install intern-geezer
为了在本地上加载我们的 JavaScript 库,我们可以调用 Bower 来安装文件。我们的bower.json文件告诉它要加载什么,我们的.bowerc文件告诉它在哪里加载。在命令行中输入以下内容:
bower install
现在,我们需要加载 ArcGIS JavaScript 库以进行本地测试。由于我们在Grunt.js文件中为这个任务定义了所有内容,我们只需在命令行中输入以下内容即可下载文件并启动测试:
grunt slurp
现在我们的项目应该已经启动并运行。我们现在可以专注于为我们的应用程序编写测试。
编写我们的测试
我们将使用与创建对象时相同的dojo模块定义风格来编写测试,类似于我们之前讨论的 Jasmine 测试。然而,由于 Intern 是为 AMD 设计的,我们可以在模块define()语句中加载 intern 项。在我们的测试文件夹中,我们将创建一个名为Census.js的文件,并让该文件包含我们的测试模块定义。
我们将首先创建我们的define()语句的占位符。我们将加载 Intern 和 Chai 来运行我们的测试。使用 Chai,我们将尝试使用assert模块,其中所有测试语句都以assert开头。我们还将加载我们的Census小部件以及所有必要的模块,如地图。由于 Intern 通常在没有要求 HTML DOM 的情况下运行,但我们的地图和Census小部件需要它,我们将添加一些模块来向 DOM 添加元素:
define([
'intern!object',
'intern/chai!assert',
'app/Census',
'esri/map',
'dojo/dom-construct',
'dojo/_base/window'
], function(registerSuite, assert, Census, Map, domConstruct, win) {
//…
});
在模块定义中,我们将添加我们的地图和Census小部件的变量,然后使用registerSuite()将我们的测试套件注册到 Intern。在registerSuite()函数中,我们将传递一个包含测试名称Census Widget的对象。我们还将添加两个方法,setup()和teardown(),在所有测试运行前后调用:
…
var map, census;
registerSuite({
name: 'Census Widget',
// before the suite starts
setup:function () {},
// after all the tests have run
teardown: function () {},
});
…
在设置函数中,我们需要创建 DOM、要附加到 DOM 的地图以及要附加到地图的Census小部件。我们将使用dojo/dom-construct和dojo/_base/window模块来创建这两个 DOM。一旦创建了 DOM 元素,我们就可以初始化我们的地图和人口普查 dijit。在我们的teardown函数中,我们将销毁地图,以避免占用宝贵的内存空间。代码应该如下所示:
…
setup: function () {
// create a map div in the body, load esri css, and create the map for our tests
domConstruct.place('<link rel="stylesheet" type="text/css" href="//js.arcgis.com/3.13/dijit/themes/claro/claro.css">', win.doc.getElementsByTagName("head")[0], 'last');
domConstruct.place('<link rel="stylesheet" type="text/css" href="//js.arcgis.com/3.13/esri/css/esri.css">', win.doc.getElementsByTagName("head")[0], 'last');
domConstruct.place('<div id="map" style="width:300px;height:200px;" class="claro"><div id="census- widget"></div></div>', win.body(), 'only');
map = new Map("map", {
basemap: "topo",
center: [-122.45, 37.75],
zoom: 13,
sliderStyle: "small"
});
census = new Census({
map: map,
mapService: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/"
}, "census-widget");
},
…
teardown: function () {
map.destroy();
},
…
现在地图和Census小部件已经初始化,我们可以开始添加测试。我将向您介绍三个相对简单的测试。您需要进一步扩展它们。在这个练习中,我们将测试以下Census dijit:
-
它应该包含所有其工作部件
-
当我们发出已知请求时,它应该返回正确的查询结果
-
图表小部件的部分应该按预期工作
当编写第一个测试时,我们将给它一个标签Test Census widget creation。对于相应的函数,我们将首先测试 dijit 的baseClass是否符合预期。我们将使用assert.strictEqual()来测试值是否正确。请注意,assert.strictEqual()需要三个参数,两个要比较的值和一个测试的字符串描述。该测试应该看起来像以下这样:
…
'Test Census widget creation': function() {
assert.strictEqual(
census.baseClass,
"y2k-census",
"census.baseClass should return a string 'y2k-census'"
);
},
…
我们的第二个测试可能看起来有点棘手,因为它是一个异步测试。然而,Intern 被设计用来处理异步测试。如果需要,我们可以在 Intern 考虑任务失败之前调整其超时时间,但现在我们只是加载测试。
在查询测试中,我们将向地图服务发送包含所有州的查询,并请求一个州列表。在查询之后使用then()语句,我们可以获取结果并使用assert.strictEqual()进行测试。测试应该看起来像以下这样:
…
'Test Census query for dropdown data': function () {
census.queryShapeAndData({
url: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3",
fields: ["STATE_NAME", "STATE_FIPS"],
where: "1=1",
}).then(function (featureSet) {
assert.strictEqual(
featureSet.features.length,
51,
"There should be 51 states returned, including the District of Columbia"
);
});
},
…
最后,对于绘图函数,我们将编写测试来检查我们用来将特征属性数据转换为 D3 可以使用格式的函数。在这个例子中,我们将通过census.ethnicData()方法传递属性数据,并测试输出是否符合预期值。像其他测试库一样,我们可以在本规范中添加多个测试。测试应该看起来像以下这样:
'Test Census Graphing Attributes': function () {
var ethnicAttributes = {
WHITE: 10,
BLACK: 20,
AMERI_ES: 12,
ASIAN: 11,
HAWN_PI: 4,
HISPANIC: 23,
OTHER: 7,
MULT_RACE: 17
};
var data = census.ethnicData(ethnicAttributes);
assert.strictEqual(
data.length,
8,
"The translation from graphic attributes to d3-based data should have 8 attributes in the ethnicData function"
);
assert.strictEqual(
data[4].name,
"Hawaiian / Pacific Islander",
"The item in index 4 should have a name of Hawaiian / Pacific Islander data"
);
assert.strictEqual(
data[5].population,
23,
"Out of the Hispanic column, the data index of 5 should have a population of 23."
);
}
检查结果
为了检查我们的测试,我们需要在浏览器中加载我们的文件。由于我们将 Intern 作为 node 模块安装,我们需要查看其测试运行页面。该文件位于node_modules/intern/client.html下。您需要通过提供一个查询参数config=tests/intern来指定在浏览器中加载测试的位置。以下是在浏览器中可能查看的 URL 示例(它可能取决于您的设置):http://localhost/MasteringArcGIS/Chapter10/node_modules/intern/client.html?config=tests/intern。
当您正确查看页面时,您将看到 Intern 图标和通过/失败报告。失败的测试将以红色突出显示,而通过的测试将以绿色突出显示。您应该看到以下图像类似的内容:

摘要
在本章中,我们回顾了为什么我们要为我们的 Web 应用程序设置测试的原因。我们回顾了不同的测试方法、开发实践和用于测试我们应用程序的 JavaScript 库。使用 Intern,我们为我们的应用程序设置了测试。
在下一章中,我们将探讨 ArcGIS Server 的网络应用程序的未来,并检查 ArcGIS Online。
第十一章。ArcGIS 开发的未来
对于那些想要为 ArcGIS Server 开发地图应用程序的人来说,未来会怎样?毕竟,互联网的方向随着每一次新技术而改变,今天流行的明天可能就不流行了。Flex 和 Silverlight API 曾经非常流行,但现在只在进行维护。JavaScript 在 ArcGIS Server 开发中的未来会怎样?
如果 ESRI 的最新发布有任何迹象,JavaScript 仍然是网络开发的优选语言。然而,ArcGIS 平台已经发布了新的功能,可以帮助快速启动应用程序开发。其中两个,ArcGIS Online 和 Web AppBuilder,都是基于 ArcGIS JavaScript API 的。我们将探讨这些技术,并使用 ArcGIS Online 创建的地图开发一个应用程序。
在本章中,我们将涵盖以下主题:
-
如何使用 ArcGIS Online 创建网络地图
-
Web AppBuilder 是什么以及它与 ArcGIS Online 的关系
-
如何在 ArcGIS JavaScript API 应用程序中使用 ArcGIS Online 网络地图
ArcGIS Online
ArcGIS Online 是 ESRI 在云中创建网络地图的在线工具。ArcGIS Online 为用户提供了一种创建网络地图、混合匹配地图服务和其他数据源、修改弹出窗口以及与公众或其组织共享生成的网络地图的方法。这是 ArcGIS Online 页面的截图,您可以在自定义地图中添加来自不同来源的数据:

除了通过网络地图引用数据外,ArcGIS Online 订阅允许您上传和存储数据。可以通过 ArcGIS Server 生成的瓦片地图包创建瓦片地图服务。可以创建要素服务来存储、更新和检索要素数据。您还可以使用其他 ArcGIS Server 扩展,如通过 ArcGIS Online 进行地理编码和路线规划。这意味着您不需要使用自己的硬件来托管数据。您可以在 ArcGIS Online 云中维护您的要素数据和瓦片地图。
现在,我相信你一定在想知道 ESRI 是如何负担得起提供这些功能的。更高级的服务需要消耗积分,这些积分可以通过 ESRI 购买。每次简单交易可能只需消耗一小部分积分,但重复使用会累积。瓦片地图服务和要素服务也消耗积分,按每月存储的千兆字节计费。根据你组织的规模,你每年可以获得一定数量的积分,并且可以选择购买更多。然而,如果你有一个开发者账户,ArcGIS Online 目前为测试你的应用程序提供每月 50 积分。
ArcGIS Portal
如果您为一家不希望其数据通过如 ArcGIS Online 这样的公共平台托管的企业的组织工作,也存在一个私有版本。ArcGIS Portal 是 ArcGIS Online 的以组织为中心的版本,可以在私有或受保护的网络中工作。登录可以通过多种常见的安全协议进行验证,如 SAML 和 oAuth2。
ArcGIS Portal 与 ArcGIS Online 具有许多相同的功能。您可以创建和共享网络地图。您可以在组织中上传、编目和搜索数据源,并且使用管理员访问权限,您可以设置用户角色和权限。主要区别在于您创建和共享的数据保留在公司网络内。在 ArcGIS Portal 发生的事情将留在 ArcGIS Portal 内。
Web AppBuilder
开发者反复为不同的网站制作相同的地图工具。几乎所有通用的网络地图应用都有一个绘图工具、一个测量工具、一个打印工具、一个图例等等。ESRI 中国的开发者实施了一个想法,创建一个应用程序,构建使用 ArcGIS Online 网络地图的 JavaScript 网络地图应用。他们称这个工具为 Web AppBuilder,如下面的截图所示:

Web AppBuilder 使用 ArcGIS JavaScript API 创建全页面的网络地图应用。这些应用使用响应式设计元素,适用于任何屏幕。发布者可以选择颜色方案、标题文本、标志以及一系列小部件。Web AppBuilder 应用通过 ArcGIS Online 发布和托管,因此您不需要自己的托管环境来创建网络地图应用。
您可以直接从 ArcGIS Online 创建通用的 Web Appbuilder 应用。使用现有的 AGOL 网络地图,您可以创建一个应用程序,添加几个默认小部件,如打印、测量和查询。您可以定义标题、颜色方案(从列表中选择)以及您使用的每个小部件的参数。
开发者版
默认 Web AppBuilder 的问题之一是没有空间进行自定义开发。如果您不喜欢 查询 小部件搜索结果的方式,您没有更好的选择。ESRI 认识到这一点,并发布了开发者版以进行自定义开发。
Web AppBuilder 开发者版是一个 Node.js 应用程序,允许您使用默认小部件和您创建的任何自定义小部件生成网络地图应用。小部件基于 Dojo 的 dijit 系统,并且可以轻松添加到系统中。Web AppBuilder 及其创建的应用程序与 ArcGIS Online 账户相关联。
Web AppBuilder 开发者版生成运行独立网站所需的 HTML、样式表和 JavaScript。配置数据存储在 JSON 文件中。除了创建自定义小部件外,还可以创建自定义主题,修改元素的样式和位置以满足您的需求。
为 Web AppBuilder 开发者版制作的小部件具有高度的扩展性。可以使用 i18n(国际化)和本地化原则为国际市场编写小部件组件。配置可以将在小部件中存储的标签名称以多种语言保存,这些标签名称将与应用程序一起正确加载。小部件配置也可以在应用程序部署之前进行修改。可以使用与应用程序本身类似的方式来编写小部件的设置,使用 dijit 表单元素来修改配置。
截至撰写本文时,ArcGIS Online 不会托管使用开发者版创建的应用程序。使用开发者版生成应用程序托管在开发者的自有平台上。由于通过开发者版生成的文件是静态的 HTML、JavaScript、CSS 和图像文件,因此它们可以以很少或没有服务器端处理的方式进行托管。
注意
有关 Web AppBuilder 的更多信息,请参考 ESRI 网站 doc.arcgis.com/en/web-appbuilder/。有关 Web AppBuilder 开发者版的更多信息,包括下载说明和教程,请访问 developers.arcgis.com/web-appbuilder/。
更多 Node.js
谈到 Node.js,ArcGIS JavaScript API 有许多依赖于 Node.js 的工具。ArcGIS API for JavaScript Web Optimizer 是一个由 ESRI 托管的 Node.js 项目,它将你的自定义模块与最小数量的 ESRI 和 Dojo 模块打包,以简化构建过程。你可以通过 ESRI 的 内容分发网络(CDN)进行托管,或者下载并自行托管。
如果你决定更多地关注 Web AppBuilder 开发者版,也有相应的 Node.js 项目。generator-esri-appbuilder-js 项目生成一个包含所有必要文件的模板 Web AppBuilder 小部件。它会询问你一些基本信息,例如小部件名称和作者信息,以及小部件将需要哪些类型的文件。该项目基于 Yeoman,这是一个 Node.js 模板生成工具。
注意
有关 ArcGIS API for JavaScript Web Optimizer 的更多信息,请访问 developers.arcgis.com/javascript/jshelp/inside_web_optimizer.html。有关 generator-esri-appbuilder-js 项目,你可以在 NPM 上找到它 www.npmjs.com/package/generator-esri-appbuilder-js,或者查看源代码 github.com/Esri/generator-esri-appbuilder-js。
我们的应用程序
现在我们已经了解了 ESRI 的最新和最优秀的功能,让我们回到我们的故事。Y2K 社会联系我们了,他们非常喜欢我们使用应用程序完成的工作。他们迫不及待地想与公众分享所有这些信息。
有趣的是,Y2K 协会购买了一些 ArcGIS Online 许可证。现在他们希望您以该格式存储 web 地图数据,这样他们就可以在发现时管理和添加关于 2000 年的新数据。
我们的目的是为 Y2K 协会创建一个 ArcGIS Online webmap 并将其添加到我们现有的应用中。我们将把地图图层、弹出模板和图表添加到 webmap 中。然后我们将 webmap 加载到我们现有的应用中,删除不必要的功能,并使其他自定义功能与 webmap 协同工作。
创建 webmap
我们需要创建一个用于我们应用的 webmap。如果您还没有为 ArcGIS Online 设置开发者账户,请访问developers.arcgis.com/en/sign-up/注册开发者账户。登录后,点击顶部菜单中的地图。它将加载一个空地图,类似于本章开头的图像。
我们可以先保存地图的副本。这将允许我们命名地图,编写描述、搜索词等。在顶部工具栏中,点击保存下拉菜单并选择保存。这将创建一个对话框,允许我们输入地图的名称,编写摘要,并添加便于搜索地图的标签。我们可以将地图放入我们的基础文件夹,或者为应用创建一个新的文件夹。将标题添加为Y2K 人口普查地图,并给出简要描述。它应该看起来类似于以下截图:

点击保存地图后,地图应该有一个新的名称。如果您查看 URL,您也会注意到一些奇怪的字符。您的 webmap 现在已被分配了一个新的 webmap ID (?webmap=XXXXXXXXXXXXXXXXX),这是一个 ArcGIS Online 用来引用您的地图的唯一字符串。请保留这个 webmap ID,因为我们稍后会用到它。
更改底图
使用 ArcGIS Online 托管您的 webmap 的最好功能之一是所有可用的底图背景。从卫星和航空照片到简单的灰度地图,都有适合您数据的好背景。
在我们应用的先前版本中,我们使用国家地理底图来框定我们的数据。我们也可以为这个 webmap 做同样的事情。点击工具栏左上角的底图,您将能够查看许多 web 地图缩略图。选择国家地理底图,地图应该会更新。底图选择器应该看起来像以下截图:

添加人口普查地图服务
ArcGIS Online 提供了多种方法向地图添加图层。无论我们是否有 ArcGIS 服务器地图服务或简单的 CSV 文件,ArcGIS Online 都可以容纳各种数据。要向地图添加图层,请点击左上角的 添加 按钮,并从下拉选项中选择。以下几节将讨论向网络地图添加图层的一些不同方法。
搜索图层
ArcGIS Online 为公众和组织使用提供了广泛的地图图层。ArcGIS Online 提供了一个搜索框和匹配地图及图层列表,以便添加到你的网络地图中。如果你组织内部已经共享了图层和服务,你可以搜索关键词和标签以将该图层添加到你的地图中。你还可以添加其他人标记为公开数据的地图服务和图层。
备注
如果你的网络地图包含仅限于你的组织或组织内特定组的数据层,那么查看网络地图的人将被提示登录以证明他们可以查看这些层。如果登录失败,网络地图可能会加载,但受限数据将不会加载到地图上。
浏览 ESRI 图层
ESRI 拥有大量自己的地图数据,可供公众使用。这些数据涵盖了广泛的主题和兴趣。你可以按类别筛选,并点击将地图图层添加到现有地图中。目前,主要类别包括以下:
-
影像
-
底图
-
历史地图
-
人口统计和生活习惯
-
地貌
-
地球观测
-
城市系统
-
交通
-
边界和地点
虽然你可能想从这个位置抓取人口普查数据,但请记住,这显示的是更当前的数据。由于我们在这个案例中使用的是 2000 年的数据,我们将忽略此选项。然而,我们会记住这一点,因为它可能在其他项目中很有用。
从网络添加图层
此选项允许我们从多个网络地图服务中添加。虽然我们预期可以添加 ArcGIS 服务器地图服务,但其他一些选择可能会让你感到惊讶。除了 ArcGIS 服务器地图和要素服务外,我们还可以添加以下其他图层类型:
-
WMS 服务
-
WMTS 服务
-
铺砖服务
-
KML 文件
-
GeoRSS 文件
-
CSV 文件
注意,CSV 文件指的是一个在线可用的文件,很可能是通过另一个服务。如果你有要上传的 CSV 文件,你会选择下一个选项。
从文件添加图层
在这里,我们可以上传一个文件,并让 ArcGIS Online 为我们渲染它。对话框要求上传一个文件,并根据文件格式,它还会要求提供信息以确定地理位置。可用于添加地理数据的可接受文件包括以下内容:
-
一个包含最多 1,000 个特征并存储在
.zip文件夹中的形状文件 -
一个制表符或逗号分隔的文本或 CSV 文件
-
一个 GPS 交换格式(GPX),可以从许多 GPS 设备中获得
添加地图注释
此选项允许您在地图上绘制自定义的点、线和多边形作为其他用户或应用的备注。您可以从多种预定义的特征类型和独特的渲染器中进行选择。根据您选择的备注模板,您可以添加带有自定义符号的点、线和多边形。
我们的应用程序数据源
对于我们的应用程序数据源,我们将从网络中添加一个图层。选择从网络添加图层后,从下拉列表中选择 ArcGIS 网络服务,并输入或粘贴 2000 年人口普查地图服务 URL 到空白处。不要勾选用作底图复选框。最后,点击添加图层将其添加到地图中。您可以在以下图像中看到提示的截图:

样式
ArcGIS Online 允许您重新样式许多动态和特征层,即使它们在原始地图上没有被这样样式化。您可以为每个层添加自定义渲染器来改变颜色、透明度和图片符号。您可以将没有样式的简单地图转换为具有独特价值符号的自定义地图,讲述您想要讲述的故事。
对于我们的网络地图,我们在地图上看到州和县的简单黑色轮廓。我们将更改样式,使它们看起来更有吸引力,并与国家地理底图背景融合。
首先,在内容下点击人口普查地图服务层以展开地图层。您将看到构成该层的不同子层,例如州、县、街区组和街区点。通过点击每个子层,您可以看到每个层的符号。
我们将开始更改州的符号。点击州右侧的小向下三角形,选择更改样式。第一个菜单将出现在左侧,允许您选择标签和符号。在底图和我们的弹出窗口之间,我们将对所在州有一个很好的了解,所以我们将保留标签不变。在选择绘图样式下的单符号绘图中点击选项按钮。
下一个提示允许您更改地图上层的符号和可见范围。如果层已经设置了最小和最大可见性,这不会使它们更宽松,但可以使它们更严格。点击符号链接以获取颜色提示。从这里,您可以设置多边形的轮廓颜色和内部填充颜色。它应该看起来像以下截图:

将轮廓颜色改为较深的橙色,然后点击确定。我们将在州中保留透明的填充。对县和街区组重复相同的流程,每次选择较浅的橙色色调。
创建弹出窗口
现在我们已经调整了图层,让我们看看弹出窗口。ArcGIS Online 允许您为您的网络地图上的地图图层和要素服务创建和配置弹出窗口。它们提供了一系列弹出窗口配置,包括要素属性列表和所见即所得(WYSIWYG)富文本编辑器。两者都允许一些值格式化,例如数字的千位分隔符或日期的格式化。WYSIWYG 编辑器还允许您使用文本颜色并根据值属性添加链接。
对于我们的应用程序,我们将坚持显示属性列表。点击州(向下指向的小三角形)的选项按钮,并选择配置弹出窗口。在左侧的下一个菜单中,我们可以修改标题和内容。记住,在任何空白处,字段名称都由方括号包围,并将被替换为值。
在显示下拉菜单旁边,选择字段属性列表。这将创建一个包含所有字段属性的冗长列表。我们不想看到所有这些属性,甚至大多数属性,因此我们将配置我们看到的字段。点击字段列表下的配置属性链接。
您将看到一个包含可用于查看的字段列表的对话框。每个字段都有一个用于可见性的复选框,一个显示在方括号中的字段名称,以及一个字段别名。可以通过点击它们并输入不同的值来更改字段别名。取消选中这里的复选框意味着它不会出现在弹出窗口中。对于数值和日期字段,当选择此类字段时,右侧将出现额外的格式化提示。请看以下截图:

由于我们在标题中看到了状态名称,我们可以取消选中除2000 年人口、平方英里和每平方英里人口之外的所有选项。我们将重命名别名以使其更具吸引力。最后,我们将点击确定。属性列表应该反映我们目前可见的三个字段。
添加图表
ArcGIS Online 上的弹出窗口配置控件还允许您根据要素属性添加图像、图表和图形。这些图像和图表出现在我们已显示的内容下的弹出窗口中。这些图形使用dojox/charting库绘制。
返回到州图层,我们将添加各州中的民族、性别和年龄的图表。在左下角,在弹出媒体下,点击添加按钮。将出现一个下拉菜单,让您选择想要看到的图像或图表。对于我们的民族图表,我们将选择饼图。
在下一个提示中,我们得到了配置饼图所需的参数。它需要一个标题、一个说明以及要添加到图表中的选定字段。我们还得到了一个选项,可以将数据与数据中的另一个字段进行归一化。如果我们正在比较不同地区的相对人口水平,那么与总人口进行归一化将是有益的,但因为我们正在制作饼图,并且每个民族群体的人口总和应该达到 100%,所以我们跳过归一化。您可以在以下截图中看到提示的示例:

对于州弹出窗口,我们将给图表命名为民族图。然后,我们将添加一个说明来解释图表的内容。最后,我们将确保WHITE、BLACK、AMERI_ES、ASIAN、HISPANIC、MULT_RACE、HAWN_PI和OTHER字段被选中。请注意,如果您在上一节配置弹出窗口字段名称属性时没有重新格式化字段名称,属性将保持您离开时的样子。您也可以在这里编辑字段别名。您不必一定在弹出窗口中使它们可见来使它们可图表化(并格式化它们的别名)。最后,点击确定,你应该能在弹出媒体列表中看到您的民族图表。
我们将重复添加性别和年龄图表的步骤。对于性别饼图,我们将重复前面的步骤添加另一个饼图。我们将将其标记为性别图表,并确保MALES和FEMALES字段被选中。对于年龄柱状图,我们将从第一个菜单中选择柱状图。在第二个提示中,我们将图表标题设置为年龄图表,并添加AGE_UNDER5、AGE_5_17、AGE_18_21、AGE_22_29、AGE_30_39、AGE_40_49、AGE_50_64和AGE_65_UP字段。
一旦配置好图表,请务必点击弹出菜单底部的保存弹出窗口。这将使用新的弹出窗口配置重新加载地图。点击一个州来测试弹出窗口是否出现。你应该能看到我们添加的字段,以及民族图表在州弹出窗口中。图表的左右两侧也添加了箭头,以便用户可以查看性别和年龄图表。将鼠标悬停在图表上会显示图表背后的标签和数字。它们应该看起来像以下图片中显示的弹出窗口。
这张截图显示了民族图:

这张截图显示了性别图表:

这张截图显示了年龄图表:

ArcGIS Online 网上图表的用途
ArcGIS Online 不仅提供地图,还提供了可以进行分析、收集数据和与他人共享地图的工具。上一工具栏中可用的其他功能如下。
分享
共享让你可以控制谁可以访问你的 webmap。你可以与世界共享地图,与组织内的人共享,或者只保留给自己。你可以通过共享地图的 URL 来让其他人访问地图。你还可以将 webmap 嵌入任何网站作为 iframe,或者使用其中一个 web 应用模板创建网络应用程序。
打印
工具栏中的打印控制项会在浏览器的新标签页中打开一个可打印的 webmap 副本。可打印的副本显示了标题、当前范围的地图以及归属数据。页面格式化为打印在 ANSI(A 型纸大小)的纸张上。
路线
点击路线,你将在屏幕左侧部分获得一个路线小部件。路线小部件让你可以从起点到终点获取路线,中间可以有任意多个停靠点。可以为汽车、卡车或步行计算路线和时间。你还可以收集往返路线数据以及路线上的交通数据。请记住,路线和交通路由需要消耗信用点来使用。
测量
此处的测量小部件模仿了esri/dijit/Measurement中的测量dijit。你可以绘制点、线和多边形来收集坐标、长度和面积。你还可以将测量单位更改为公制或英制单位。当使用点收集纬度和经度时,它将以十进制度数显示,以及度分秒。
书签
书签工具允许你在 webmap 上保存位置。书签小部件捕获当前地图范围并为其命名。当在地图上创建多个区域的书签时,你可以通过点击书签名称快速导航到它们。你也可以编辑名称和删除不需要的书签。书签数据会与其他 webmap 功能一起保存。
面向 ArcGIS Online webmap 的开发
如果你的组织对 ArcGIS Online 提供的 webmap 和工具感到满意,你可能会从网站运行你的面向公众的 GIS,而无需再开发其他工具。有时你需要定制功能。有时当你点击地图时,你需要一个应用程序从其他数据源进行定制请求。也许你需要一个复杂的查询页面或查询构建器来收集结果。无论你的特殊需求是什么,你都可以将 ArcGIS Online webmap 带入你的定制应用程序。
使用 AGOL 网络地图对您的应用程序有什么好处?它省去了许多地图配置步骤。我们通常会在应用程序中硬编码的所有瓦片、动态和要素层现在可以合并到一个网络地图中,并在我们的应用程序中加载。我们之前必须通过代码分配的所有弹出窗口和 infoTemplate 现在可以从网络地图中继承。每当需要更改地图服务层和弹出窗口时,都可以通过 ArcGIS Online 进行更改,而不是更改代码并希望用户没有仍在运行缓存的配置脚本副本。配置可以由其他人处理,这样您就有更多时间编码。
创建网络地图
为了引入我们之前创建的网络地图,我们需要更新我们的 app.js 文件。在 app.js 文件 require() 方法中的模块列表中,我们需要添加对 esri/arcgis/utils 模块的引用。这将为我们提供将网络地图添加到应用程序所需的工具:
require([, "esri/arcgis/utils", ],function (, arcgisUtils, ) {
});
通常,我们会使用 arcgisUtils.createMap() 方法来构建我们的新网络地图。它需要与 esri/map 构造函数类似的参数,包括地图应放置的 HTML 元素 id 的引用,以及一个 options 对象。然而,arcgisUtils.createMap() 方法还需要一个 ArcGIS Online 网络地图 id。您可以通过在 ArcGIS Online 中打开您的网络地图以进行编辑来获取此 id。如果您查看 URL,您会看到搜索参数 webmap=XXXXXXXXXXXXXXX,其中那些 X 被替换为字母数字字符。您可以将 id 数字复制并粘贴到您的应用程序中,以创建网络地图,如下所示:
require([, "esri/arcgis/utils", ],function (, arcgisUtils, ) {
arcgisUtils.createMap("450d4fb709294359ac8d03a3069e34d3",
"mapdiv", {});
});
arcgisUtils.createMap() 方法的选项包括但不限于以下内容:
-
bingMapKey:您在 Bing Maps 订阅中提供的唯一密钥字符串。 -
editable:一个Boolean值,您可以使用它来禁用任何要素层编辑。 -
geometryServiceURL:指向 ArcGIS Server 几何服务的 URL 链接。 -
ignorePopups:一个Boolean,如果为真,将禁用所有弹出窗口。 -
mapOptions:您在创建普通地图时通常会传递的正常选项。 -
usePopupManager:一个Boolean值,当为真时,告诉InfoWindow获取所有具有弹出模板的点击特征。当为真时,它还处理地图服务子层的弹出窗口可见性,无论父层是否可见。如果为假,当父层未开启时,子层的弹出窗口可能加载或不加载,您将不得不手动控制弹出窗口的可见性,对于复杂的地图尤其如此。
arcgisUtils.createMap() 方法将返回一个 dojo/Deferred 承诺,一旦下载了网络地图数据并创建了地图,就会使用 then() 触发成功回调。回调将返回一个包含以下内容的响应对象:
-
map:创建的esri/map对象 -
itemInfo:一个包含对象的对象:-
item:一个包含地图标题、描述片段和嵌套数组形式的范围的对象 -
itemInfo:一个包含有关地图服务、渲染器、弹出窗口、书签以及其他用于创建网络地图的地图数据的 JSON 对象 -
errors:在加载数据和创建地图服务时遇到的错误数组
-
-
clickEventHandler和clickEventListener:当填充时,这些控制显示弹出窗口的地图点击事件。如果您需要关闭弹出窗口,可以运行以下代码:// response object was returned by arcgisUtils.createMap() response.clickEventHandler.remove(); -
要恢复弹出窗口,请运行以下代码行(假设您已加载了
dojo/on模块):response.clickEventHandler = dojoOn(map, "click", response.clickEventListener);
注意
在创建网络地图时,如果您将usePopupManager参数设置为true(默认为false),则clickEventHandler和clickEventListener参数将为 null。在这种情况下,弹出窗口的可见性将通过map.setInfoWindowOnClick(boolean)方法来控制。
由于我们在第八章中引入了 ESRI Bootstrap,因此我们需要在esri/arcgis/utils模块中使用createMap()构造函数的 ESRI Bootstrap 版本。由于地图可能需要一些时间才能加载,我们将把普查小部件的加载移到延迟回调中。代码应如下所示:
/* not used because we're using the boostrapMap version
var deferred = arcgisUtils.createMap("450d4fb709294359ac8d03a3069e34d3", "map", {}); */
var deferred = BootstrapMap.createWebMap("450d4fb709294359ac8d03a3069e34d3", "map", {});
deferred.then(function (response) {
// map and census widget definition moved inside deferred callback
var map = response.map;
var census = new Census({
map: map,
mapService: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/"
}, "census-widget");
dojoOn(dom.byId("census-btn"), "click", lang.hitch(census, census.show));
});
在我们的普查小部件中
现在我们已经在网络地图中定义了我们地图的许多部分,我们可以删除处理相同信息的许多小部件代码。在我们的普查define语句的模块引用中,我们可以删除对以下模块的引用:
-
州、县、街区组和街区点模板
-
IdentifyTask和IdentifyParameter -
InfoTemplates -
D3/d3
记得删除模块引用以及它们对应的变量。
在我们小部件的主体中,我们可以删除对identifytask或地图点击或infowindow选择事件的任何引用,因为它们控制了弹出窗口。我们还可以删除创建弹出窗口上图表的所有方法。我们剩下的只是一个填充州、县和街区组下拉菜单的小部件,以便我们可以查看这些数据。
对于删除这么多代码,请不要感到难过。开发者生活中的一大乐趣就是删除代码行,因为它们可能是错误的可能来源。
在我们的应用程序中访问网络地图
如果我们需要从 ArcGIS Online 网络地图配置中获取某些信息,我们会怎么做?如果我们需要这些信息,ArcGIS JavaScript API 为我们提供了检索它的工具。通过使用esri/arcgis/utils和esri/request模块,我们可以检索并处理我们的网络地图数据。
要访问我们的 web 地图配置数据,我们需要两件信息:托管数据的网站 URL 和 web 地图 ID。托管 webmap 数据的服务 URL 可以在 esri/arcgis/utils 模块的 arcgisUrl 属性中找到。默认情况下,它指向 www.arcgis.com/sharing/rest/content/items,但如果你的 webmap 在 ArcGIS 门户的私有企业网络中托管,则应将此 URL 字符串替换为你的门户服务的适当 URL。
在我们的 JavaScript 代码中,我们可以将托管 webmap 数据的 URL 和 webmap ID 结合起来,形成一个 URL,以便访问我们 webmap 的 JSON 数据。我们可以使用 esri/request 模块从 URL 请求数据,并在我们的应用程序中需要的地方使用它。以下是一个示例代码片段:
require([…, "esri/arcgis/utils", "esri/request", …],
function (…, arcgisUtils, esriRequest, …) {
…
// replace fake webmapid value with real webmap id
var webmapid = "d2klfnml2kjfklj2nfkjeh2ekrj";
var url = arcgisUtils.arcgisUrl + "/" + webmapid + "/data";
esriRequest({
url: url,
handleAs: "json",
content: {f: "json"}
}).then(function (response) {
console.log(response);
// do something with the JSON data.
});
…
});
如果你查看我们 webmap 的 JSON 结果,无论是使用地图控制台还是浏览器的 JSON 查看插件,你都可以看到我们应用程序的 webmap 数据包括地图服务图层、背景底图,甚至关于我们弹出窗口配置的详细信息。如果需要,这个响应中包含了很多可能有用的信息。
通过代码与地图交互
在我们之前的版本中,我们为每个图层加载 InfoTemplates 以定义弹出窗口中显示的内容的格式。现在,由于我们通过 ArcGIS Online 定义了所有这些信息,我们不需要进行额外的 HTTP 请求来加载 HTML 模板。你可以在地图上点击,弹出窗口就会准备好使用。
然而,当我们尝试使用自定义的下拉选择器来弹出弹出窗口时,我们遇到了问题。在旧版本中,我们会查询数据,添加适当的模板,并将其插入到 InfoWindow 中。现在,用户点击会为我们获取适当的数据。但我们如何在不点击地图的情况下点击地图呢?
使用一个未记录的技巧,我们可以用我们的数据触发地图点击事件。我们可以将地图缩放到要素范围,并在完成后触发地图点击事件。这个技巧已经在多个使用 ArcGIS Online webmap 的网站上成功部署。
在 Census.js 小部件中查找 _updateInfoWindowFromQuery() 方法。它目前包含设置地图范围和填充地图 InfoWindow 内容的步骤。我们将保留范围设置,但删除对 map.infoWindow 的任何引用。相反,在地图范围改变后调用的异步回调函数内部,我们将添加代码来调用 map.onClick() 方法以触发点击事件:
_updateInfoWindowFromQuery: function (results) {
var resultExtent = graphicsUtils.graphicsExtent(results.features);
this.map.setExtent(resultExtent).then(lang.hitch(this, function () {
// do something
this.map.onClick();
}));
},
map.onClick()函数需要一个包含三个属性的对象:一个图形、一个mapPoint和一个包含屏幕上该点坐标的screenPoint。我们可以使用我们的查询结果中的第一个来获取被点击的图形。对于快速且简单的点击位置,我们可以使用当前范围的中心,这是我们通过resultExtent.getCenter()获得的。至于我们地图点的相应screenPoint,地图有将地理点转换为屏幕点以及相反的方法。我们将使用map.toScreen()方法与中心点来获取其在屏幕上的坐标。然后我们将结果输入到map.onClick()方法中,并测试应用程序。该函数看起来应该如下所示:
_updateInfoWindowFromQuery: function (results) {
var resultExtent = graphicsUtils.graphicsExtent(results.features);
this.map.setExtent(resultExtent).then(lang.hitch(this, function () {
var center = resultExtent.getCenter();
var centerScreen = this.map.toScreen(center);
this.map.onClick({
graphic: results.features[0],
mapPoint: center,
screenPoint: centerScreen
});
}));
},
我们可以通过加载我们的应用程序并点击Census按钮来测试代码。它应该显示我们的州、县和街区数据下拉模态。当我们选择像加利福尼亚这样的州时,它应该查询、缩放,并且之前配置的弹出窗口应该瞬间出现。现在,我们有一个完全功能性的普查应用程序,同时让 ArcGIS Online 处理大部分工作:

摘要
在本章中,我们探讨了 ESRI 为 ArcGIS 平台提供的最新工具:ArcGIS Online 和 Web AppBuilder。我们使用 ArcGIS Online 创建了一个 webmap,添加了图层,格式化它们,并创建了弹出窗口。我们将 webmap 添加到了我们自己的应用程序中。
在这本书的整个过程中,我们深入研究了 ArcGIS JavaScript API。我们从一个简单的带有点击事件和地理数据的 webmap 应用程序开始。你学习了如何使用 API 和 Dojo 框架提供的工具来构建可以跨项目重复使用的自定义控件和模块。然后你尝试添加其他 JavaScript 库,以便不熟悉 dojo 的开发者也能做出贡献。你学习了如何通过 Web 应用程序编辑地理数据。你还学习了如何为桌面和移动设备样式化你的地图应用程序。你甚至学习了如何对代码进行单元测试。最后,你学习了 ESRI 如何通过 ArcGIS Online 和 Web AppBuilder 简化 webmap 开发,从而更快地将应用程序带给公众。你无疑已经取得了很大的进步。
注意
你接下来要去哪里?有很多优秀的书籍、博客和其他资源。你可以在 ESRI 的论坛上了解 ArcGIS 开发的最新趋势,论坛地址为geonet.esri.com,或者找到最新 ArcGIS API 的支持,地址为developers.arcgis.com。对于博客,你可以查看 Dave Bouwman 的博客,地址为blog.davebouwman.com,或者 Rene Rubalcava 的博客,地址为odoe.net。对于书籍,你可能想了解如何通过 Hussein Nasser 的《Managing ArcGIS for Server》来管理 ArcGIS Server 以提供你的网络地图。你可能还想阅读 Rene Rubalcava 的《ArcGIS Web Development》,Eric Pimpler 的《Developing Web and Mobile ArcGIS Server Applications with JavaScript》,或者 Matthew Sheehan 的《Developing Mobile Web Applications》。



浙公网安备 33010602011771号