OpenLayers-3-x-秘籍-全-

OpenLayers 3.x 秘籍(全)

原文:zh.annas-archive.org/md5/c2c602442468c70156da920a343b9914

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

我们生活在信息时代,其中大量的信息易于以地理方式表示。这可能是使地理信息系统(GIS)成为许多公司最重要的方面之一的主要特征。与 GIS 相关的技术是一个不断发展的行业,因此 GIS 已成为许多专业人士渴望的技能。

互联网的通用性和浏览器性能的提升使得互联网成为当前 GIS 的主要部分成为可能,并由于其能力降低了桌面应用程序的重要性:浏览器允许我们向大众展示数据可视化,创建在线数据编辑器等。

现在,OpenLayers 是最完整、功能最强大的开源 JavaScript 库,可以创建任何类型的网络制图应用程序。除了提供一套优秀的组件,如地图、图层或控件外,OpenLayers 还提供了使用许多不同数据格式访问大量数据源的能力,实现了来自开放地理空间联盟(OGC)的许多标准,并且与几乎所有的浏览器兼容。

本书涵盖的内容

第一章, 网络制图基础,向读者介绍了 OpenLayers,并描述了创建地图、管理图层堆栈、处理控件以及如何在项目中添加 OpenLayers 的基本方法。

第二章, 添加栅格图层,主要围绕处理和理解主要栅格图层。OpenLayers 提供了与主要服务提供商(如 Google 或 Bing)一起工作的机会,以及与开源的(如 OpenStreetMap)集成,或者与 WMS 服务器一起工作。

第三章, 处理矢量图层,探讨了矢量图层的力量,并解释了我们可以如何使用不同的格式从不同的来源加载数据,以及我们如何创建特征、标记和弹出窗口。

第四章, 处理事件,描述了事件的重要性以及当它们由 OpenLayers 组件(如地图或图层)触发时,我们如何做出反应,每次向地图添加图层、加载图层、添加特征等。

第五章, 添加控件,解释了如何管理控件,并描述了 OpenLayers 向用户提供的最常用和最重要的控件:添加或编辑特征、测量距离、获取特征信息等。

第六章, 主题化,描述了 OpenLayers 是如何设计来控制其组件的外观的。本章展示了我们如何改变控件的位置或外观,并介绍了创建全新主题的基础知识。

第七章, 特征样式,完全致力于展示我们如何控制特征的显示:我们用来样式化特征的多种方式、渲染器的概念、根据特征属性进行样式化等。

第八章, 高级内容,探讨了 OpenLayers 的一些高级主题:处理投影、请求远程数据、创建新的控件等。它收集了一些食谱,展示了 OpenLayers 为开发者提供的可能性。

阅读本书所需的条件

由于 OpenLayers 是一个必须集成在 HTML 页面中的 JavaScript 库,这意味着用户必须熟悉这些技术。

要运行食谱,您需要将代码包放置在提供页面的 HTTP 服务器中。食谱代码所需的全部库依赖项,如 OpenLayers 或 Dojo 工具包(dojotoolkit.org),都包含在包本身中。

一些食谱需要从源包中包含的 PHP 脚本请求数据。这些脚本的功能是生成一些随机数据,以便稍后将其集成到地图中。为了正确运行这些食谱,读者需要一个支持 PHP 的 HTTP 服务器。

XAMPP(www.apachefriends.org/en/xampp.html)或 Bitnami Stacks(bitnami.org/stack/lampstack)等解决方案是安装所需堆栈的简单方法。

为了更好的用户体验,我们创建了一个主要应用程序,允许运行所需的食谱并显示其源代码。假设读者已经安装并配置了本地 Web 服务器,并且将代码包复制到 HTTP 服务器根内容文件夹中的openlayers-cookbook文件夹中,用户可以通过在浏览器中访问localhost/openlayers-cookbook/index.html URL 来运行主应用程序。

我们在许多食谱中使用了 Dojo 工具包,因为它允许我们创建丰富的组件,如滑块或切换按钮。Dojo 的 JavaScript 库和 CSS 文件包含在index.html文件中,因此 HTML 食谱文件不需要包含它们。如果读者计划将食谱作为独立的 Web 页面运行,他/她需要在食谱文件中包含 Dojo 的 JavaScript 库和 CSS 文件,否则食谱将无法正常工作。

这本书面向的对象

这本书非常适合需要创建网络地图应用的 GIS 相关专业人士。

从基础知识到高级主题,本书的食谱直接涵盖了用户在日常工作中可能遇到的最常见问题。

习惯用法

在这本书中,您将找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些风格的示例,以及它们含义的解释。

文本中的代码单词显示如下:“topLayer()bottomLayer()动作也相似,它们将指定的图层移动到堆栈的顶部或底部。”

代码块设置如下:

<style> 
    html, body { 
        width: 100%; 
        height: 100%; 
        margin: 0; 
        padding: 0; 
    } 
</style>

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“以下截图中的图层不透明度设置为50%。”

注意

警告或重要提示以如下框形式出现。

注意

技巧和窍门如下所示。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大收益的标题非常重要。

要发送一般反馈,只需发送电子邮件到 <feedback@packtpub.com>,并在邮件主题中提及书名。

如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们在 www.packtpub.com/authors 上的作者指南。

客户支持

现在,您已经成为 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您在 www.packtpub.com 的账户下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/support 报告它们,选择您的书,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分中现有的勘误列表。

侵权

互联网上版权材料的侵权是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以追究补救措施。

请通过 <copyright@packtpub.com> 与我们联系,并提供疑似侵权材料的链接。

我们感谢您在保护我们作者以及为我们提供有价值内容的能力方面提供的帮助。

问题

如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章. 网络地图基础

在本章中,我们涵盖了:

  • 创建一个简单的全屏地图

  • 包含 OpenLayers 的不同方法

  • 理解基本图层和非基本图层

  • 避免需要基本图层

  • 玩转地图选项

  • 管理地图的堆叠图层

  • 管理地图控件

  • 在地图视图中移动

  • 限制地图范围

简介

每个历史都有开始,同样每个食谱都是从最初的调料开始的。

本章向我们展示了在开始使用 OpenLayers 创建我们的第一个网络地图应用程序时需要了解的基本和更重要的事情。

正如我们将在本章和下一章中看到的,OpenLayers 是一个庞大且复杂的框架,但与此同时,它也非常强大和灵活。

与其他库(如简单但功能更强大的 Leaflet 项目 leaflet.cloudmade.com)相比,OpenLayers 试图实现开发者创建网络 GIS 应用程序所需的所有功能。也就是说,不仅包括地图、图层或标准格式等 GIS 相关概念,还包括文档元素的操纵或辅助函数以进行异步请求。

简单来说,我们在下一段中描述了框架的大致轮廓。

OpenLayers 中的主要概念是地图。它表示信息渲染的视图。地图可以包含任意数量的图层,这些图层可以是栅格图层或矢量图层。在它的过程中,每个图层都有一个数据源,以它自己的格式提供数据:一个 PNG 图像,一个 KML 文件等等。此外,地图还可以包含控件,这些控件有助于与地图及其内容进行交互:平移、缩放、要素选择等等。

让我们从通过示例学习 OpenLayers 开始吧。

创建一个简单的全屏地图

当你在地图应用程序中工作时,首要且重要的任务是创建地图本身。地图是应用程序的核心,你将在其中添加和可视化数据。

本食谱将指导您完成创建我们第一个非常简单的网络地图应用程序的过程。

注意

假设已经配置并准备好了网络服务器。记住,我们的食谱不过是 HTML、CSS 和 JavaScript 代码,因此我们需要一个网络服务器来提供这些代码,以便在浏览器端进行解释。

准备工作

使用 OpenLayers 进行编程主要与编写 HTML 代码和,当然,JavaScript 代码有关。因此,我们只需要一个文本编辑器就可以开始编写我们的食谱。

存在着许多优秀的文本编辑器,其中许多具有网络编程功能。因为我们将要开始探索像 OpenLayers 这样的开源项目,我们将参考几个优秀的开源项目。

对于 Windows 用户来说,Notepad++ (notepad-plus-plus.org) 是默认文本编辑器的一个很好的替代品。它简单快捷,提供语法高亮显示,并可以添加插件以扩展功能。

另一方面,你不仅可以找到支持网页开发的完整开发框架,而且这些框架不仅提供语法高亮,还提供自动完成、代码导航等功能,而不是仅仅使用文本编辑器。

在这个组中,有两个项目在开源项目宇宙中是明星:Eclipse (www.eclipse.org) 和 NetBeans (netbeans.org)。这两个项目都是基于 Java 的,并且可以在任何平台上运行。

你可以在recipe/ch01/ch01_simple_map_book.html文件中找到源代码。

如何做到这一点...

  1. 让我们先创建一个新的空index.html文件,并在其中插入以下代码块。我们将在下一节中一步一步地解释它:

    <!DOCTYPE html> 
    <html> 
        <head> 
            <title>Creating a simple map</title> 
            <meta http-equiv="Content-Type" 
                content="text/html; charset=UTF-8"> 
    
            <!-- Include OpenLayers library --> 
            <script type="text/javascript" 
                src="http://openlayers.org/api/2.11/
                OpenLayers.js"></script> 
            <style> 
                html, body { 
                    width: 100%; 
                    height: 100%; 
                    margin: 0; 
                    padding: 0; 
                } 
            </style> 
    
            <!-- The magic comes here --> 
            <script type="text/javascript"> 
                function init() { 
                    // Create the map using the specified 
                    // DOM element 
                    var map = new OpenLayers.Map("rcp1_map"); 
    
                    // Create an OpenStreeMap raster layer 
                    // and add to the map 
                    var osm = new OpenLayers.Layer.OSM(); 
                    map.addLayer(osm); 
    
                    // Set view to zoom maximum map extent 
                    map.zoomToMaxExtent(); 
                } 
            </script> 
        </head> 
        <body onload="init()"> 
            <div id="rcp1_map" style="width: 100%; 
                height: 100%;"></div> 
        </body> 
    </html>
    
    

    小贴士

    下载示例代码

    你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

  2. 在你的浏览器中打开文件,查看结果。你将看到一个全屏地图,在左上角有一些控件,如下面的截图所示:

如何做到这一点...

它是如何工作的...

让我们一步一步地解释这个神秘步骤。首先,我们创建了一个 HTML5 文档,查看doctype声明代码<!DOCTYPE html>

head部分,我们通过script元素包含了对 OpenLayers 库的引用,如下所示:

<script type="text/javascript" 
src="img/OpenLayers.js"></script>

我们添加了一个style元素来强制文档占据整个页面,如下所示:

<style> 
    html, body { 
        width: 100%; 
        height: 100%; 
        margin: 0; 
        padding: 0; 
    } 
</style>

style元素之后是带有一些 JavaScript 代码的script元素,但我们将它在最后进行解释。

head部分开始后,body部分开始。我们的body与一个onload事件相关联。这意味着,一旦浏览器完全加载了body部分的所有内容,init()函数将被调用:

<body onload="init()">

最后,在body中,我们放置了一个具有标识符rcp1_mapdiv元素,OpenLayers 将使用它来渲染地图。

再次,我们强制元素填充整个父元素的空间:

<div id="rcp1_map" style="width: 100%; height: 100%;"></div>

小贴士

关于样式的说明...

div元素的width/height设置为100%意味着它将填充父元素的 100%空间。在这种情况下,父元素是body元素,它也被设置为填充 100%的页面空间。更多和更好的关于 CSS 的信息可以在www.w3schools.com/css找到。

现在,让我们看看head部分中的script元素。

如我们之前提到的,使用onload事件,我们确保在浏览器加载完整个body元素后执行init函数,这意味着我们可以无任何问题地访问<div id="rcp1_map" ...>

首先,我们创建了一个OpenLayers.Map对象,它将在之前提到的div元素中渲染。这是通过在构造函数中传递DOM元素标识符来实现的:

// Create the map using the specified DOM element
var map = new OpenLayers.Map("rcp1_map");

接下来,我们创建了一个新的栅格图层,它将显示来自OpenStreetMaps项目的图像:

// Create an OpenStreetMap raster layer and add to the map
var osm = new OpenLayers.Layer.OSM();

一旦创建,我们就将其添加到地图中:

map.addLayer(osm);

最后,将地图视图设置为最大有效范围:

// Set view to zoom maximum map extent
map.zoomToMaxExtent();

还有更多...

记住,做事情没有唯一的方法。

本书中的配方并没有作为独立应用程序编写。相反,为了提高用户体验,我们创建了一个丰富的应用程序,允许你选择并运行所需的配方,同时还有查看源代码的可能性。

还有更多...

因此,书中编写配方的方法略有不同,因为它们必须与应用程序的设计集成。例如,它们不需要包含 OpenLayers 库,因为这在主应用程序的另一个地方已经包含了。

此外,如何做部分中介绍的方法更倾向于独立应用程序。

如果你正在查看这个配方的源代码,位于recipes/ch01/ch01_simple_map.html,我们会看到略微不同的代码:

<!-- Map DOM element -->
<div id="ch1_simple_map" style="width: 100%; height: 95%;"></div>

<!-- The magic comes here -->
<script type="text/javascript">

    // Create the map using the specified DOM element
    var map = new OpenLayers.Map("ch1_simple_map");

    // Create an OpenStreeMap raster layer and add to the map
    var osm = new OpenLayers.Layer.OSM();
    map.addLayer(osm);

    // Set view to zoom maximum map extent
    map.zoomToMaxExtent();
</script>

如我们所见,它包含了前几节中描述的主要部分。我们有一个div元素来包含地图实例,以及一个包含所有必需 JavaScript 代码的script元素。

为了创建丰富的应用程序,我们必须使用 Dojo Toolkit 框架(dojotoolkit.org),它提供了几乎任何所需的功能:访问和修改文档对象结构、事件处理、国际化等等。但我们选择它的主要原因是因为,除了提供一套优秀的同质化小部件(标签页、按钮、列表等等)来创建外观和感觉都很好的应用程序之外。

教授 Dojo 超出了本书的范围,但它的使用非常简单和具体,不会干扰本配方的目标,即教授 OpenLayers。

参见

  • 包含 OpenLayers 的不同方法配方

  • 理解基本层和非基本层配方

包含 OpenLayers 的不同方法

根据我们工作的环境,即开发或生产,我们可以以不同的方式将 OpenLayers 包含到我们的项目中。

环境指的是我们流程中某个阶段所需的服务器层。这样,开发环境与开发过程相关联,程序员每天在这里工作,进行测试和检查。

生产环境指的是项目的最终阶段。它必须在稳定的服务器上运行,且没有依赖问题。

如我们很快将看到的,我们可以将包含 OpenLayers JavaScript 代码的解决方案总结为两组,一组是在远程服务器上托管代码的,另一组是在我们自己的服务器上托管代码的。

让我们开始,看看每种解决方案的优缺点。

准备工作

创建一个名为 myProject 的文件夹,它将包含所有我们的项目文件和库依赖项。然后创建一个 index.html 文件,并继续使用 创建一个简单的全屏地图 菜单中给出的代码。

注意

假设项目文件夹位于可以被 web 服务器文件夹访问的文件夹中,因此它可以提供其内容。

现在,从项目的网页 www.openlayers.org 下载 OpenLayers 代码。

注意

在撰写本书时,OpenLayers 版本 2.11 是稳定版本,可以在 openlayers.org/download/OpenLayers-2.11.tar.gz 找到。

将包保存到 myProject 文件夹中,并解压缩它。我们需要有一个类似于以下截图的文件夹结构:

准备就绪

如何操作...

我们有三种方法将 OpenLayers 库包含到我们的代码中:

  • <script type="text/javascript" src="img/OpenLayers.js"> </script>

  • <script type="text/javascript" src="img/OpenLayers.js"></script>

  • <script type="text/javascript" src="img/OpenLayers.js"></script>

它是如何工作的...

第一个选项包括一个由 OpenLayers 项目服务器托管的全压缩文件。它使用简单,但无法在离线模式下本地工作:

<script type="text/javascript" 
src="img/OpenLayers.js"></script>

注意

压缩的全压缩文件 OpenLayers.js 的大小接近 1 MB,因此在具有大量请求的生产环境中,可能最好将此文件托管在内容分发网络或 CDN (en.wikipedia.org/wiki/Content_delivery_network) 上。

第二个选项与第一个选项非常相似,但全压缩文件附加到项目中。此选项适用于需要 OpenLayers 在自己的服务器上,并且与你的应用程序代码一起使用的场景。

<script type="text/javascript" src="../js/
OpenLayers-2.11/OpenLayers.js"></script>

最后,第三个选项包括 OpenLayers 库的未压缩代码,实际上它还包括许多其他文件,如图层、控件等所需的文件。

<script type="text/javascript" src="../js/
OpenLayers-2.11/lib/OpenLayers.js"></script> 

此选项主要供想要扩展 OpenLayers 并需要调试代码的程序员使用。

小贴士

我鼓励你在这种模式下工作。使用一些工具,例如 Firefox 网络浏览器的 Firebug 或 Chrome 浏览器控制台,并在 OpenLayers 类上设置断点,以更好地理解正在发生的事情。

值得注意的是,使用这种方法会从服务器加载很多文件,每个类一个,这意味着会发起更多的服务器请求。

这种方法最显著的影响是页面加载时间比前几种选项长得多。

还有更多...

如果你选择下载 OpenLayers 并将其包含在你的项目中,你不需要放置整个未压缩的包。正如你所见,它包含了很多文件和文件夹:源代码、构建脚本、测试代码和其他工具,但只有少数是真正需要的。

在这种情况下,你需要附加的只有:

  • 全压缩的 OpenLayers.js 文件

  • themeimg文件夹

参见

  • 理解基图层和非基图层食谱

  • 创建一个简单的全屏地图食谱

理解基图层和非基图层

在使用 OpenLayers 时,您需要清楚的第一件事之一就是基图层概念。

基图层是一种特殊的图层,它始终可见,并确定一些地图属性,如投影和缩放级别。

一个地图可以拥有多个基图层,但一次只能有一个处于活动状态。

此外,如果您向地图添加多个标记的基图层,则第一个添加的基图层将被用作地图的当前活动基图层。

本食谱将向您展示如何向地图添加图层,并将它们标记为基图层。我们将创建一个包含两个并排地图的页面,每个地图都将有一个图层切换器控件,允许您控制地图图层。

理解基图层和非基图层

准备工作

我们假设您已经创建了一个index.html文件,并像我们在不同的方式包含 OpenLayers食谱中看到的那样包含了 OpenLayers 库。

如何做到这一点...

  1. 首先,创建必要的 HTML 代码以使我们的地图并排显示:

    <table style="width: 100%; height: 100%;"> 
        <tr> 
            <td> 
                <p>Map with one non base layer:</p> 
                <div id="ch01_base_nonbase_map_a" 
                    style="width: 100%; height: 500px;">
                </div> 
            </td> 
            <td> 
                <p>Map with two base layers</p> 
                <div id="ch01_base_nonbase_map_b" 
                    style="width: 100%; height: 500px;">
                </div> 
            </td> 
        </tr> 
    </table>
    
    
  2. 在此之后,添加一个script元素(<script type="text/javascript"></script>),其中包含初始化每个地图所需的代码。左侧的地图将包含两个图层,一个基图层和一个非基图层:

    // 
    // Initialize left map 
    // 
    
    // Create the map using the specified DOM element 
    var map_a = new OpenLayers.Map("ch01_base_nonbase_map_a"); 
    // Add a WMS layer 
    var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0", 
    { 
        layers: 'basic' 
    }); 
    map_a.addLayer(wms); 
    // Add a WMS layer 
    var topo = new OpenLayers.Layer.WMS("USA Topo Maps", "http://terraservice.net/ogcmap.ashx", 
    { 
        layers: "DRG" 
    }, 
    { 
        opacity: 0.5, 
        isBaseLayer: false 
    }); 
    map_a.addLayer(topo); 
    // Add LayerSwitcher control 
    map_a.addControl(new OpenLayers.Control.LayerSwitcher()); 
    
    // Set view to zoom maximum map extent 
    // NOTE: This will fail if there is no base layer defined 
    map_a.setCenter(new OpenLayers.LonLat(-100, 40), 5);
    
    
  3. 右侧的地图将包含两个基图层:

    // 
    // Initialize right map 
    // 
    
    // Create the map using the specified DOM element 
    var map_b = new OpenLayers.Map("ch01_base_nonbase_map_b"); 
    // Add a WMS layer 
    var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0", 
    { 
        layers: 'basic' 
    }); 
    map_b.addLayer(wms); 
    // Add a WMS layer 
    var topo = new OpenLayers.Layer.WMS("USA Topo Maps", "http://terraservice.net/ogcmap.ashx", 
    { 
        layers: "DRG" 
    }); 
    map_b.addLayer(topo); 
    // Add LayerSwitcher control 
    map_b.addControl(new OpenLayers.Control.LayerSwitcher()); 
    
    // Set view to zoom maximum map extent 
    // NOTE: This will fail if there is no base layer defined 
    map_b.setCenter(new OpenLayers.LonLat(-100, 40), 5);
    
    

工作原理...

让我们看看左侧地图的解释。我们首先做的是创建一个OpenLayers.Map实例,它将在为其准备的div元素中渲染,位于左侧:

var map_a = new OpenLayers.Map("ch01_base_nonbase_map_a");

接下来,我们已经创建了两个图层并将它们添加到地图中。将第二个图层变为非基图层的魔法来自于构造函数中指定的属性:

var topo = new OpenLayers.Layer.WMS("USA Topo Maps", "http://terraservice.net/ogcmap.ashx", 
{ 
    layers: "DRG" 
}, 
{ 
    opacity: 0.5, 
    isBaseLayer: false 
});

在 OpenLayers 中,所有图层类都是从基类OpenLayers.Layer继承的。这个类定义了所有图层共有的某些属性,例如opacityisBaseLayer

isBaseLayer布尔属性被地图用来知道一个图层是否必须作为基图层或非基图层。

注意

非基图层也称为叠加图层

如您所想象,opacity属性是一个介于 0.0 到 1.0 之间的浮点值,指定了图层的透明度。我们将其设置为50%的透明度,以便允许通过叠加图层查看,即能够看到基图层。

对于右侧地图,我们没有添加任何特定属性的图层。默认情况下,这使得 WMS 图层成为基图层。

如果您展开图层切换器控件,您会看到在左侧地图中您可以显示/隐藏叠加图层,但无法隐藏基图层。相比之下,在右侧地图中,两者都是基图层,它们是互斥的,这意味着一次只能有一个处于活动状态。

更多...

当你玩转图层切换器控制时,会内部调用地图的 setBaseLayer(newBaseLayer) 方法。该方法负责更改地图使用的活动底图。

除了在构造时指定属性外,你还可以使用 setOpacity(opacity)setIsBaseLayer(isBaseLayer) 设置器方法在运行时更改值。

相关内容

  • 避免需要底图 菜谱

  • 管理地图堆叠图层 菜谱

避免需要底图

可能会有这样的情况,你不想有底图,只想处理一些图层。

想象一个在线 GIS 编辑器,用户可以添加和删除图层,但他们没有义务始终显示一个图层。

这个菜谱展示了我们如何轻松地避免在地图内设置底图的要求。

避免需要底图

如何操作...

  1. 和往常一样,创建一个 DOM 元素来渲染地图:

    <div id="ch1_avoid_baselayer" style="width: 100%; 
    height: 100%;"></div>
    
    
  2. 现在创建一个新的 OpenLayers.Map 实例,并将 allOverlays 属性设置为 true

    // Create the map using the specified DOM element 
    var map = new OpenLayers.Map("ch1_avoid_baselayer", { 
        allOverlays: true 
    });
    
    
  3. 添加两个图层以查看结果。同时添加图层切换器控制:

    // Add a WMS layer 
    var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0", 
    { 
        layers: 'basic' 
    }); 
    map.addLayer(wms); 
    // Add a WMS layer 
    var topo = new OpenLayers.Layer.WMS("USA Topo Maps", "http://terraservice.net/ogcmap.ashx", 
    { 
        layers: "DRG" 
    },
    { 
        opacity: 0.5 
    }); 
    map.addLayer(topo); 
    // Add LayerSwitcher control 
    map.addControl(new OpenLayers.Control.LayerSwitcher()); 
    
    
  4. 将地图视图中心定位到某个不错的地方:

    // Set view to zoom maximum map extent 
    // NOTE: This will fail if there is no base layer defined 
    map.setCenter(new OpenLayers.LonLat(-100, 40), 5);
    
    

它是如何工作的...

当地图的属性 allOverlays 设置为 true 时,地图会忽略图层的 isBaseLayer 属性。

如果你展开图层切换器控制,你会看到它包含两个叠加图层,没有底图,你可以显示或隐藏它们,如果需要,可以留一个没有图层的空白地图。

此外,在这个菜谱中,我们使用了 map.setCenter() 方法,它需要一个位置、一个 OpenLayers.LonLat 实例和一个缩放级别才能工作。

还有更多...

当在 allOverlays 模式下工作时,最低层将充当底图,尽管所有图层都将被标记为 isBaseLayer 设置为 false

相关内容

  • 理解底图和非底图图层 菜谱

  • 在地图视图中移动 菜谱

  • 限制地图范围 菜谱

玩转地图的选项

当你创建一个地图来可视化数据时,你需要考虑一些重要的事情:要使用的投影、可用的缩放级别、图层请求将使用的默认瓦片大小等等。

大多数这些重要的事情都包含在所谓的地图属性中,如果你在 allOverlays 模式下工作,你需要特别注意它们。

这个菜谱展示了如何设置一些最常见的地图属性。

准备工作

在继续之前,重要的是要注意,OpenLayers.Map 类的实例可以通过三种方式创建:

  • 指定地图将被渲染的 DOM 元素的标识符:

    var map = new OpenLayers.Map("map_id");
    
    
  • 指定 DOM 元素的标识符,并指定一组选项:

    var map = new OpenLayers.Map("map_id", {some_options_here});
    
    
  • 只指定一组选项。这样我们就可以稍后设置地图将被渲染的 DOM 元素:

    var map = new OpenLayers.Map({some_options_here});
    
    

如何操作...

执行以下步骤:

  1. 创建一个 DOM 元素来渲染地图:

    <div id="ch1_map_options" style="width: 100%; 
    height: 100%;"></div>
    
    
  2. 定义一些地图选项:

    var options = { 
        div: "ch1_map_options", 
        projection: "EPSG:4326", 
        units: "dd", 
        displayProjection: new OpenLayers.Projection("EPSG:900913"),
        numZoomLevels: 7
    };
    
    
  3. 通过传递 options 创建地图:

    var map = new OpenLayers.Map(options);
    
    
  4. 添加 MousePosition 控制以查看鼠标在地图上的位置:

    map.addControl(new OpenLayers.Control.MousePosition());
    
    
  5. 添加 WMS 图层并设置地图视图到某个期望的位置:

    var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0", 
    { 
        layers: 'basic' 
    }); 
    map.addLayer(wms); 
    map.setCenter(new OpenLayers.LonLat(-100, 40), 5);
    
    

它是如何工作的...

在这种情况下,我们使用了五个地图选项来初始化我们的 OpenLayers.Map 实例。

我们使用了 div 选项来传递将要渲染地图的 DOM 元素的标识符:div: "ch1_map_options"

注意

OpenLayers.Map 类为其大多数选项使用一些默认值:projection="EPSG:4326", units="degrees" 等。

projection 选项用于设置地图用于渲染来自图层的投影:projection: "EPSG:4326"。请注意,它必须是一个包含投影代码的字符串。在其他类或选项中,它也可以是一个 OpenLayers.Projection 实例。

地图的投影有一些影响。首先,填充 WMS 图层的瓦片将使用地图的投影请求,如果没有其他投影被图层明确使用。因此,您需要确保 WMS 服务器接受该投影。其次,矢量图层的数据将从每个矢量图层的特定投影转换为地图的投影,因此您需要在创建它们时设置矢量图层的投影选项。

注意

对于除 EPSG:4326EPSG:900913 之外的翻译,您需要在您的网络应用程序中包含 Proj4js 项目 (proj4js.org)。

教学地图投影超出了本书的范围。有关详细信息,请参阅维基百科 (en.wikipedia.org/wiki/Map_projection)。

EPSG 代码是一种命名和分类可用投影集的方法。Spatial Reference (spatialreference.org) 网站是查找更多关于它们的好地方。

units 选项指定地图使用的单位是十进制度:units: "dd"。此选项与某些分辨率选项相关。

displayProjection 选项允许我们指定必须用于显示鼠标位置的投影:displayProjection: new OpenLayers.Projection("EPSG:900913")。在这种情况下,我们的地图位于 EPSG:4326 投影,也称为 WGS84,以度为单位,但我们以 EPSG:900913 显示鼠标位置,也称为 球面墨卡托,它以米为单位。

最后,numZoomLevels 设置用户可以更改的可用缩放级别数量。7 的值意味着用户可以从 缩放级别 0 切换到 缩放级别 6

更多...

来自 Google Maps 或 OpenStreetMap 等来源的图像是特殊情况,其中图像金字塔之前已使用球面墨卡托投影 - EPSG:900913 创建。这意味着在请求瓦片时不能设置投影,因为它隐含的。

如果您将图层放置在除地图使用的投影之外的其他投影中,它将被自动禁用。

参见

  • 理解基本和非基本图层,食谱

  • 管理地图的堆叠层,食谱

  • 管理地图控件,食谱

  • 处理投影,食谱在第八章,超越基础

管理地图的堆叠层

地图是 OpenLayers 的核心概念。它允许我们可视化来自不同类型图层的信息,并为我们提供了管理附加到其上的图层的方法。

在这个食谱中,我们将学习如何控制图层。这很重要,因为添加、删除或重新排序图层是我们几乎在每一个网络地图应用中都需要执行的操作。

管理地图的堆叠层

应用程序将在左侧显示地图,在右侧显示控制面板,并有一些按钮来控制图层。

注意

记住我们使用了 Dojo 工具包框架(dojotoolkit.org)来编写一个更美观、更丰富的应用程序,以展示本书的食谱。

由于这个原因,你可以在 HTML 元素中看到一些奇怪的属性,例如dojoType="dijit.form.Button"onClick="topLayer"。不必担心,在我们这本书中涵盖的 OpenLayers 代码中,这没有任何影响。

如何做到这一点...

  1. 首先,创建一个index.html文件来放置创建应用程序布局所需的代码。我们将其放置在一个表格中。在左侧放置地图:

    <table class="tm"> 
        <tr> 
            <td class="left"> 
                <div id="ch1_managing_layers" 
                    style="width: 100%; height: 500px;">
                </div> 
            </td> 
    
    
  2. 然后,在右侧放置控件:

            <td class="right"> 
                <p>Maximize the layer switcher control to see 
                    the map layers and move it clicking the 
                    buttons:</p> 
    
                <table class="tb"> 
                    <tr> 
                        <td>Select layer:</td> 
                        <td> 
                            <select id="layerSelection" 
                                data-dojo-type=
                                "dijit.form.Select"> 
                                <option value="JPL">
                                    JPL</option> 
                                <option value="WorldMap">
                                    WorldMap</option> 
                                <option value="Canada">
                                    Canada</option> 
                            </select> 
                        </td> 
                    </tr> 
                    <tr> 
                        <td>Move to top:</td> 
                        <td><button dojoType=
                            "dijit.form.Button" onClick=
                            "topLayer">Top</button></td> 
                    </tr> 
                    <tr> 
                        <td>Move up:</td> 
                        <td><button dojoType=
                             "dijit.form.Button" onClick=
                             "raiseLayer">Up</button></td> 
                    </tr> 
                    <tr> 
                        <td>Move down:</td> 
                        <td><button dojoType=
                            "dijit.form.Button" onClick=
                            "lowerLayer">Down</button></td> 
                    </tr> 
                    <tr> 
                        <td>Move to bottom:</td> 
                        <td><button dojoType=
                            "dijit.form.Button" onClick=
                            "bottomLayer">Bottom</button>
                        </td> 
                    </tr> 
                </table> 
    
            </td> 
        </tr> 
    </table>
    
    
  3. 创建一个在allOverlays模式下工作的OpenLayers.Map实例:

    var map = new OpenLayers.Map("ch1_managing_layers", {
        allOverlays: true
    });
    
    
  4. 向地图添加一些图层:

    var jpl = new OpenLayers.Layer.WMS("JPL", 
        [
        "http://t1.hypercube.telascience.org/tiles?",
        "http://t2.hypercube.telascience.org/tiles?",
        "http://t3.hypercube.telascience.org/tiles?",
        "http://t4.hypercube.telascience.org/tiles?"
        ], 
        {
            layers: 'landsat7'
        });
    var worldmap = new OpenLayers.Layer.WMS("WorldMap", 
        "http://vmap0.tiles.osgeo.org/wms/vmap0", 
    {
        layers: 'basic', 
        format: 'image/png' 
    },
    {
        opacity: 0.5
    });
    var canada = new OpenLayers.Layer.WMS("Canada", 
        "http://www2.dmsolutions.ca/cgi-bin/mswms_gmap",
    {
        layers: "bathymetry,land_fn,park",
        transparent: "true", 
        format: "image/png" 
    },
    {
        opacity: 0.5
    });
    map.addLayers([jpl, worldmap, canada]);
    
    
  5. 添加一个图层切换控件(以显示图层)并居中地图视图:

    map.addControl(new OpenLayers.Control.LayerSwitcher({
        ascending: false
    }));
    map.setCenter(new OpenLayers.LonLat(-100, 40), 4);
    
    
  6. 最后,添加当前四个按钮被点击时将触发的 JavaScript 代码:

    function raiseLayer() {
        var layerName = 
            dijit.byId('layerSelection').get('value');
        var layer = map.getLayersByName(layerName)[0];
        map.raiseLayer(layer, 1);
    }
    function lowerLayer() {
        var layerName = 
            dijit.byId('layerSelection').get('value');
        var layer = map.getLayersByName(layerName)[0];
        map.raiseLayer(layer, -1);
    }
    function topLayer() {
        var layerName = 
            dijit.byId('layerSelection').get('value');
        var layer = map.getLayersByName(layerName)[0];
        var lastIndex = map.getNumLayers()-1;
        map.setLayerIndex(layer, lastIndex);
    }
    function bottomLayer() {
        var layerName = 
            dijit.byId('layerSelection').get('value');
        var layer = map.getLayersByName(layerName)[0];
        map.setLayerIndex(layer, 0);
    }
    
    

它是如何工作的...

关于布局的 HTML 代码没有太多可说的。我们使用了一个表格将地图放在左侧,将按钮组放在右侧。此外,我们还为按钮关联了动作,当它们被点击时将执行这些动作。

关于 OpenLayers 代码,我们创建了一个在allOverlays模式下工作的地图实例。这将使我们能够移动任何图层而不用担心基础图层:

var map = new OpenLayers.Map("ch1_managing_layers", {
    allOverlays: true
});

之后,我们创建了三个 WMS 图层并将它们添加到地图中。对于其中一些,我们将opacity属性设置为50%以便可以看到它们:

map.addLayers([jpl, worldmap, canada]);

非常重要的是要注意,我们在 HTML 选择元素的选项值属性中使用了与图层相同的名称。这将在以后允许我们通过名称选择地图的图层。

接下来,我们通过将ascending属性设置为false添加了一个OpenLayers.Control.LayerSwitcher控件:

map.addControl(new OpenLayers.Control.LayerSwitcher({
    ascending: false
}));

你可以将地图想象为存储在堆栈中的图层,并且它们从底部到顶部渲染,因此上面的图层可以隐藏在下面的图层下面,这取决于其不透明度和范围。

小贴士

默认情况下,ascending属性为true,层切换控件以相反的顺序显示地图的层,即控制中首先绘制底层,最后绘制顶层。你可以通过将ascending设置为false来避免这种情况。

最后,我们需要查看的是负责按钮操作的代码,这是这个配方中最有趣的代码。

让我们看看raiseLayer()操作(它与lowerLayer()操作非常相似):

function raiseLayer() {
    var layerName = dijit.byId('layerSelection').get('value');
    var layer = map.getLayersByName(layerName)[0];
    map.raiseLayer(layer, 1);
}

首先,我们在选择元素中获取当前选中层的名称(如果你不完全理解这一行,不要担心,这更多与 Dojo 框架相关,而不是 OpenLayers)。

然后,我们使用map.getLayersByName()方法,它返回一个包含所有具有指定名称的层的数组。因此,我们得到数组的第一个元素。

现在我们有了层实例的引用。我们可以使用map.raiseLayer()方法在地图中提升它。你可以通过指定一个delta数字来提升一个或多个位置,或者,就像在lowerLayer()函数中一样,你可以通过指定一个负值来降低一个或多个位置。

内部OpenLayers.Map将层存储在数组(layers属性)中,并且它们按照在数组中存储的顺序渲染(因此第一个元素是底层)。

topLayer()bottomLayer()操作也类似,它们将指定的层移动到堆栈的顶部或底部。它们都使用map.setLayerIndex()方法,该方法负责将层移动到指定的位置。

注意

map.setLayerIndex()方法被map.raiseLayer()内部用于移动层。

因为底层对应于层数组中的第一层,所以bottomLayer()操作是最容易实现的,因为我们只需要将层移动到第一个位置:

function bottomLayer() {
    var layerName = dijit.byId('layerSelection').get('value');
    var layer = map.getLayersByName(layerName)[0];
    map.setLayerIndex(layer, 0);
}

对于topLayer()操作,我们需要将层移动到最后的位置。为此,我们可以从map.getNumLayers()方法获得帮助,它返回地图中层的总数。这样,如果我们有四个层在地图中,最后一个对应于索引3(因为索引值从0变为3)。

function topLayer() {
    var layerName = dijit.byId('layerSelection').get('value');
    var layer = map.getLayersByName(layerName)[0];
    var lastIndex = map.getNumLayers()-1;
    map.setLayerIndex(layer, lastIndex);
}

还有更多...

OpenLayers.Map类有许多方法可以操作包含的层。我们在这些配方中看到了一些,例如添加层、通过名称获取层、在堆栈中上下移动等。但你还可以找到更多方法来删除层、通过位置获取层等。

参见

  • 管理地图控件的配方

  • 在地图视图中移动的配方

  • 限制地图范围的配方

管理地图控件

OpenLayers 附带了许多与地图交互的控件:平移、缩放、显示概览图、编辑要素等。

与层类似,OpenLayers.Map类有管理附加到地图上的控件的方法。

管理地图控件

如何做到这一点...

按照给定的步骤进行:

  1. 创建一个新的 HTML 文件并添加 OpenLayers 依赖项。

  2. 现在,添加必要的代码来创建按钮和div元素以容纳地图实例:

    <div class="sample_menu" dojoType="dijit.MenuBar">
        <span class="title">Controls: </span>
        <div dojoType="dijit.form.ToggleButton" 
            iconClass="dijitCheckBoxIcon" 
            onChange="updateMousePosition">MousePosition
        </div>
        <div dojoType="dijit.form.ToggleButton" 
            iconClass="dijitCheckBoxIcon" 
            onChange="updatePanPanel">PanPanel</div>
        <div dojoType="dijit.form.ToggleButton" 
            iconClass="dijitCheckBoxIcon" 
            onChange="updateZoomPanel">ZoomPanel</div>
    </div>
    <!-- Map DOM element -->
    <div id="ch1_managing_controls" style="width: 100%; height: 500px;"></div>
    
    
  3. script元素部分,创建地图实例:

    var map = new OpenLayers.Map("ch1_managing_controls", {
        controls: [
        new OpenLayers.Control.Navigation()
        ]
    });
    
    
  4. 向地图添加一些图层并居中视图:

    var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0",
    {
        layers: 'basic'
    },
    {
        wrapDateLine: false
    });
    map.addLayer(wms);
    // Center the view
    map.setCenter(OpenLayers.LonLat.fromString("0,0"),3);
    
    
  5. 最后,添加与按钮关联的动作代码:

    function updateMousePosition(checked) {
        if(checked) {
            map.addControl(new 
                OpenLayers.Control.MousePosition());
        } else {
            var controls = 
                map.getControlsByClass
                ("OpenLayers.Control.MousePosition");
            console.log(controls);
            map.removeControl(controls[0]);
        }
    }
    function updatePanPanel(checked) {
        if(checked) {
            map.addControl(new 
                OpenLayers.Control.PanPanel());
        } else {
            var controls = map.getControlsByClass
                ("OpenLayers.Control.PanPanel");
            map.removeControl(controls[0]);
        }
    }
    function updateZoomPanel(checked) {
        if(checked) {
            // Place Zoom control at specified pixel
            map.addControl(new OpenLayers.Control.ZoomPanel()
                , new OpenLayers.Pixel(50,10));
        } else {
            var controls = map.getControlsByClass
                ("OpenLayers.Control.ZoomPanel");
            map.removeControl(controls[0]);
        }
    }
    
    

它是如何工作的...

每个按钮动作函数都会检查切换按钮是否被选中或取消选中,并根据值将控制添加到地图或从地图中删除控制:

if(checked) {
    // Place Zoom control at specified pixel
    map.addControl(new OpenLayers.Control.ZoomPanel(), new OpenLayers.Pixel(50,10));
} else {
    var controls = map.getControlsByClass("OpenLayers.Control.ZoomPanel");
    map.removeControl(controls[0]);
}

通过map.addControl()方法添加控制相对简单,该方法接受一个控制实例(可选地,一个OpenLayers.Pixel实例),并将控制添加到地图的指定位置。

注意

通常,控制位置是通过修改控制所使用的 CSS 类中的topleft值来控制的。如果你使用一个OpenLayers.Pixel值来定位控制,那么该值将覆盖 CSS 中的值。

要删除一个控制,我们需要有一个要删除的实例的引用。map.getControlsByClass()方法返回一个指定类的控制数组,帮助我们获取所需控制的引用。接下来,我们可以使用map.removeControl()方法来删除它。

更多...

注意,在这个菜谱中,我们通过传递一个以不同方式创建的OpenLayers.LonLat实例来居中地图的视图。我们不是使用new操作符,而是使用了OpenLayers.LonLat.fromString方法,它从一个字符串创建了一个新的实例:

map.setCenter(OpenLayers.LonLat.fromString("0,0"),3);

此外,在这个菜谱中创建的地图实例仅初始化了一个控制,即OpenLayers.Control.Navigation(),这允许我们使用鼠标导航地图:

var map = new OpenLayers.Map("ch1_managing_controls", {
    controls: [
    new OpenLayers.Control.Navigation()
    ]
});

注意

将空数组传递给controls属性将创建一个没有任何控制与之关联的地图实例。此外,如果没有指定controls属性,OpenLayers 将为地图创建一组默认控制,包括OpenLayers.Control.NavigationOpenLayers.Control.PanZoom控制。

参见

  • 管理地图堆叠层菜谱

  • 在地图视图中移动菜谱

在地图视图中移动

除非你想创建一个完全静态的地图,没有用户平移或缩放所需的控制,否则你希望用户能够导航和探索地图。

有可能存在控制不够用的情况。想象一个用户可以搜索如珠穆朗玛峰的 Web 应用程序,应用程序必须找到其位置并飞到那里。在这种情况下,你需要通过代码导航,而不是使用控制。

这个菜谱展示了OpenLayers.Map类的一些方法,这些方法将允许你提高用户体验。

在地图视图中移动

应用布局包含三个主要部分。在顶部有一个标签来显示当前地图中心位置和缩放级别。当地图移动或缩放改变时,它会自动更新。

地图位于中心,右侧有一堆控件来设置和测试与视图交互的主要地图方法。

正如你所看到的,地图上没有附加控件,所以与地图交互的唯一方式是通过右侧的控件。

注意

我们省略了创建应用程序布局所需的 HTML 代码,所以如果你对 HTML 代码感兴趣,可以查看 Packt Publishing 网站上的源代码。

如何操作...

  1. 创建一个包含 OpenLayers 依赖项的 HTML 文件。

    注意

    上一个截图中的按钮和布局的 HTML 代码非常长,并且与本书的目标无关,所以这里我们避免编写它。

  2. 添加一个div元素来包含地图实例:

    <div id="ch1_moving_around" style="width: 100%; 
        height: 500px;"></div>
    
    
  3. 创建一个地图实例。这次我们指定一个监听器,用于更新地图顶部标签的中心点和缩放值的一些事件:

    var map = new OpenLayers.Map("ch1_moving_around", {
        controls: [],
        eventListeners: {
            "move": changeListener,
            "moveend": changeListener,
            "zoomend": changeListener
        }
    });
    function changeListener() {
        var center = map.getCenter();
        document.getElementById("center").innerHTML = 
            "("+center.lon + " lon , " + center.lat + 
            " lat)";
        var zoom = map.getZoom();
        document.getElementById("zoom").innerHTML = zoom + 
        " level";
    }
    
    
  4. 添加一个图层并定位视图:

    var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0",
    {
        layers: 'basic'
    });
    map.addLayer(wms);
    map.setCenter(new OpenLayers.LonLat(0, 0), 2);
    
    
  5. 插入将由按钮动作执行的代码:

    function moveByPx() {
        var x = dijit.byId('movebyxpix').get('value');
        var y = dijit.byId('movebyypix').get('value');
    
        map.moveByPx(x,y);
    }
    function moveTo() {
        var lon = dijit.byId('movetolon').get('value');
        var lat = dijit.byId('movetolat').get('value');
        var zoom = dijit.byId('movetozoom').get('value');
        var force = dijit.byId
            ('movetoforceZoomChange').get('checked');
        var drag = dijit.byId
            ('movetodragging').get('checked');
    
        map.moveTo(new OpenLayers.LonLat(lon, lat), zoom, {
            forceZoomChange: force,
            dragging: drag
        });
    }
    function setCenter() {
        var lon = dijit.byId('setCenterlon').get('value');
        var lat = dijit.byId('setCenterlat').get('value');
        var zoom = dijit.byId('setCenterzoom').get('value');
        var force = dijit.byId
            ('setCenterforceZoomChange').get('checked');
        var drag = dijit.byId
            ('setCenterdragging').get('checked');
    
        map.setCenter(new OpenLayers.LonLat(lon, lat), 
        zoom, {
            forceZoomChange: force,
            dragging: drag
        });
    }
    function pan() {
        var x = dijit.byId('panxpix').get('value');
        var y = dijit.byId('panypix').get('value');
        var anim = dijit.byId('pananimate').get('checked');
        var drag = dijit.byId('pandragging').get('checked');
    
        map.pan(x,y, {
            animate: anim,
            dragging: drag
        });
    }
    function panTo() {
        var lon = dijit.byId('panTolon').get('value');
        var lat = dijit.byId('panTolat').get('value');
    
        map.panTo(new OpenLayers.LonLat(lon, lat));
    }
    
    

它是如何工作的...

为了更新顶部中心点和缩放级别值,我们实例化了Map对象,并附加了一些事件监听器。实际上,相同的监听器函数附加到了所有三个事件上:

var map = new OpenLayers.Map("ch1_moving_around", {
    controls: [],
    eventListeners: {
        "move": changeListener,
        "moveend": changeListener,
        "zoomend": changeListener
    }
});

changeListener()函数中,我们使用map.getCenter(),它返回一个OpenLayers.LonLat对象,以及map.getZoom()来获取当前值并更新顶部的左上角标签。

function changeListener() {
    var center = map.getCenter();
    document.getElementById("center").innerHTML = 
        "("+center.lon + " lon , " + center.lat + " lat)";
    var zoom = map.getZoom();
    document.getElementById("zoom").innerHTML = zoom + " level";
}

对于每个按钮,都会执行一个动作。它们负责获取所需的值并调用一个map方法。

map.moveByPx() 方法通过指定像素值移动地图。注意,它移动地图;它不会平移,所以不要期望有任何效果。

function moveByPx() {
    var x = dijit.byId('movebyxpix').get('value');
    var y = dijit.byId('movebyypix').get('value');

    map.moveByPx(x,y);
}

map.moveTo()方法与上一个类似,但它将视图移动到指定的位置(而不是增量)并使用OpenLayers.LonLat实例指定。

map.setCenter()方法与map.moveTo()类似,但它将视图中心定位在指定的位置。

最后,有两个与平移相关的动作,它们可以产生平滑的移动效果。map.pan()方法通过指定像素增量移动视图。map.panTo()方法做类似的事情,它将视图移动到指定的位置。

参见

  • 管理地图的堆叠图层 菜谱

  • 限制地图范围 菜谱

限制地图范围

经常会有这样的情况,你希望向用户展示数据,但只限于特定的区域,这些区域与你的可用数据相对应(一个国家、一个地区、一个城市等等)。

在这种情况下,允许用户探索整个世界没有意义,因此你需要限制用户可以导航的范围。

在这个菜谱中,我们介绍了一些限制用户可探索区域的方法。

限制地图范围

如何操作...

  1. 创建一个地图实例。看看构造函数中使用的几个属性:

    var map = new OpenLayers.Map("ch1_restricting_view", {
        maxExtent: OpenLayers.Bounds.fromString
            ("-180,-90,180,90"),
        restrictedExtent: OpenLayers.Bounds.fromString
            ("-180,-90,180,90")
    });
    
    
  2. 和往常一样,添加一些图层来查看内容并定位视图:

    var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0",
    {
        layers: 'basic'
    });
    map.addLayer(wms);
    map.setCenter(new OpenLayers.LonLat(0, 0), 2);
    
    
  3. 添加当按钮被点击时将执行的函数:

    function updateMaxExtent() {
        var left = dijit.byId('left_me').get('value');
        var bottom = dijit.byId('bottom_me').get('value');
        var right = dijit.byId('rigth_me').get('value');
        var top = dijit.byId('top_me').get('value');      
        map.setOptions({
            maxExtent: new OpenLayers.Bounds(left, bottom, 
            right, top)
        });
    }
    function updateRestrictedExtent() {
        var left = dijit.byId('left_re').get('value');
        var bottom = dijit.byId('bottom_re').get('value');
        var right = dijit.byId('rigth_re').get('value');
        var top = dijit.byId('top_re').get('value');
        map.setOptions({
            restrictedExtent: new OpenLayers.Bounds(left, 
            bottom, right, top)
        });
    }
    
    

它是如何工作的...

正如你所见,地图是通过使用两个属性maxExtentrestrictedExtent实例化的,这两个属性负责限制我们可以探索的地图区域。

虽然相似,但这两个属性有不同的含义。设置maxExtent属性会限制视口,使其中心不能超出指定的边界。通过设置restrictedExtent属性,地图本身不能超出给定的边界。

当按钮被点击时,响应的函数会从输入字段获取值,并通过map.setOptions()方法应用新值:

map.setOptions({
    maxExtent: new OpenLayers.Bounds(left, bottom, right, top)
});

我们可以将创建新的OpenLayers.Map实例时使用的相同属性传递给map.setOptions()方法,它将负责更新它们。

还有更多...

限制地图范围并不是限制我们向用户展示信息的唯一方法。图层也有类似的属性来过滤或限制它们必须渲染的信息。

参见

  • 在地图视图中移动菜谱

第二章:添加栅格图层

在本章中,我们将介绍以下内容:

  • 使用 Google Maps 图像

  • 使用 Bing 图像

  • 添加 WMS 图层

  • 包装日期线选项

  • 改变缩放效果

  • 改变图层不透明度

  • 使用单瓦片模式的 WMS

  • 缓存图层数据以改善地图导航

  • 创建图像图层

  • 在 WMS 图层中设置瓦片大小

简介

本章主要介绍如何处理栅格图层。我们试图通过一系列的食谱,总结出在日常工作使用 OpenLayers 时最常见和重要的用例。

图像是在 GIS 系统中处理的最重要类型的数据之一。

OpenLayers 提供了几个类来与不同的图像提供商集成,从专有提供商如 Google Maps 和 Bing Maps,到开源的如 OpenStreetMap,甚至是任何 WMS 服务提供商。

任何图层类型的基类是 OpenLayers.Layer 类,它提供了一套通用属性并定义了其他任何类的通用行为。

此外,许多图层继承自 OpenLayers.Layer.Grid 类,它将图层划分为不同的缩放级别。这样,每个缩放级别覆盖相同的区域,但使用更大的瓦片集。例如,在零级,一个瓦片覆盖整个世界;在一级,四个瓦片覆盖整个世界,以此类推。正如我们所看到的,在每个级别上,瓦片的数量和它们的分辨率都会增加。

本章介绍了如何使用栅格图层,特别关注 WMS 图层,以及如何管理最常用的属性。

使用 Google Maps 图像

Google Maps 可能是全球最知名的网页地图应用。他们的瓦片层图像方式为人们所熟知;他们习惯了其图层风格,因此你可能对在自己的网络地图项目中使用它们感兴趣。

使用 Google Maps 图像

OpenLayers 使用 OpenLayers.Layer.Google 类,实际上是在 Google Maps API 上的包装代码,允许我们在 OpenLayers API 中以统一的方式使用 Google Maps 瓦片。

注意

不要混淆 Google Maps API 和 Google Maps 图像。Google Maps API 是一组 JavaScript 代码,可免费使用,而访问 Google Maps 图像则有一些使用限制,并且根据访问次数,可能需要支付一定的费用。

如何做到...

要使用 Google Maps 图像,请执行以下步骤:

  1. 创建一个 HTML 文件并添加 OpenLayers 依赖项。

  2. 按如下方式包含 Google Maps API:

    <script type="text/javascript" src="img/js?v=3.5&sensor=false"></script>
    
    
  3. 添加一个 div 元素来包含地图,如下所示:

    <!-- Map DOM element -->
    <div id="ch2_google" style="width: 100%; height: 100%;"></div>
    
    
  4. script 元素内,添加创建地图实例并添加图层切换控件的相关代码,如下所示:

    <!-- The magic comes here --> 
    <script type="text/javascript"> 
    
        // Create the map using the specified DOM element 
        var map = new OpenLayers.Map("ch2_google"); 
        map.addControl(new OpenLayers.Control.LayerSwitcher()); 
    
    
  5. 创建一些基于 Google 的地图,并添加如下:

        var streets = new OpenLayers.Layer.Google("Google Streets", { 
            numZoomLevels: 20 
        }); 
        var physical = new OpenLayers.Layer.Google("Google Physical", { 
            type: google.maps.MapTypeId.TERRAIN 
        }); 
        var hybrid = new OpenLayers.Layer.Google("Google Hybrid", { 
            type: google.maps.MapTypeId.HYBRID, numZoomLevels: 20 
        }); 
        var satellite = new OpenLayers.Layer.Google("Google Satellite", { 
            type: google.maps.MapTypeId.SATELLITE, numZoomLevels: 22 
        }); 
        map.addLayers([physical, streets, hybrid, satellite]); 
    
    
  6. 最后,将地图中心定位在所需位置,如下所示:

        map.setCenter(new OpenLayers.LonLat(0, 0), 2); 
    </script>
    
    

它是如何工作的...

如你所见,代码有三个主要部分。首先我们放置了一个div元素,它将被用作地图,如下所示:

<div id="ch2_google" style="width: 100%; height: 100%;"></div>

接下来,我们包含了 Google Maps API 代码,如下所示:

<script type="text/javascript" src="img/js?v=3.5&sensor=false"></script>

注意

记住,OpenLayers 仅仅是一个包装器,因此我们需要在我们的应用程序中使用真实的 Google Maps API 代码。

<script type="text/javascript"> </script>元素中,我们添加了初始化地图和添加图层切换控件所需的代码,如下所示:

    var map = new OpenLayers.Map("ch2_google"); 
    map.addControl(new OpenLayers.Control.LayerSwitcher()); 

最后,我们添加了一些知名的 Google Maps 图层,并居中地图的视口,如下所示:

    var streets = new OpenLayers.Layer.Google("Google Streets", { 
        numZoomLevels: 20 
    }); 
    var physical = new OpenLayers.Layer.Google("Google Physical", { 
        type: google.maps.MapTypeId.TERRAIN 
    }); 
    var hybrid = new OpenLayers.Layer.Google("Google Hybrid", { 
        type: google.maps.MapTypeId.HYBRID, numZoomLevels: 20 
    }); 
    var satellite = new OpenLayers.Layer.Google("Google Satellite", { 
        type: google.maps.MapTypeId.SATELLITE, numZoomLevels: 22 
    }); 
    map.addLayers([physical, streets, hybrid, satellite]); 
    map.setCenter(new OpenLayers.LonLat(0, 0), 2); 

图层的类型由 Google Maps API 类google.maps.MapTypeId定义,你可以在code.google.com/apis/maps/documentation/javascript/reference.html#MapTypeId找到。

注意

注意,我们正在使用两个 API,OpenLayers 和 Google Maps API,因此查看 Google Maps API 以更好地了解其功能会是个不错的选择。

文档可以在developers.google.com/maps/documentation/javascript/tutorial找到。

还有更多...

在这个配方中,我们展示了如何使用 Google Maps API 版本 3 将 Google 影像添加到你的 OpenLayers 项目中。

对于之前的版本 2,Google 要求你注册为用户并获取一个 API 密钥,你需要使用这个密钥来初始化OpenLayers.Layer.Google实例。这个密钥随后在每次瓦片请求中都会使用,以识别你,这样 Google 就能了解你的使用情况。

正如你所见,版本 3 在 OpenLayers 中使用起来要简单得多。

参见

  • 添加 WMS 图层配方

  • 使用 Bing 影像配方

  • 在第一章的“理解基本和非基本图层”配方中,网络制图基础

使用 Bing 影像

Bing Maps,之前被称为 Virtual Earth,是由微软提供的地图服务。

与 Google Maps 一样,OpenLayers 提供了一个OpenLayers.Layer.Bing类,这为我们带来了在项目中添加 Bing 影像的可能性。

使用 Bing 影像

准备工作

Bing Maps 要求你注册为消费者用户。一旦注册,你将获得一个 API 密钥,用于初始化OpenLayers.Layer.Bing图层,并且这个密钥将在每次请求中使用以验证你的 Bing Maps 服务身份。

与 Google Maps 相反,Bing 不需要任何 JavaScript 代码,OpenLayers.Layer.Bing类也不作为包装器。Bing Maps 提供了一个 REST 服务,可以直接使用你的 API 密钥访问瓦片。

注意

你可以在msdn.microsoft.com/en-us/library/ff428642.aspx了解如何注册为用户。

此外,你还可以在msdn.microsoft.com/en-us/library/ff701713.aspx了解 Bing Maps REST 服务。

到目前为止,假设您有一个 API 密钥,将在下一部分代码中使用。

如何做到这一点...

在本节中,我们将看到如何使用 Bing 影像。要使用 Bing 影像,请执行以下步骤:

  1. 创建一个 HTML 文件并添加 OpenLayers 依赖项。

  2. 添加一个DOM元素来放置地图,如下所示:

    <div id="ch2_bing" style="width: 100%; height: 100%;"></div>
    
    
  3. 在一个script元素中创建地图实例并添加一个图层切换器控件,如下所示:

    <script type="text/javascript"> 
        // Create the map using the specified DOM element 
        var map = new OpenLayers.Map("ch2_bing"); 
        map.addControl(new OpenLayers.Control.LayerSwitcher()); 
    
    
  4. 创建一些 Bing 图层,添加到地图中并居中地图的视口,如下所示:

        var bingApiKey = "your_bing_API_must_be_put_here"; 
        var road = new OpenLayers.Layer.Bing({ 
            name: "Road", 
            type: "Road", 
            key: bingApiKey 
        }); 
        var hybrid = new OpenLayers.Layer.Bing({ 
            name: "Hybrid",        
            type: "AerialWithLabels", 
            key: bingApiKey 
        }); 
        var aerial = new OpenLayers.Layer.Bing({ 
            name: "Aerial", 
            type: "Aerial", 
            key: bingApiKey 
        }); 
        map.addLayers([road, hybrid, aerial]); 
    
        map.setCenter(new OpenLayers.LonLat(0, 0), 2); 
    </script>
    
    

它是如何工作的...

在这个食谱中,我们需要考虑的主要点是我们在使用 Microsoft 服务。我们使用我们的 API 密钥请求一个 URL 并获取一个瓦片。因此,每个 Bing 图层在实例化时都必须包含一个key参数,如下所示:

var bingApiKey = "your_bing_API_must_be_put_here"; 
var road = new OpenLayers.Layer.Bing({ 
    name: "Road", 
    type: "Road", 
    key: bingApiKey 
}); 

我们知道name参数在所有图层中都是通用的。name参数用于为图层添加一个描述性名称,它将被切换器控件使用。

如前所述,key参数在每次瓦片请求中使用,并标识我们为注册的 Bing 消费者用户。

type参数是必要的,用于指定我们想要从 Bing Maps 获取的瓦片类型。Bing 提供了道路、航空或带有标签的航空等多种类型。

注意

您可以在msdn.microsoft.com/en-us/library/ff701716.aspx找到有关 Bing Maps 图层类型的更多信息。

参见

  • 使用 Google Maps 影像的食谱

  • 添加 WMS 图层的食谱

  • 在第一章的理解基本图层和非基本图层食谱中,网络制图基础

添加 WMS 图层

Web Map Service (WMS) 是由开放地理空间联盟 (OGC) 开发的一个标准,由许多地理空间服务器实现,其中我们可以找到免费和开源项目 GeoServer (geoserver.org) 和 MapServer (mapserver.org)。有关 WMS 的更多信息,可以在en.wikipedia.org/wiki/Web_Map_Service找到。

作为一个非常基本的总结,您可以将 WMS 服务器理解为一个正常的 HTTP 网络服务器,它接受带有一些 GIS 相关参数(如投影、边界框等)的请求,并返回类似于以下截图的地图。

添加 WMS 图层

注意

我们将使用远程 WMS 服务器,因此您不需要安装一个。作为建议,请注意,我们不对这些服务器负责,它们可能存在问题,或者您阅读本节时可能不可用。

可以使用任何其他 WMS 服务器,但必须知道 URL 和图层名称。

如何做到这一点...

要添加 WMS 图层,请执行以下步骤:

  1. 创建一个 HTML 文件并添加 OpenLayers 依赖项。

  2. 添加一个div元素来容纳地图,如下所示:

    <div id="ch2_wms_layer" style="width: 100%; height: 100%;"></div>
    
    
  3. 按以下方式创建地图实例:

    // Create the map using the specified DOM element
    var map = new OpenLayers.Map("ch2_wms_layer");
    
    
  4. 现在,添加两个 WMS 图层。第一个将是基本图层,第二个将是叠加图层,如下所示:

    // Add a WMS layer 
    var wms = new OpenLayers.Layer.WMS("Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0", 
    { 
        layers: 'basic' 
    }); 
    
    // Add Nexrad WMS layer 
    var nexrad = new OpenLayers.Layer.WMS("Nexrad", "http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi", 
    { 
        layers: "nexrad-n0r", 
        transparent: "true", 
        format: 'image/png' 
    }, 
    { 
        isBaseLayer: false 
    }); 
    
    map.addLayers([wms, nexrad]); 
    
    
  5. 最后,我们添加了一个图层切换控件并居中视图,如下所示:

    // Add layer switcher control 
    map.addControl(new OpenLayers.Control.LayerSwitcher()); 
    // Set the center of the view 
    map.setCenter(new OpenLayers.LonLat(-90,40), 4);
    
    

它是如何工作的...

OpenLayers.Layer.WMS类构造函数需要四个参数来实例化(实际上第四个是可选的),它们是:

new OpenLayers.Layer.WMS(name, url, params, options)

参数如下:

  • name对所有层都是通用的,并用作用户友好的描述

  • url是一个字符串,必须指向 WMS 服务器

  • params参数是一个对象,可以包含 WMS 请求中使用的任何参数:层、格式、样式等

注意

检查 WMS 标准以了解你可以在params中使用哪些参数。

层的使用是强制性的,所以你总是需要指定这个值。此外,如果你使用 SRS WMS 请求参数,请注意它总是被忽略,因为它来自基础层的投影或地图的投影。

  • options参数是一个可选的对象,它包含用于图层对象的特定属性:opacityisBaseLayer

在这个配方中,我们添加了一个基础层,如下所示:

var wms = new OpenLayers.Layer.WMS("Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0", 
{ 
    layers: 'basic' 
}); 

它使用了nameurlparams参数,表明basic是唯一需要请求的图层。

之后,我们在爱荷华州立大学的服务器上添加了一个带有 NEXRAD(en.wikipedia.org/wiki/NEXRAD)气象雷达信息的第二层叠加(你可以在mesonet.agron.iastate.edu/ogc)找到更多信息),如下所示:

var nexrad = new OpenLayers.Layer.WMS("Nexrad", "http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi", 
{ 
    layers: "nexrad-n0r", 
    transparent: "true", 
    format: 'image/png' 
}, 
{ 
    isBaseLayer: false 
}); 

在这种情况下,除了layers参数外,我们还使用了transparentformat参数。

format参数用于 WMS 请求中,以指定我们希望以什么图像格式接收图像。

transparent属性设置为true。如果没有设置,我们会得到带有一些彩色雷达数据的白色瓦片,这将隐藏底层。使用transparent: "false"进行测试。

对于这个图层,我们还设置了图层参数isBaseLayerfalse,以表示我们希望它作为叠加层使用。

还有更多...

WMS 服务器无论我们请求的边界框中是否有信息都会返回图像。

之前提到的 Nexrad WMS 层,显示带有白色背景的瓦片图像并不理想,因此我们使用了transparent参数来解决这个问题。

当你将transparent参数设置为true时,无论你指定什么格式,WMS 类都会确保使用format image/pngimage/gif进行请求,以保证那些没有数据的像素的透明度。

最后,请记住我们可以在请求中传递任何由 WMS 标准定义的参数,只需在params参数中指定它们即可。

参见

  • 使用谷歌地图影像配方

  • 使用单瓦片模式的 WMS配方

  • 更改图层不透明度配方

  • 缓冲层数据以改善地图导航配方

包装日期线选项

可能会有这样的情况,你不希望你的地图在-180 或+180 经度度结束,因为你正在该区域工作,需要一个连续的地图。例如,想象一个地图,在左边你可以看到俄罗斯的尽头,在右边是阿拉斯加,如下面的截图所示:

包裹日期线选项

这个属性是基类OpenLayers.Layer的一个常见属性,称为wrapDateLine

如何做到这一点...

要包裹日期线选项,执行以下步骤:

  1. 创建一个 HTML 文件并添加 OpenLayers 依赖项。

  2. 在开始时,我们放置了一个复选框来激活/停用包裹数据线功能,如下所示:

    注意

    不要担心dojoType="dijit.form.CheckBox"属性,因为它是因为在示例中使用了 Dojo Toolkit (dojotoolkit.org)。

    想象它就像一个普通的 HTML 输入元素。

  3. 接下来,我们添加了用于渲染地图的DOM元素,如下所示:

    <div id="ch2_wrapdataline" style="width: 100%; height: 100%;"></div>
    
    
  4. script元素内创建地图实例,如下所示:

    <script type="text/javascript">
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch2_wrapdataline");
    
    
  5. 现在,创建一个指定wrapDateLine属性的 WMS 图层,如下所示:

        // Add a WMS layer
        var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://labs.metacarta.com/wms/vmap0",
        {
            layers: 'basic'
        },
        {
            wrapDateLine: true
        });
        map.addLayer(wms);   
    
        // Center map view
        map.setCenter(new OpenLayers.LonLat(-110,0), 2);
    
    
  6. 最后,实现一个将更改wrapDateLine属性值的函数,如下所示:

        function wrapDateLine(checked) {
            wms.wrapDateLine = checked;
            wms.redraw();
        }
    </script>
    
    

它是如何工作的...

这个菜谱的所有魔法都藏在OpenLayers.Layer类中的wrapDateLine属性里。你需要将其设置为true,以便在它们的经度轴上包裹并创建一个连续的图层。

此外,我们创建了一个函数,它会根据复选框的变化来激活/停用wrapDateLine属性,如下面的代码所示:

    function wrapDateLine(checked) {
        wms.wrapDateLine = checked;
        wms.redraw();
    }

注意,在更改属性值后,我们需要重新绘制图层,以便它生效。这是通过从OpenLayers.Layer基类继承的redraw()方法完成的。

wrapDateLine属性不是地图的属性,而是每个图层的属性;所以如果你想整个地图都有相同的行为,你需要在所有图层中将其设置为true

参见

  • 添加 WMS 图层菜谱

  • 使用单瓦片模式的 WMS菜谱

改变缩放效果

平移和缩放效果是与用户导航体验非常相关的关键动作。

在第一章,网络制图基础中,菜谱在地图视图中移动展示了如何控制并创建地图平移的方式。

以同样的方式,你可以控制图层之间两个缩放级别之间的过渡效果。

OpenLayers.Layer类有一个transitionEffect属性,它决定了在缩放级别改变时应用于图层的效果。目前只允许两个值:nullresize

null值表示不会应用过渡效果,因为当你更改缩放级别时,你可能会看到图层消失,直到新缩放级别的瓦片加载完成。

当我们放大到某个级别时,resize值会使当前瓦片调整大小,以适应新的缩放,直到新级别的瓦片在后台加载。这样图像总是可见的,我们避免了看到空白地图几秒钟的丑陋效果。

如何操作...

要更改缩放级别,执行以下步骤:

  1. 创建一个 HTML 文件并包含所需的 OpenLayers 依赖项。

  2. 对于这个菜谱,我们将添加一个复选框按钮,允许我们在单个图层之间切换过渡效果,如下所示:

    Transition effect: <input dojoType="dijit.form.CheckBox" checked onChange="transitionEffect" /> Resize
    
    
  3. 接下来,添加div元素,如下所示,它包含地图:

    <!-- Map DOM element -->
    <div id="ch2_transition_effect" style="width: 100%; height: 100%;"></div>
    
    
  4. 添加初始化地图并创建一个 WMS 图层的 JavaScript 代码,如下所示:

    <!-- The magic comes here -->
    <script type="text/javascript">
    
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch2_transition_effect");
    
        // Add a WMS layer
        var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0",
        {
            layers: 'basic'
        },
        {
            wrapDateLine: true,
            transitionEffect: 'resize'
        });
        map.addLayer(wms);   
    
        // Center map view
        map.setCenter(new OpenLayers.LonLat(0,0), 3);
    
    
  5. 最后,放置一个切换transitionEffect属性值的函数,如下所示:

        function transitionEffect(checked) {
            if(checked) {
                wms.transitionEffect = 'resize';
            } else {
                wms.transitionEffect = null;
            }
        }
    </script>
    
    

它是如何工作的...

如菜谱开头所述,所有魔法都集中在transitionEffect属性上。

由于该属性是针对图层而不是OpenLayers.Map属性,如果你想将相同的效果应用于整个地图,你需要将其设置在其所有包含的图层上。

更多...

一个或多个OpenLayers.Tile.Image 形成一个栅格图层,因此当它被渲染时,绘制瓦片的实际工作由瓦片本身简化。

虽然transitionEffect是在OpenLayers.Layer类(或其子类)中定义的,但每个单独的瓦片负责绘制过渡效果。

如果你计划创建一个新的缩放过渡效果,你还需要查看OpenLayers.Tile.Image的代码。

参见

  • 添加 WMS 图层 菜谱

  • 更改图层不透明度 菜谱

  • 使用单瓦片模式 WMS 菜谱

更改图层不透明度

当你与许多图层(包括栅格和矢量图层)一起工作时,你可能会遇到一个图层在另一个图层之上,从而隐藏了下面的图层的情况。这在没有将transparent属性设置为true的栅格 WMS 图层(如 OpenStreetMaps、Google 和 Bing)或瓦片图层时更为常见。以下截图中的图层不透明度设置为50%

更改图层不透明度

OpenLayers.Layer 基类有一个opacity属性,由具体的子类实现,允许我们修改图层的透明度。它是一个浮点值,范围从0.0(完全透明)到1.0(完全不透明)。

如何操作...

可以更改图层的不透明度。要更改图层的不透明度,请执行以下步骤:

  1. 创建一个 HTML 文件并添加所需的 OpenLayers 依赖项。

    注意

    我们有意省略了用于滑块控制的 HTML 代码。在这里,我们专注于 OpenLayers 的代码。如果你对滑块的代码感兴趣,可以在菜谱的源代码中找到。

  2. 添加一个div元素来包含地图,如下所示:

    <div id="ch2_opacity" style="width: 100%; height: 100%;"></div>
    
    
  3. 接下来,创建一个地图实例并添加两个图层,如下所示:

    // Create the map using the specified DOM element 
    var map = new OpenLayers.Map("ch2_opacity"); 
    
    // Add a WMS layer 
    var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0", 
    { 
        layers: 'basic' 
    }); 
    map.addLayer(wms);  
    
    // Add coast line layer 
    var wms2 = new OpenLayers.Layer.WMS("Coast Line", "http:// vmap0.tiles.osgeo.org/wms/vmap0", 
    { 
        layers: 'coastline_01,coastline_02' 
    }, 
    { 
        isBaseLayer: false 
    }); 
    map.addLayer(wms2); 
    
    
  4. 添加一个图层切换控件并居中地图的视口,如下所示:

    map.addControl(new OpenLayers.Control.LayerSwitcher()); 
    
    // Center map view 
    map.setCenter(new OpenLayers.LonLat(0,0), 3);
    
    
  5. 最后,实现一个函数,该函数接收滑动控制器的更改并更改图层的不透明度,如下所示:

    function opacity(value) { 
        wms2.setOpacity(value/100); 
    }
    
    

它是如何工作的...

正如我们评论的那样,opacity 属性是这个菜谱的关键,修改它的方式不是直接更改属性值,而是使用 setOpacity() 方法。

setOpacity() 方法负责修改 opacity 属性,除了修改任何 DOM 元素(例如,瓦片的图像)外,还会发出一个 changelayer 事件,通知任何感兴趣的监听器有关图层更改的通知。

参见

  • 包裹日期线选项菜谱

  • 在第一章的理解基本和非基本图层菜谱中,网络制图基础

  • 添加 WMS 图层菜谱

  • 缓冲图层数据以改善地图导航菜谱

使用单瓦片模式的 WMS

Web Map Service (WMS) 是一个用于提供地理参考地图图像的协议。

基本思想是,给定一个边界框和一些其他参数,例如图层名称,客户端向 WMS 服务器发送一个 HTTP 请求,服务器计算并返回一个包含指定图层和指定边界框内所有数据的图像。

在 OpenLayers 中,当你将 WMS 图层添加到地图中时,OpenLayers.Layer.WMS 实例会提供一些参数,例如分辨率和瓦片大小。WMS 服务器会计算每个缩放级别的正确瓦片数量,并将图层分成这么多瓦片。

这样,当你将 WMS 图层添加到地图中时,不仅只有一个请求发送到服务器,而且每个瓦片都会发送一个请求,以形成当前缩放级别的瓦片。

注意

当配置了缓存系统时,从服务器端的角度来看,将 WMS 图层分成瓦片可能更好。这样瓦片只生成一次,然后多次提供服务。

如果你有多于一个使用指向同一 WMS 服务器的 WMS 图层的网络制图应用程序,所有瓦片都可以从缓存中提供,这将大大减少服务器的负载。

将图层分成瓦片不是唯一与 WMS 图层一起工作的方式;如果需要,你可以使用所谓的单瓦片模式。

在这种模式下,只使用一张图像来覆盖整个视图——地图的边界框,而不是使用瓦片模式所需的多个瓦片。

每次图层必须刷新(当你移动地图或更改缩放级别时),就会向 WMS 服务器发送一个请求,请求新地图边界框的数据。

正如你所见,在单瓦片模式下,对服务器的请求次数远少于瓦片模式。相比之下,在瓦片模式下,每个瓦片请求很容易缓存,因为每个缩放级别的瓦片边界框是固定的;而在单瓦片模式下,每个请求通常与其他请求略有不同(因为边界框有细微的变化)并且会导致对 WMS 服务器的请求,从而产生相应的计算时间。

如何实现...

按照以下步骤使用 WMS 的单瓦片模式:

  1. 创建一个 HTML 文件并添加 OpenLayers 依赖项。

  2. 现在,我们将创建两个并排的地图,每个地图都包含一个 WMS 图层,如下所示:

    <table style="width: 100%; height: 95%;"> 
        <tr> 
            <td style="width: 50%;"> 
                <p>WMS layer:</p> 
                <div id="ch02_wms_non_singleTile" style="width: 100%; height: 100%;"></div> 
            </td> 
            <td style="width: 50%;"> 
                <p>WMS using <em>singleTile</em> property:</p> 
                <div id="ch02_wms_singleTile" style="width: 100%; height: 100%;"></div> 
            </td> 
        </tr> 
    </table>
    
    
  3. 接下来,编写所需的 JavaScript 代码以初始化两个地图。第一个将包含一个正常的 WMS 图层,如下所示:

    <script type="text/javascript"> 
    
        // Create the map using the specified DOM element 
        var map_a = new OpenLayers.Map("ch02_wms_non_singleTile");    
    
        // Add a WMS layer 
        var wms = new OpenLayers.Layer.WMS("Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0", 
        { 
            layers: 'basic' 
        }); 
        map_a.addLayer(wms); 
    
        // Set the center of the view 
        map_a.setCenter(new OpenLayers.LonLat(-90,0), 2);
    
    
  4. 第二个地图将包含一个指向同一服务器但在单瓦片模式下工作的 WMS 图层,如下所示:

        // Create the map using the specified DOM element 
        var map_b = new OpenLayers.Map("ch02_wms_singleTile");    
    
        // Add a WMS layer 
        var wms = new OpenLayers.Layer.WMS("Basic", "http:// vmap0.tiles.osgeo.org/wms/vmap0", 
        { 
            layers: 'basic' 
        }, 
        { 
            singleTile: true 
        }); 
        map_b.addLayer(wms); 
    
        // Set the center of the view 
        map_b.setCenter(new OpenLayers.LonLat(-90,0), 2); 
    </script>
    
    

它是如何工作的...

菜单代码相当简单。开头冗长的解释,即 WMS 图层如何在单瓦片模式下工作,实际上只需使用OpenLayers.Layer类的singleTile属性即可实现。拖动或缩放地图以查看其工作方式的不同。

参见

  • 缓存图层数据以改善地图导航菜谱

  • 添加 WMS 图层菜谱

  • 更改图层不透明度菜谱

缓存图层数据以改善地图导航

地图导航是改善用户体验的重要因素。

当我们平移地图时,很多时候我们会看到空白区域(表示内容正在加载),几秒钟后图像出现。

在栅格图层和在单瓦片模式下工作的 WMS 图层上,我们可以通过增加请求次数或增加服务器端的计算时间来改善这一点。

注意

大多数栅格图层都继承自基类OpenLayers.Layer.Grid,该类负责将每个缩放级别划分为瓦片。

对于在单瓦片模式下工作的 WMS 图层,网格仅由一个瓦片组成,该瓦片填充整个地图视图。

改善地图导航背后的想法很简单;在用户在地图视图中平移之前加载瓦片,以便它们在用户平移地图视图之前加载。

本菜谱向您展示了如何为栅格图层和在单瓦片模式下工作的 WMS 图层预加载内容,以便您可以改善用户的导航体验。

如何操作...

  1. 创建一个 HTML 文件并包含 OpenLayers 依赖项。

  2. 我们将创建两个并排的地图,并在每个地图上方添加一个旋转控件,该控件来自 Dojo Toolkit 框架(dojotoolkit.org),用于控制buffersingleTile属性值:

    <table style="width: 100%; height: 95%;"> 
        <tr> 
            <td style="width: 50%;"> 
                <p> 
                    WMS layer with <em>buffer</em>: <input id="buffer_a" dojoType="dijit.form.NumberSpinner" onChange="changeBufferA" 
                                                           intermediateChanges="true" style="width:100px" value="0" smallDelta="1" constraints="{min:0,max:5}" /> 
                </p> 
                <div id="ch02_wms_buffer" style="width: 100%; height: 100%;"></div> 
            </td> 
            <td style="width: 50%;"> 
                <p> 
                    WMS using <em>singleTile</em> property and <em>ratio</em>: <input id="buffer_b" dojoType="dijit.form.NumberSpinner" onChange="changeBufferB" 
                                                                                      intermediateChanges="true" style="width:100px" value="1.0" smallDelta="0.1" constraints="{min:0.0,max:2.0}" /> 
                </p> 
                <div id="ch02_wms_ratio" style="width: 100%; height: 100%;"></div> 
            </td> 
        </tr> 
    </table>
    
    
  3. 左侧面板将展示如何控制可以加载到地图视图之外瓦片数量:

    <script type="text/javascript"> 
        // Create the map using the specified DOM element 
        var map_a = new OpenLayers.Map("ch02_wms_buffer");    
    
        // Add a WMS layer 
        var wms_a = new OpenLayers.Layer.WMS("Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0", 
        { 
            layers: 'basic' 
        }, 
        { 
            buffer: 0 
        }); 
        map_a.addLayer(wms_a); 
    
        // Set the center of the view 
        map_a.setCenter(new OpenLayers.LonLat(-90,0), 3);
    
    
  4. 右侧面板显示了如何控制在单瓦片模式下工作的 WMS 图层中可以预加载的数据量。

        // Create the map using the specified DOM element 
        var map_b = new OpenLayers.Map("ch02_wms_ratio");    
    
        // Add a WMS layer 
        var wms_b = new OpenLayers.Layer.WMS("Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0", 
        { 
            layers: 'basic' 
        }, 
        { 
            singleTile: true, 
            ratio: 1 
        }); 
        map_b.addLayer(wms_b); 
    
        // Set the center of the view 
        map_b.setCenter(new OpenLayers.LonLat(-90,0), 3);
    
    
  5. 最后,以下是负责在旋转控件上更改代码的代码,如下所示:

        // Handle events 
        function changeBufferA(value) { 
            wms_a.addOptions({buffer: value}); 
        } 
        function changeBufferB(value) { 
            map_b.removeLayer(wms_b); 
            wms_b.destroy(); 
            wms_b = new OpenLayers.Layer.WMS("Basic", "http:// vmap0.tiles.osgeo.org/wms/vmap0", 
            { 
                layers: 'basic' 
            }, 
            { 
                singleTile: true, 
                ratio: value 
            }); 
            map_b.addLayer(wms_b); 
        } 
    </script>
    
    

它是如何工作的...

左侧地图包含一个在默认瓦片模式下工作的 WMS 图层。在此模式下,基类OpenLayers.Layer.Grid中的buffer属性指定了必须加载到地图视图之外的瓦片数量。

当用户更改buffer属性的旋转控件值时,我们只需使用以下代码行更新它:

    function changeBufferA(value) { 
        wms_a.addOptions({buffer: value}); 
    } 

右侧的地图另一方面有一个在单瓦片模式下工作的 WMS 图层(见 singleTile 属性设置为 true)。在这种模式下,只发出一个请求来获取图像,该图像填充整个地图视图。

我们可以通过 ratio 属性来控制图像的大小,该属性属于 OpenLayers.Layer.WMS 类。值为 1.0 的比率意味着图像具有与地图视图精确尺寸相同的尺寸。默认的比率值是 1.5,这意味着我们请求的图像尺寸是地图视图尺寸加上一半。

在这种情况下,比率值在创建图层时设置一次,要更新它,我们需要删除之前的图层并使用新值创建一个新的图层。这如下所示:

    function changeBufferB(value) { 
        map_b.removeLayer(wms_b); 
        wms_b.destroy(); 
        wms_b = new OpenLayers.Layer.WMS("Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0", 
        { 
            layers: 'basic' 
        }, 
        { 
            singleTile: true, 
            ratio: value 
        }); 
        map_b.addLayer(wms_b); 
    } 

注意

我们首先从地图中删除图层,然后调用 destroy() 方法来释放图层使用的内部资源,避免内存泄漏。

更多...

记住,我们加载的瓦片越多,对服务器的请求就越多。对于单瓦片模式的 WMS 图层也是如此;你请求的边界框越大,服务器上的计算时间就越长。

因此,过多地增加 bufferratio 值并不总是最佳解决方案。

考虑你的数据和用户如何探索它。如果你的数据更适合在扩展中探索——在同一缩放级别的一个大区域——那么一个一或两个像素的缓冲区可能是个好主意。如果你的数据主要被缩放,但用户对探索大区域不感兴趣,那么默认值就足够了。

参见

  • 使用单瓦片模式的 WMS 菜单

  • 设置 WMS 图层中的瓦片大小 菜单

  • 添加 WMS 图层 菜单

创建图像图层

有时候,像 Google Maps、OpenStreetMap 或 WMS 这样的瓦片图层并不是你需要的。完全有可能你有一个地理参考图像,知道它的投影和边界框,并想在地图上渲染它。

在这些情况下,OpenLayers 提供了 OpenLayers.Layer.Image 类,允许我们基于简单图像创建图层。地理参考图像如下截图所示:

创建图像图层

如何做...

要创建图像图层,执行以下步骤:

  1. 让我们创建一个包含 OpenLayers 依赖项的 HTML 文件。

  2. 首先,添加将包含地图的 div 元素,如下所示:

    <!-- Map DOM element -->
    <div id="ch2_image" style="width: 100%; height: 100%;"></div>
    
    
  3. 接下来,初始化地图并添加一个 WMS 基本图层,如下所示:

    <!-- The magic comes here -->
    <script type="text/javascript">
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch2_image", {
            allOverlays: true
        });
        map.addControl(new OpenLayers.Control.LayerSwitcher());
    
        // Add WMs layer
        var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0",
        {
            layers: 'basic'
        });
        map.addLayer(wms);
    
    
  4. 现在,定义图像 URL、其范围和大小,并按如下方式创建图像图层:

        // Add an Image layer
        var img_url = "http://localhost:8080/openlayers-cookbook/data/nexrad.png";
        var img_extent = new OpenLayers.Bounds(-131.0888671875, 30.5419921875, -78.3544921875, 53.7451171875);
        var img_size = new OpenLayers.Size(780, 480);
    
        var image = new OpenLayers.Layer.Image("Image Layer", img_url, img_extent, img_size, {
            isBaseLayer: false,
            alwaysInRange: true // Necessary to always draw the image
        });
        map.addLayer(image);
    
        // Center the view
        map.setCenter(new OpenLayers.LonLat(-85, 40), 3);
    </script>
    
    

它是如何工作的...

OpenLayers.Layer.Image 类的构造函数需要五个参数,如下所示:

  • name: 这是图层的期望描述性名称

  • url: 这是图像的 URL

  • extent: 这是一个 OpenLayers.Bounds 类的实例,包含图像的边界框

  • size: 这是一个 OpenLayers.Size 的实例,包含图像的像素尺寸

  • options: 这表示一个包含图层不同选项的 JavaScript 对象

在这个菜谱中使用的图像之前是从 NEXRAD 获得的(见 mesonet.agron.iastate.edu/current/mcview.phtml),因此我们知道它们的边界框的确切坐标。它们是:

var img_extent = new OpenLayers.Bounds(-131.0888671875, 30.5419921875, -78.3544921875, 53.7451171875);

注意

重要提示:边界必须以与地图相同的投影表示,在这种情况下为 EPSG:4326。

我们还知道图像的大小(以像素为单位):

var img_size = new OpenLayers.Size(780, 480);

给定图像范围和大小,OpenLayers 计算适当的分辨率(将其视为缩放级别),在这些分辨率下必须显示图像。

在这种情况下,我们始终希望在地图上显示图像,无论我们处于哪个缩放级别,因此我们已将 alwaysInRange 属性设置为 true

参见

  • Adding WMS layer 菜谱

  • 使用单瓦片模式的 Using WMS with single tile mode 菜谱

  • Buffering the layer data to improve the map navigation 菜谱

在 WMS 图层中设置瓦片大小

OpenLayers.Layer.Grid 类是一种特殊的图层,它将图层划分为由瓦片网格组成的不同缩放级别的图层。

OpenLayers.Layer.WMS 类是前面类的子类,除了在单瓦片模式下工作外,它还可以在瓦片模式下工作。

当然,控制 WMS 请求的瓦片大小可以影响性能。默认情况下,瓦片大小为 256 x 256 像素,但我们可以将此设置为任何所需的值。更大的瓦片大小意味着对服务器的请求更少,但生成更大图像的计算时间更长。相反,较小的瓦片大小意味着对服务器的请求更多,计算较小图像的时间更少。

如何做到这一点...

要设置瓦片大小,请执行以下步骤:

  1. 创建一个包含 OpenLayers 库依赖关系的 HTML 文件。

  2. 添加一个 div 元素,该元素将包含地图,如下所示:

    <!-- Map DOM element -->
    <div id="ch2_tilesize" style="width: 100%; height: 100%;"></div>
    
    
  3. 接下来,初始化地图并添加两个图层,如下所示:

    <!-- The magic comes here -->
    <script type="text/javascript">
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch2_tilesize", {
            allOverlays: true,
            tileSize: new OpenLayers.Size(256, 256)
        });
        map.addControl(new OpenLayers.Control.LayerSwitcher());
    
        // Add WMs layer
        var wms1 = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0",
        {
            layers: 'basic'
        });
        map.addLayer(wms1);
    
    
  4. 对于第二层,指定瓦片大小如下:

        var wms2 = new OpenLayers.Layer.WMS("Coast Line", "http://vmap0.tiles.osgeo.org/wms/vmap0",
        {
            layers: 'coastline_01,coastline_02'
        },
        {
            tileSize: new OpenLayers.Size(512, 512),
            opacity: 0.65
        });
        map.addLayer(wms2); 
    
        // Center the view
        map.setCenter(new OpenLayers.LonLat(-85, 40), 3);
    </script>
    
    

它是如何工作的...

在这个菜谱中没有什么神秘之处。tileSize 属性对于 OpenLayers.MapOpenLayers.Layer.Grid 子类都是可用的。

tileSize 必须是 OpenLayers.Size 类的实例,表示像素宽度和高度。

当在地图实例中设置瓦片大小时,所有图层都使用此值,除非您为每个单独的图层指定另一个值。

默认情况下,OpenLayers.Map 实例配置为使用 256 x 256 大小的瓦片。因此,第一层使用 256 x 256 像素的瓦片大小向 WMS 服务器发出请求。

另一方面,我们已为第二层指定了 512 x 512 的瓦片大小值,因此对 WMS 的请求是在等待 512 x 512 大小的瓦片。

还有更多...

对于瓦片服务,如 Google Maps 或 OpenStreetMap,tileSize 属性被简单地忽略,因为这些服务已经预先计算了固定 256 x 256 大小的图像。

瓦片大小值为 256 x 256 像素的原因是每个图像文件的大小(以字节为单位)对于带宽使用是最优的。

参见

  • 使用单瓦片模式的 WMS 菜单

  • 缓冲层数据以改善地图导航 菜单

第三章.处理矢量图层

在本章中,我们将涵盖:

  • 添加 GML 图层

  • 添加 KML 图层

  • 以编程方式创建特征

  • 从 WKT 读取和创建特征

  • 在地图上添加标记

  • 使用点特征作为标记

  • 处理弹出窗口

  • 从 WFS 服务器添加特征

  • 使用聚类策略

  • 在 WFS 请求中过滤特征

  • 直接使用协议读取特征

简介

本章讨论矢量图层。除了栅格信息外,矢量信息是我们在 GIS 系统中可以处理的其他重要信息类型。

本章试图总结您在 OpenLayers 中可能需要使用的一些最常见和重要的菜谱。

在 GIS 中,现实世界现象通过特征的概念来表示。它可以是一个地点——比如一个城市或村庄——它可以是道路或铁路,它可以是地区、湖泊、国家的边界或类似的东西。

每个特征都有一组属性:人口、长度等等。它通过几何符号进行视觉表示:点、线、多边形等等,使用一些视觉风格:颜色、半径、宽度等等。

如您所见,在处理矢量信息时需要考虑许多概念。幸运的是,OpenLayers 为我们提供了处理它们的类。我们将在本章中了解更多关于这些类的内容。

矢量图层的基础类是OpenLayers.Layer.Vector类,它定义了所有子类的公共属性和行为。

OpenLayers.Layer.Vector类包含一组特征。这些特征是OpenLayers.Feature.Vector子类的实例(实际上,它们是从更通用的OpenLayers.Feature类继承而来的)。

每个特征都有一个attributes属性和一个与它关联的OpenLayers.Geometry类实例。

矢量图层本身或每个特征都可以关联一个与之相关的视觉风格,该风格将用于在地图上渲染特征。

除了屏幕上的表示外,我们还需要考虑数据源。OpenLayers 提供类来从/向许多来源或协议读取/写入特征,并使用不同的格式:GML、KML、GeoJSON、GeoRSS 等等。

矢量图层可以可选地关联一个OpenLayers.Protocol类的实例和一个OpenLayers.Strategy类实例的列表。第一个负责使用某些协议(如 HTTP 或 WFS)读取/写入数据,而第二个(策略)负责控制诸如何时加载或刷新图层中的数据等任务:只加载一次,每次图层移动时,每隔几秒等等。

让我们开始,看看这些类是如何工作的。

添加 GML 图层

地理标记语言(GML)是一种用于表达地理特征的 XML 语法。它是一个 OGC 标准,并且被 GIS 社区广泛接受。

添加 GML 图层

在这个菜谱中,我们将向您展示如何从 GML 文件创建矢量图层。

注意

您可以在 Packt Publishing 网站上找到此书的源代码中附加的 GML 格式文件。

如何做...

  1. 创建一个包含所需 OpenLayers 依赖项的 HTML 文件,并插入以下代码。首先添加 div 元素来容纳地图:

    <!-- Map DOM element --> 
    <div id="ch3_gml" style="width: 100%; 
        height: 100%;"></div> 
    
    
  2. 接下来,添加初始化地图、添加基本层和图层切换控件所需的 JavaScript 代码:

    <!-- The magic comes here --> 
    <script type="text/javascript"> 
        // Create the map using the specified DOM element 
        var map = new OpenLayers.Map("ch3_gml");    
    
        var layer = new 
            OpenLayers.Layer.OSM("OpenStreetMap"); 
        map.addLayer(layer); 
    
        map.addControl(new 
            OpenLayers.Control.LayerSwitcher()); 
        map.setCenter(new OpenLayers.LonLat(0,0), 2); 
    
    
  3. 最后,添加一个带有 GML 数据的矢量层:

        map.addLayer(new OpenLayers.Layer.Vector("Europe (GML)", { 
            protocol: new OpenLayers.Protocol.HTTP({ 
                url: "http://localhost:8080/
                    openlayers-cookbook/recipes/
                    data/europe.gml", 
                format: new OpenLayers.Format.GML() 
            }), 
            strategies: [new OpenLayers.Strategy.Fixed()] 
        })); 
    </script>
    
    

它是如何工作的...

在使用 OpenLayers.Layer.Vector 类之前,我们需要考虑一些方面。

如果我们需要从某个来源加载数据,那么我们需要设置一个协议和策略。在这种情况下,我们使用了一个固定的策略,通过 OpenLayers.Strategy.Fixed 类实例,这意味着数据内容只加载一次。它永远不会刷新或再次加载。

new OpenLayers.Layer.Vector("Europe (GML)", { 
        protocol: new OpenLayers.Protocol.HTTP({ 
            url: "http://localhost:8080/openlayers-cookbook/recipes/data/europe.gml", 
            format: new OpenLayers.Format.GML() 
        }), 
        strategies: [new OpenLayers.Strategy.Fixed()] 
    })

要加载的数据可以通过 HTTP 协议和一个指向文件的 URL 访问。该协议作为 OpenLayers.Protocol.HTTP 类的一个实例,负责从指定的资源读取数据,并需要一个 URL 和一个格式来了解如何读取数据。

OpenLayers 提供了许多格式类来读取/写入数据,但在这个菜谱中,我们使用了 OpenLayer.Format.GML 实例,因为我们的数据源是一个 GML 文件。

参见

  • 添加 KML 层 菜谱

  • 程序化创建要素 菜谱

添加 KML 层

Google Maps 的到来导致 GIS 和网络制图领域的爆炸式增长。Google 不仅引入了一个 API,还引入了一些文件格式。

关键孔标记语言 (KML) 已成为最广泛使用的格式之一,最终它成为了一个 OGC 标准。

添加 KML 层

这个菜谱将向您展示如何轻松地从 KML 文件中添加要素。您可以在 Packt Publishing 网站上找到此书源代码中附加的 KML 格式文件。

如何做...

  1. 创建一个包含 OpenLayers 库的 HTML 文件,并在其中插入以下代码。首先,添加将容纳地图的 DOM 元素:

    <!-- Map DOM element -->
    <div id="ch3_kml" style="width: 100%; height: 100%;"></div>
    
    
  2. 接下来,初始化地图实例,添加基本层,添加图层切换控件,并居中视图:

    <!-- The magic comes here --> 
    <script type="text/javascript"> 
    
        // Create the map using the specified DOM element 
        var map = new OpenLayers.Map("ch3_kml");    
    
        var layer = new OpenLayers.Layer.OSM("OpenStreetMap"); 
        layer.wrapDateLine = false;
        map.addLayer(layer); 
    
        map.addControl(new OpenLayers.Control.LayerSwitcher()); 
        map.setCenter(new OpenLayers.LonLat(0,0), 2); 
    
    
  3. 最后,添加一个将加载数据从 KML 文件中的矢量层:

        // Global Undersea Fiber Cables
        map.addLayer(new OpenLayers.Layer.Vector("Global 
            Undersea Fiber Cables", {
            protocol: new OpenLayers.Protocol.HTTP({
                url: "http://localhost:8080/
                openlayers-cookbook/recipes/
                data/global_undersea.kml",
                format: new OpenLayers.Format.KML({
                    extractStyles: true,
                    extractAttributes: true
                })
            }),
            strategies: [new OpenLayers.Strategy.Fixed()]
        }));
    </script> 
    
    

它是如何工作的...

在初始化地图后,我们居中视图并添加了一些控件。然后我们添加了一个矢量层。

因为我们想从 KML 文件加载数据,该文件可以通过 HTTP 协议访问,所以我们设置了一个 OpenLayers.Protocol.HTTP 实例作为矢量层的协议。它使用文件的 URL,并使用 OpenLayers.Format.KML 实例作为 format 属性。

此外,我们还将 OpenLayers.Strategy.Fixed 实例设置为矢量层的策略,这使得文件只加载一次。

此外,我们还使用了几个 OpenLayers.Format.KML 类,extractStylesextractAttributes,以保持源 KML 文件中指定的颜色样式和属性。否则,OpenLayers 将应用默认样式。

更多...

KML 格式,就像 GML 一样,提供了大量的选项和可能性,但代价是复杂性。

在 KML 格式中,地标可以附加描述,如果你在谷歌地图中加载 KML 文件,当你点击它们时,地标描述会以气球(或弹出窗口)的形式显示。

在 OpenLayers 中,这种方法略有不同。正如我们将在使用弹出窗口食谱中看到的那样,加载 KML 数据和显示它们的行为完全不同。因此,不要期望加载数据的矢量层也会附加控制点击事件、显示弹出窗口等所需的代码。这是我们的工作。

参见

  • 添加 GML 层食谱

  • 创建特征的方法食谱

  • 使用弹出窗口食谱

创建特征的方法

从外部源加载数据不是唯一与矢量层一起工作的方式。

想象一个网络地图应用程序,用户可以实时创建新特征:城市、河流、感兴趣区域等,并将它们以某种样式添加到矢量层中。这种场景需要能够以编程方式创建和添加特征的能力。

在本食谱中,我们将看到一些创建和管理特征的方法。

如何做...

  1. 首先,创建一个新的 HTML 文件,并添加所需的 OpenLayers 依赖项。添加div元素以容纳地图:

    <!-- Map DOM element -->
    <div id="ch3_features_programmatically" 
        style="width: 100%; height: 100%;"></div>
    
    
  2. 接下来,初始化地图实例并添加一个基本层:

    <!-- The magic comes here -->
    <script type="text/javascript">
    
        // Create the map using the specified DOM element
        var map = new 
            OpenLayers.Map("ch3_features_programmatically");    
    
        // Add a WMS layer
        var wms = new OpenLayers.Layer.WMS("Basic", 
            "http://vmap0.tiles.osgeo.org/wms/vmap0",
        {
            layers: 'basic'
        });
        map.addLayer(wms);
    
        map.addControl(new 
            OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(0,0), 2);
    
    
  3. 现在,创建三个矢量层以放置三种不同类型的特征:

        // Create some empty vector layers
        var pointLayer = new 
            OpenLayers.Layer.Vector("Points");
        var lineLayer = new OpenLayers.Layer.Vector("Lines");
        var polygonLayer = new 
            OpenLayers.Layer.Vector("Polygon");
    
        // Add layers to the map
        map.addLayers([polygonLayer, lineLayer, pointLayer]);
    
    
  4. 调用创建点、线和多边形特征并将其添加到每个先前层的函数:

        // Fill layers
        initializePointLayer();
        initializeLineLayer();
        initializePolygonLayer();
    
        // Create some random points.
        function initializePointLayer() {
            var pointFeatures = [];
            for(var i=0; i< 50; i++) {
                var px = Math.random()*360-180;
                var py = Math.random()*180-90;
    
                var pointGeometry = new 
                    OpenLayers.Geometry.Point(px, py);
                var pointFeature = new 
                    OpenLayers.Feature.Vector(pointGeometry);
                pointFeatures.push(pointFeature);
            }
            pointLayer.addFeatures(pointFeatures);
        }
    
        // Create some random lines
        function initializeLineLayer() {
            for(var j=0; j< 2; j++) {
                var pointGeometries = [];
                for(var i=0; i< 10; i++) {
                    var px = Math.random()*240-120;
                    var py = Math.random()*100-50;
    
                    var pointGeometry = new 
                        OpenLayers.Geometry.Point(px, py);
                    pointGeometries.push(pointGeometry);
                }
                var lineGeometry = new OpenLayers.Geometry.
                    LineString(pointGeometries);
                var lineFeature = new 
                    OpenLayers.Feature.Vector(lineGeometry);
                lineLayer.addFeatures(lineFeature);
            }
        }
    
        // Create some random polygons
        function initializePolygonLayer() {
            for(var j=0; j< 2; j++) {
                var pointGeometries = [];
                for(var i=0; i< 5; i++) {
                    var px = Math.random()*240-180;
                    var py = Math.random()*100-90;
    
                    var pointGeometry = new 
                        OpenLayers.Geometry.Point(px, py);
                    pointGeometries.push(pointGeometry);
                }
                var linearGeometry = new OpenLayers.Geometry.
                    LinearRing(pointGeometries);
                var polygonGeometry = new OpenLayers.
                    Geometry.Polygon([linearGeometry]);
                var polygonFeature = new OpenLayers.
                    Feature.Vector(polygonGeometry);
                polygonLayer.addFeatures(polygonFeature);
            }
        }
    </script>
    
    

它是如何工作的...

如本章引言所述,矢量层包含一组特征。每个特征代表现实世界中的某种现象,并与其关联一个几何形状和样式,这将决定其视觉表示。

让我们来看看负责创建随机点的代码:

        var pointFeatures = [];
        for(var i=0; i< 50; i++) {
            var px = Math.random()*360-180;
            var py = Math.random()*180-90;

            var pointGeometry = new 
                OpenLayers.Geometry.Point(px, py);
            var pointFeature = new 
                OpenLayers.Feature.Vector(pointGeometry);
            pointFeatures.push(pointFeature);
        }
        pointLayer.addFeatures(pointFeatures);

在这种情况下,每个特征由一个点几何形状表示,因为我们首先需要创建一个带有点坐标的OpenLayers.Geometry.Point实例。

注意

记住,要以地图使用的适当投影表达坐标,或者在矢量层中设置正确的投影,以便 OpenLayers 可以转换坐标。

一旦我们有了几何实例,我们可以通过传递要由特征使用的所需几何实例来创建一个新的OpenLayers.Feature.Vector实例。

注意,我们将在另一章中介绍如何使用特征样式。它将以默认的 OpenLayers 样式进行渲染。

所有特征都存储在一个数组中,并使用addFeatures()方法一次性传递给矢量层。

在难度顺序中,接下来是创建线,在几何对象术语中被称为 LineStrings。当你想要将一个特征表示为 LineString 时,你需要使用几何类OpenLayers.Geometry.LineString的一个实例。正如我们可以在下面的代码块中看到的那样,线字符串构造函数需要一个OpenLayers.Geometry.Point实例的数组,该数组符合构成线的点集。

            var pointGeometries = [];
            for(var i=0; i< 10; i++) {
                var px = Math.random()*240-120;
                var py = Math.random()*100-50;

                var pointGeometry = new OpenLayers.Geometry.Point(px, py);
                pointGeometries.push(pointGeometry);
            }
            var lineGeometry = new OpenLayers.Geometry.LineString(pointGeometries);
            var lineFeature = new OpenLayers.Feature.Vector(lineGeometry);
            lineLayer.addFeatures(lineFeature);

注意

OGC 的简单特征访问规范(www.opengeospatial.org/standards/sfa)包含了对标准的深入描述。它还包含一个 UML 类图,其中你可以看到所有几何类和层次结构。

最后,我们找到了创建一些多边形的代码。

多边形是表示州或国家等区域的优秀几何形状。我们可以将多边形视为一组简单的线,其中起点和终点相同,称为LineRing,并用某种颜色填充。但请注意,多边形可以是非常复杂的结构,这可能会使我们必须表达它们的方式变得复杂。

例如,考虑一个内部有空洞的区域。在这种情况下,我们有两个线环来描述外部和内部周界。我们还必须指定哪些部分必须着色。

看看下面的代码:

            var pointGeometries = [];
            for(var i=0; i< 5; i++) {
                var px = Math.random()*240-180;
                var py = Math.random()*100-90;

                var pointGeometry = new 
                    OpenLayers.Geometry.Point(px, py);
                pointGeometries.push(pointGeometry);
            }
            var linearGeometry = new 
                OpenLayers.Geometry.LinearRing(pointGeometries);
            var polygonGeometry = new 
                OpenLayers.Geometry.Polygon([linearGeometry]);
            var polygonFeature = new 
                OpenLayers.Feature.Vector(polygonGeometry);
            polygonLayer.addFeatures(polygonFeature);

在这里,我们通过传递一个OpenLayers.Geometry.Point的数组,其中包含构成线环的点集,创建了一个OpenLayers.Geometry.LineRing实例。

一旦我们有一个或多个线环,我们可以创建OpenLayers.Geometry.Polygon类的新实例,该实例将用于渲染我们新的矢量层特征。

参见

  • 向地图添加标记菜谱

  • 从 WKT 读取和创建特征菜谱

  • 使用弹出窗口工作菜谱

  • 第七章中的使用符号化样式特征菜谱

从 WKT 读取和创建特征

OpenLayers 附带了一套优秀的格式类,用于从/向不同的文件数据格式读写。GeoJSON、GML 或 GPX 是我们能找到的许多格式中的一些。

如果你已经阅读了本章中的添加 GML 层菜谱,你就会知道矢量类可以读取存储在文件中的特征,指定数据源的格式,并将包含的特征放置在地图上。

这个菜谱想要展示的就是这一点。我们将看到负责从文件中读取数据并使用格式类将其转换为对应特征的神奇步骤。

注意

为了简单起见,我们只看看如何从 WKT 文本中读取特征。你可以从en.wikipedia.org/wiki/Well-known_text了解更多关于 WKT(Well-Known Text)格式的信息。

如前一个屏幕截图所示,我们将在左侧创建一个地图,在右侧我们将放置一些文本区域组件来添加和获取 WKT 格式的特征。

从 WKT 读取和创建特征

如何做到这一点...

  1. 创建一个新的 HTML 文件,并添加 OpenLayers 依赖项。然后,为地图、文本区域和按钮添加以下 HTML 代码:

    <!-- Map DOM element -->
    <table style="width: 100%; height: 95%;">
        <tr>
            <td>
                <div id="ch3_reading_wkt" style="width: 100%; 
                    height: 100%;"></div>
            </td>
            <td style="width: 30%; vertical-align: top;">
                <p>Write the WKT describing features:</p>
                <textarea id="wktText" 
                    dojoType="dijit.form.SimpleTextarea" 
                    rows="10" style="width: 100%;">
                    MULTIPOLYGON 
                    (((40 40, 20 45, 45 30, 40 40)),
                    ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35),
                    (30 20, 20 25, 20 15, 30 20)))</textarea>
                <button dojoType="dijit.form.Button" 
                    onClick="addFeature">Add Feature</button>
                <button dojoType="dijit.form.Button" 
                    onClick="clearLayer">Clear Layer</button>
            </td>
        </tr>
    </table>
    
    

    注意

    记住,我们正在使用 Dojo 工具包框架(dojotoolkit.org)来改进我们的组件,因此一些元素将具有如 dojoType="dijit.form.Button" 这样的属性。

  2. 现在,我们将初始化地图组件并放置一个基本图层:

    <!-- The magic comes here -->
    <script type="text/javascript">
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch3_reading_wkt");    
    
        // Add a WMS layer
        var wms = new OpenLayers.Layer.WMS("Basic", 
            "http://vmap0.tiles.osgeo.org/wms/vmap0",
        {
            layers: 'basic'
        });
        map.addLayer(wms);
    
        map.addControl(new 
            OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(0,0), 2);
    
    
  3. 让我们继续创建一个矢量层来保存我们将从 WKT 中读取的特征:

        // Create some empty vector layers
        var wktLayer = new 
            OpenLayers.Layer.Vector("wktLayer");
        // Add layers to the map
        map.addLayer(wktLayer);
    
    
  4. 我们需要几个函数来处理按钮事件。第一个函数负责清理矢量层:

        function clearLayer() {
            wktLayer.removeAllFeatures();
        }
    
    
  5. 第二个函数读取 WKT 字符串中的数据,并将特征放置在矢量层上:

        function addFeature() {
            // Read features and add to the vector layer
            var text = dijit.byId('wktText').get('value');
            var wkt = new OpenLayers.Format.WKT();
            var features = wkt.read(text);
            wktLayer.addFeatures(features);
    
            // Dump the vector layer features to WKt format
            var currentWkt = wkt.write(wktLayer.features);
            dijit.byId('wktFeatures').set('value', currentWkt);
        }
    </script>
    
    

它是如何工作的...

所有格式类都继承自 OpenLayers.Format 基类,该基类定义了格式类的基行为,即具有 readwrite 方法。

read() 方法应该以某种格式(JSON 字符串、WKT 字符串等)读取数据,并返回一个特征数组,作为 OpenLayers.Feature.Vector 类的实例。

另一方面,write() 方法接收一个特征数组,并返回一个表示所需格式的字符串。

注意

根据格式子类的不同,readwrite 方法可以接受额外的参数。始终要小心,并阅读 API 文档。

要从 WKT 字符串中读取特征,我们只需要实例化所需的格式类,并通过传递一个有效的字符串作为参数来调用其 read 方法:

        var wkt = new OpenLayers.Format.WKT();
        var features = wkt.read(text);
        wktLayer.addFeatures(features);

然后,我们获取矢量层的当前特征,并通过将它们传递给 write 方法将它们转换为 WKT 表示:

        // Dump the vector layer features to WKt format
        var currentWkt = wkt.write(wktLayer.features);
        dijit.byId('wktFeatures').set('value', currentWkt);

参见

  • 添加 GML 层的配方

  • 通过编程创建特征的配方

  • 直接使用协议读取特征的配方

将标记添加到地图

标记在网络地图应用中被广泛使用。它们允许我们通过在期望的位置显示一个图标来快速识别兴趣点(POI)。

这个配方展示了如何使用 OpenLayers.MarkerOpenLayers.Layer.Markers 类将标记添加到我们的地图中。

将标记添加到地图

如何做到这一点...

  1. 首先,创建一个依赖于 OpenLayers 库的 HTML 页面。添加将包含地图的 div 元素:

    <!-- Map DOM element -->
    <div id="ch3_markers" style="width: 100%; height: 100%;"></div>
    
    Create the map instance, add a base layer and a layer switcher control:
    <!-- The magic comes here -->
    <script type="text/javascript">
    
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch3_markers");    
    
        layer = new OpenLayers.Layer.OSM("OpenStreetMap");
        map.addLayer(layer);
    
        map.addControl(new 
            OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(0,0), 3);
    
    
  2. 现在,添加一个新的图层类型,OpenLayers.Layer.Markers,专门设计用来包含 OpenLayers.Marker 实例:

        var markers = new 
            OpenLayers.Layer.Markers("Markers");
        map.addLayer(markers);
    
    
  3. 我们现在将通过使用数组中的一个随机图标在随机位置创建标记:

        // Create some random markers with random icons
        var icons = [
             // Here goes an array of image file names
        ];
    
        for(var i=0; i< 150; i++) {
            // Compute a random icon and lon/lat position.
            var icon = Math.floor(Math.random() * 
                icons.length);
            var px = Math.random() * 360 - 180;
            var py = Math.random() * 170 - 85;
    
            // Create size, pixel and icon instances
            var size = new OpenLayers.Size(32, 37);
            var offset = new OpenLayers.Pixel(-(size.w/2), 
                -size.h);
            var icon = new OpenLayers.Icon('./recipes/data/
                icons/'+icons[icon], size, offset);
            icon.setOpacity(0.7);
    
            // Create a lonlat instance and transform it to 
            // the map projection.
            var lonlat = new OpenLayers.LonLat(px, py);
            lonlat.transform(new 
                OpenLayers.Projection("EPSG:4326"), new 
                OpenLayers.Projection("EPSG:900913"));
    
            // Add the marker
            var marker = new OpenLayers.Marker(lonlat, icon);
    
            // Event to handler when the mouse is over
            // Inflate the icon and change its opacity
            marker.events.register("mouseover", marker, 
                function() {
                console.log("Over the marker "+this.id+" 
                    at place "+this.lonlat);
                this.inflate(1.2);
                this.setOpacity(1);
            });
            // Event to handler when the mouse is out
            // Inflate the icon and change its opacity
            marker.events.register("mouseout", marker, 
                function() {
                console.log("Out the marker "+this.id+" at 
                    place "+this.lonlat);
                this.inflate(1/1.2);
                this.setOpacity(0.7);
            });
    
            markers.addMarker(marker);
        }
    </script>
    
    

它是如何工作的...

OpenLayers.Layer.Markers 类是 OpenLayers.Layer 基类的一个直接子类,并且专门设计用来包含标记。

另一方面,标记由 OpenLayers.Layer.Markers 类的实例表示。每个标记都有一个关联的点,用 OpenLayers.LonLat 类的实例表示,以及使用 OpenLayers.Icon 类的实例表示的图标。

一个图标需要一个要加载的图像的 URL,一个表示为 OpenLayers.Size 实例的 大小,以及一个表示为 OpenLayers.Pixel 实例的 偏移量

此外,对于每个已注册的标记,我们有两个监听器,一个用于知道鼠标何时悬停在标记上,另一个用于知道鼠标何时离开标记。这样,我们就可以修改标记的大小和透明度,以突出显示鼠标何时选中或取消选中它。

在处理函数内部,我们使用了 inflate() 方法,以改变图标的尺寸,增加其比例,以及 setOpacity() 方法,以改变图标的透明度:

        marker.events.register("mouseover", marker, function() {
            console.log("Over the marker "+this.id+" at place "+this.lonlat);
            this.inflate(1.2);
            this.setOpacity(1);
        });

注意

对于 JavaScript 初学者,请记住调用处理标记事件的匿名函数的对象是标记本身。因为 this 关键字引用的是标记,我们可以调用 inflate()setOpacity() 方法。

更多内容...

通过 OpenLayers.MarkerOpenLayers.Layer.Markers 类使用标记并不是我们展示地图中 POI 的唯一方式。

正如你在 使用点特征作为标记 配方中所看到的,我们还可以使用特征来显示 POI,这是一个可以通过使用策略、格式等改进的替代方案。

此外,OpenLayers 提供了一些类,如 OpenLayers.Layer.GeoRSSOpenLayers.Layer.Text,它们可以自动从 GeoRSS 和 CSV 文件中创建标记。它们相对简单,是为了特定的用途实现的,而且,你很快就会需要比这些类提供的更多灵活性。

参见

  • 使用点特征作为标记 的配方

  • 通过编程创建特征 的配方

  • 直接使用协议读取特征 的配方

使用点特征作为标记

显示标记不仅限于使用 OpenLayers.MarkerOpenLayers.Layer.Markers 类。

标记可以理解为兴趣点(POI),我们在其中放置一个图标来识别它,并与之关联一些信息:一座纪念碑、一个停车场、一座桥梁等。

在这个配方中,我们将学习如何使用这些特征与一个与点几何类型关联的创建标记。

使用点特征作为标记

如何做到这一点...

  1. 一旦你创建了包含 OpenLayers 依赖项的正确 HTML 文件,添加一个 div 元素来包含地图:

    <div id="ch3_feature_markers" style="width: 100%; 
    height: 100%;"></div>
    
    
  2. 开始初始化地图实例并添加一个基本层和控制:

        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch3_feature_markers");    
    
        var layer = new 
            OpenLayers.Layer.OSM("OpenStreetMap");
        map.addLayer(layer);
    
        map.addControl(new 
            OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(0,0), 2);
    
    
  3. 接下来,添加一个矢量层,它将包含一组随机标记:

        var pointLayer = new 
            OpenLayers.Layer.Vector("Features", {
            projection: "EPSG:933913"
        });
        map.addLayer(pointLayer);
    
    
  4. 创建一些随机点。为了提高性能,我们将所有点添加到一个数组中,然后一次性使用 addFeatures 方法将它们添加到矢量层中:

        // Create some random feature points
        var pointFeatures = [];
        for(var i=0; i< 150; i++) {
            var px = Math.random() * 360 - 180;
            var py = Math.random() * 170 - 85;
    
            // Create a lonlat instance and transform it 
            // to the map projection.
            var lonlat = new OpenLayers.LonLat(px, py);
            lonlat.transform(new 
                OpenLayers.Projection("EPSG:4326"), 
                new OpenLayers.Projection("EPSG:900913"));
    
            var pointGeometry = new 
                OpenLayers.Geometry.Point
                (lonlat.lon, lonlat.lat);
            var pointFeature = new 
                OpenLayers.Feature.Vector(pointGeometry);
    
            pointFeatures.push(pointFeature);
        }
        // Add features to the layer
        pointLayer.addFeatures(pointFeatures); 
    
    
  5. 现在,将两个事件监听器附加到矢量层上,用于 featureselectedfeatureunselected 事件。监听器将负责更改特征样式:

        // Event handler for feature selected
        pointLayer.events.register("featureselected", null, 
            function(event){
            var layer = event.feature.layer;
            event.feature.style = {
                fillColor: '#ff9900',
                fillOpacity: 0.7,
                strokeColor: '#aaa',
                pointRadius: 12
            };
            layer.drawFeature(event.feature);
        });
        // Event handler for feature unselected
        pointLayer.events.register("featureunselected", null, 
            function(event){
            var layer = event.feature.layer;
            event.feature.style = null;
            event.feature.renderIntent = null;
            layer.drawFeature(event.feature);
        });
    
    
  6. 最后,我们需要将一个 SelectFeature 控制器附加到地图上,并引用矢量层:

        // Add select feature control required to trigger events on the vector layer.
        var selectControl = new OpenLayers.Control.SelectFeature(pointLayer);
        map.addControl(selectControl);
        selectControl.activate();
    
    

它是如何工作的...

这个想法很简单,向层中添加点要素并监听它们的选取事件以更改样式。

与使用OpenLayers.Marker实例的方式不同,我们需要使用以下代码将监听器附加到矢量层而不是要素本身:

    pointLayer.events.register("featureselected", null, 
        function(event){
        // Code here
    });

在监听器函数中,我们可以通过event变量访问选定的要素或它所属的矢量层:

        var layer = event.feature.layer;
        event.feature.style = {
            fillColor: '#ff9900',
            fillOpacity: 0.7,
            strokeColor: '#aaa',
            pointRadius: 12
        };

注意

在第七章,要素样式中,我们将学习更多关于要素样式以及如何使用图像改进其外观,类似于OpenLayers.Marker类。

一旦要素样式更改,我们就可以在矢量层上调用drawFeature()来刷新地图上的要素:

        layer.drawFeature(event.feature);

要允许矢量层触发事件,我们需要将一个SelectFeature控件附加到地图上,引用矢量层,并激活它。没有它,监听器永远不会被调用。

    var selectControl = new OpenLayers.Control.SelectFeature(pointLayer);
    map.addControl(selectControl);
    selectControl.activate();

参见

  • 程序化创建要素配方

  • 向地图添加标记配方

  • 处理弹出窗口配方

  • 使用集群策略配方

处理弹出窗口

网络地图应用的一个共同特征是能够显示与地图包含的要素相关的信息。要素意味着任何我们可以用点、线、多边形等视觉表示的真实现象或方面。

当然,我们可以选择一个要素,检索其关联信息并在我们的应用程序布局中的任何位置显示它,但最常见的方式是通过弹出窗口显示。

处理弹出窗口

如何做到这一点...

  1. 创建一个包含 OpenLayers 依赖项的 HTML 文件。然后添加div元素以容纳地图:

    <div id="ch3_popups" style="width: 100%; height: 100%;">
    </div>
    
    
  2. 在 JavaScript 部分,初始化地图并添加一个基本图层:

        var map = new OpenLayers.Map("ch3_popups");    
        var layer = new 
            OpenLayers.Layer.OSM("OpenStreetMap");
        map.addLayer(layer);
        map.addControl(new 
            OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(0,0), 2);
    
    
  3. 创建一个矢量层并向其中添加一些要素:

        var pointLayer = new 
            OpenLayers.Layer.Vector("Features", {
            projection: "EPSG:900913"
        });
        map.addLayer(pointLayer);
    
    
  4. 接下来,向矢量层添加一些随机要素:

        var pointFeatures = [];
        for(var i=0; i< 150; i++) {
            var icon = Math.floor(Math.random() * 
                icons.length);
            var px = Math.random() * 360 - 180;
            var py = Math.random() * 170 - 85;
    
            // Create a lonlat instance and transform it to 
            // the map projection.
            var lonlat = new OpenLayers.LonLat(px, py);
            lonlat.transform(new 
                OpenLayers.Projection("EPSG:4326"), new 
                OpenLayers.Projection("EPSG:900913"));
    
            var pointGeometry = new 
                OpenLayers.Geometry.Point(lonlat.lon, 
                lonlat.lat);
            var pointFeature = new 
                OpenLayers.Feature.Vector(pointGeometry, 
                null, {
                pointRadius: 16,
                fillOpacity: 0.7,
                externalGraphic: 
                    'http://localhost:8080/
                    openlayers-cookbook/recipes/data/
                    icons/'+icons[icon]
            });
    
            pointFeatures.push(pointFeature);
        }
        // Add features to the layer
        pointLayer.addFeatures(pointFeatures); 
    
    

    注意

    您需要将之前的 URL 更改为您自定义服务器的正确地址。

  5. 最后,添加负责管理要素选取以显示弹出窗口的代码:

    // Add select feature control required to trigger events on the vector layer.    
        var selectControl = new 
            OpenLayers.Control.SelectFeature(pointLayer, {
            hover: true,
            onSelect: function(feature) {
                var layer = feature.layer;
                feature.style.fillOpacity = 1;
                feature.style.pointRadius = 20;
                layer.drawFeature(feature);
    
                var content = "<div><strong>Feature:</strong> 
                    <br/>" + feature.id +
                    "<br/><br/><strong>Location:</strong> 
                    <br/>" + feature.geometry +"</div>";
    
                var popup = new OpenLayers.Popup.FramedCloud(
                feature.id+"_popup", 
                feature.geometry.getBounds().
                getCenterLonLat(),
                new OpenLayers.Size(250, 100),
                content,
                null, 
                false, 
                null);
                feature.popup = popup;
                map.addPopup(popup);
            },
            onUnselect: function(feature) {
                var layer = feature.layer;
                feature.style.fillOpacity = 0.7;
                feature.style.pointRadius = 16;
                feature.renderIntent = null;
                layer.drawFeature(feature);
    
                map.removePopup(feature.popup);
            }
        });
        map.addControl(selectControl);
        selectControl.activate(); 
    
    

它是如何工作的...

在创建矢量层之后,我们首先创建了一些随机的点要素。

由于我们在计算十进制度数的随机纬度和经度值("EPSG:4326"投影),我们需要将其转换为地图使用的投影。在这种情况下,因为OpenStreetMap是基本图层,它使用"EPSG:900913"投影作为地图的投影。

        var lonlat = new OpenLayers.LonLat(px, py);
        lonlat.transform(new OpenLayers.Projection("EPSG:4326"), 
            new OpenLayers.Projection("EPSG:900913"));

        var pointGeometry = new 
            OpenLayers.Geometry.Point(lonlat.lon, lonlat.lat);
        var pointFeature = new 
            OpenLayers.Feature.Vector(pointGeometry, null, {
            pointRadius: 16,
            fillOpacity: 0.7,
            externalGraphic: 'http://localhost:8080/
                openlayers-cookbook/recipes/
                data/icons/'+icons[icon]
        });

在这里,我们正在创建具有自定义样式的要素。OpenLayers.Feature.Vector类的构造函数接受三个参数:一个必需的几何参数,以及两个可选参数,要素的属性和要素的样式

我们的特征没有特殊属性,所以我们传递了一个null值,但另一方面,我们使用自定义样式来显示图标图像,而不是简单的点来表示它们。

一旦我们获得了想要的特征,就在特征被选择时显示一个带有一些美好描述的弹出窗口。

为了实现这一点,我们使用了SelectFeature控件。给定一个图层,此控件允许用户选择特征。我们可以使用options参数自定义控件的行为:

var selectControl = new 
    OpenLayers.Control.SelectFeature(pointLayer, {
    hover: true,
    onSelect: function(feature) { ... },
    onUnselect: function(feature) { ... }
});

在这个菜谱中,我们使用了以下三个选项:

  • hover: 它表示无需点击即可选择或取消选择特征,只需将鼠标移到按钮上即可。

  • onSelect: 当特征被选择时执行此函数。它接收选择的特征作为参数。

  • onUnselect: 当特征被取消选择时执行此函数。它接收取消选择的特征作为参数。

现在,让我们看看如何创建弹出窗口。

这里需要注意的重要一点是,弹出窗口被添加到地图中。它们不是添加到特征或图层中。因此,要显示或隐藏弹出窗口,我们只需使用addPopup()removePopup()方法将其添加或从地图中移除。

OpenLayers 提供了一些用作弹出窗口的类,但它们都继承自基本类OpenLayers.Popup

我们选择了OpenLayers.Popup.FramedCloud子类,这是一个视觉上相当不错的样式弹出窗口。构造函数需要以下参数:

  • id: 一个字符串,用于在所有可以附加到地图的弹出窗口中标识弹出窗口

  • lonlat: 弹出窗口必须出现的位置

  • contentSize: 弹出窗口的尺寸,作为OpenLayers.Size类的实例

  • contentHTML: 要作为内容放入的 HTML 字符串

  • anchor: 弹出窗口将被锚定的对象

  • closeBox: 布尔值,指示是否必须显示关闭按钮

  • closeBoxCallback: 当用户点击关闭按钮时将被执行的功能

使用所有这些参数,我们创建FramedCloud弹出窗口的代码如下:

            var popup = new OpenLayers.Popup.FramedCloud(
            feature.id+"_popup", 
            feature.geometry.getBounds().getCenterLonLat(),
            new OpenLayers.Size(250, 100),
            content,
            null, 
            false, 
            null);

创建后,我们将其添加到地图中,使其自动可见:

            feature.popup = popup;
            map.addPopup(popup);

我们还在特征中存储了弹出窗口的引用。这样,我们可以在特征被取消选择时执行的功能中轻松找到弹出窗口的引用并将其从地图中移除:

            map.removePopup(feature.popup);

注意

作为备注,地图的addPopup()方法有一个第二个可选参数exclusive,如果设置,则在添加新弹出窗口时自动从地图中移除所有现有弹出窗口。

参见

  • 向地图添加标记的菜谱

  • 使用点特征作为标记的菜谱

从 WFS 服务器添加特征

Web 特征服务(WFS)是一个 OGC 标准,它提供独立平台调用,向服务器请求地理特征。在实践中,这意味着客户端向实现 WFS 标准的服务器发出 HTTP 请求,并获取一组 GML(地理标记语言,en.wikipedia.org/wiki/Geography_Markup_Language)格式的特征。

注意

你可以在关于 WFS 的教程中找到对 WFS 的良好介绍,该教程可在 www.e-education.psu.edu/geog585/book/export/html/1724 找到。如果你想了解更多关于这个话题的信息,可以在 OGC 网站上找到完整的规范 www.opengeospatial.org/standards/wfs.

从 OpenLayers 的角度来看,WFS 仅仅是我们可以读取以填充矢量图层的数据源之一。

在继续之前,有一个重要的要点需要考虑。OpenLayers 在加载数据时(例如 GML、KML 或 GeoRSS 文件)做出的大多数请求都是通过辅助类 OpenLayers.Request 以异步方式进行的。

任何 JavaScript 调用都受到浏览器强加的安全模型限制,这避免了跨域请求。这意味着你只能向网页最初来自的服务器发出请求。

有不同的方法可以避免这一点,但一个简单的方法是在服务器端使用代理。

注意

你可以在以下链接中阅读更清晰的解释

developer.yahoo.com/javascript/howto-proxy.html.

代理的想法很简单,我们不是直接向跨域请求,而是向同一域上的一个脚本发出请求,该脚本负责进行跨域请求并返回结果。

一个脚本,比如 PHP、Python 或 Java servlet,不受跨域请求的限制。它只受浏览器在 JavaScript 调用中的安全限制。

OpenLayers 提供了一个作为 Python 脚本的代理实现,我们可以在我们的应用程序中使用它。它可以在源代码包中的 examples/proxy.cgi 文件中找到。

这不是唯一的选择。在这个食谱中,我们将使用来自 MapBuilder 项目的 PHP 代理文件(请参阅书中源代码中的 utils/proxy.php 文件)。

如何做...

  1. 创建一个 HTML 文件,设置 OpenLayers 依赖项,并添加一个 div 元素来容纳地图:

    <!-- Map DOM element --> 
    <div id="ch3_wfs" style="width: 100%; 
        height: 100%;"></div> 
    
    
  2. OpenLayers.ProxyHost 变量设置为我们的代理 URL:

    <!-- The magic comes here --> 
    <script type="text/javascript"> 
        OpenLayers.ProxyHost = "./utils/proxy.php?url="; 
    
    
  3. 初始化地图并添加一个基本图层:

        // Create the map using the specified DOM element 
        var map = new OpenLayers.Map("ch3_wfs");    
    
        var baseLayer = new 
            OpenLayers.Layer.OSM("OpenStreetMap"); 
        map.addLayer(baseLayer); 
    
        map.addControl(new 
            OpenLayers.Control.LayerSwitcher()); 
        map.setCenter(new OpenLayers.LonLat(0,0), 2); 
    
    
  4. 最后,创建一个使用 WFS 协议访问数据源的矢量图层:

        var statesLayer = new 
            OpenLayers.Layer.Vector("States", { 
            protocol: new OpenLayers.Protocol.WFS({ 
                url: "http://demo.opengeo.org/geoserver/wfs", 
                featureType: "states", 
                featureNS: "http://www.openplans.org/topp" 
            }), 
            strategies: [new OpenLayers.Strategy.BBOX()] 
        }); 
        map.addLayer(statesLayer); 
    </script>
    
    

它是如何工作的...

第一个重要步骤是设置 OpenLayers.ProxyHost 变量:

    OpenLayers.ProxyHost = "./utils/proxy.php?url="; 

OpenLayers 中的大多数 JavaScript 请求都是通过辅助类 OpenLayers.Request 进行的,该类会检查前一个变量是否已设置。如果是这样,所有请求都将通过代理进行。

然后,这个食谱中的主要操作是创建一个从 WFS 服务器填充数据的矢量图层:

    var statesLayer = new OpenLayers.Layer.Vector("States", { 
        protocol: new OpenLayers.Protocol.WFS({ 
            url: "http://demo.opengeo.org/geoserver/wfs", 
            featureType: "states", 
            featureNS: "http://www.openplans.org/topp" 
        }), 
        strategies: [new OpenLayers.Strategy.BBOX()] 
    }); 

如你所见,唯一要做的就是设置图层要使用的协议。在这种情况下,我们使用 OpenLayers.Protocol.WFS 类的一个实例。

WFS 协议构造函数有很多参数,但最重要的如下:

  • url: WFS 服务器的 URL

  • featureType: 要查询的特征

  • featureNS: 特征的命名空间

其他重要选项以及或多或少常用的选项如下:

  • geometryName: 指定存储特征几何信息的属性名称。默认为the_geom

  • srsName: 请求中使用的空间参考系统。默认为"EPSG:4326"

最后,向量层使用一个OpenLayers.Strategy.BBOX策略,该策略负责在地图的视口每次更改时刷新图层的内容。

更多...

许多时候,支持 WMS 和 WFS 协议的地图服务器,可以以栅格和向量格式同时提供相同的信息。

想象一组存储在 PostgreSQL/PostGIS 中的区域和地图服务器,例如 GeoServer,其中配置了一个国家图层,既可以通过 WMS 请求作为栅格图像提供,也可以通过 WFS 请求作为向量 GML 格式提供。

在这些情况下,如果我们之前创建了一个OpenLayers.Layer.WMS图层,有一个简单的方法可以创建一个新的 WFS 协议实例,即使用静态方法OpenLayers.Protocol.WFS.fromWMSLayer

给定一个 WMS 层和一些选项,该方法初始化一个OpenLayers.Protocol.WFS实例,假设 WFS 的urlsrsName和其他属性与 WMS 实例相同。

参见

  • 在 WFS 请求中过滤特征的方法

  • 与弹出窗口一起工作的方法

  • 使用点特征作为标记的方法

  • 直接使用协议读取特征的方法

使用聚类策略

正如我们在章节引言中看到的,向量层的行为由我们附加给它们的策略决定。

想象一个场景,我们想要展示世界上每个城市的所有博物馆。当用户在地图内导航并设置缩放级别以查看整个世界时会发生什么?我们简单地看到一个点云,所有点都在同一个地方。

解决这个问题的方法是聚类每个缩放级别的特征。

使用聚类策略

这个方法展示了在向量层上使用聚类策略是多么简单,该策略负责将特征聚类以避免我们刚才提到的情况。

如何实现...

  1. 创建一个 HTML 文件,并在其中插入以下代码:

    <!-- Map DOM element -->
    <div id="ch3_cluster" style="width: 100%; 
        height: 100%;"></div>
    
    <!-- The magic comes here -->
    <script type="text/javascript">
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch3_cluster");    
    
        layer = new OpenLayers.Layer.OSM("OpenStreetMap");
        map.addLayer(layer);
    
        map.addControl(new 
           OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(0,0), 2);
    
    
  2. 如您所见,向量层正在使用两种策略:

        // World Cities
        var citiesLayer = new OpenLayers.Layer.Vector("World 
            Cities (GeoJSON)", {
            protocol: new OpenLayers.Protocol.HTTP({
                url: "http://localhost:8080/
                openlayers-cookbook/recipes/
                data/world_cities.json",
                format: new OpenLayers.Format.GeoJSON()
            }),
            strategies: [
                new OpenLayers.Strategy.Fixed(), 
                new OpenLayers.Strategy.Cluster({distance: 
                    15})
            ]
        });
        map.addLayer(citiesLayer);
    </script>
    
    

它是如何工作的...

一个向量层可以与多个策略相关联。在这个方法中,我们添加了OpenLayers.Strategy.Fixed策略,它只加载一次图层内容,以及OpenLayers.Strategy.Cluster策略,该策略自动聚类特征以避免由重叠引起的丑陋特征云:

        strategies: [
            new OpenLayers.Strategy.Fixed(), 
            new OpenLayers.Strategy.Cluster({distance: 15})
        ]

每次我们更改缩放级别时,聚类策略都会计算所有特征之间的距离,并将符合同一聚类某些参数的所有特征添加到聚类中。

我们可以使用以下主要参数来控制聚类策略的行为:

  • distance: 被考虑在同一个簇中的特征之间的像素距离。默认设置为 20 像素。

  • threshold: 如果簇中的特征数量小于阈值,则它们将直接添加到图层而不是簇中

还有更多...

OpenLayers 提供了一套基本但非常常见的策略,我们可以将其组合到矢量图层中:

  • 策略,每次地图视口改变时请求特征

  • 刷新 策略,在一段时间后定期更新图层特征

  • 过滤 策略以限制图层必须请求的特征

我们鼓励那些更高级的 JavaScript 读者仔细查看 OpenLayers 源代码,并了解更多关于策略如何工作的信息。

相关内容

  • 以编程方式创建特征 食谱

  • 从 WFS 服务器添加特征 食谱

WFS 请求中的特征过滤

与 WFS 服务器交互时的一个关键概念是过滤器的概念。

在许多其他规范中,OGC 定义了一个标准,该标准定义了用于过滤的符号表示法,即 Filter Encoding Specification

过滤器类似于 SQL 中的 WHERE 子句,并允许我们选择满足某些条件的特征。

注意

您可以在 OGC 网站上找到 Filter Encoding Specification,网址为 www.opengeospatial.org/standards/filter

正如我们在 第七章 中将看到的,特征样式,过滤器不仅用于查询特征,还用于定义样式规则。

OpenLayers 提供了一套适合与规范定义的过滤器一起工作的类:属性过滤器(PropertyIsEqualToPropertyIsLessThan 等),逻辑过滤器和空间过滤器(IntersectsWithin 等)。

WFS 请求中的特征过滤

此食谱展示了使用过滤器类的基本用法来限制在 WFS 服务器上查询的特征。

准备工作

我们将要查询一个远程 WFS 服务器,因此我们需要一个代理脚本,该脚本配置在我们的服务器上以执行真实的 WFS 请求。

有关代理脚本的信息,请参阅本章的 从 WFS 服务器添加特征 食谱。

如何做到这一点...

  1. 创建一个 HTML 文件并插入以下代码:

    <!-- Map DOM element -->
    <div id="ch3_filtering" style="width: 100%; 
        height: 100%;"></div>
    
    <!-- The magic comes here -->
    <script type="text/javascript">
    
    
  2. JavaScript 代码中的第一步是设置解决跨域请求策略所需的代理脚本:

        OpenLayers.ProxyHost = "./utils/proxy.php?url=";
    
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch3_filtering"); 
    
    
  3. 将 OSM 设置为底图:

        var baseLayer = new 
            OpenLayers.Layer.OSM("OpenStreetMap");
        map.addLayer(baseLayer);
    
        map.addControl(new 
            OpenLayers.Control.LayerSwitcher());
    
    
  4. 要将地图视口中心定位在具体位置,我们需要将所需位置从纬度/经度转换为底图使用的投影,即地图使用的投影:

        var center = new OpenLayers.LonLat(-100, 41);
        center.transform(new 
            OpenLayers.Projection("EPSG:4326"), 
            map.getProjectionObject());
        map.setCenter(center, 4);
    
    
  5. 添加一个矢量图层,它请求一些状态:

        // Filter features with the query.
        var statesLayer = new 
            OpenLayers.Layer.Vector("States", {
            protocol: new OpenLayers.Protocol.WFS({
                url: "http://demo.opengeo.org/geoserver/wfs",
                featureType: "states",
                featureNS: "http://www.openplans.org/topp"
            }),
            strategies: [new OpenLayers.Strategy.BBOX()],
            filter: new OpenLayers.Filter.Logical({
                type: OpenLayers.Filter.Logical.AND,
                filters: [
                    new OpenLayers.Filter.Comparison({
                        type: OpenLayers.Filter.
                            Comparison.GREATER_THAN,
                        property: "MALE",
                        value: "700000"
                    }),
                    new OpenLayers.Filter.Spatial({
                        type: 
                            OpenLayers.Filter.Spatial.WITHIN,
                        value: OpenLayers.Bounds.fromArray
                            ([-120, 10,-90,50])
                    })
                ]
            })
        });
    
        map.addLayer(statesLayer);    
    </script>
    
    

它是如何工作的...

此食谱的主要部分是涉及矢量图层实例化的代码。构造函数接收两个参数,名称和一个选项对象。在选项对象中,我们设置了三个属性:

    var statesLayer = new OpenLayers.Layer.Vector("States", {
        protocol: ...,
        strategies: ...,
        filter: ...
    });

让我们看看该层中使用的协议、策略和过滤器。我们正在查询一个 WFS 服务器,因此我们需要使用一个 OpenLayers.Protocol.WFS 实例来与之通信:

OpenLayers.Protocol.WFS instance to talk to it:
        protocol: new OpenLayers.Protocol.WFS({
            url: "http://demo.opengeo.org/geoserver/wfs",
            featureType: "states",
            featureNS: "http://www.openplans.org/topp"
        })

注意

与 WMS 一样,WFS 服务器有 GetCapabilities 动作,它允许客户端了解它提供的功能:特征的类型、可用的操作等等。

检查菜谱中使用的服务器返回的响应:demo.opengeo.org/geoserver/wfs?request=GetCapabilities

作为策略,我们希望层在地图视口每次修改时刷新功能,因此 OpenLayers.Strategy.BBOX 是正确的实例:

        strategies: [new OpenLayers.Strategy.BBOX()],

最后,有一个 filter 属性,在这个菜谱中执行所有魔法。我们尝试使用一个或多或少完整的过滤器,它包括一个逻辑过滤器、一个比较过滤器和一个空间过滤器:

        filter: new OpenLayers.Filter.Logical({
            type: OpenLayers.Filter.Logical.AND,
            filters: [
                new OpenLayers.Filter.Comparison({
                    type: OpenLayers.Filter.
                        Comparison.GREATER_THAN,
                    property: "MALE",
                    value: "700000"
                }),
                new OpenLayers.Filter.Spatial({
                    type: OpenLayers.Filter.Spatial.WITHIN,
                    value: OpenLayers.Bounds.fromArray
                        ([-120, 10,-90,50])
                })
            ]
        })

根据过滤器的类型,它们可以有不同的属性和不同的允许值。

我们的过滤器查询 WFS 服务器中指定层内的所有状态,这些状态位于由 [-120, 10,-90,50] 定义的边界框内,并且 MALE 人口数量大于 700,000

更多...

OpenLayers.Protocol 类有一个 defaultFilter 属性,它允许我们为请求设置默认过滤器。

矢量层中指定的过滤器将是逻辑 AND 运算符,它在发出请求之前合并。

参见

  • 从 WFS 服务器添加功能 菜单

  • 直接使用协议读取功能 菜单

直接使用协议读取功能

OpenLayers 允许我们从不同的来源和资源读取数据。正如我们在章节引言中所描述的,OpenLayers 提供了辅助类:协议和格式。

协议旨在简化从不同来源检索数据的任务:通过 HTTP、从 WFS 服务器等。

另一方面,格式简化了从(或写入)给定数据格式的任务。从不同来源加载数据并知道如何直接与可以极大地简化此任务的协议一起工作是非常常见的。

直接使用协议读取功能

例如,这个菜谱展示了我们如何通过直接与协议实例工作,在同一个矢量层中添加来自不同数据源的功能。

如何操作...

  1. 创建一个 HTML 文件并添加 OpenLayers 依赖项。然后创建一个 DOM 元素来保存地图:

    <!-- Map DOM element -->
    <div id="ch3_protocol" style="width: 100%; 
        height: 100%;"></div>
    
    
  2. 接下来,初始化地图,添加一些基本图层,并定位视口:

    <script type="text/javascript">
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch3_protocol");    
    
        var baseLayer = new 
            OpenLayers.Layer.OSM("OpenStreetMap");
        map.addLayer(baseLayer);
    
        map.addControl(new 
            OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(0,0), 2);
    
    
  3. 现在,创建一个矢量层:

        var vectorLayer = new 
            OpenLayers.Layer.Vector("Vector Layer");
        map.addLayer(vectorLayer);
    
    
  4. 创建两个指向所需远程文件的协议:

        // Create HTTP protocol to read GML file
        var gmlReq = new OpenLayers.Protocol.HTTP({
            url: "http://localhost:8080/
            openlayers-cookbook/recipes/data/
            world_cities.json",
            format: new OpenLayers.Format.GeoJSON(),
            callback: addFeaturesFromResponse
        });
        gmlReq.read();
    
        // Create HTTP protocol to read KML file
        var kmlReq = new OpenLayers.Protocol.HTTP({
            url: "http://localhost:8080/
            openlayers-cookbook/recipes/data/
            global_undersea.kml",
            format: new OpenLayers.Format.KML({
                extractStyles: true,
                extractAttributes: true
            }),
            callback: addFeaturesFromResponse
        });
        kmlReq.read();
    
    
  5. 最后,添加当协议实例从远程文件加载数据时要执行的回调函数:

        // Translate features from EPSG:4326 to OSM 
        // projection and add to the layer only 
        // the Point geometry features.
        function addFeaturesFromResponse(response) {
            for(var i=0; i<response.features.length; ++i) {        
                if(response.features[i].
                geometry.CLASS_NAME == 
                "OpenLayers.Geometry.Point") {
                    response.features[i].geometry.transform
                    (vectorLayer.projection, 
                    map.getProjectionObject());
                    vectorLayer.addFeatures
                        ([response.features[i]]);
                }
            }
        }
    </script>
    
    

工作原理...

这个菜谱的目标是展示我们如何直接与协议一起工作,并在同一个矢量层中从不同的数据源加载数据。

由于这个原因,我们创建了一个空的矢量层,没有指定要使用的协议和策略:

    var vectorLayer = new OpenLayers.Layer.Vector("Vector Layer");

之后,我们创建了一个 OpenLayers.Protocol.HTTP 实例,用于读取远程的 GeoJSON 文件:

    var gmlReq = new OpenLayers.Protocol.HTTP({
        url: "http://localhost:8080/
            openlayers-cookbook/recipes/data/world_cities.json",
        format: new OpenLayers.Format.GeoJSON(),
        callback: addFeaturesFromResponse
    });

注意我们如何指定一个回调函数,该函数将在文件加载并读取后调用,并使用所需的格式读取它。该函数接收一个类型为 OpenLayers.Protocol.Response 的参数,其中包含一个 features 数组属性,其中包含从文件中读取的特征集。

要使协议开始读取过程,我们只需调用:

gmlReq.read();

最后,让我们看看回调函数。当两个协议都完成数据读取时,会调用此函数。我们已实现它以将特征转换为正确的投影,并将类型为 OpenLayers.Geometry.Point: 的特征添加到矢量层中:

    function addFeaturesFromResponse(response) {
        for(var i=0; i<response.features.length; ++i) {        
            if(response.features[i].geometry.CLASS_NAME == "OpenLayers.Geometry.Point") {
                response.features[i].geometry.transform(vectorLayer.projection, map.getProjectionObject());
                vectorLayer.addFeatures([response.features[i]]);
            }
        }
    }

如我们所见,这是在矢量层中过滤我们放入内容的另一种方式,但请注意,过滤是在客户端而不是在服务器端进行的。这意味着整个数据都是从服务器传输到客户端的。

更多内容...

我们想提到的是,在这个配方中我们没有设置 OpenLayers.ProxyHost 变量。这是因为我们通过 AJAX 请求的文件与加载 HTML 文件的域相同。

参见

  • 添加 GML 层 的配方

  • 从 WFS 服务器添加特征 的配方

  • 在 WFS 请求中过滤特征 的配方

第四章. 使用事件

在本章中,我们将介绍以下内容:

  • 创建并排地图比较器

  • 实现地图图层的工作进度指示器

  • 监听矢量图层特征的事件

  • 监听非 OpenLayers 事件

简介

本章专注于事件,这是任何 JavaScript 程序中的重要概念。尽管本章内容简短,但这里解释的概念对于使用 OpenLayers 来说非常重要。

事件是 JavaScript 的核心。它们是产生反应的冲动。作为地图应用程序的程序员,我们感兴趣的是在地图缩放改变、图层加载或特征被添加到图层时做出反应。任何可能发出事件的类都负责管理其监听器(那些在事件被触发时感兴趣的人)并在某些情况下发出事件。

例如,我们可以注册一个函数,监听OpenLayers.Map实例上的zoomend事件。每当地图实例改变其缩放时,它负责触发zoomend事件,因此所有监听器都将通过新事件得到通知。

为了帮助完成所有这些过程,OpenLayers 有一个OpenLayers.Events类,负责注册监听器并简化向所有监听器触发事件的操作。具体来说,它允许:

  • 定义事件

  • 注册监听器

  • 触发事件以通知所有监听器

许多类,如OpenLayers.MapOpenLayers.Layer,都有一个events属性,它是一个OpenLayers.Events的实例,负责注册对通知感兴趣的事件监听器。

此外,这些类通常定义一个EVENT_TYPES数组属性(它是常量)并列出你可以为该类注册的可用事件。例如,对于OpenLayers.Map类,EVENT_TYPES被设置为以下内容:

    EVENT_TYPES: [ 
        "preaddlayer", "addlayer","preremovelayer", "removelayer", 
        "changelayer", "movestart",
        "move", "moveend", "zoomend", "popupopen", "popupclose",
        "addmarker", "removemarker", "clearmarkers", "mouseover",
        "mouseout", "mousemove", "dragstart", "drag", "dragend",
        "changebaselayer"]

作为程序员,你需要查看 OpenLayers API 文档(dev.openlayers.org/releases/OpenLayers-2.11/doc/apidocs/files/OpenLayers/Map-js.html),或者你也可以参考源代码以了解可以在每个类上注册的可用事件。

创建并排地图比较器

我们将创建一个地图比较器。目标是使两个来自不同提供商的地图并排显示,并使用OpenLayers.Map实例提供的某些事件来保持地图在相同的位置和缩放级别上同步。

创建并排地图比较器

如何实现...

要使两个地图并排显示,请执行以下步骤:

  1. 开始创建一个带有 OpenLayers 库依赖的 HTML。

  2. 现在,添加所需的 HTML 代码以使两个地图并排显示。这里我们使用一个带有行和两列的表格:

    <table style="width: 100%; height: 95%;">
        <tr>
            <td>
                <div id="ch04_map_a" style="width: 100%; height: 100%;"></div>
            </td>
            <td>
                <div id="ch04_map_b" style="width: 100%; height: 100%;"></div>
            </td>
        </tr>
    </table>
    
    
  3. 现在,让我们编写 JavaScript 代码。创建两个地图并使用期望的图像提供者进行初始化。这里我们使用了OpenStreetMapBing:

    <script type="text/javascript">
        // Create left hand side map
        var map_a = new OpenLayers.Map("ch04_map_a");    
        var layer_a = new OpenLayers.Layer.OSM("OpenStreetMap");
        map_a.addLayer(layer_a);
        map_a.setCenter(new OpenLayers.LonLat(0,0), 2);
    
        // Create right hand side map
        var map_b = new OpenLayers.Map("ch04_map_b");    
        var bingApiKey = "AvcVU_Eh1H2_xVcK0EeRO70MD7Zm6qwLhrVC12C3D997DylUewCWaKR9XTZgWwu6";
        var layer_b = new OpenLayers.Layer.Bing({
            name: "Road",
            type: "Road",
            key: bingApiKey
        });
        map_b.addLayer(layer_b);
        map_b.setCenter(new OpenLayers.LonLat(0,0), 2);
    
    
  4. 现在,在两个图层上注册movezoomend事件:

        // Register events on map_a using 'on':
        map_a.events.on({
            "move": moveListener,
            "zoomend": zoomListener
        });
    
        // Register events on map_a using 'register':
        map_b.events.register("move", null, moveListener);
        map_b.events.register("zoomend", null, zoomListener);
    
    
  5. 最后,实现每次事件发生时被调用的listener函数:

        // Listener functions
        function moveListener(event) {
            if(event.object == map_a) {
                map_b.setCenter(map_a.getCenter());
            } else {
                map_a.setCenter(map_b.getCenter());
            }
        }
        function zoomListener(event) {
            if(event.object == map_a) {
                map_b.zoomTo(map_a.getZoom()-1);
            } else {
                map_a.zoomTo(map_b.getZoom()+1);
            }
        }
    </script>
    
    

它是如何工作的...

为了使两个地图始终在相同的位置和缩放级别上保持同步,我们需要知道地图何时移动以及缩放级别何时改变。

move事件在每次地图移动时都会触发。此外,还有movestartmoveend事件,它们只在move动作开始或结束时触发,但在这里它们并不有用,因为我们需要捕捉到每一次移动。

当地图的缩放级别改变时,会触发zoomend事件。那么,我们如何监听地图中的事件呢?这是通过events属性实现的,它是一个OpenLayers.Events的实例。

有两种方式(实际上还有第三种,我们将在还有更多部分看到)来注册地图事件的监听器——使用onregister方法。

在第一张地图上,我们使用了on方法一次性注册多个事件:

    map_a.events.on({
        "move": moveListener,
        "zoomend": zoomListener
    });

on方法需要一个对象,其中其属性的名称是事件名称,值是在事件触发时被调用的监听函数。

on方法接受一个特殊属性,称为scope。这允许我们将所有指定的事件注册为在相同的上下文中执行。也就是说,当listener函数执行时,this关键字将指向scope属性中指定的对象。

注意

对于刚开始学习 JavaScript 的人来说,上下文可能是一个高级话题。一个有趣的讨论可以在stackoverflow.com/questions/1798881/javascript-context找到。

在第二张地图上,我们使用了register方法,它允许我们一次注册一个事件监听器:

    map_b.events.register("move", null, moveListener);
    map_b.events.register("zoomend", null, zoomListener);

events.register()函数接受四个参数:

  • type: 这是我们要监听的事件。

  • object: 这是函数执行的环境(类似于on方法中的scope属性)。

  • function: 这是事件触发时要执行的功能。

  • priority: 这是一个布尔值。如果它是true,则监听器将在事件队列的前端排队,而不是在末尾。

现在,我们将被通知任何两个地图产生的movezoomend事件。

重要的是要注意,OpenLayers 事件机制总是通过传递一个event参数来调用listener函数。这个event对象包含由触发事件的source对象编写的任何信息,以及以下三个总是自动添加的属性:

  • type: 包含事件名称(移动、zoomend等)

  • object: 指向触发事件的对象

  • element: 与事件相关的DOM元素

让我们看看我们的监听器函数。moveListener函数检查哪个地图触发了事件,然后获取地图的center,并将相同的center分配给另一个地图:

    function moveListener(event) {
        if(event.object == map_a) {
            map_b.setCenter(map_a.getCenter());
        } else {
            map_a.setCenter(map_b.getCenter());
        }
    }

如您所见,我们可以通过event.object.获取触发事件的地图的引用。

类似地,zoomListener函数获取源事件地图上的缩放级别,并将其应用于另一个地图。

    function zoomListener(event) {
        if(event.object == map_a) {
            map_b.zoomTo(map_a.getZoom()-1);
        } else {
            map_a.zoomTo(map_b.getZoom()+1);
        }
    }

注意

Bing 地图在其影像上具有与OpenStreetMap不同的分辨率级别。我们可以这样说,与其他影像提供商相比,它相差一个缩放级别,因为我们是在添加或从缩放级别中减去这个值。

更多...

正如我们可以对监听事件感兴趣一样,我们也可以对停止通知感兴趣。

OpenLayers.Events类有ununregister方法,允许我们在某些事件被触发时注销我们的监听器函数。

on方法类似,un方法允许注销多个监听器,而unregister方法允许一次注销一个监听器。以这个配方为例,我们可以在地图上注销事件如下:

    map_a.events.un({
        "move": moveListener,
        "zoomend": zoomListener
    });
    map_b.events.unregister("move", null, moveListener);

注册事件监听器的另一种方法

除了onregister方法之外,还有第三种注册事件监听器的方法。

当创建OpenLayers.MapOpenLayers.LayerOpenLayers.Control实例时,我们可以使用eventListeners属性,就像我们使用on方法来注册一组监听器一样。例如:

map = new OpenLayers.Map('map', { 
    eventListeners: { 
        "move": moveListener, 
        "zoomend": zoomListener 
    } 
});

实际上发生的情况是传递给eventListener属性的对象直接通过on方法用来初始化监听器。

参见

  • 在第二章的使用 Bing 影像配方中,添加栅格图层

  • 实现地图图层的工作进度指示器的配方

  • 监听矢量图层事件的配方

实现地图图层的工作进度指示器

在创建优秀应用程序的艺术中,最重要的考虑因素是用户体验。一个好的应用程序完成它必须做的事情,但通过让用户感到舒适。

当与远程服务器一起工作时,大多数时间用户都在等待数据检索。例如,当与 WMS 图层一起工作时,每次我们更改缩放级别,用户都必须等待几秒钟,直到从服务器获取数据并且瓦片开始渲染。

通过使用图标、进度条等来向用户显示一些反馈,告知应用程序正在工作但需要一些时间,这将非常棒。

这个配方展示了我们如何通过通知应用程序正在从不同的服务器加载数据,利用一些图层事件,来给用户一些反馈。

注意

就像在这本书的许多其他配方中一样,我们使用了 Dojo 工具包框架(dojotoolkit.org)来提供更好的用户体验。我们可以看到的主要区别是,一个基本的 HTML 页面是它提供的丰富小部件(按钮、工具栏、进度条等)的集合。如果 HTML 页面上的某些内容不清楚,请不要担心,这本书的目标不是教授 Dojo,这不会改变对 OpenLayers 概念的说明。

如何做到这一点...

执行以下步骤:

  1. 创建一个包含 OpenLayers 依赖项的 HTML 文件。

  2. 首先,我们将添加显示进度条的 HTML 代码。注意它如何通过使用 Dojo 框架简单创建。使用data-dojo-typedata-dojo-props属性标记一个普通的span元素。

    <span data-dojo-type="dijit.ProgressBar" style="width: 100px;" id="progress"
          data-dojo-props="'indeterminate': true, 
          label:''"></span>
    
    
  3. 和往常一样,放置div元素以容纳地图:

    <div id="ch04_work_progress" style="width: 100%; height: 100%;"></div>
    
    
  4. 对于开始 JavaScript 部分代码,我们需要考虑到我们正在从远程 WFS 服务器请求功能,因为这是我们需要做的第一件事来设置要使用的代理 URL:

    <!-- The magic comes here -->
    <script type="text/javascript">
        OpenLayers.ProxyHost = "./utils/proxy.php?url=";
    
    
  5. 现在,创建地图和两个图层——一个 WMS 图层,它是基础图层,和一个 WFS 图层:

        // Create left map
        var map = new OpenLayers.Map("ch04_work_progress");   
        var wms = new OpenLayers.Layer.WMS("Basic", 
        "http://labs.metacarta.com/wms/vmap0",
        {
            layers: 'basic'
        });        
        var wfs = new OpenLayers.Layer.Vector("States", {
            protocol: new OpenLayers.Protocol.WFS({
                url: "http://demo.opengeo.org/geoserver/wfs",
                featureType: "states",
                featureNS: "http://www.openplans.org/topp"
            }),
            strategies: [new OpenLayers.Strategy.BBOX()]
        });
        map.addLayers([wms, wfs]);
    
    
  6. 添加图层切换控件并集中地图:

        map.addControl(new 
        OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(-100, 41), 8);
    
    
  7. 在 WMS 和 WFS 图层上注册事件监听器:

        // Register events on layers using 'on':
        wms.events.on({
            "loadstart": updateLoader,
            "loadend": updateLoader, 
            "loadcancel": updateLoader
        });
        wfs.events.on({
            "loadstart": updateLoader,
            "loadend": updateLoader, 
            "loadcancel": updateLoader
        });
    
    
  8. 最后,实现监听函数,以便在两个图层中的任何一个加载其内容时显示进度条:

        // Listener functions
        var wmsLoading = false;
        var wfsLoading = false;
        function updateLoader(event) {
            var progress = dijit.byId('progress');
            if(event.type == "loadstart") {
                if(event.object == wms) {
                    wmsLoading = true;
                }
                if(event.object == wfs) {
                    wfsLoading = true;
                }
    
                var label = "";
                if(wmsLoading) {
                    label += "WMS ";
                }
                if(wfsLoading) {
                    label += "+ WFS";
                }
    
                progress.set('value', 'Infinity');
                progress.set('label', label);
                dojo.style(progress.domNode, "visibility", "visible");
            } else {
                if(event.object == wms) {
                    wmsLoading = false;
                }
                if(event.object == wfs) {
                    wfsLoading = false;
                }
                progress.set('value', '0');
                dojo.style(progress.domNode, "visibility", "hidden");
            }
        }
    </script>
    
    

它是如何工作的...

在创建地图和两个图层之后,在两个图层上注册我们的监听函数,用于事件loadstart, loadendloadcancel

    wms.events.on({
        "loadstart": updateLoader,
        "loadend": updateLoader, 
        "loadcancel": updateLoader
    });
    wfs.events.on({
        "loadstart": updateLoader,
        "loadend": updateLoader, 
        "loadcancel": updateLoader
    });

这些是所有图层都有的常见事件,因为它们是从OpenLayers.Layer类继承而来的。

当图层开始加载数据时,会触发loadstart事件,而loadendloadcancel则会在过程结束或被取消时触发。

在这个前提下,繁琐的updateLoader监听函数负责在两个图层中的任何一个加载数据时显示一个不确定的进度条和文本消息。文本消息可以是 WMS、WFS 或 WMS WFS,具体取决于正在加载内容的图层。

更多...

正如我们之前提到的,这个配方中使用的这些事件对所有图层都是通用的。

OpenLayers.Layer类的具体子类可以有自己的事件,例如OpenLayers.Layer.Vector,它有事件来通知特征被添加、删除等情况。

参见

  • 第二章中的添加 WMS 图层配方,添加栅格图层

  • 从 WFS 服务器添加功能中的从 WFS 服务器添加功能配方,处理矢量图层

  • 创建并排地图比较器的配方

  • 监听矢量图层特征事件的配方

监听矢量图层特征事件

当与矢量层一起工作时,通常会遇到需要了解正在发生什么的情况,即当新特征即将被添加到层中或特征已被修改、删除等情况。幸运的是,矢量层具有触发大量事件的能力。

本菜谱的目标是展示在矢量层中监听事件是多么容易,以及了解其上正在发生什么。

我们将加载一个包含世界各地一些城市的 GML 文件,并根据某些特征属性设置其填充颜色。

监听矢量层特征事件

如何操作...

  1. 创建一个 HTML 文件并添加 OpenLayers 库的依赖文件。然后,添加一个 div 元素来定义放置地图实例的位置:

    <div id="ch04_vector_layer_listener" style="width: 100%; height: 100%;"></div>
    
    
  2. 初始化地图实例,添加基本层,并中央化视口:

    <!-- The magic comes here -->
    <script type="text/javascript">
        // Create map
        var map = new 
        OpenLayers.Map("ch04_vector_layer_listener");    
        var layer = new 
        OpenLayers.Layer.OSM("OpenStreetMap");
        map.addLayer(layer);
        map.setCenter(new OpenLayers.LonLat(0,0), 4);
    
    
  3. 创建一个矢量层来读取 GML 文件。同时,通过注册一个监听 beforefeatureadded 事件的监听器来初始化它:

        var vectorLayer = new 
        OpenLayers.Layer.Vector("States", {
            protocol: new OpenLayers.Protocol.HTTP({
                url: "http://localhost:8080/openlayers-
                cookbook/recipes/data/world_cities.json",
                format: new OpenLayers.Format.GeoJSON()
            }),
            strategies: [new OpenLayers.Strategy.Fixed()],
            eventListeners: {
                "beforefeatureadded": featureAddedListener
            }
        });
        map.addLayer(vectorLayer);
    
    
  4. 编写监听函数的代码。定义一个颜色方案,根据 POP_RANK 属性为每个特征分配填充颜色:

        // Define color palette
        var colors = [
            "#CC0000",
            "#FF0000",
            "#FF3300",
            "#FF6600",
            "#FF9900",
            "#FFCC00",
            "#FFFF00"
        ];
        function featureAddedListener(event){
            // Set feature color depending on the rank attribute
            var feature = event.feature;
            var rank = feature.attributes.POP_RANK;
            feature.style = OpenLayers.Util.extend({}, OpenLayers.Feature.Vector.style['default']);
            feature.style.fillColor = colors[rank-1];
        }
    </script>
    
    

工作原理...

在初始化地图和基本层之后,我们必须创建一个矢量层:

    var vectorLayer = new OpenLayers.Layer.Vector("States", {
        protocol: new OpenLayers.Protocol.HTTP({
            url: "http://localhost:8080/openlayers-cookbook/recipes/data/world_cities.json",
            format: new OpenLayers.Format.GeoJSON()
        }),
        strategies: [new OpenLayers.Strategy.Fixed()],
        eventListeners: {
            "beforefeatureadded": featureAddedListener
        }
    });

作为一种协议,我们使用 OpenLayers.Protocol.HTTP 实例,它将通过 HTTP 协议从指定的 URL 获取数据,并使用 OpenLayers.Format.GeoJSON 格式读取器读取它。

矢量层使用 OpenLayers.Strategy.Fixed,这意味着无论我们是否移动地图的视口,内容都只加载一次。

有几种方法可以注册事件监听器。其中一种方法是通过 onregister 方法,但我们选择在初始化层时使用 eventListener 属性同时注册事件监听器。

这样,每次当特征即将被添加到层中(在它被添加之前),监听函数都会被调用,并接收一个作为参数的 event 对象,其中包含与层事件相关的某些信息:

    function featureAddedListener(event){
        var feature = event.feature;
        var rank = feature.attributes.POP_RANK;
        feature.style = OpenLayers.Util.extend({}, OpenLayers.Feature.Vector.style['default']);
        feature.style.fillColor = colors[rank-1];
    }

从事件中,我们可以获取到特征及其属性的引用。在这里,我们使用 POP_RANK 属性来选择特征的填充颜色。

注意

关于我们可以更改的特征样式属性,更多信息请参阅 dev.openlayers.org/releases/OpenLayers-2.11/doc/apidocs/files/OpenLayers/Feature/Vector-js.html#OpenLayers.Feature.Vector.OpenLayers.Feature.Vector.style

更多内容...

在本菜谱中,我们可以使用 OpenLayers.Util.extend 方法设置特征的初始样式,然后设置所需的填充颜色:

feature.style = OpenLayers.Util.extend(
    {},
    OpenLayers.Feature.Vector.style['default']
);

OpenLayers.Util.extend 方法需要两个参数——目标和源对象。其功能是将源对象中找到的所有属性复制到目标对象。

注意

OpenLayers.Util.extend 方法对于在 OpenLayers 中创建层次结构和继承非常重要。然而,它的命名空间是 OpenLayers.Util,它位于 OpenLayers/BaseTypes/Class.js 文件中,该文件讨论了其重要性。

另一方面,OpenLayers.Feature.Vector.style 是一个对象,其中包含一些预定义的样式,如 default, selected, delete 等。

因此,前面的行意味着可以创建一个新的对象,它扩展了一个空对象,并包含 OpenLayers.Feature.Vector.style['default'] 对象中的所有属性。

参见

  • 使用符号化器设置要素样式 策略在 第七章,设置要素样式

  • 在 第三章 的 添加 GML 层 策略中,处理矢量层

  • 创建并排地图比较器 策略

  • 监听非 OpenLayers 事件 策略

监听非 OpenLayers 事件

在开发网络地图应用时,OpenLayers 的使用只是我们需要使用的工具集中的一个部分。添加其他组件,如按钮、图片、列表等,并与它们交互是其他我们必须完成的任务。

OpenLayers.Map 实例或 OpenLayers.Layer 子类交互很简单,因为它们会触发特定的事件,但如果我们想在按钮或任何 DOM 元素上监听事件怎么办呢?

为了这个目的,OpenLayers 为我们提供了 OpenLayers.Event 类(不要与复数 OpenLayers.Events 类混淆)。这是一个辅助类,它允许我们在浏览器无关的方式下监听非 OpenLayers 元素中的事件。

注意

不幸的是,在 JavaScript 中注册事件监听器的方法在所有浏览器中并不相同。此外,Microsoft 与 W3C(万维网联盟)在注册监听器的方式上有所不同。你可以在 www.quirksmode.org/js/events_advanced.html 找到更多信息。

如果你的项目使用 jQuery、Dojo 或 ExtJS 等库或框架,你可能会使用它们的功能来访问 DOM 元素、注册事件等。

如果你正在处理一个没有上述库的更简单的项目,通过 OpenLayers.Event 类注册事件是一个好主意,因为它与浏览器无关,这意味着你的应用程序将与更多浏览器兼容。

此外,还有一个原因要阅读这个策略,那就是 OpenLayers 使用 OpenLayers.Event 类来内部实现许多处理程序和控制,我们将在未来的章节中看到。

让我们看看如何通过 OpenLayers.Event 类在 HTML 元素上监听事件。

监听非 OpenLayers 事件

策略是创建六个按钮,并将六个点要素添加到矢量层中。然后当鼠标进入按钮时突出显示要素,当鼠标离开时取消选择。

如何操作...

要监听非 OpenLayers 事件,请按照以下步骤操作:

  1. 创建一个带有 OpenLayers 库依赖的 HTML。开始为按钮添加一些 CSS 样式。以下代码定义了当按钮未被选中(鼠标不在上面)时的样式,以及当鼠标悬停在按钮上时具有不同背景色的样式:

    <style>
        .square {
            border: 1px solid #888;
            background-color: #0099FF;
            color: #fff;
            padding: 3px;
        }
        .square:hover {
            background-color: #0086d2;
        }
    </style>
    
    
  2. 创建一个表格来存放六个按钮。一个按钮将由一个带有标识符的 span 元素表示:

    <table>
        <tr>
            <td><span id="f0" class="square">Feature 
            1</span></td>
            <td><span id="f1" class="square">Feature 
            2</span></td>
            <td><span id="f2" class="square">Feature 
            3</span></td>
            <td><span id="f3" class="square">Feature 
            4</span></td>
            <td><span id="f4" class="square">Feature 
            5</span></td>
            <td><span id="f5" class="square">Feature 
            6</span></td>
        </tr>
    </table>
    <br/>
    
    
  3. 添加一个 div 元素来存放地图:

    <div id="ch04_dom_events" style="width: 100%; height: 100%;"></div>
    
    
  4. 现在,添加所需的 JavaScript 代码以实例化地图对象,设置基本图层,并添加矢量图层:

    <!-- The magic comes here -->
    <script type="text/javascript">
        // Create left map
        var map = new OpenLayers.Map("ch04_dom_events");    
        var osm = new OpenLayers.Layer.OSM();        
        // Create a vector layer with one feature for each 
        previous SPAN element
        var vectorLayer = new 
        OpenLayers.Layer.Vector("Features");
    
    
  5. 用六个特征填充矢量图层。每个特征都将包含代表它的按钮的标识符:

        var pointFeatures = [];
        for(var i=0; i< 6; i++) {
            // Create the ID
            var id = "f"+i;
            // Regiter listeners to handle when mouse enters 
            and leaves the DOM element
            OpenLayers.Event.observe(OpenLayers.Util.
            getElement(id), 'mouseover', mouseOverListener);
            OpenLayers.Event.observe(OpenLayers.Util.
            getElement(id), 'mouseout', mouseOutListener);
    
            // Create a random point
            var px = Math.random()*360-180;
            var py = Math.random()*160-80;
            var pointGeometry = new 
            OpenLayers.Geometry.Point(px, py);
            OpenLayers.Projection.transform(pointGeometry, 
            new OpenLayers.Projection("EPSG:4326"), new 
            OpenLayers.Projection("EPSG:900913"));
            var pointFeature = new 
            OpenLayers.Feature.Vector(pointGeometry, {
                elem_id: id
            });
            pointFeatures.push(pointFeature);
        }
        vectorLayer.addFeatures(pointFeatures);
    
        map.addLayers([osm, vectorLayer]);
        map.setCenter(new OpenLayers.LonLat(0, 0), 1);
    
    
  6. 最后,添加实现事件监听器的代码:

        // Listeners
        function mouseOverListener(event) {
            var id = event.target.id;
            var feature = vectorLayer.
            getFeaturesByAttribute('elem_id', id);
            vectorLayer.drawFeature(feature[0], "select");
        }
        function mouseOutListener(event) {
            var id = event.target.id;
            var feature = vectorLayer.
            getFeaturesByAttribute('elem_id', id);
            vectorLayer.drawFeature(feature[0], "default");
        }
    </script>
    
    

它是如何工作的...

我们创建了六个按钮,从 f0f5 进行标识,我们想要创建六个代表它们的特征。为此,在 for 循环中,首先创建一个带有标识符的字符串:

        var id = "f"+i;

然后,为 mouseovermouseout 事件注册一个事件监听器函数:

        OpenLayers.Event.observe(OpenLayers.Util.getElement(id), 
        'mouseover', mouseOverListener);
        OpenLayers.Event.observe(OpenLayers.Util.getElement(id), 
        'mouseout', mouseOutListener);

这是通过使用 OpenLayers.Event.observe 方法实现的,它需要三个参数。这些参数如下:

  • elementParam: 我们想要监听 tis 事件的 DOM 元素引用或其标识符

  • name: 你想要监听的事件

  • observer: 将作为监听器的函数

因为我们需要传递 DOM 元素引用,所以我们需要先获取它。当其标识符可用时,我们可以使用辅助方法 OpenLayers.Util.getElement 来获取元素引用。

elementParam 定义中,你可以看到使用 OpenLayers.Util.getElement 并不是严格必要的。如果我们传递一个 ID,OpenLayers.Event.observe 方法将内部使用 OpenLayers.Util.getElement 函数来获取元素引用,所以接下来的两行将产生相同的结果:

        OpenLayers.Event.observe(id, 'mouseover', mouseOverListener);
        OpenLayers.Event.observe(OpenLayers.Util.getElement(id), 'mouseover', mouseOverListener);

注意

OpenLayers.Util 类有许多方法可以帮助处理 DOM 元素、数组以及许多其他功能。我们鼓励您查看。

一旦注册了监听器,我们创建一个随机点功能并将其添加到矢量图层:

        var px = Math.random()*360-180;
        var py = Math.random()*160-80;
        var pointGeometry = new OpenLayers.Geometry.Point(px, py);

记住要将点坐标转换为地图使用的投影。在这种情况下,因为基本图层是 OSM,并且地图没有指定投影属性,所以将使用 OSM 投影:

        OpenLayers.Projection.transform(pointGeometry, new 
        OpenLayers.Projection("EPSG:4326"), new 
        OpenLayers.Projection("EPSG:900913"));
        var pointFeature = new 
        OpenLayers.Feature.Vector(pointGeometry, {
            elem_id: id
        });
        pointFeatures.push(pointFeature);

我们通过传递一个自定义属性 elem_id 创建了这个功能,该属性将存储代表该功能的按钮的标识符。这样我们就有了一个连接功能和按钮的引用。

以下截图显示了自定义属性是如何存储在功能的 attributes 属性中的:

它是如何工作的...

到目前为止,我们有了六个按钮和六个功能,它们将相应的按钮标识符作为自定义属性存储。现在,任务是实现监听器函数。让我们看看 mouseOverListener 函数。

    function mouseOverListener(event) {
        var id = event.target.id;
        var feature = 
        vectorLayer.getFeaturesByAttribute('elem_id', id);
        vectorLayer.drawFeature(feature[0], "select");
    }

从事件,它是一个浏览器 MouseEvent,我们获取触发事件的元素标识符:

        var id = event.target.id;

接下来,使用 OpenLayers.Layers.Vector.getFeatureByAttribute 方法,我们获取具有 elem_idid 的矢量层内的特征数组。当然,这里它将始终返回只包含一个元素的数组:

        var feature = 
        vectorLayer.getFeaturesByAttribute('elem_id', id);

现在,我们有了特征。只需用不同的渲染意图重新绘制它。选择突出显示选定的特征并将其样式恢复到默认值:

        vectorLayer.drawFeature(feature[0], "select");

注意

我们将在 第七章 的 样式化特征 中了解更多关于样式化特征的内容。同时,考虑将渲染意图作为预定义样式来渲染特征。

更多...

OpenLayers 定义了一个全局变量 $,它指向 OpenLayers.Util.getElement 函数,如果它不存在。这样我们就可以以简短的方式获取一个元素的引用。

例如,下面两行有相同的结果:

$("some_ID")
OpenLayers.Util.getElement("some_ID")

在使用 $ 函数时要小心。许多 JavaScript 库,其中最著名的是 jQuery 库 (jquery.com),也将全局 $ 对象定义为操作它的常用方式。因此,请检查你应用程序中导入库的顺序以及 $ 函数真正指向的位置。

作为一种好奇,当通过 OpenLayers.Util.getElement 函数根据其标识符获取元素引用时:

$("some_ID")

jQuery 库要求你使用 # 字符:

$("#some_ID")

停止观察

我们可能对观察某些事件感兴趣,同样,我们可能也希望停止观察它。

OpenLayers.Event.observe 方法类似,给定一个元素引用或一个字符串标识符,OpenLayers.Event.stopObservingElement 方法允许我们停止观察某些 DOM 元素。

参见

  • 在 第三章 的 程序化创建特征 菜单,处理矢量层

  • 在 第七章 的 使用符号化样式化特征 菜单,样式化特征

  • 创建并排地图比较器 菜单

  • 监听矢量层特征事件 菜单

添加控件

在本章中,我们将介绍以下内容:

  • 添加一些视觉控件

  • 添加 NavigationHistory 控件

  • 处理地理位置

  • 将控件放置在地图外部

  • 在多个矢量图层上编辑要素

  • 修改要素

  • 测量距离和面积

  • 从数据源获取要素信息

  • 从 WMS 服务器获取信息

第五章:简介

本章从基础知识出发,探讨了 OpenLayers 为我们开发者提供的最重要和最常用的控件。控件使我们能够导航地图、操作图层、放大或缩小,执行编辑要素、测量距离等操作。本质上,控件允许我们进行交互。

OpenLayers.Control类是所有控件的基类,包含控件可能具有的常见属性和方法。我们可以总结如下:

  • 控件附加到地图上

  • 控件可以触发事件

  • 控件可以被激活或禁用

  • 控件可以有视觉表示(如按钮)或没有视觉表示(如拖动操作)

控件与处理器密切相关。虽然控件旨在包含动作的逻辑,但它们将低级任务委托给处理器,例如了解鼠标或键盘事件。例如,OpenLayers.Control.DragPan控件负责通过响应鼠标事件来拖动地图。虽然监听鼠标事件的任务委托给OpenLayers.Handler.DragPan类的内部实例,但移动地图的任务由控件本身完成。

与控件类似,OpenLayers.Handler类是所有控件使用的现有处理器的基础类。

让我们看看一些菜谱,这将帮助我们更好地理解控件。

添加和删除控件

OpenLayers 提供了大量的控件,这些控件在地图应用中常用。

本菜谱展示了如何使用具有视觉表示的最常用控件。列表包括 OverviewMap 控件、Scale 和 ScaleLine 控件、Graticule 控件、LayerSwitcher 控件、PanZoomBar 控件、MousePosition 控件和 Permalink 控件:

添加和删除控件

如何操作...

  1. 首先添加按钮的代码:

    <button data-dojo-type="dijit.form.ToggleButton" data-dojo-props="iconClass:'dijitCheckBoxIcon', checked: true, onChange: layerSwitcherChanged">LayerSwitcher</button>
    <button data-dojo-type="dijit.form.ToggleButton" data-dojo-props="iconClass:'dijitCheckBoxIcon', checked: true, onChange: panZoomBarChanged">PanZoomBar</button>
    <button data-dojo-type="dijit.form.ToggleButton" data-dojo-props="iconClass:'dijitCheckBoxIcon', checked: true, onChange: mousePositionChanged">MousePosition</button>
    <button data-dojo-type="dijit.form.ToggleButton" data-dojo-props="iconClass:'dijitCheckBoxIcon', checked: true, onChange: overviewMapChanged">OverviewMap</button>
    <button data-dojo-type="dijit.form.ToggleButton" data-dojo-props="iconClass:'dijitCheckBoxIcon', checked: true, onChange: graticuleChanged">Graticule</button>
    <button data-dojo-type="dijit.form.ToggleButton" data-dojo-props="iconClass:'dijitCheckBoxIcon', checked: true, onChange: scaleChanged">Scale</button>
    <button data-dojo-type="dijit.form.ToggleButton" data-dojo-props="iconClass:'dijitCheckBoxIcon', checked: true, onChange: scaleLineChanged">ScaleLine</button>
    <button data-dojo-type="dijit.form.ToggleButton" data-dojo-props="iconClass:'dijitCheckBoxIcon', checked: true, onChange: permalinkChanged">Permalink</button>
    
    

    注意

    我们正在使用Dojo Toolkit(dojotoolkit.org/)来创建最丰富的用户界面,多亏了它提供的精美组件。本菜谱的目标不是教授 Dojo,而是教授 OpenLayers,因此我们可以自由地更改与 HTML 相关的代码,使用复选框元素作为输入,而不是 Dojo 切换按钮,并处理onclick事件。

    本菜谱的重要性在于读者学习了创建不同控件、将它们附加到地图上以及激活或禁用它们。

  2. 接下来,添加div元素以容纳地图:

    <div id="ch05_visual_controls" style="width: 100%; height: 90%;"></div>
    
    
  3. 创建地图实例并添加基本图层:

    <!-- The magic comes here -->
    <script type="text/javascript">
        // Create map
        var map = new OpenLayers.Map("ch05_visual_controls", {
            controls: []
        });    
        var osm = new OpenLayers.Layer.OSM();        
        map.addLayer(osm);
    
    
  4. 添加控件集:

        // Add controls
        var layerSwitcher = new OpenLayers.Control.LayerSwitcher({'ascending':false});
        var panZoomBar = new OpenLayers.Control.PanZoomBar();
        var mousePosition = new OpenLayers.Control.MousePosition();
        var overviewMap = new OpenLayers.Control.OverviewMap({maximized: true});
        var graticule = new OpenLayers.Control.Graticule({displayInLayerSwitcher: false});
        var scale = new OpenLayers.Control.Scale();
        var scaleline = new OpenLayers.Control.ScaleLine();
        var permalink = new OpenLayers.Control.Permalink();
    
        map.addControls([layerSwitcher, panZoomBar, mousePosition, overviewMap,
            graticule, scale, scaleline, permalink]);
    
        map.setCenter(new OpenLayers.LonLat(0, 0), 2);
    
    
  5. 最后,根据相应按钮的状态添加或删除控件的代码:

        function layerSwitcherChanged(checked) {
            if(checked) {
                layerSwitcher = new OpenLayers.Control.LayerSwitcher({'ascending':false});
                map.addControl(layerSwitcher);
            } else {
                map.removeControl(layerSwitcher);
                layerSwitcher.destroy();
            }
        }
        function panZoomBarChanged(checked) {
            if(checked) {
                panZoomBar = new OpenLayers.Control.PanZoomBar();
                map.addControl(panZoomBar);
            } else {
                map.removeControl(panZoomBar);
                panZoomBar.destroy();
            }
        }
        function mousePositionChanged(checked) {
            if(checked) {
                mousePosition = new OpenLayers.Control.MousePosition();
                map.addControl(mousePosition);
            } else {
                map.removeControl(mousePosition);
                mousePosition.destroy();
            }
        }
    
    
  6. 每个函数都接收一个 checked 参数,表示按钮是否被按下。根据其值,我们简单地从地图中添加或删除控件:

    function overviewMapChanged(checked) {
            if(checked) {
                overviewMap = new OpenLayers.Control.OverviewMap({maximized: true});
                map.addControl(overviewMap);
            } else {
                map.removeControl(overviewMap);
                overviewMap.destroy();
            }
        }
        function graticuleChanged(checked) {
            if(checked) {
                graticule = new OpenLayers.Control.Graticule({displayInLayerSwitcher: false});
                map.addControl(graticule);
            } else {
                map.removeControl(graticule);
                graticule.destroy();
            }
        }
        function scaleChanged(checked) {
            if(checked) {
                scale = new OpenLayers.Control.Scale();
                map.addControl(scale);
            } else {
                map.removeControl(scale);
                scale.destroy();
            }
        }
        function scaleLineChanged(checked) {
            if(checked) {
                scaleline = new OpenLayers.Control.ScaleLine();
                map.addControl(scaleline);
            } else {
                map.removeControl(scaleline);
                scaleline.destroy();
            }
        }
        function permalinkChanged(checked) {
            if(checked) {
                permalink = new OpenLayers.Control.Permalink();
                map.addControl(permalink);
            } else {
                map.removeControl(permalink);
                permalink.destroy();
            } 
        }
    </script>
    
    

它是如何工作的...

我们首先做的事情是创建地图实例,强制它没有任何控件附加到它。这是通过将 controls 属性设置为空数组来完成的:

    // Create map
    var map = new OpenLayers.Map("ch05_visual_controls", {
        controls: []
    }); 

注意

当我们创建一个没有指定 controls 属性的 OpenLayers.Map 实例时,OpenLayers 会自动向其中添加下一组默认控件:导航、平移缩放、ArgParser 和版权信息。

接下来,我们创建了控件,并使用 addControls 方法将所有控件添加到地图中:

    var layerSwitcher = new OpenLayers.Control.LayerSwitcher({'ascending':false});
    ...
    ...
    ...
    var permalink = new OpenLayers.Control.Permalink();

    map.addControls([layerSwitcher, panZoomBar, mousePosition, overviewMap, graticule, scale, scaleline, permalink]);

在继续之前,让我们看看在控件实例化中使用的属性。

在图层切换器上,我们已经将属性 ascending 设置为 false。这意味着图层将按降序排序,即它们将以相反的顺序添加到地图中:

    var layerSwitcher = new OpenLayers.Control.LayerSwitcher({'ascending':false});

在 OverviewMap 控件上,maximized 属性允许我们扩展创建的控件:

    var overviewMap = new OpenLayers.Control.OverviewMap({maximized: true});

最后,对于 Graticule 控件,displayInLayerSwitcher 属性允许在 LayerSwitcher 控件中打开或关闭它:

    var graticule = new OpenLayers.Control.Graticule({ displayInLayerSwitcher: false});

多亏了 Dojo Toolkit,我们创建的按钮具有切换按钮的行为。每个按钮都有一个与之关联的函数,该函数在按钮状态从选中变为未选中时执行。在概述地图按钮的情况下,关联的函数是 overviewMapChanged,它在 data-dojo-props 属性中的 onChange 事件内指定:

<button data-dojo-type="dijit.form.ToggleButton" data-dojo-props="iconClass:'dijitCheckBoxIcon', checked: true, onChange: overviewMapChanged">OverviewMap</button>

作为 onChange 事件监听器的函数接收一个布尔参数,指示按钮是否被选中或取消选中。

所有监听函数都很相似。根据检查参数的值,它从地图中删除(并销毁)控件或创建一个新的控件:

    function overviewMapChanged(checked) {
        if(checked) {
            overviewMap = new OpenLayers.Control.OverviewMap({maximized: true});
            map.addControl(overviewMap);
        } else {
            map.removeControl(overviewMap);
            overviewMap.destroy();
        }
    }

与图层一样,使用 removeControl() 方法从地图实例中删除控件不会释放控件可能使用的资源。我们需要使用 destroy() 方法显式地这样做。

参见

  • 将控件放置在地图外部 的配方

  • 在 第六章 的 使用 img 文件夹理解主题如何工作 配方(为主题化 PanZoomBar 控件)中,主题化

添加导航历史记录控件

在我们的地图应用程序中,最常用的控件可能是导航控件。OpenLayers.Control.Navigation 控件集成了(使用了)一些其他控件,例如 OpenLayers.Control.DragPanOpenLayers.Control.ZoomBox 或滚轮处理程序,这允许我们平移和缩放地图。

在导航、移动或缩放时,存储用户执行的导航操作的历史可能很有趣,这样他/她就可以回到或前进到先前的地方。幸运的是,我们不需要重新发明轮子。OpenLayers 为我们提供了OpenLayers.Control.NavigationHistory控件。

这个示例展示了将其添加到我们的应用程序中并从中受益是多么容易。

添加导航历史控件

如截图所示,我们将在地图上方添加一个按钮,该按钮将启用或禁用导航组件。

如何实现...

  1. 创建一个包含所需 OpenLayers 依赖项的 HTML 文件。添加用于启用/禁用导航控件的切换按钮代码:

    <button data-dojo-type="dijit.form.ToggleButton" data-dojo-props="iconClass:'dijitCheckBoxIcon', checked: true, onChange: navigationChanged">Navigation</button>
    
    
  2. 接下来,添加一个div元素来包含地图:

    <div id="ch05_nav_history" style="width: 100%; height: 90%;"></div>
    
    
  3. 现在,创建地图实例并添加一个基础图层:

    <script type="text/javascript">
        // Create map
        var map = new OpenLayers.Map("ch05_nav_history", {
            controls: []
        });    
        var osm = new OpenLayers.Layer.OSM();        
        map.addLayer(osm);
    
    
  4. 添加导航和导航历史控件:

        // Add controls
        var navigation = new OpenLayers.Control.Navigation();
        var history = new OpenLayers.Control.NavigationHistory();
        var panel = new OpenLayers.Control.Panel();
        panel.addControls([history.next, history.previous]);
    
        map.addControls([navigation, history, panel]);
        map.setCenter(new OpenLayers.LonLat(0, 0), 4);
    
    
  5. 实现负责启用/禁用导航控件的函数:

        function navigationChanged(checked) {
            if(checked) {
                navigation.activate();
            } else {
                navigation.deactivate();
            }
        }
    </script>
    
    

它是如何工作的...

首先,让我们谈谈导航控件。使用它并不神秘。只需创建一个控件实例并将其添加到地图中:

    var navigation = new OpenLayers.Control.Navigation();
    ....
    ....
    map.addControls([navigation, ...]);

在开始创建的按钮使用了 Dojo Toolkit,这使得我们能够轻松地将其转换为切换按钮。此外,我们还添加了一个监听函数来检查按钮的状态是否在选中和不选中之间改变。navigationChanged函数根据checked值激活或禁用控件:

    function navigationChanged(checked) {
        if(checked) {
            navigation.activate();
        } else {
            navigation.deactivate();
        }
    }

每个控件都有一个activate()deactivate()方法。它们在基类OpenLayers.Control中定义,并且所有具体的控件都继承或覆盖了这些方法。

相比于从地图中移除和添加控件,使用activatedeactivate更受欢迎。这样,就不需要创建或附加控件的实例。控件只是处于待机状态,直到我们再次激活它。

这一切都与导航控件相关,让我们看看如何添加导航历史控件,因为这只是一个两步的过程。

OpenLayers.Control.NavigationHistory控件有点特别。它包含用于存储先前和后续访问位置的栈,以及其他内容,还包含对两个按钮(OpenLayers.Control.Button控件类的实例)的引用,这允许我们在导航历史中前进和后退。这些按钮的引用可以在previousnext属性中找到。

默认情况下,在地图中添加 NavigationHistory 控件后,不会出现任何按钮。显示上一个和下一个按钮的责任在我们。为此,以及其他类似的目的,OpenLayers 为我们提供了OpenLayers.Control.Panel控件类。这是一种特殊的控件,可以包含或组合其他控件。所以,考虑到所有这些,我们现在可以解释如何将导航历史控件添加到地图中。

首先,我们需要创建 OpenLayers.Control.NavigationHistory 实例并将其添加到地图中。其次,我们需要添加一个面板来显示两个按钮,并添加这两个按钮:

    var history = new OpenLayers.Control.NavigationHistory();
    var panel = new OpenLayers.Control.Panel();
    panel.addControls([history.next, history.previous]);

最后,必须将面板本身作为新的控件添加到地图中:

    map.addControls([navigation, history, panel]);

如您所见,我们已经添加了导航、导航历史和带有按钮的面板作为地图控件,仅仅是因为这三者都是控件。

在 第六章 的 主题 中,我们将看到如何更改此控件使用的图标。

相关内容

  • 添加和删除控件 的配方

  • 将控件放置在地图外部 的配方

  • 在 第六章 的 主题 中的 使用主题文件夹理解主题工作方式 配方

处理地理位置

随着 HTML5 的到来,规范中引入了许多新的 API 和概念之一是识别加载网页的客户端位置的可能性,通过 地理位置 API (dev.w3.org/geo/api/spec-source.html)。当然,在网络地图应用的世界中,这开辟了新的巨大可能性。

在这个配方中,我们将展示如何轻松地识别用户的当前位置并将地图的视口中心对准它:

处理地理位置

每当用户点击 地理位置 按钮,地图的视口将移动到当前用户的当前位置,并在其上放置一个标记。此外,当鼠标悬停在标记上时,将显示一个包含当前位置的弹出窗口。

准备工作

如我们在配方开头所述,地理位置是浏览器必须实现的功能,因此我们需要一个符合 HTML5 的浏览器才能使此控件工作。

如何操作...

  1. 首先创建包含 OpenLayers 依赖项的 HTML 文件,然后添加按钮和地图元素的 HTML 代码:

    <button data-dojo-type="dijit.form.Button" data-dojo-props="onClick: geolocationClick">Geolocation</button>
    <div id="ch05_geolocating" style="width: 100%; height: 90%;"></div>
    
    
  2. 然后,初始化地图实例并添加一个基本图层:

    <script type="text/javascript">
        // Create map
        var map = new OpenLayers.Map("ch05_geolocating");    
        var osm = new OpenLayers.Layer.OSM();        
        map.addLayer(osm);
    
    
  3. 现在,将 OpenLayers.Control.Geolocate 控制器添加到地图中:

        // Add controls
        var geolocate = new OpenLayers.Control.Geolocate({
            eventListeners: {
                "locationupdated": locateMarker,
                "locationfailed": function() {
                    console.log('Location detection failed');
                }
            }
        });
        map.addControl(geolocate);
    
    
  4. 创建并添加到地图中,放置标记的标记层:

        var markers = new OpenLayers.Layer.Markers("Markers");
        map.addLayer(markers);
    
    
  5. 为视图设置初始位置:

        map.setCenter(new OpenLayers.LonLat(0, 0), 6);
    
    
  6. 实现与 地理位置 按钮关联的监听器函数:

        function geolocationClick() {
            geolocate.deactivate();
            geolocate.activate();
        }
    
    
  7. 最后,实现每次检测到客户端位置时执行的功能。此函数的目的是在当前客户端的位置上向地图添加一个标记,并在鼠标悬停在标记上时显示一个带有坐标的弹出窗口:

        function locateMarker(event) {
    
    
  8. 首先删除任何之前的标记:

            // Remove any existing marker
            markers.clearMarkers();
    
    
  9. 然后,创建用于标记的图标:

            var size = new OpenLayers.Size(32, 37);
            var offset = new OpenLayers.Pixel(-(size.w/2), -size.h);
            var icon = new OpenLayers.Icon('./recipes/data/icons/symbol_blank.png', size, offset);
            icon.setOpacity(0.7);
    
            // Create a lonlat instance from the event location.
            // NOTE: The coordinates are transformed to the map's projection by
            // the geolocate control.
            var lonlat = new OpenLayers.LonLat(event.point.x, event.point.y);
    
            // Add the marker
            var popup = null;
            var marker = new OpenLayers.Marker(lonlat, icon);
    
    
  10. 然后,注册一个鼠标悬停事件的监听器,该监听器将显示弹出窗口:

            marker.events.on({
                "mouseover": function() {
                    if(popup) {
                        map.removePopup(popup);
                    }
    
                    var content = "<strong>Longitude:</strong> " + lonlat.lon + "<br/>" + "<strong>Latitude:</strong> " + lonlat.lat;
    
                    popup = new OpenLayers.Popup.FramedCloud(
                    "popup", lonlat, new OpenLayers.Size(250, 100), content,
                    null, true, null);
    
                    map.addPopup(popup);
                }
            });
    
            markers.addMarker(marker);
        }
    </script>
    
    

它是如何工作的...

第一步是创建 OpenLayers.Control.Geolocate 控制器实例并将其添加到地图中:

    var geolocate = new OpenLayers.Control.Geolocate({
        eventListeners: {
            "locationupdated": locateMarker,
            "locationfailed": function() {
                console.log('Location detection failed');
            }
        }
    });

控件可以触发三个事件:

  • locationupdated: 当浏览器返回新位置时触发此事件

  • locationfailed: 如果地理位置失败,则触发此事件

  • locationuncapable: 如果你在不支持地理位置的浏览器中激活控件,则会触发此事件。

在这个菜谱中,我们为locationupdatedlocationfailed事件附加了一个事件监听器函数。

要使用 Geolocate 控件,我们需要调用其activate()方法。然后,OpenLayers 将请求浏览器获取当前用户的位置,浏览器会询问我们是否想要分享我们的位置。如果我们接受,则将触发一个带有当前位置作为参数的locationupdated事件。

在菜谱中,当按钮被点击时,会调用geolocationClick函数,并强制激活控件:

    function geolocationClick() {
        geolocate.deactivate();
        geolocate.activate();
    }

然后,当locationupdated事件被触发时,locateMarker函数被执行,传递一个带有所有相关事件信息的event参数,包括客户端坐标:

    function locateMarker(event) {...}

注意

存储在event.point中的坐标由Geolocate控件转换,以与地图相同的坐标系一致。

此函数的目的是在地图上添加一个标记到当前客户端的位置,并在鼠标悬停在标记上时显示一个带有坐标的弹出窗口。

还有更多...

OpenLayers.Control.Geolocate控件有几个有趣的属性。

首先,bind属性默认设置为true,允许我们指定地图的中心是否必须更新为控件检测到的位置。

watch属性默认设置为false,允许定期更新位置。

此外,我们还可以向控件传递一个geolocationOptions对象,该对象在规范中定义(见dev.w3.org/geo/api/spec-source.html#position_options_interface)),以更好地配置控件。

参见

  • 添加和删除控件菜谱

  • 修改特征菜谱

将控件放置在地图外部

默认情况下,所有控件都放置在地图上。这样,像 PanPanel、EditingToolbar 或 MousePosition 这样的控件就会渲染在地图上方,覆盖任何图层。这是默认行为,但 OpenLayers 足够灵活,允许我们将控件放置在地图之外:

将控件放置在地图外部

在这个菜谱中,我们将创建一个地图,其中导航工具栏和鼠标位置控件放置在地图外部和上方。

如何操作...

  1. 创建一个 HTML 文件并添加 OpenLayers 依赖项。添加以下 CSS 代码,用于重新定义我们将要使用的控件的一些方面:

    <style>
        .olControlNavToolbar {
            top: 0px;
            left: 0px;
            float: left;
        }
        .olControlNavToolbar div {
            float: left;
        }
    </style>
    
    
  2. 现在,添加 HTML 代码以将两个控件放置在地图上方:

    <table>
        <tr>
            <td>
                Navigation: <div id="navigation" class="olControlNavToolbar"></div>
            </td>
            <td>
                Position: <div id="mouseposition" style="font-size: smaller;"></div>
            </td>
        </tr>
    </table>
    
    <div id="ch05_control_outside" style="width: 100%; height: 90%;"></div>
    
    
  3. 创建地图实例并添加基本图层:

    <script type="text/javascript">
        // Create map
        var map = new OpenLayers.Map("ch05_control_outside");    
        var osm = new OpenLayers.Layer.OSM();        
        map.addLayer(osm);
        map.setCenter(new OpenLayers.LonLat(0, 0), 3);
    
    
  4. 现在,添加鼠标位置和导航工具栏控件:

        var mousePosition = new OpenLayers.Control.MousePosition({
            div: document.getElementById('mouseposition') 
        });
        map.addControl(mousePosition);
    
        var navToolbarControl = new OpenLayers.Control.NavToolbar({
            div: document.getElementById("navigation")
        });
        map.addControl(navToolbarControl);
    </script>
    
    

它是如何工作的...

之前的代码看起来相当简单。我们向地图添加了两个控件:一个OpenLayers.Control.MousePosition控件,它显示鼠标在地图上的当前坐标,以及OpenLayers.Control.NavToolbar

OpenLayers.Control.NavToolbar 控件只不过是一个包含其他控件的面板控件:一个 OpenLayers.Control.Navigation 控件(用于移动地图的手形图标),以及一个 OpenLayers.Control.ZoomBox 控件(用于在给定框上缩放的放大镜图标)。

那么,将控件放置在地图外的秘诀在哪里?答案是每个控件的构建中。

基类 OpenLayers.Control 有一个指向用于存放控件的 div 元素的 div 属性。默认情况下,构造函数中没有指定 div 元素,因此控件会创建一个新的用于此目的。

如果你指定了一个 div 元素用于控件实例化,那么它将用作控件将被渲染的位置。

对于鼠标位置控件,我们使用了以下代码:

Position: <div id="mouseposition" style="font-size: smaller;"></div>
...
var mousePosition = new OpenLayers.Control.MousePosition({
    div: document.getElementById('mouseposition') 
});

这意味着我们将控件放置在之前创建的 div 元素上,该元素由 mouseposition 字符串标识。

对于导航工具栏,它略有不同:

Navigation: <div id="navigation" class="olControlNavToolbar"></div>
...
var navToolbarControl = new OpenLayers.Control.NavToolbar({
    div: document.getElementById("navigation")
});

在这种情况下,我们设置了 OpenLayers 定义的 CSS 类 olControlNavToolbar。为什么?

注意

当我们没有指定 div 属性时,控件会创建一个并应用一些默认的 CSS 类,这些类设置了控件图标、边框、背景颜色等。从导航工具栏中移除 div 属性并查看结果。将创建一个 div 元素并将其放置在地图上,并附加一些类,例如 olControlNavToolbar,它将包含表示平移和缩放操作的按钮等元素。

当我们指定要使用的 div 属性时,不会自动创建样式,因此如果未指定一些 CSS,控件可能会消失或无法很好地渲染。

一旦这一点明确,我们可以说我们没有使用鼠标位置控件的 CSS 类,因为它只包含一些文本。好吧,我们只设置了字体大小。

导航控件是一个更复杂的控件,它包含两个其他控件,我们需要对其样式进行一点调整。

注意

正如我们将在 第六章 中看到的,主题,OpenLayers 在处理控件时的大部分灵活性都归因于 CSS 类的使用。所有控件默认都有一个与之关联的 CSS 类,它定义了其位置、图标、颜色等。

在菜谱开头设置的 CSS 代码中,我们重新定义了导航工具栏在 div 中的位置,并指出我们希望包含的元素、按钮和流向在 left 方向上:

<style>
    .olControlNavToolbar {
        top: 0px;
        left: 0px;
        float: left;
    }
    .olControlNavToolbar div {
        float: left;
    }
</style>

参见

  • 第六章,主题

在多个矢量图层上编辑要素

当处理矢量信息时,在 GIS 应用程序中,我们最可能做的最常见的事情之一是:添加新要素。

OpenLayers 有很多控件,因此没有必要重新发明轮子。我们有一套工具,我们唯一需要做的就是学习如何使用每一个。

对于这个具体目的,添加新要素。OpenLayers 有 OpenLayers.Control.EditingToolbar 控件,它显示一个带有一些按钮的工具栏,用于添加多边形、折线和点:

在多个矢量图层上编辑要素

由于地图中可以包含多个矢量图层,控制需要我们指定它必须工作的图层。

除了展示如何轻松使用控件外,本菜谱的目标是展示我们如何使用相同的控件向多个图层添加要素。

这样,这个小应用程序将包含一个具有两个矢量图层的地图。多亏了单选按钮,我们将能够选择我们想要创建新要素的图层。

如何做到这一点...

  1. 首先,添加 HTML 代码以创建几个单选按钮,这将允许我们选择我们想要在哪个矢量图层上绘制:

    <form action="">
        Vector Layer A: <input id="rbA" type="radio" dojoType="dijit.form.RadioButton" onChange="layerAChanged" name="layer" value="layerA" checked/>
        Vector Layer B: <input id="rbB" type="radio" dojoType="dijit.form.RadioButton" onChange="layerBChanged" name="layer" value="layerB"/>
    </form>
    <div id="ch05_editing_vector" style="width: 100%; height: 100%;"></div>
    
    
  2. 现在,创建一个地图实例并添加一个基本图层:

    <script type="text/javascript">
        // Create map
        var map = new OpenLayers.Map("ch05_editing_vector");    
        var osm = new OpenLayers.Layer.OSM();        
        map.addLayer(osm);
        map.addControl(new OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(0, 0), 3);
    
    
  3. 添加两个矢量图层:

        var vectorLayerA = new OpenLayers.Layer.Vector("Vector layer A");
        var vectorLayerB = new OpenLayers.Layer.Vector("Vector layer B");
        map.addLayers([vectorLayerA, vectorLayerB]);
    
    
  4. 添加编辑工具栏控件,最初与第一个矢量图层相关联:

        var editingToolbarControl = new OpenLayers.Control.EditingToolbar(vectorLayerA);
        map.addControl(editingToolbarControl);
    
    
  5. 最后,实现处理单选按钮更改的代码。它将更改与编辑工具栏控件关联的图层:

        function layerAChanged(checked) {
            if(checked) {
                var controls = editingToolbarControl.getControlsByClass("OpenLayers.Control.DrawFeature");
                for(var i=0; i< controls.length; i++) {
                    controls[i].layer = vectorLayerA;
                }
            }
        }
        function layerBChanged(checked) {
            if(checked) {
                var controls = editingToolbarControl.getControlsByClass("OpenLayers.Control.DrawFeature");
                for(var i=0; i< controls.length; i++) {
                    controls[i].layer = vectorLayerB;
                }
            }
        }
    </script>
    
    

它是如何工作的...

使用 OpenLayers.Control.EditingToolbar 控件并没有多少神秘之处。在构造函数中,我们需要指定我们想要添加新要素的矢量图层。

控件将在地图上方显示一些按钮,允许我们创建新的多边形、折线或点。这些新要素将被添加到指定的图层。

因此,将要素添加到其他矢量图层的秘密在于如何更改控件引用的图层。

OpenLayers.Control.EditingToolbar 控制不过是一个包含四个控件的面板。我们鼓励读者查看其 initialize 方法。

编辑工具栏包含一个导航控件,由手形图标表示,以及三个 OpenLayers.Control.DrawFeature 控件的实例。

DrawFeature 控制是编辑工具栏控制的精髓。给定一个矢量图层和一个处理器,该控制允许在图层上绘制要素。

如我们在本章开头所述,控件与处理器密切相关。在这里,我们可以看到处理器负责检测鼠标事件并将其转换为点、路径或多边形创建事件。另一方面,draw feature 控制监听这些事件并创建相应的地图要素。

让我们总结一下关键点:

  • 编辑工具栏,作为一个面板,包含了一系列控件:一个导航控件和三个 DrawFeature 控件

  • 此外,EditingToolbar 控件需要一个要编辑的矢量图层的引用

  • 矢量图层引用被传递给三个 DrawFeature 控件,因此它们可以在图层上添加新要素

现在,我们可以看到通过更改绘制要素控件中的图层引用,我们改变了添加要素的图层。这正是监听单选按钮事件的功能所做的事情:

    function layerAChanged(checked) {
        if(checked) {
            var controls = editingToolbarControl.getControlsByClass("OpenLayers.Control.DrawFeature");
            for(var i=0; i< controls.length; i++) {
                controls[i].layer = vectorLayerA;
            }
        }
    }

如代码所示,我们从编辑工具栏中通过调用 getControlsByClass 方法获取所有绘制要素控件,然后对每个控件通过更改 layer 属性来更改图层引用。

还有更多...

如果我们查看 OpenLayers.Control.EditingToolbar 类的 initialize() 方法的代码:

        var controls = [
          new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Point, {'displayClass': 'olControlDrawFeaturePoint'}),
          new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Path, {'displayClass': 'olControlDrawFeaturePath'}),
          new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Polygon, {'displayClass': 'olControlDrawFeaturePolygon'})
        ];

我们可以看到它正在将 displayClass 属性传递给 OpenLayers.Control.DrawFeature 控件。

这个属性也适用于从 OpenLayers.Control 类继承的所有控件,并指定必须应用于将要用于绘制控件的 div 元素的 CSS 类。

参见

  • 将控件放置在地图外部 菜谱

  • 修改要素 菜谱

修改要素

当在网页地图应用上工作时,很可能允许用户添加新功能的能力是一个期望的要求,但修改诸如移动顶点、旋转要素、缩放等功能又如何呢?

再次,OpenLayers 简化了我们的开发生活,为我们提供了强大的 OpenLayers.Control.ModifyFeature 控件:

修改要素

这次我们将创建一个小应用程序,它将为我们提供两个重要的控件:首先,添加新功能;其次,修改它们。为此,我们将使用 OpenLayers.Control.EditingToolbarOpenLayers.Control.ModifyFeature 控件。

具体来说,我们将看到如何重塑、调整大小、旋转和拖动要素。此外,我们还将看到如何过滤哪些类型的要素可以被修改所影响。

如何实现...

  1. 让我们从创建管理修改要素控件的控件开始:

    <form action="">
        <button data-dojo-type="dijit.form.ToggleButton" data-dojo-props="iconClass:'dijitCheckBoxIcon', checked: false, onChange: modifyChanged">Modify</button>
        Reshape: <input id="reshape" dojoType="dijit.form.CheckBox"onChange="changeMode" name="layer"/>
        Resize: <input id="resize" dojoType="dijit.form.CheckBox" onChange="changeMode" name="layer"/>
        Rotate <input id="rotate" dojoType="dijit.form.CheckBox" onChange="changeMode" name="layer"/>
        Drag: <input id="drag" dojoType="dijit.form.CheckBox" onChange="changeMode" name="layer"/>
        Filter: <select dojoType="dijit.form.Select" id="filter" onChange="changeFilter" name="filter" style="width: 200px;">
            <option value="ALL" selected>No Filter</option>
            <option value="POINT">POINT</option>
            <option value="PATH">PATH</option>
            <option value="POLYGON">POLYGON</option>
        </select>
    </form>
    
    
  2. 添加用于承载地图的元素:

    <div id="ch05_modify" style="width: 100%; height: 100%;"></div>
    
    
  3. 通过初始化地图并添加基本图层开始 JavaScript 编码:

    <script type="text/javascript">
        // Create map
        var map = new OpenLayers.Map("ch05_modify");    
        var osm = new OpenLayers.Layer.OSM();        
        map.addLayer(osm);
        map.addControl(new OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(0, 0), 3);
    
    
  4. 现在,添加一个矢量图层来添加和修改其要素:

        var vectorLayer = new OpenLayers.Layer.Vector("Vector layer");
        map.addLayer(vectorLayer);
    
    
  5. 将编辑工具栏控件附加到之前的图层,并将其添加到地图中:

        var editingToolbarControl = new OpenLayers.Control.EditingToolbar(vectorLayer);
        map.addControl(editingToolbarControl);
    
    
  6. 类似地,将 ModifyFeature 控件附加到矢量图层,并将其添加到地图中:

        var modifyControl = new OpenLayers.Control.ModifyFeature(vectorLayer);
        map.addControl(modifyControl);
    
    
  7. 添加修改修改要素控件行为的监听器函数。首先添加一个激活或停用控件的功能:

        function modifyChanged(checked) {
            if(checked) {
                modifyControl.activate();
            } else {
                modifyControl.deactivate();
            }
        }
    
    
  8. 接下来,添加一个改变修改方式的功能:

        function changeMode() {
            var reshape = dijit.byId("reshape").get("checked");
            var resize = dijit.byId("resize").get("checked");
            var rotate = dijit.byId("rotate").get("checked");
            var drag = dijit.byId("drag").get("checked");
    
            var mode = null;
            if(reshape) {
                mode |= OpenLayers.Control.ModifyFeature.RESHAPE;
            }
            if(resize) {
                mode |= OpenLayers.Control.ModifyFeature.RESIZE;
            }
            if(rotate) {
                mode |= OpenLayers.Control.ModifyFeature.ROTATE;
            }
            if(drag) {
                mode |= OpenLayers.Control.ModifyFeature.DRAG;
            }
    
            modifyControl.deactivate();
            modifyControl.mode = mode;
            modifyControl.activate();
        }
    
    
  9. 最后添加一个函数来过滤受控制影响的几何类型:

        function changeFilter(value) {
    
            modifyControl.deactivate();
            map.removeControl(modifyControl);
            modifyControl.destroy();
    
            var geometryTypes = null;
            if(value=="POINT") {
                geometryTypes = ["OpenLayers.Geometry.Point"];
            } else if(value=="PATH") {
                geometryTypes = ["OpenLayers.Geometry.LineString"];
            } else if(value=="POLYGON") {
                geometryTypes = ["OpenLayers.Geometry.Polygon"];
            } 
            modifyControl = new OpenLayers.Control.ModifyFeature(vectorLayer, {
                geometryTypes: geometryTypes
            });
            map.addControl(modifyControl);
            modifyControl.activate();
        }
    </script>
    
    

它是如何工作的...

编辑工具栏允许我们绘制点、路径和多边形。一旦将一些要素添加到图层中,我们可以点击 修改 切换按钮来激活或停用修改要素控件。这个动作由 modifyChanged 函数处理:

    function modifyChanged(checked) {
        if(checked) {
            modifyControl.activate();
        } else {
            modifyControl.deactivate();
        }
    }

默认情况下,修改功能控制器允许重塑任何类型的功能,无论它是点、路径还是多边形,也就是说我们可以移动或向功能添加一个新顶点。

使用复选框,我们可以修改控制器的行为,例如,允许调整大小或拖动选定的功能。

函数changeMode正在监听任何复选框的变化,并负责修改控制器处理的操作。

有关动作的指定是通过控制器的mode属性来完成的。我们可以在实例化时设置它,或者稍后通过修改属性来设置,就像我们在本食谱中所做的那样。

此外,控制器允许同时处理许多动作。我们可以使用逻辑或运算符指定所有这些动作。例如:

mode = OpenLayers.Control.ModifyFeature.RESHAPE | OpenLayers.Control.ModifyFeature.RESIZE;

如您所见,我们强制停用控制器,然后激活它,以便新的mode值生效。

最后,我们可以控制控制器行为的另一个方面,那就是我们可以修改的功能类型。使用选择框,我们可以选择可以被控制器修改的几何类型。函数changeFilter正在监听选择框的变化,并更改修改功能控制的配置。

此几何过滤器是通过使用控制器的geometryType属性来完成的。

不幸的是,此属性只能在实例化时设置,稍后做出的更改没有效果。因此,我们需要从地图中删除控制器,并创建一个新的控制器,带有要过滤的期望几何形状。

注意

从地图中删除控制器不会释放控制器可能使用的资源。我们需要销毁它以释放资源。

更多...

在阅读本食谱后,我们知道如何修改功能。但是,如果我们想在功能被修改时监听事件怎么办?如何知道何时将进行修改?

答案很简单,我们需要监听我们正在修改的矢量层中的事件。

注册事件,如beforefeaturemodifiedfeatureselectedvertexremoved,使我们能够确切地知道正在发生什么,并根据我们的要求做出反应。

参考内容

  • 多矢量层编辑功能食谱

  • 添加和删除控制器食谱

  • 在第四章 控制中的监听矢量层功能事件食谱

测量距离和面积

在许多 GIS 应用中,测量距离或面积的能力是非常重要的。

在本食谱中,我们将看到 OpenLayers 中的测量控制器为开发者提供了哪些功能。

应用程序将显示一个简单的地图,上面有一些按钮,如下面的截图所示。测量切换按钮激活或停用控制器,而单选按钮则为我们提供了选择要测量的内容:路径或区域:

测量距离和面积

此外,我们还可以设置两个控件选项。测地线控件表示面积计算的距离是否必须使用测地线度量而不是平面度量。立即选项在每次移动鼠标时更新测量很有用。

如何实现...

在这里,我们将只编写重要的代码片段,而不是整个源代码。因此,我们避免在这里放置构建测量按钮、复选框、选项单选按钮以及包含地图实例的div元素的 HTML 代码。

  1. 让我们看看 JavaScript 代码。首先,实例化地图,添加一个基本图层,并居中地图显示:

        var map = new OpenLayers.Map("ch05_measure");    
        var osm = new OpenLayers.Layer.OSM();        
        map.addLayer(osm);
        map.addControl(new OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(0, 0), 3);
    
    
  2. 现在,添加测量控制。请注意,我们为事件measuremeasurepartial:注册了两个监听函数:

        var measureControl = new OpenLayers.Control.Measure(OpenLayers.Handler.Path, {
            persist: true,
            eventListeners: {
                'measure': measure,
                'measurepartial': measurepartial
            }
        });
    
    
  3. 接下来,放置用于激活或禁用控制的测量切换按钮的代码:

        function measureClick(checked) {
            var path = dijit.byId('path').get('checked');
            var polygon = dijit.byId('polygon').get('checked');
            var regular = dijit.byId('regular').get('checked');
    
            if(checked){
                if(path) {
                    measureControl.updateHandler(OpenLayers.Handler.Path, {persist: true});
                } else if(polygon) {
                    measureControl.updateHandler(OpenLayers.Handler.Polygon, {persist: true});
                } else if(regular) {
                    measureControl.updateHandler(OpenLayers.Handler.RegularPolygon, {persist: true});
                }
                map.addControl(measureControl);
                measureControl.activate();
            } else {
                measureControl.deactivate();
                map.removeControl(measureControl);
            }
    
            dojo.byId('value').innerHTML = "";
        }
    
    
  4. 实现对measuremeasurepartial控件事件的监听函数:

        function measure(event) {
            var message = event.measure + " " + event.units;
            if(event.order>1) {
                message += "2";
            }
            dojo.byId('value').innerHTML = message;
        }
    
        function measurepartial(event) {
            var message = event.measure + " " + event.units;
            dojo.byId('value').innerHTML = message;
        }
    
    
  5. 最后,放置用于更改测地线立即选项的函数的代码:

        function changeImmediate(checked) {
            measureControl.setImmediate(checked);
        }
        function changeGeodesic(checked) {
            measureControl.geodesic = checked;
        }
    
    

它是如何工作的...

让我们分析一下我们是如何初始化测量控制的:

    var measureControl = new OpenLayers.Control.Measure(OpenLayers.Handler.Path, {
        persist: true,
        eventListeners: {
            'measure': measure,
            'measurepartial': measurepartial
        }
    });

我们需要传递给控件的一个参数是用于处理程序。

像许多其他控件一样,OpenLayers.Control.Measure类利用处理程序与地图进行交互。在这种情况下,测量控件可以利用任何允许绘制几何形状的处理程序。总结一下,流程如下:

  • 控件被激活,并将绘制地图上某些几何形状的任务委托给处理程序

  • 一旦处理程序绘制了所需的几何形状(例如路径或多边形),该要素就被返回到控件

  • 控件计算几何形状的距离或面积,并触发一个事件

注意

实际上,我们使用返回实现getArea()getLength()方法的几何形状处理程序有限制。例如,如果您尝试使用OpenLayers.Handler.Box处理程序与测量控件一起使用,一旦激活控件并绘制一个矩形,您将在浏览器控制台中收到一个错误。这是因为矩形处理程序返回一个没有getLengthgetArea方法的OpenLayers.Bounds实例。

在我们的代码中,我们初始化测量控制设置如下:

  • persist属性设置为true。此属性表示由处理程序创建的几何形状必须保留在地图上,直到新的测量开始。

  • 两个事件监听器,针对measuremeasurepartial事件。measure事件在测量动作完成后触发。measurepartial在每次测量更新时触发(仅当immediate属性为true时)。

当按下测量切换按钮时,执行measureClick函数。此函数检查必须用于测量的处理程序类型,并将其设置在控件上。

这可以通过测量控制上的updateHandler方法来完成。例如:

measureControl.updateHandler(OpenLayers.Handler.Polygon, {persist: true});

此外,measureClick函数将控制添加到地图上,并在按钮开启时激活,或者在按钮关闭时停用并从地图中移除控制。

对于控制选项按钮,我们设置了两个与复选框关联的监听函数。

立即复选框改变时,changeImmediate函数将被执行。这通过使用setImmediate方法,改变控制的immediate属性,允许在每次测量更新时通过鼠标移动触发事件:

    function changeImmediate(checked) {
        measureControl.setImmediate(checked);
    }

大地测量复选框设置geodesic属性的值。这次我们可以直接修改属性,而不需要 setter 函数:

    function changeGeodesic(checked) {
        measureControl.geodesic = checked;
    }

geodesic属性设置为true时,控制将使用大地测量度量而不是平面度量来计算测量值。

更多...

测量的一个重要部分是用几何实例完成的。

所有的几何类,如OpenLayers.Geometry.PolygonOpenLayers.Geometry.LineString,都包含计算它们的面积或长度的方法。

通过查看测量控制源代码,我们可以看到一旦其关联的处理程序返回一个几何体,它就简单地调用几何方法来获取面积或长度并触发一个事件。

参考内容

  • 使用地理位置菜谱

  • 在多个矢量层上编辑要素菜谱

  • 修改要素菜谱

从数据源获取要素信息

我们几乎每天都在处理网络地图应用。我们知道如何创建地图并添加栅格和矢量层。不仅如此,我们还知道如何从不同的数据源获取矢量数据:GeoJSON 文件、KML 文件或从 WFS 服务器。

在这一点上,与矢量层相关,我们可能有一个问题:我们如何检索要素的信息?幸运的是,OpenLayers 为我们提供了一些可以回答这个问题的控制。

在这个菜谱中,我们将看到OpenLayers.Control.GetFeature控制类在查询要素数据源方面的实际应用。

我们将创建一个包含一个底图层和两个矢量层的地图。一个来自 WFS 服务器,包含美国,另一个来自 GML 文件,包含欧洲国家。

在地图上方,一个按钮允许我们激活/停用GetFeature控制,两个单选按钮允许我们在要查询的美国或欧洲层之间进行选择。

如何做到这一点...

  1. 让我们开始创建带有 OpenLayers 依赖项的 HTML 文件。然后添加地图和按钮的 HTML 代码:

    <button dojoType="dijit.form.ToggleButton" id="getfeatureButton" onChange="getFeatureClick" iconClass='dijitCheckBoxIcon' checked="false">Activated</button>
    Get Information from:
    USA <input id="usa" dojoType="dijit.form.RadioButton" onChange="changeHandler" checked name="layer"/>
    Europe <input id="europe" dojoType="dijit.form.RadioButton" onChange="changeHandler" name="layer"/>
    
    <div id="ch05_getfeature" style="width: 100%; height: 100%;"></div>
    
    
  2. 在 JavaScript 部分,设置要使用的代理脚本:

        OpenLayers.ProxyHost = "./utils/proxy.php?url=";
    
    
  3. 初始化地图并添加一个底图层:

        var map = new OpenLayers.Map("ch05_getfeature");    
        // Add a WMS layer
        var wms = new OpenLayers.Layer.WMS("Basic", "http://labs.metacarta.com/wms/vmap0",
        {
            layers: 'basic'
        });
        map.addLayer(wms);
        map.addControl(new OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(0, 40), 3);
    
    
  4. 添加两个矢量层,第一个来自 WFS 服务器,第二个来自 GML 文件:

        var statesLayer = new OpenLayers.Layer.Vector("States", {
            protocol: new OpenLayers.Protocol.WFS({
                url: "http://demo.opengeo.org/geoserver/wfs",
                featureType: "states",
                featureNS: "http://www.openplans.org/topp"
            }),
            strategies: [new OpenLayers.Strategy.BBOX()]
        });
        map.addLayer(statesLayer);
    
        var europeLayer = new OpenLayers.Layer.Vector("Europe (GML)", {
            protocol: new OpenLayers.Protocol.HTTP({
                url: "http://localhost:8080/openlayers-cookbook/recipes/data/europe.gml",
                format: new OpenLayers.Format.GML()
            }),
            strategies: [new OpenLayers.Strategy.Fixed()]
        });
        map.addLayer(europeLayer);
    
    
  5. 添加一个第三层,用于显示选定的要素:

        var selected = new OpenLayers.Layer.Vector("Selected", {
            styleMap: new OpenLayers.Style(OpenLayers.Feature.Vector.style["temporary"])
        });
        map.addLayer(selected);
    
    
  6. 现在添加GetFeature控制:

        var getFeature = new OpenLayers.Control.GetFeature({
            protocol: statesLayer.protocol,
            box: true,
            hover: false,
            multipleKey: "shiftKey",
            toggleKey: "ctrlKey",
            eventListeners: {
                "featureselected": function(event) {
                    selected.addFeatures([event.feature]);
                },
                "featureunselected": function(event) {
                    selected.removeFeatures([event.feature]);
                }
            }
        });
        map.addControl(getFeature);
    
    
  7. 插入激活/停用控制的代码:

        function getFeatureClick(checked) {
            if(checked) {
                getFeature.activate();
            } else {
                getFeature.deactivate();
            }
        }
    
    
  8. 最后,添加将改变由控制查询的层的代码:

        function changeHandler() {
            var usa = dijit.byId('usa').get('checked');
            if(usa) {
                getFeature.protocol = statesLayer.protocol;
            } else {
                getFeature.protocol = europeLayer.protocol;
            }
        }
    
    

它是如何工作的...

由于我们正在使用 WFS 层,并且后续的GetFeature控制查询也使用 AJAX,因此我们需要配置一个要使用的代理脚本。

在初始化地图后,我们仅使用OpenLayers.Layer.WMS类添加了一个基本 WMS 层,并指定了服务器的 URL 和 WMS 层名称:

    var wms = new OpenLayers.Layer.WMS("Basic", "http://labs.metacarta.com/wms/vmap0",
    {
        layers: 'basic'
    });

这两个矢量层没有太多秘密。多亏了OpenLayers.Protocol子类,我们可以通过简单地指定正确的协议轻松地从不同的数据源创建矢量层:

    var statesLayer = new OpenLayers.Layer.Vector("States", {
        protocol: new OpenLayers.Protocol.WFS(...),
        strategies: [new OpenLayers.Strategy.BBOX()]
    });

    var europeLayer = new OpenLayers.Layer.Vector("Europe (GML)", {
        protocol: new OpenLayers.Protocol.HTTP(...),
        strategies: [new OpenLayers.Strategy.Fixed()]
    });

此外,我们创建了一个名为selected的第三个矢量层。为什么?让我们首先解释一下OpenLayers.Control.GetFeature是如何工作的:

    var getFeature = new OpenLayers.Control.GetFeature({
        protocol: statesLayer.protocol,
        box: true,
        hover: false,
        multipleKey: "shiftKey",
        toggleKey: "ctrlKey",
        eventListeners: {
            "featureselected": function(event) {
                selected.addFeatures([event.feature]);
            },
            "featureunselected": function(event) {
                selected.removeFeatures([event.feature]);
            }
        }
    });

默认情况下,OpenLayers.Control.GetFeature在发生点击事件时开始工作。然后,使用指定的protocol实例,它查询点击位置下的数据源特征。

此外,通过使用box属性,我们可以允许使用选择绑定框选择特征。第三次选择是通过悬停动作完成的,但我们已将其设置为false来禁用它。

如其名所示,multipleKeytoggleKey属性用于定义用于选择多个特征和切换其选择状态的控件键。

最后,eventListeners属性允许我们在构造函数调用时注册我们想要监听的事件。在这种情况下,我们将被通知当特征被选中或取消选中时。

让我们回到第三个矢量层,即selected层。

与其他控制(如OpenLayers.Control.SelectFeatureOpenLayers.Control.GetFeature)不同,OpenLayers.Control.GetFeature没有修改特征的可视样式。也就是说,如果你使用SelectFeature控制,你会看到每次选择一个特征时,其颜色和边框都会改变以指示它已被选中。

另一方面,使用OpenLayers.Control.GetFeature,当选择一个特征时不会发生任何事情。控制会对数据源进行查询并触发一个事件。你负责对那个事件进行操作。

在这个菜谱中,我们从事件中检索特征并将其添加到所选层:

            "featureselected": function(event) {
                selected.addFeatures([event.feature]);
            },
            "featureunselected": function(event) {
                selected.removeFeatures([event.feature]);
            }

如果我们查看所选层的实例化代码,它看起来像以下代码:

    var selected = new OpenLayers.Layer.Vector("Selected", {
        styleMap: new OpenLayers.Style(OpenLayers.Feature.Vector.style["temporary"])
    });

我们为该层提供了不同的样式。在这种情况下,我们正在获取在OpenLayers.Feature.Vector中定义的"temporary"样式并将其应用于层,以便特征具有不同的外观。

注意

关于样式的更多讨论请参阅第七章,样式化特征

还有更多...

重要的是要注意,OpenLayers.Control.SelectFeature控制与OpenLayers.control.GetFeature控制类似,但它们有重要的区别。

首先,OpenLayers.control.GetFeature控件会对数据源进行查询,返回由选择涉及的特征。openLayers.Control.SelectFeature控件不进行请求,它在客户端工作,并从指定的矢量图层检索选定的特征。

其次,每次使用OpenLayers.Control.SelectFeature控件选择特征时,矢量层都会触发一个featureselected事件。因此,我们可以在矢量层中注册监听器以通知选择事件。

使用OpenLayers.control.GetFeature控件,矢量层不会触发任何事件。事件是由控件触发的,因此我们需要在控件中注册监听器。

最后,使用OpenLayers.control.GetFeature控件,我们可以使用任何支持空间过滤的协议。正因为如此,我们可以将GetFeature控件用于 WFS 服务器或 GML 文件。

参见

  • 选择和转换特征配方

  • 从 WMS 服务器获取信息配方

  • 第三章中的添加 GML 图层配方,矢量图层

  • 第三章中的在 WFS 请求中过滤特征矢量图层

  • 第四章中的监听矢量图层特征事件控件

从 WMS 服务器获取信息

现在,Web 地图服务(WMS)在 GIS 世界中扮演着重要的角色,主要是因为在客户端渲染大量的矢量数据,无论其是在桌面上的浏览器,都会消耗很多资源。

如果我们考虑 OpenStreetMap 项目,其中我们拥有大量关于街道、地点等的矢量数据,我们可以看到渲染数据的主要方式是栅格方式。

在这种情况下,WMS 服务器允许我们从 shapefile、从.geotiff文件、从空间数据库等获取矢量或栅格数据,并将它们全部渲染成一张单独的图像。不仅如此,如果配置得当,WMS 服务器还允许我们查询给定点的特征信息。

使用 OpenLayers,可以通过OpenLayers.Control.WMSGetFeatureInfo控件轻松实现。

在下面的屏幕截图中,我们可以看到我们当前的配方看起来是什么样子。给定一些关于美国各州的矢量信息,服务器返回一个栅格图像。

从 WMS 服务器获取信息

一旦激活了控件,地图上的任何点击事件都会触发对 WMS 服务器的请求以获取特征信息。

如何实现...

  1. 创建一个包含 OpenLayers 库依赖项的 HTML 文件,并添加按钮和地图div元素的代码:

    <button dojoType="dijit.form.ToggleButton" id="featureInfoButton" onChange="featureInfoChange" iconClass='dijitCheckBoxIcon' checked="false">Activated</button>
    <div id="ch05_wmsfeatureinfo" style="width: 100%; height: 100%;"></div>
    
    
  2. 设置代理脚本,并初始化地图实例:

        OpenLayers.ProxyHost = "./utils/proxy.php?url=";
        // Create map
        var map = new OpenLayers.Map("ch05_wmsfeatureinfo"); 
    
    
  3. 现在,添加两个 WMS 图层。第一个将作为基础图层,而第二个将是一个带有美国各州的叠加图层:

        var wms = new OpenLayers.Layer.WMS("Basic", "http://demo.opengeo.org/geoserver/wms",
        {
            layers: 'topp:naturalearth'
        });
        map.addLayer(wms);
         var wms2 = new OpenLayers.Layer.WMS("Basic", "http://demo.opengeo.org/geoserver/wms",
        {
            layers: 'topp:states',
            transparent: true
        },{
            isBaseLayer: false
        });
        map.addLayer(wms2);
    
    
  4. 添加图层切换控件并居中地图的视口:

        map.addControl(new OpenLayers.Control.LayerSwitcher());
        map.setCenter(new OpenLayers.LonLat(-90, 40), 4);
    
    
  5. 然后添加 WMSGetFeatureInfo 控件的代码:

        var featureInfo = new OpenLayers.Control.WMSGetFeatureInfo({
            url: 'http://demo.opengeo.org/geoserver/wms', 
            title: 'Identify features by clicking',
            queryVisible: true,
            eventListeners: {
                "getfeatureinfo": function(event) {
                    map.addPopup(new OpenLayers.Popup.FramedCloud(
                    "chicken", 
                    map.getLonLatFromPixel(event.xy),
                    null,
                    event.text,
                    null,
                    true
                ));
                }
            }
        });
        map.addControl(featureInfo);
    
    
  6. 最后,添加代码以在按钮点击时激活/停用控件:

        function featureInfoChange(checked) {
            if(checked) {
                featureInfo.activate();
            } else {
                featureInfo.deactivate();
            }
        }
    
    

它是如何工作的...

WMS 服务器实现了不同的请求类型。最重要的是 GetMap 请求,它允许我们根据一些参数(如边界框、图层名称等)获取图像。

注意

所有这些解释更接近于理解 WMS 标准,而不是使用 OpenLayers。因此,花时间学习 WMS 标准提供了什么以及它是如何工作的。你可以在维基百科上找到一个非常简短的描述:en.wikipedia.org/wiki/Web_Map_Service,以及完整的规范在 OGC:www.opengeospatial.org/standards/wms

此外,WMS 服务器可以实现 GetFeatureInfo 请求。这种类型的请求允许我们,给定一个点和在 WMS 服务器上配置的一些图层名称,从特征中检索信息,即我们可以从作为栅格图像渲染的图层中获取特征属性。

让我们描述这个菜谱的代码,这是本书的目标,而不是解释 WMS 服务器是如何工作的。

因为,控件将执行 AJAX 请求,我们需要设置一个代理脚本来使用:

    OpenLayers.ProxyHost = "./utils/proxy.php?url=";

WMS 图层来自令人惊叹的 OpenGeo 项目(opengeo.org)的公共服务器。第一个图层充当基础图层。第二个图层必须是一个叠加图层,因为我们已将 isBaseLayer 属性设置为 false。此外,为了避免图层隐藏基础图层,我们已将用于 WMS 请求的 transparent 属性设置为 true

     var wms2 = new OpenLayers.Layer.WMS("Basic", "http://demo.opengeo.org/geoserver/wms",
    {
        layers: 'topp:states',
        transparent: true
    },{
        isBaseLayer: false
    });

添加 WMSGetFeatureInfo 控件很简单,我们需要设置 WMS 服务器 URL、一些期望的属性,并注册一些事件监听器来处理返回的信息:

    var featureInfo = new OpenLayers.Control.WMSGetFeatureInfo({
        url: 'http://demo.opengeo.org/geoserver/wms', 
        queryVisible: true,
        eventListeners: {...}
    });

因为我们想显示包含数据的弹出窗口,我们在 getfeatureinfo 事件上注册了一个函数,该事件在控件获取服务器数据时触发:

        eventListeners: {
            "getfeatureinfo": function(event) {
                map.addPopup(new OpenLayers.Popup.FramedCloud(
                "chicken", 
                map.getLonLatFromPixel(event.xy),
                null,
                event.text,
                null,
                true
            ));
            }
        }

注意

要查询 WMS 服务器中图层的详细信息,它必须配置为可查询图层。如果我们请求一个不可查询的图层,则控件将触发 nogetfeatureinfo 事件。

默认情况下,控件请求地图中所有 WMS 图层的数据。使用 queryVisible 属性,我们可以限制查询仅限于当前可见的图层,并忽略那些隐藏的图层。

还有更多...

WMSGetFeatureInfo 控件还有其他有趣的属性。

hover 属性设置为 true,我们可以强制控件在鼠标点击地图时查询服务器,同时也在鼠标悬停事件上查询。

使用 layers 属性,它接受一个 OpenLayers.Layer.WMS 图层数组的对象,我们可以控制哪些图层必须在服务器上查询。如果没有指定,图层将从地图中获取。

此外,如果图层被配置为与多个服务器一起工作,则仅使用第一个服务器进行查询。

此外,需要注意的是,WMS 服务器可以以不同的格式返回数据,例如,纯文本、HTML 响应,或者 GML 格式。

使用infoFormat属性,我们可以向服务器指示我们期望的响应类型。默认情况下是 HTML。

参见

  • 从数据源获取要素信息 菜单

  • 选择和转换特征 菜单

  • 添加 WMS 图层 菜单

第六章。主题化

在本章中,我们将涵盖:

  • 使用 img 文件夹理解主题的工作原理

  • 使用 theme 文件夹理解主题的工作原理

  • 在栅格层中定义瓦片

  • 创建新的 OpenLayers 主题

  • 在控件外启动操作

简介

值得注意的是,在软件应用程序中,第一印象是最重要的,它们由两个因素决定:外观和感觉。

本章旨在展示我们如何通过主题化 OpenLayers 来改善我们的网络制图应用程序的外观和感觉。

与许多其他网络应用程序一样,OpenLayers 库的外观和感觉是通过图像和 CSS 类控制的,这些类定义了任何 OpenLayers 组件的位置、尺寸和视觉方面。

在此刻,随着 2.11 版本的推出,我们可以在捆绑发行版中找到 imgtheme 文件夹,并且两者都用于控制 OpenLayers 应用程序的外观。

提示

请记住,在按照第一章中“包含 OpenLayers 的不同方法”配方所述使用 OpenLayers 时,将这些文件夹放置到您的项目中。

简介

theme 文件夹包含 CSS 文件,以及一些在 CSS 中使用的图像,而 img 文件夹只包含图像,以更硬编码(且不推荐)的方式由一些控件使用。

我们可以说,使用 theme 文件夹和 CSS 样式是实现控件的首选方式,而使用 img 文件夹则保留给那些尚未更新以使用 CSS 样式的控件。

成为一位杰出的网页设计师超出了本书的范围,但确实,如果我们想稍微调整一下 OpenLayers 的外观,我们需要了解一些 HTML 和 CSS 的工作原理。

注意

我们可以在 en.wikipedia.org/wiki/Cascading_Style_Sheets 找到 CSS 标准的描述,但我们在网上可以找到大量的优秀教程,例如 www.csstutorial.net

浏览器使用三种主要技术:HTML、CSS 和 JavaScript。用非常简短的句子总结,我们可以这样说:

  • HTML 使用段落、标题、部分等定义网页的内容。

  • CSS 定义了 HTML 元素的视觉方面,即段落必须使用的文本颜色、标题的文本大小等。

  • 最后,JavaScript 是一种由浏览器处理的编程语言,可以用来动态地操作页面的任何方面。例如,我们可以添加新的 HTML 元素,更改 CSS,并在将表单发送到服务器之前检查表单字段是否有效。

注意

文档对象模型 (DOM) 是一个用于访问和操作 HTML 文档的标准。我们可以将其视为将 HTML 文档分解为一个元素和属性的树。

查看:www.w3schools.com/htmldom/default.asp

OpenLayers 属于第三类。它是一个 JavaScript 库,允许我们使用地图、图层或特征等概念来创建网络地图应用程序,同时抽象出渲染它们所需的 HTML DOM 元素和 CSS 方面。

当我们创建一个 OpenLayers 组件,例如地图、图层或控制实例时,它也会创建所需的 HTML 元素来渲染它们,并将它们放置在 HTML 页面的 DOM 结构的正确位置。

本章的目标是展示如何为主题化最重要的 OpenLayers 组件。因此,使用许多后续的菜谱,我们将能够创建一个具有定制外观的新网络地图应用程序。

使用 img 文件夹理解主题的工作方式

使用 img 文件夹理解主题的工作方式

如本章 简介 部分所述,有一些控制可以通过存储在 img 文件夹中的图片简单地主题化。

这种方式是为主题化控制的最古老方式,对于新的实现,首选的主题化方式是使用 CSS,即使用 theme 文件夹。

在其更新之前,我们可以使用任何两种主题化形式的控制,因此了解如何主题化两者是很重要的。

在这个菜谱中,我们将描述如何使用 img 文件夹中的图片基于旧方式为主题化的 PanZoomBar 控制器。

如何做到这一点...

  1. 创建一个包含 OpenLayers 依赖项的 HTML 文件,并在文档的 body 元素中开始添加 div 元素以包含地图:

    <div id="ch06_theming_img" style="width: 100%; height: 90%;"></div>
    
    
  2. 现在,添加以下 JavaScript 代码以初始化地图并添加一个基本图层:

        var map = new OpenLayers.Map("ch06_theming_img ");    
        var osm = new OpenLayers.Layer.OSM();        
        map.addLayer(osm);
        map.setCenter(new OpenLayers.LonLat(0, 0), 2);
    
    
  3. 最后,只需创建一个 OpenLayers.Control.PanZoomBar 控制实例并将其添加到地图中:

    var panZoomBar = new OpenLayers.Control.PanZoomBar();
    map.addControl(panZoomBar);
    
    

它是如何工作的...

当我们创建控制时,实际上发生的是 OpenLayers 自动创建一组 HTML 元素并将它们放置在页面的 DOM 结构中。

从 OpenLayers 的 JavaScript API 视角来看,这仅仅是向地图添加一个控制组件,但从 HTML 代码的角度来看,这意味着在页面上创建了一组复杂的元素来表示所有按钮和图片,以形成一个漂亮的平移和缩放控制:

<div id="OpenLayers.Control.PanZoomBar_71" style="... left: 4px; top: 4px; ..." class="olControlPanZoomBar olControlNoSelect" ...> 
    <div id="OpenLayers.Control.PanZoomBar_71_panup" ...> 
        <img id="OpenLayers.Control.PanZoomBar_71_panup_innerImage" ... src="img/north-mini.png"> 
    </div> 
    <div id="OpenLayers.Control.PanZoomBar_71_panleft" ...> 
        <img id="OpenLayers.Control.PanZoomBar_71_panleft_innerImage" ... src="img/west-mini.png"> 
    </div> 
     <div id="OpenLayers.Control.PanZoomBar_71_panright" ...> 
        <img id="OpenLayers.Control.PanZoomBar_71_panright_innerImage" ... src="img/east-mini.png"> 
    </div> 
     <div id="OpenLayers.Control.PanZoomBar_71_pandown" ...> 
        <img id="OpenLayers.Control.PanZoomBar_71_pandown_innerImage" ... src="img/south-mini.png"> 
    </div> 
    <div id="OpenLayers.Control.PanZoomBar_71_zoomin" ...> 
        <img id="OpenLayers.Control.PanZoomBar_71_zoomin_innerImage" ... src="img/zoom-plus-mini.png"> 
    </div> 
</div>

通过查看生成的代码,我们可以说:

  • 主要控制元素使用一个名为 olControlPanZoomBar 的 CSS 类

  • 所使用的所有图片都是从 OpenLayers 的 img 文件夹中加载的,例如 img/north-mini.pngimg/east-mini.pngimg/zoom-minus-mini.png

  • 控制的位置及其按钮的设置是在 style 属性中进行的,而不是使用 CSS 类

结论很简单:要改变这个控制的外观,只能通过更改 img 文件夹中的图片来实现。

此外,如果我们想将控件放置在不同的位置,我们需要调整OpenLayers.Control.PanZoom.XOpenLayers.Control.PanZoom.Y属性,这些属性用于设置主控件 HTML 元素style属性中topleft属性的值。例如,设置:

OpenLayers.Control.PanZoom.X = 50;

生成以下 HTML 代码:

<div id="OpenLayers.Control.PanZoomBar_71" style="position: absolute; left: 50px; top: 4px; ...

这意味着,尽管你在olControlPanZoomBar类中重新定义了topleft属性,但它们不会生效,因为style属性中指定的属性具有优先级:

.olControlPanZoomBar { 
    top: 50px; 
    left: 50px; 
}

还有更多...

有几点重要的事情需要注意。

首先,每个 OpenLayers 实例都有一个 ID 属性。我们可以在创建实例时手动设置它,或者让 OpenLayers 为我们计算一个,但请注意,ID 必须是唯一的。

在这种情况下,PanZoomBar控件的 ID 是字符串OpenLayers.Control.PanZoomBar_71,它也用于识别 HTML 元素。

其次,如果一个控件使用了 CSS 类,按照惯例,类的名称将是olControl后跟控件的名称,例如:olControlPanZoomBar

参见

  • 使用主题文件夹理解主题如何工作配方

  • 第五章中的添加和移除控件配方,添加控件

使用主题文件夹理解主题如何工作

如我们在本章引言中解释的,有一些 OpenLayers 控件强烈依赖于 CSS 类来作为主题。

在这个组中,我们可以找到PanPanel控件,它是由一组四个按钮组成的小控件,允许用户在四个方向上平移地图:

使用主题文件夹理解主题如何工作

如何做到这一点...

  1. 创建一个 HTML 页面并添加 OpenLayers 库依赖项:

    <script type="text/javascript" src="img/OpenLayers.js"></script>
    
    
  2. 然后,我们需要包含要使用的主题的 CSS 文件。这里我们使用默认主题:

    <link rel="stylesheet" href="./js/OpenLayers-2.11/theme/default/style.css" type="text/css">
    
    
  3. 在文档的body元素中,添加div元素以容纳地图:

    <div id="ch06_theming_theme" style="width: 100%; height: 90%;"></div>
    
    
  4. script元素中,添加创建带有基本层的地图的代码:

        var map = new OpenLayers.Map("ch06_theming_img ");    
        var osm = new OpenLayers.Layer.OSM();        
        map.addLayer(osm);
        map.setCenter(new OpenLayers.LonLat(0, 0), 2);
    
    
  5. 最后,创建一个OpenLayers.Control.PanPanel实例并将其添加到地图中:

    var panControl = new OpenLayers.Control.PanPanel(); 
    map.addControl(panControl);
    
    

它是如何工作的...

OpenLayers.Control.PanPanel实例添加到地图中时,实际上是在 DOM 页面结构中添加了一组新的 HTML 元素:

<div id="OpenLayers.Control.PanPanel_71" style="position: absolute; z-index: 1006; " class="olControlPanPanel olControlNoSelect" unselectable="on"> 
    <div class="olControlPanNorthItemInactive"></div> 
    <div class="olControlPanSouthItemInactive"></div> 
    <div class="olControlPanEastItemInactive"></div> 
    <div class="olControlPanWestItemInactive"></div> 
</div>

对于控件,有一个主要元素包含其他表示四个按钮的元素。

主要 HTML 元素附加了一个名为olControlPanPanel的 CSS 类。这个类名是由 OpenLayers 自动创建的,并遵循以下约定:olControl加上控件名称。

在之前的 HTML 代码中使用的所有 CSS 类都可以在theme/default/style.css主题文件中的源代码中找到。

还有更多...

通过查看控件使用的 CSS 类,我们可以更好地理解其工作原理。

首先,我们通过修改 CSS 类的属性来更改控件的位置:

.olControlPanPanel {
    top: 10px;
    left: 5px;
}  

接下来,CSS 代码设置了要使用的图像和按钮的大小:

.olControlPanPanel div {
    background-image: url(img/pan-panel.png);
    height: 18px;
    width: 18px;
    cursor: pointer;
    position: absolute;
}

我们可以看到图像精灵是如何从文件 theme/default/img/pan-panel.png: 中提取的:

还有更多...

注意

图像精灵是将多个图像放入同一个文件中的集合。稍后,我们可以使用 CSS 属性来获取图像精灵的一部分,用于元素。

图像精灵在页面加载时减少了向服务器发送的请求数量。

接下来,每个按钮定义了提取用作按钮的图像片段所需的属性:

.olControlPanPanel .olControlPanSouthItemInactive { 
    top: 36px; 
    left: 9px; 
    background-position: 18px 0; 
}

我们可以看到,只需一点 CSS 知识,我们就可以修改几乎任何想要的控件属性。

参见

  • [理解如何使用 img 文件夹工作主题] 菜谱

  • 第五章中的添加和删除控件菜谱,添加控件

  • 第一章中的创建一个简单的全屏地图菜谱,网络地图基础

  • 第一章中的不同方式包含 OpenLayers 菜谱,网络地图基础

在栅格层中划分瓦片

为了展示使用 CSS 如何轻松地更改元素的外观,在这个菜谱中我们将向任何栅格层的所有瓦片添加边框,以显示每个瓦片的界限:

在栅格层中划分瓦片

如何做到...

  1. 创建一个包含 OpenLayers 依赖项的 HTML 文件,并在 head 部分添加一个包含以下 CSS 代码的 style 元素:

    <style>
        .olTileImage {
            border: 1px solid #999;
        }
    </style>
    
    
  2. 接下来,在文档的 body 元素中,添加 div 元素来包含地图:

    <div id="ch06_tile_borders" style="width: 100%; height: 90%;"></div>
    
    
  3. 现在,添加以下 JavaScript 代码以初始化地图并添加一个基本层:

        var map = new OpenLayers.Map("ch06_tile_borders");    
        var osm = new OpenLayers.Layer.OSM();        
        map.addLayer(osm);
        map.setCenter(new OpenLayers.LonLat(0, 0), 2);
    
    

它是如何工作的...

创建地图实例和层的代码相当简单,我们只是创建了它们的实例并将层添加到地图中。最后,我们使地图的视口居中。

尽管这看起来令人难以置信,但这个菜谱的所有魔法都在顶部的 CSS 代码中:

    .olTileImage {
        border: 1px solid #999;
    }

每个栅格层类都使用图像来渲染数据瓦片。为此,层创建了一些 HTML 元素并将它们添加到 DOM 结构中,如下所示:

<div id="OpenLayers.Layer.OSM_315" ... class="olLayerDiv"> 
     <div ...> 
        <img id="OpenLayersDiv335" style="position: relative; width: 256px; height: 256px; " class="olTileImage" ...> 
    </div> 
    <div ...> 
        <img id="OpenLayersDiv337" style="position: relative; width: 256px; height: 256px; " class="olTileImage" ...> 
    </div> 
</div>

每个 OpenLayers 组件都被转换为一个或多个使用 CSS 类来定义其可视化方式的 HTML 元素。

正如你所见,一个 div 元素被创建来代表整个由 OpenLayers.Layer.OSM_315 标识的层,其类参数被设置为 olLayerDiv CSS。在其中我们可以找到指向要渲染的瓦片的 img 元素。这些元素应用了 olTileImage 类。

多亏了这个菜谱中的 CSS 类,我们只需指定适当的属性就可以在每个瓦片上设置边框。

参见

  • [理解如何使用主题文件夹工作主题] 菜谱

创建一个新的 OpenLayers 主题

有可能我们希望 OpenLayers 主题的外观完全不同。

正如我们在本章引言和其他菜谱(理解使用 img 文件夹中的主题工作方式理解使用主题文件夹中的主题工作方式)中提到的,OpenLayers 主题是基于图像和 CSS 文件:

创建新的 OpenLayers 主题

在这个菜谱中,我们将看到如何基于theme/default文件夹中可以找到的默认主题创建一个新的 OpenLayers 主题。我们将更改一些最常用控件的某些方面,例如比例或比例线、概览图或图层切换器。

准备工作

要创建一个新的主题,我们需要为 OpenLayers 发行版的imgtheme文件夹的内容创建副本。这两个文件夹都包含用于控件的图像,因此很容易看出良好的图形设计对于创建良好的主题是多么重要。

本章使用的名为绿色主题的主题分为recipes/data/green_imgrecipes/data/green_theme文件夹。

如何做到这一点...

  1. 创建一个 HTML 文件。在head部分,我们需要附加 OpenLayers 库的依赖项以及我们自定义主题的 CSS 样式表:

        <head> 
            <title>Creating a new OpenLayers theme</title> 
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
    
            <!-- Include OpenLayers library --> 
            <script type="text/javascript" src="img/OpenLayers.js"></script> 
    
            <!-- OpenLayers Theme --> 
            <link rel="stylesheet" href="../../js/OpenLayers-2.11/theme/default/style.css" type="text/css"> 
    
            <!-- Out customer Green Theme --> 
            <link rel="stylesheet" href="../data/green_theme/style.css" type="text/css"> 
    
    
  2. 在继续查看head部分的代码之前,让我们在将包含地图实例和导航历史工具的body部分中编写以下代码:

        <body onload="init()"> 
            <div id="ch06_theme" style="width: 100%; height: 100%;"></div> 
            <div id="history" class="historyClass"></div> 
        </body> 
    
    
  3. 再次,在head部分中,添加以下必要的样式代码,用于之前用于包含导航历史工具的div元素:

            <style> 
                .historyClass { 
                    position: absolute; 
                    top: 5px; 
                    right: 125px; 
                    z-index: 9999; 
                } 
            </style> 
    
    
  4. script部分中,添加以下 JavaScript 代码,指定 OpenLayers 可以找到的文件夹img:的位置:

                OpenLayers.ImgPath = "http://localhost:8080/openlayers-cookbook/recipes/data/green_img/"; 
    
    
  5. 实现init函数,该函数在body加载时执行。它创建地图实例,并添加一个基本图层和一组最常用的控件:

                function init() { 
                    // Create map 
                    var map = new OpenLayers.Map("ch06_theme", { 
                        controls: [] 
                    });    
                    var osm = new OpenLayers.Layer.OSM();        
                    map.addLayer(osm); 
                    map.setCenter(new OpenLayers.LonLat(0, 0), 2); 
    
                    var vectorLayer = new OpenLayers.Layer.Vector("Vector Layer"); 
                    map.addLayer(vectorLayer); 
    
                    // Add controls 
                    map.addControl(new OpenLayers.Control.Navigation()); 
                    map.addControl(new OpenLayers.Control.LayerSwitcher({roundedCorner: false})); 
                    map.addControl(new OpenLayers.Control.PanZoomBar({zoomWorldIcon: true})); 
                    map.addControl(new OpenLayers.Control.MousePosition()); 
                    map.addControl(new OpenLayers.Control.OverviewMap()); 
                    map.addControl(new OpenLayers.Control.Scale()); 
                    map.addControl(new OpenLayers.Control.ScaleLine()); 
                    map.addControl(new OpenLayers.Control.Permalink()); 
                    map.addControl(new OpenLayers.Control.EditingToolbar(vectorLayer)); 
    
                    var history = new OpenLayers.Control.NavigationHistory(); 
                    var panel = new OpenLayers.Control.Panel({ 
                        div: document.getElementById('history') 
                    }); 
                    panel.addControls([history.next, history.previous]); 
                    map.addControls([history, panel]); 
                } 
    
    

它是如何工作的...

让我们简要地描述一下 JavaScript 代码。我们创建了一个地图,添加了一个基本图层,并最终添加了一组控件。

特别注意我们是如何添加导航历史控件的,因为这是应用程序的第一个主题点:

        var history = new OpenLayers.Control.NavigationHistory(); 
        var panel = new OpenLayers.Control.Panel({ 
            div: document.getElementById('history') 
        }); 
        panel.addControls([history.next, history.previous]); 
        map.addControls([history, panel]); 

        ...
        ...
        <div id="history" class="historyClass"></div> 
        ...
        ...

我们已经实例化了控件,并将其按钮放置在Panel控件上。除了在网页的特定div元素中渲染Panel控件外,我们还设置了其div属性指向所需元素。

CSS 的historyClass是一个类,允许我们将浮动在右侧(靠近编辑工具栏控件)的控件放置:

.historyClass { 
    position: absolute; 
    top: 5px; 
    right: 125px; 
    z-index: 9999; 
}

输出将如以下截图所示:

如何工作...

主题的其余部分基于两个重要的文件夹。

由于一些控件基于img文件夹中的图像,我们在 JavaScript 代码的开头设置了该文件夹的路径。例如PanZoomBarLayerSwitcher控件需要以下文件夹来获取它们的图标:

OpenLayers.ImgPath = "http://localhost:8080/openlayers-cookbook/recipes/data/green_img/";

以下截图显示了PanZoomBar控件的图标:

如何工作...

另一方面,例如ScaleScaleLine控件,MousePosition,或者LayerSwitcher控件的许多方面,都是通过theme文件夹中的style.css文件及其图像通过 CSS 定义的。这是通过文档的<link>标签包含的:

<!-- OpenLayers Theme --> 
<link rel="stylesheet" href="../../js/OpenLayers-2.11/theme/default/style.css" type="text/css"> 

<!-- Out customer Green Theme --> 
<link rel="stylesheet" href="../data/green_theme/style.css" type="text/css">

green_theme/style.css文件并不包含对 OpenLayers 默认主题中找到的所有类的完整重新定义。我们只是重新定义了一些影响某些控件颜色或位置的类。为此,我们首先包含了default/style.css文件,然后是我们的自定义green_theme/style.css文件,该文件仅通过添加或更改样式来重新定义了一些类。

没有关于如何为主题 CSS 控件的方法的魔法配方。我们需要检查控件生成的 HTML 代码,看看它们使用了哪些 CSS 类,以及我们可以应用哪些其他类。让我们看看一些主题化的控件。

LayerSwitcher控件上,我们更改了字体大小,背景和边框颜色,添加了边框半径(仅适用于 CSS3 兼容的浏览器),并将基础和叠加部分的标题改为斜体:

.olControlLayerSwitcher {
    font-size: x-small;
    font-weight: normal;
}
.olControlLayerSwitcher .layersDiv {
    background-color: #38535c;
    border-radius: 1em;
    border-width: 3px 0 3px 3px;
    border-style: solid;
    border-color: #b6c6ce;
}
.olControlLayerSwitcher .layersDiv .baseLbl,
.olControlLayerSwitcher .layersDiv .dataLbl {
    font-style: italic;
    font-weight: bolder;
}

以下截图显示了使用前面代码所做的更改的LayerSwitcher控件:

如何工作...

对于OverviewMap控件,我们添加了背景和边框颜色,边框半径,并将其稍微向上移动,为下面的控件留出空间:

.olControlOverviewMapContainer {
    bottom: 20px;
}
.olControlOverviewMapElement {
    background-color: #38535c;
    border-radius: 1em 0 0 1em;
    border-width: 3px 0 3px 3px;
    border-style: solid;
    border-color: #b6c6ce;
}

以下截图显示了使用前面代码所做的更改的OverviewMap控件:

如何工作...

ScaleScaleLine控件已经移动到左侧,并且颜色已经改变,以符合绿色主题:

.olControlScaleLine {
    bottom: 10px;
    font-size: x-small;
}
.olControlScaleLineTop {
    border: solid 2px #38535c;
    border-top: none;
}
.olControlScaleLineBottom {
    border: solid 2px #38535c;
    border-bottom: none;
}
.olControlScale {
    left: 10px;
    bottom: 40px;
    font-size: x-small;
}

以下截图显示了使用前面代码所做的更改的ScaleScaleLine控件:

如何工作...

对于MousePositionPermalink控件,我们稍微移动了它们并更改了文本颜色:

div.olControlMousePosition {
    bottom: 5px;
    right: 60px;
    font-size: small;
}

.olControlPermalink {
    bottom: 5px;
    font-size: x-small;
}
.olControlPermalink a {
    color: #38535c;
    text-decoration: none;
}

以下截图显示了使用前面代码所做的更改的MousePositionPermalink控件:

如何工作...

最后,对于NavigationHistory控件,我们需要重新定义一个 CSS 类,以便按钮水平排列,而不是默认的垂直排列:

.olControlNavigationHistory {
    float: right;
}

如何工作...

还有更多...

正如我们提到的,没有简单的方法来为主题组件。我们需要考虑将渲染组件的 HTML 代码,它使用的图像,以及可能应用的 CSS 样式。

改变你的地图应用程序外观的最简单方法是像我们在这里做的那样玩弄图标和 CSS。更彻底的改进包括创建自己的控件或将控件放置在外部框架提供的按钮上,例如 Dojo (dojotoolkit.org),jQueryUI (jqueryui.com),或 ExtJS (www.sencha.com/products/extjs),并编写必要的代码来激活或禁用控件。GeoExt 项目 (geoext.org) 是一个很好的例子,该项目基于之前的 ExtJS 项目提供了丰富的组件。

参见

  • 使用img文件夹理解主题如何工作的配方

  • 使用theme文件夹理解主题如何工作的配方

  • 在第五章“添加控件”中的添加导航历史控件配方

  • 在第五章“添加控件”中的将控件放置在地图外配方

控件外开始操作

另一种不同且彻底改变我们应用程序外观的方法是将控件放置在地图外,并将它们附加到我们自己的组件上。

大多数 OpenLayers 控件都有两个特性:

  • 他们执行一些操作(编辑要素、创建线条等)

  • 他们知道如何将自己在地图上显示出来

为了实现本配方的目标,想法是将可视化与控件执行的操作分离。这样,我们可以创建一些按钮,并根据按下的按钮激活或禁用控件:

控件外开始操作

正如我们在截图中所见,我们将创建一个工具栏并放置:

  • 我们可以在OpenLayers.Control.EditingToolbar中找到的相同控件集,这将允许我们绘制点、线和多边形

  • 一个下拉按钮,允许我们启动OpenLayers.Control.Measure操作

准备工作

我们将使用 Dojo Toolkit 框架 (dojotoolkit.org/),这是我们与本书源代码一起使用的,但如果您更喜欢,可以使用纯 HTML 按钮或div元素。

如何做...

  1. 创建一个 HTML 文件并添加 OpenLayers 依赖项,包括 JavaScript 和 CSS。在将用于样式化我们自定义按钮的style元素中添加以下 CSS 类:

    <style>
        .pointer { background-image: url(./recipes/data/gis_icons/pointer.png); }
        .point { background-image: url(./recipes/data/gis_icons/point.png); }
        .line { background-image: url(./recipes/data/gis_icons/line.png); }
        .polygon { background-image: url(./recipes/data/gis_icons/polygon.png); }
        .area { background-image: url(./recipes/data/gis_icons/area-measure.png); }
        .length { background-image: url(./recipes/data/gis_icons/length-measure.png); }
    </style>
    
    
  2. 现在,让我们创建工具栏。它将包含四个切换按钮来选择编辑操作:

    <div data-dojo-type="dijit.Toolbar">
        Editor:
        <div data-dojo-type="dijit.form.ToggleButton" 
             data-dojo-props="iconClass:'dijitEditorIcon pointer', showLabel:false, onClick:pointerAction, checked:true">Pan</div>
        <div data-dojo-type="dijit.form.ToggleButton" 
             data-dojo-props="iconClass:'dijitEditorIcon point', showLabel:false, onClick:pointAction">Point</div>
        <div data-dojo-type="dijit.form.ToggleButton" 
             data-dojo-props="iconClass:'dijitEditorIcon line', showLabel:false, onClick:lineAction">Line</div>
        <div data-dojo-type="dijit.form.ToggleButton" 
             data-dojo-props="iconClass:'dijitEditorIcon polygon', showLabel:false, onClick:polygonAction">Polygon</div>
    
    
  3. 以及一个下拉按钮来选择要进行的测量类型:

        <span data-dojo-type="dijit.ToolbarSeparator"></span>
    
        <div data-dojo-type="dijit.form.DropDownButton">
            <span>Measure</span>
            <div data-dojo-type="dijit.DropDownMenu">
                <div data-dojo-type="dijit.MenuItem" data-dojo-props="iconClass:'dijitEditorIcon length', onClick:measureLengthAction">Distance</div>
                <div data-dojo-type="dijit.MenuItem" data-dojo-props="iconClass:'dijitEditorIcon area', onClick:measureAreaAction">Area</div>
            </div>
        </div>
    
    

    以下截图显示了创建的下拉按钮:

    如何做...

  4. 此外,工具栏将包含一个span元素来显示测量值:

        <span id="value"></span>
    </div>
    
    
  5. 现在,我们可以放置将包含地图的div元素:

    <div id="ch06_external" style="width: 100%; height: 90%;"></div>
    
    
  6. 现在,添加初始化地图所需的 JavaScript 代码,并添加一个基础图层和一个矢量图层以向地图添加功能:

    <script type="text/javascript">
        // Create map
        var map = new OpenLayers.Map("ch06_external");    
        var osm = new OpenLayers.Layer.OSM();        
        map.addLayer(osm);
    
        map.setCenter(new OpenLayers.LonLat(0, 0), 2);
    
        var vectorLayer = new OpenLayers.Layer.Vector("VectorLayer");
        map.addLayer(vectorLayer);
    
    
  7. 接下来,将控件添加到地图中。首先,添加与 DrawFeature 控件相关的控件:

        // Add controls
        var pointControl = new OpenLayers.Control.DrawFeature(vectorLayer, OpenLayers.Handler.Point);
        var lineControl = new OpenLayers.Control.DrawFeature(vectorLayer, OpenLayers.Handler.Path);
        var polygonControl = new OpenLayers.Control.DrawFeature(vectorLayer, OpenLayers.Handler.Polygon);
    
    
  8. 然后添加 Measure 控件,允许我们测量距离和面积:

        var measureControl = new OpenLayers.Control.Measure(OpenLayers.Handler.Path, {
            persist: true,
            immediate: true,
            eventListeners: {
                'measure': updateMeasure,
                'measurepartial': updateMeasure
            }
        }); 
        map.addControls([pointControl, lineControl, polygonControl, measureControl]);
    
    
  9. 实现处理表示 EditingToolbar(手、绘制点、绘制路径和绘制多边形)动作的函数:

        // Functions to control button actions
        var currentControl = null;
        function pointerAction() {
            _unselectButtons(this);
            _selectControl(null);
        }
        function pointAction(){
            _unselectButtons(this);
            _selectControl(pointControl);
        }
        function lineAction(){
            _unselectButtons(this);
            _selectControl(lineControl);
        }
        function polygonAction(){
            _unselectButtons(this);
            _selectControl(polygonControl);
        }
    Implement the actions for the measure control:
        function measureLengthAction(){
            _unselectButtons(this);
            measureControl.updateHandler(OpenLayers.Handler.Path, {persist: true});
            _selectControl(measureControl);
        }
        function measureAreaAction(){
            _unselectButtons(this);
            measureControl.updateHandler(OpenLayers.Handler.Polygon, {persist: true});
            _selectControl(measureControl);
        }
    
    
  10. 最后,添加两个辅助函数的代码:

        function _selectControl(control) {
            if(currentControl) {
                currentControl.deactivate();
            }
            if(control) {
                currentControl = control;
                currentControl.activate();
            }
        }
        function _unselectButtons(context) {
            dijit.registry.byClass('dijit.form.ToggleButton').forEach(function(button){
                if(context==button) return;
                button.set('checked', false);
            });
        }
        function updateMeasure(event) {
            var message = event.measure + " " + event.units;
            if(event.order>1) {
                message += "2";
            }
            dojo.byId('value').innerHTML = message;
        }
    </script>
    
    

它是如何工作的...

每个控件都必须附加到地图上,但像在这个食谱中一样,控件不需要有可见的表示。

同样地,我们可以对 OpenLayers.Map 实例调用方法以进行放大或缩小。我们可以无需面板或图标即可编程激活或停用控件。

对于允许我们创建新功能(点、线和多边形)的三个按钮,我们基于 OpenLayers.Control.DrawFeature 控件创建了三个控件。

此控件需要两个参数:矢量层(用于添加新功能)和处理程序(在创建功能时用于与地图交互):

    var pointControl = new OpenLayers.Control.DrawFeature(vectorLayer, OpenLayers.Handler.Point);
    var lineControl = new OpenLayers.Control.DrawFeature(vectorLayer, OpenLayers.Handler.Path);
    var polygonControl = new OpenLayers.Control.DrawFeature(vectorLayer, OpenLayers.Handler.Polygon);

注意

在工具栏中这三个控件(以按钮形式表示)之前,有一个指针按钮,允许我们停用当前控件并平移地图。它不需要与任何控件相关联。当按钮被点击时,我们只需停用当前控件,允许地图再次平移。

虽然在下拉按钮中从视觉上看有两个选项,但内部上,两者都对应于同一个控件,即 OpenLayers.Control.Measure 控件。当点击测量选项之一时,我们设置控件使用的处理程序:

    function measureLengthAction(){
        _unselectButtons(this);
        measureControl.updateHandler(OpenLayers.Handler.Path, {persist: true});
        _selectControl(measureControl);
    }
    function measureAreaAction(){
        _unselectButtons(this);
        measureControl.updateHandler(OpenLayers.Handler.Polygon, {persist: true});
        _selectControl(measureControl);
    }

我们已将 Measure 控件的 persist 属性指定为 true。这使得绘制的线或多边形在测量结束时仍然显示并保持在地图上可见。

immediate 属性允许测量过程在鼠标每次移动时触发一个事件。

最后,我们指定了事件监听器,即当测量完成时触发的 measure 事件和每次鼠标移动时触发的 measurepartial 事件。

两个事件都会执行 updateMeasure 函数,该函数负责更新工具栏中显示的测量值:

    function updateMeasure(event) {
        var message = event.measure + " " + event.units;
        if(event.order>1) {
            message += "2";
        }
        dojo.byId('value').innerHTML = message;
    }

负责处理逻辑以同时保持一个按钮或控件激活的代码实现于 pointAction 函数中,并使用了 currentControl 变量。

目标很简单,每次按下按钮时,当前选定的按钮会被切换,相关的控件在激活新选定的控件之前会被停用。

还有更多...

GeoExt项目(geoext.org),是一个基于 ExtJS(www.sencha.com/products/extjs/)的工具包,它提供了一套丰富的用户界面组件,以简化丰富网络应用的开发。

使用 GeoExt 创建一个图层树或网格来编辑特征属性相当简单。

注意

用于构建网络应用的 ESRI JavaScript API 基于 dojo Toolkit 框架。

不管怎样,这两个都是优秀的框架。

参见

  • 在第五章的将控件放置在地图外部配方中,添加控件

  • 在第五章的添加和删除控件配方中,添加控件

第七章。样式化特征

在本章中,我们将介绍:

  • 使用符号化器对特征进行样式化

  • 使用 StyleMap 和替换特征属性来改进样式

  • 玩转 StyleMap 和渲染意图

  • 使用唯一值规则进行工作

  • 定义自定义规则以样式化特征

  • 样式化聚合特征

简介

一旦我们知道了如何处理矢量图层,例如添加新特征或修改现有特征,我们可能会想到的问题就是:如何对它们进行样式化?

特征的视觉表示,即样式,是 GIS 应用中最重要的概念之一。它不仅从用户体验或设计师的角度来看很重要,而且作为信息需求也很重要,例如,用于识别符合某些规则的特征。

我们可视化特征的方式不仅对我们使应用程序更具吸引力很重要,而且也有助于改善我们向用户传达信息的方式。例如,给定一组代表某些温度的点,如果我们对最热的区域感兴趣,我们可以用不同的半径和颜色值来表示它们。这样,较小的半径和接近蓝色的颜色意味着冷区,而较大的半径和接近红色的颜色意味着热区。

OpenLayers 在样式化特征方面提供了很高的灵活性,这最初可能看起来有点复杂。例如,符号化器、StyleMap、规则或过滤器等概念都与样式化过程相关。

让我们看看以下菜谱中的所有这些内容。

使用符号化器对特征进行样式化

要查看样式化特征的最低形式,我们将创建一个小型地图编辑器,允许通过指定一些样式属性来添加新特征:

使用符号化器对特征进行样式化

每个OpenLayers.Feature.Vector实例都可以与其关联一个样式。这个样式被称为符号化器,它只是一个具有一些字段的 JavaScript 对象,这些字段指定了填充颜色、描边等。例如:

{ 
    fillColor: "#ee9900", 
    fillOpacity: 0.4, 
    strokeColor: "#ee9900", 
    strokeOpacity: 1, 
    strokeWidth: 1 
}

在代码中,每次要向地图添加特征时,代码将从左侧的控件中获取填充和描边属性,并为新特征创建一个新的符号化器哈希值。

准备工作

源代码有两个主要部分,一个用于 HTML,其中放置了所有控件,另一个用于 JavaScript 代码。

HTML 部分包含大量与选择填充和描边属性相关的代码。这些控件来自 Dojo Toolkit 项目(dojotoolkit.org),因为它们不是本菜谱的目标,所以我们没有在这里介绍。我们鼓励读者查看本书的代码包。

让我们看看 JavaScript 代码。

如何操作...

  1. 在创建包含 OpenLayers 依赖项的 HTML 文件(参见 准备就绪 部分中的 HTML 代码)后,在 div 元素中创建 map 实例,其标识符为 ch07_using_symbolizers,并添加一个基本图层:

        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch07_using_symbolizers");
    
        var osm = new OpenLayers.Layer.OSM();
        map.addLayer(osm);
    
        map.setCenter(new OpenLayers.LonLat(0,0), 3)
    
    
  2. 现在,添加一个矢量层,其中将放置新特征:

        var vectorLayer = new OpenLayers.Layer.Vector("Features");
        vectorLayer.events.register('beforefeatureadded', vectorLayer, setFeatureStyle);
        map.addLayer(vectorLayer);
    
    
  3. 添加 OpenLayers.Control.EditingToolbar 控件,允许向之前的矢量层添加新特征:

        var editingControl = new OpenLayers.Control.EditingToolbar(vectorLayer);
        map.addControl(editingControl);
    
    
  4. 添加负责获取并应用于新特征的样式的代码:

        function setFeatureStyle(event) {
            var fillColor = dijit.byId('fillColor').get('value');
            var fillOpacity = dijit.byId('fillOpacity').get('value')/100;
            var strokeColor = dijit.byId('strokeColor').get('value');
            var strokeWidth = dijit.byId('strokeWidth').get('value');
            var strokeOpacity = dijit.byId('strokeOpacity').get('value')/100;
            var pointRadius = dijit.byId('pointRadius').get('value');
    
            var style = OpenLayers.Util.extend({}, OpenLayers.Feature.Vector.style['default']);
            style.fillColor = fillColor;
            style.fillOpacity = fillOpacity;
            style.strokeColor = strokeColor;
            style.strokeWidth = strokeWidth;
            style.strokeOpacity = strokeOpacity;
            style.pointRadius = pointRadius;
    
            event.feature.style = style;
        }
    
    

它是如何工作的...

理念是,每次使用 EditingToolbar 控件将特征添加到图层时,创建一个符号化器并将其应用于新特征。

第一步是在矢量层中注册一个 beforefeatureadded 事件监听器,以便我们能在每次新特征即将添加时得到通知:

    vectorLayer.events.register('beforefeatureadded', vectorLayer, setFeatureStyle);

每次添加新特征时都会调用 setFeatureStyle 函数。每次调用都会传递一个 event 参数,指向要添加的特征(event.feature)和矢量层的引用(event.object)。

提示

event.object 引用了在 event.register(event_type, object, listener) 方法中作为 object 参数传递的对象。

    function setFeatureStyle(event) {
        var fillColor = dijit.byId('fillColor').get('value');
        var fillOpacity = dijit.byId('fillOpacity').get('value')/100;
        var strokeColor = dijit.byId('strokeColor').get('value');
        var strokeWidth = dijit.byId('strokeWidth').get('value');
        var strokeOpacity = dijit.byId('strokeOpacity').get('value')/100;
        var pointRadius = dijit.byId('pointRadius').get('value');

        var style = OpenLayers.Util.extend({}, OpenLayers.Feature.Vector.style['default']);
        style.fillColor = fillColor;
        style.fillOpacity = fillOpacity;
        style.strokeColor = strokeColor;
        style.strokeWidth = strokeWidth;
        style.strokeOpacity = strokeOpacity;
        style.pointRadius = pointRadius;

        event.feature.style = style;
    }

一旦我们从 Dojo 小部件中获取属性值,我们就创建一个新的符号化器。

OpenLayers.Feature.Vector 类在 style 数组属性中定义了一些样式符号化器,因此最快的方式是复制其中之一(实际上我们已经扩展了它),然后修改其一些属性。

更多内容...

在这里可能出现的疑问是:在样式化时,是应用于矢量层的规则优先,还是应用于单个特征的符号化器优先?

答案是:样式从下到上应用,也就是说,如果我们在一个特征中指定了符号化器,则它将被用来渲染它,否则,任何分配给矢量层的规则或 StyleMap 都将应用于其特征。

参见

  • 使用 StyleMap 和替换特征属性改进样式 的配方

  • 玩转 StyleMap 和渲染意图 的配方

使用 StyleMap 和替换特征属性改进样式

我们可以总结说,有三种方式来样式化一个特征。第一种是直接将符号化器散列应用于特征(参见 使用符号化器样式化特征 配方)。第二种是将样式应用于图层,使得其中的每个特征都变得样式化。

在许多情况下,第二种方法是首选的。这是一种通用的方式,通过设置一些样式和规则来样式化图层中的所有特征。

这个配方展示了我们如何使用 StyleMap 实例,以及我们如何轻松地样式化图层中的所有点,而无需对每个特征应用样式。此配方的输出应类似于以下截图:

使用 StyleMap 和替换特征属性改进样式

此外,我们将使用的技巧允许我们涉及特征的属性来选择点半径和颜色,更动态地创建它们所有。

如何做到这一点...

  1. 一旦我们创建了包含 OpenLayers 依赖项的 HTML 文件,就开始创建将包含地图实例的 div 元素:

    <div id="ch07_styleMap" style="width: 100%; height: 95%;"></div>
    
    
  2. 现在,创建地图实例并添加一个基本图层:

    <script type="text/javascript">
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch07_styleMap");
    
        var osm = new OpenLayers.Layer.OSM();
        map.addLayer(osm);
    
        map.setCenter(new OpenLayers.LonLat(0,0), 2)
    
    
  3. 现在,让我们开始定义整个图层的样式。首先为点创建一个调色板:

        // Create stylemap for the layer
        var colors = ['#EBC137','#E38C2D','#DB4C2C','#771E10','#48110C'];
    
    
  4. 从之前的符号化器哈希创建一个样式实例:

        var style = OpenLayers.Util.extend({}, OpenLayers.Feature.Vector.style["default"]);
        style.pointRadius = "${radius}";
        style.fillColor = '${colorFunction}';
    
        var defaultStyle = new OpenLayers.Style(style, {
            context: {
                colorFunction: function(feature) {
                    return colors[feature.attributes.temp];
                }
            }
        });
    
    
  5. 应用所需的 StyleMap 创建矢量图层:

        // Create the vector layer
        var vectorLayer = new OpenLayers.Layer.Vector("Features", {
            styleMap: new OpenLayers.StyleMap(defaultStyle)
        });
        map.addLayer(vectorLayer);
    
    
  6. 最后,创建一些随机点。每个特征将有两个属性 radiustemp,它们具有随机值:

        // Create random feature points.
        var pointFeatures = [];
        for(var i=0; i< 150; i++) {
            var px = Math.random() * 360 - 180;
            var py = Math.random() * 170 - 85;
    
            // Create a lonlat instance and transform it to the map projection.
            var lonlat = new OpenLayers.LonLat(px, py);
            lonlat.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));
    
            var pointGeometry = new OpenLayers.Geometry.Point(lonlat.lon, lonlat.lat);
            var pointFeature = new OpenLayers.Feature.Vector(pointGeometry);
    
            // Add random attributes
            var radius = Math.round(Math.random() * 15 + 4);
            var temp = Math.round(Math.random() * 4);
            pointFeature.attributes.radius = radius;
            pointFeature.attributes.temp = temp;
    
            pointFeatures.push(pointFeature);
        }
        // Add features to the layer
        vectorLayer.addFeatures(pointFeatures); 
    
    </script>
    
    

它是如何工作的...

让我们来描述首先添加到矢量图层中的随机点特征。

想法是在随机位置创建一些随机点。正因为如此,我们创建了一些随机的 x-y 值,将它们转换为地图坐标,创建几何形状,最后创建一个具有该几何形状的特征:

        var px = Math.random() * 360 - 180;
        var py = Math.random() * 170 - 85;

        // Create a lonlat instance and transform it to the map projection.
        var lonlat = new OpenLayers.LonLat(px, py);
        lonlat.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));

        var pointGeometry = new OpenLayers.Geometry.Point(lonlat.lon, lonlat.lat);
        var pointFeature = new OpenLayers.Feature.Vector(pointGeometry);

此外,我们在每个特征中设置了一对具有随机值的属性(radiustemp):

        // Add random attributes
        var radius = Math.round(Math.random() * 15 + 4);
        var temp = Math.round(Math.random() * 4);
        pointFeature.attributes.radius = radius;
        pointFeature.attributes.temp = temp;

这些属性将在特征样式定义中稍后使用。

让我们来描述矢量图层样式的创建。

我们希望每个特征都使用属性 radius 作为点的半径和 temp 属性作为点的颜色来表示。

第一步是创建一个通过哈希复制(实际上扩展)的符号化器,它在 OpenLayers.Feature.Vector.style["default"]) 中定义。

    var style = OpenLayers.Util.extend({}, OpenLayers.Feature.Vector.style["default"]);

如果你查看源代码,你会发现 OpenLayers.Feature.Vector.style["default"]) 被定义为:

{ 
    fillColor: "#ee9900", 
    fillOpacity: 0.4, 
    hoverFillColor: "white", 
    hoverFillOpacity: 0.8, 
    strokeColor: "#ee9900", 
    strokeOpacity: 1, 
    strokeWidth: 1, 
    strokeLinecap: "round", 
    strokeDashstyle: "solid", 
    hoverStrokeColor: "red", 
    hoverStrokeOpacity: 1, 
    hoverStrokeWidth: 0.2, 
    pointRadius: 6, 
    hoverPointRadius: 1, 
    hoverPointUnit: "%", 
    pointerEvents: "visiblePainted", 
    cursor: "inherit" 
}

一旦我们有一个新的符号化器副本,我们就更改 fillColorpointRadius 属性。这里的挑战是什么?嗯,我们不想为这些属性使用固定值,我们希望这些属性从它们所修饰的特征的属性中获取它们的值。

幸运的是,OpenLayers 帮助我们使用 属性替换 语法。同样,我们可以按如下方式写入一个字面值:

pointRadius: 15

我们可以指定半径值必须来自特征的 featureRadius 属性:

pointRadius: '${featureRadius}'

因此,在我们的示例中,我们的特征具有属性 radius,定义为可以在此处使用的随机值:

    style.pointRadius = "${radius}";

与我们可以使用一个属性作为属性值替换一样,我们也可以设置一个必须返回用作属性值的值的函数。这是 fillColor 属性的情况:

    style.fillColor = '${colorFunction}';

正如我们接下来将要看到的,函数 colorFunction 返回一个值,这个值取决于特征的 temp 属性,也就是说,我们不想直接使用 temp 属性,而想使用从它计算出的值。

不幸的是,属性替换不能直接在应用于特征或图层的符号化哈希中直接使用,它只能通过 OpenLayers.Style 实例使用。此外,多亏了 OpenLayers.Style 实例,我们可以定义函数,例如 colorFunction,用于计算样式属性值。

注意

属性替换只能通过 OpenLayers.Style 实例使用。

在菜谱中,一旦我们定义了符号化哈希,我们就可以创建一个实例,如下所示:

    var defaultStyle = new OpenLayers.Style(style, {
        context: {
            colorFunction: function(feature) {
                return colors[feature.attributes.temp];
            }
        }
    });

第一个参数是之前定义的符号化哈希,它使用了属性的替换功能。第二个参数,context,是在渲染特征的过程中传入的对象。在这里,我们定义了所需的函数,例如 colorFunction,它将在渲染过程中可用,并将根据每个特征的 temp 属性定义 fillColor 的值。

到目前为止,我们几乎完成了。唯一剩下的事情就是创建一个使用定义的 OpenLayers.Style 实例来样式化特征的向量层。

OpenLayers.Layer.Vector 类有一个 styleMap 属性,用于指定应用于特征的样式。OpenLayers.StyleMap 类可以通过传递不同的参数来实例化,但在这里我们使用之前定义的 OpenLayers.Style 样式来创建它:

    var vectorLayer = new OpenLayers.Layer.Vector("Features", {
        styleMap: new OpenLayers.StyleMap(defaultStyle)
    });
    map.addLayer(vectorLayer);

现在,我们的菜谱已经完成。正如我们所看到的,我们不需要为每个特征创建符号化并将其应用到每个特征上。我们唯一需要做的是定义一个样式并将其分配给向量层。

参见

  • 玩转 StyleMap 和渲染意图 的菜谱

  • 使用符号化样式化特征 的菜谱

  • 第三章 中 Creating features programmatically 的菜谱,Vector Layers

玩转 StyleMap 和渲染意图

有一些控件,如 SelectFeatureModifyFeatureEditingToolbar,根据特征当前的状态(即是否被选中或正在被编辑)改变特征的风格。OpenLayers 如何管理这一点?答案是,通过 渲染意图

玩转 StyleMap 和渲染意图

这个菜谱展示了我们如何修改每个渲染意图所使用的样式,以改变我们应用程序的外观。

这样,特征将以蓝色而不是橙色在地图上绘制。将要创建的临时特征将以绿色绘制。最后,那些被选中的特征,或者处于修改过程中的特征,将以橙色绘制。

如何做到这一点...

  1. 创建一个新的 HTML 文件并添加 OpenLayers 依赖项。第一步是添加 div 元素以容纳地图实例:

    <div id="ch07_rendering_intents" style="width: 100%; height: 95%;"></div>
    
    
  2. 在 JavaScript 部分,初始化地图实例,添加基本图层,并定位视口:

    <script type="text/javascript">
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch07_rendering_intents");
    
        var osm = new OpenLayers.Layer.OSM();
        map.addLayer(osm);
    
        map.setCenter(new OpenLayers.LonLat(0,0), 2)
    
    
  3. 现在我们将创建三种不同的样式:

        var defaultStyle = new OpenLayers.Style({
            fillColor: "#336699",
            fillOpacity: 0.4, 
            hoverFillColor: "white",
            hoverFillOpacity: 0.8,
            strokeColor: "#003366",
            strokeOpacity: 0.8,
            strokeWidth: 2,
            strokeLinecap: "round",
            strokeDashstyle: "solid",
            hoverStrokeColor: "red",
            hoverStrokeOpacity: 1,
            hoverStrokeWidth: 0.2,
            pointRadius: 6,
            hoverPointRadius: 1,
            hoverPointUnit: "%",
            pointerEvents: "visiblePainted",
            cursor: "inherit"
        });
        var selectStyle = new OpenLayers.Style({
            fillColor: "#ffcc00",
            fillOpacity: 0.4, 
            hoverFillColor: "white",
            hoverFillOpacity: 0.6,
            strokeColor: "#ff9900",
            strokeOpacity: 0.6,
            strokeWidth: 2,
            strokeLinecap: "round",
            strokeDashstyle: "solid",
            hoverStrokeColor: "red",
            hoverStrokeOpacity: 1,
            hoverStrokeWidth: 0.2,
            pointRadius: 6,
            hoverPointRadius: 1,
            hoverPointUnit: "%",
            pointerEvents: "visiblePainted",
            cursor: "pointer"
        });
        var temporaryStyle = new OpenLayers.Style({
            fillColor: "#587058",
            fillOpacity: 0.4, 
            hoverFillColor: "white",
            hoverFillOpacity: 0.8,
            strokeColor: "#587498",
            strokeOpacity: 0.8,
            strokeLinecap: "round",
            strokeWidth: 2,
            strokeDashstyle: "solid",
            hoverStrokeColor: "red",
            hoverStrokeOpacity: 1,
            hoverStrokeWidth: 0.2,
            pointRadius: 6,
            hoverPointRadius: 1,
            hoverPointUnit: "%",
            pointerEvents: "visiblePainted",
            cursor: "inherit"
        });
    
    
  4. 然后,创建一个包含作为三个不同渲染意图创建的三个样式的 StyleMap 实例:

        var styleMap = new OpenLayers.StyleMap({
            'default': defaultStyle,
            'select': selectStyle,
            'temporary': temporaryStyle
        });
    
    
  5. 现在,我们可以使用之前的 StyleMap 实例创建一个矢量层:

        var vectorLayer = new OpenLayers.Layer.Vector("Features", {
            styleMap: styleMap
        });
        map.addLayer(vectorLayer);
    
    
  6. 最后,我们将向地图添加一些控件,以允许添加新特征和修改现有特征:

        var editingControl = new OpenLayers.Control.EditingToolbar(vectorLayer);
        var modifyControl = new OpenLayers.Control.ModifyFeature(vectorLayer, {
            toggle: true
        });
        editingControl.addControls([modifyControl]);
        map.addControl(editingControl);
    </script>
    
    

如何工作...

每个矢量层都可以与一个 OpenLayers.StyleMap 实例相关联。单独的 StyleMap 实例存储一个或多个对 OpenLayers.Style 实例的引用,每个实例都充当一个渲染意图:

    var styleMap = new OpenLayers.StyleMap({
        'default': defaultStyle,
        'select': selectStyle,
        'temporary': temporaryStyle
    });

每个 Style 实例存储有关样式的信息,通常它们是从符号化哈希创建的,如本食谱中所示:

    var defaultStyle = new OpenLayers.Style({
        fillColor: "#336699",
        fillOpacity: 0.4, 
        hoverFillColor: "white",
        hoverFillOpacity: 0.8,
        strokeColor: "#003366",
        strokeOpacity: 0.8,
        strokeWidth: 2,
        strokeLinecap: "round",
        strokeDashstyle: "solid",
        hoverStrokeColor: "red",
        hoverStrokeOpacity: 1,
        hoverStrokeWidth: 0.2,
        pointRadius: 6,
        hoverPointRadius: 1,
        hoverPointUnit: "%",
        pointerEvents: "visiblePainted",
        cursor: "inherit"
    });

在这里,我们为三种渲染意图定义了新的样式:defaultselecttemporary,这些是大多数控件使用的已知渲染意图。

StyleMap 可以存储我们想要的任意多的渲染意图,我们并不局限于这三种常用的渲染意图。例如,我们可以定义如 redhidden 这样的渲染意图,并为它们关联一个 Style,使其以红色渲染特征或完全不显示。

小贴士

通过在样式的符号化哈希中设置属性 display"none",我们可以隐藏特征。这通常用于 delete 渲染意图。

defaultselecttemporary 这样的渲染意图在 OpenLayers 的许多组件中被广泛使用。这样,当特征被渲染时,使用 default 样式。当使用 OpenLayers.Control.SelectFeature 控件选择特征时,使用 select 渲染意图来渲染特征。当我们使用 OpenLayers.Control.EditingToolbar(内部使用 OpenLayers.Control.DrawFeature)创建新特征时,控件使用在 temporary 渲染意图上定义的样式来渲染特征。

因此,创建新的渲染意图没有问题。此外,我们可以创建我们自己的自定义控件,并让它们决定图层必须使用哪种渲染意图来渲染特征。

最后,让我们简要描述一下创建带有控制面板的代码。

首先,我们创建了一个 OpenLayers.Control.EditingToolbar 实例:

    var editingControl = new OpenLayers.Control.EditingToolbar(vectorLayer);

这是一个包含激活/停用一些 OpenLayers.Control.DrawFeature 控件的按钮的 OpenLayers.Control.Panel 控件。接下来,我们创建了一个 OpenLayers.ControlModifyFeature 实例,这是一个单独的控件,我们将其添加到 EditingToolbar 控件中,使其作为一个新按钮可见:

    var modifyControl = new OpenLayers.Control.ModifyFeature(vectorLayer, {
        toggle: true
    });
    editingControl.addControls([modifyControl]);

如何工作...

在截图上,ModifyFeature 控件由交叉图标表示。

还有更多...

样式化和渲染一个特征的流程是复杂的。以下行总结了特征样式化过程中涉及的主要步骤。

对于每个特征,矢量层必须渲染以下内容:

  • 调用方法 OpenLayers.Layer.Vector.drawFeature(feature, style)。它接受两个参数:要绘制的 feature 和要使用的 style。它可以是符号化器或渲染意图字符串。

  • 如果要素有一个 style 属性,它将用于渲染要素。

  • 否则,如果矢量层有一个 style 属性,它将用于渲染要素。

  • 否则,如果提供了 style 参数,并且它是一个样式符号化器,那么它将用于渲染要素。

  • 如果 style 是一个渲染意图字符串,那么将使用 createSymbolizer 方法从与渲染意图关联的 Style 属性创建一个符号化器。这是在符号化器内合并要素属性的地方。

参见

  • 使用符号化器的 风格化要素 菜谱

  • 使用 StyleMap 和替换要素属性来改进样式 菜谱

使用唯一值规则工作

通常,我们不仅根据要素所代表的内容来风格化要素,例如城市或村庄,我们还根据它们的属性来风格化,例如公民数量、成立年份和广场数量。

为了帮助这些情况,OpenLayers 提供了我们定义规则以决定如何风格化要素的可能性。例如,我们可以定义一个规则,对于人口超过 100,000 的所有城市要素,可以渲染一个半径为 20、颜色为棕色的点,而对于人口少于 100,000 的城市,可以渲染一个半径为 10、颜色为橙色、半透明的点。

从规则的世界开始,唯一值规则的概念是我们能找到的最简单的情况。这个想法很简单,根据要素属性的值应用一种样式或另一种样式。

在这个菜谱中,我们将加载一个 GeoJSON 文件,包含世界上的一些城市,并应用一个规则,该规则将根据流行度排名属性(POP_RANK 属性)设置点的半径,如下面的截图所示:

使用唯一值规则工作

如何做到这一点...

  1. 开始创建一个新的 HTML 文件,并添加 OpenLayers 依赖项。添加一个 div 元素来包含地图实例:

    <div id="ch07_unique_value_rules" style="width: 100%; height: 95%;"></div>
    
    
  2. script 元素中,添加初始化地图和添加基本层的所需代码:

    <script type="text/javascript">
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch07_unique_value_rules");
    
        var osm = new OpenLayers.Layer.OSM();
        map.addLayer(osm);
    
        map.setCenter(new OpenLayers.LonLat(0,0), 4)
    
    
  3. 现在定义要在 POP_RANK 特征属性上使用的不同样式:

        var styles = {
          7: { pointRadius: 4, label: "${POP_RANK}" },
          6: { pointRadius: 7, label: "${POP_RANK}" },
          5: { pointRadius: 10, label: "${POP_RANK}" },
          4: { pointRadius: 13, label: "${POP_RANK}" },
          3: { pointRadius: 15, label: "${POP_RANK}" },
          2: { pointRadius: 18, label: "${POP_RANK}", fillColor: "yellow" },
          1: { pointRadius: 21, label: "${POP_RANK}", fillColor: "green" }
        };
    
    
  4. 创建一个 StyleMap 实例并定义一个唯一值规则:

        var styleMap = new OpenLayers.StyleMap();
        styleMap.addUniqueValueRules("default", "POP_RANK", styles);
    
    
  5. 最后,添加一个包含世界上一些城市的矢量层,并使用之前的 StyleMap 实例:

        map.addLayer(new OpenLayers.Layer.Vector("World Cities (GeoJSON)", {
            protocol: new OpenLayers.Protocol.HTTP({
                url: "http://localhost:8080/openlayers-cookbook/recipes/data/world_cities.json",
                format: new OpenLayers.Format.GeoJSON()
            }),
            styleMap: styleMap,
            strategies: [new OpenLayers.Strategy.Fixed()]
        }));
    </script>
    
    

它是如何工作的...

这个菜谱的大部分魔法都集中在 OpenLayers.StyleMap.addUniqueValueRules() 方法上。所以,这个句子:

    styleMap.addUniqueValueRules("default", "POP_RANK", styles);

表示,根据 POP_RANK 属性的值,应用指定的 style 属性到 default 渲染意图。

一旦我们查看哈希样式,它就更有意义了。根据 POP_RANK 的值,代表城市的点的半径将在 421 之间变化:

    var styles = {
      7: { pointRadius: 4, label: "${POP_RANK}" },
      6: { pointRadius: 7, label: "${POP_RANK}" },
      5: { pointRadius: 10, label: "${POP_RANK}" },
      4: { pointRadius: 13, label: "${POP_RANK}" },
      3: { pointRadius: 15, label: "${POP_RANK}" },
      2: { pointRadius: 18, label: "${POP_RANK}", fillColor: "yellow" },
      1: { pointRadius: 21, label: "${POP_RANK}", fillColor: "green" }
    };

最后,我们在地图上添加了一个矢量层,该层使用之前创建的 StyleMap 实例,其中定义了唯一值规则:

    map.addLayer(new OpenLayers.Layer.Vector("World Cities (GeoJSON)", {
        protocol: new OpenLayers.Protocol.HTTP({
            url: "http://localhost:8080/openlayers-cookbook/recipes/data/world_cities.json",
            format: new OpenLayers.Format.GeoJSON()
        }),
        styleMap: styleMap,
        strategies: [new OpenLayers.Strategy.Fixed()]
    }));

此外,矢量层使用 OpenLayers.Protocol.HTTP 实例来加载 GeoJSON 文件,并使用 OpenLayers.Strategy.Fixed 实例仅加载一次源数据。

还有更多...

通过 addUniqueValueRules() 方法使用唯一值规则很简单,但正如我们所理解的,它只适用于离散值范围。

此外,灵活性较差,因为它相当于一个 等于 规则,我们无法将值范围映射到相同的样式。

参见

  • 玩转 StyleMap 和渲染意图 菜谱

  • 定义自定义规则以样式化特征 菜谱

  • 使用点特征作为标记 菜谱在 第三章,矢量层

  • 第三章中的 使用弹出窗口工作 菜谱,矢量层

定义自定义规则以样式化特征

在继续本菜谱之前,我们将简要解释一下。目标,就像本章中的其他菜谱一样,是根据特征的属性值或特征类型来样式化矢量层的特征。

因此,OpenLayers.Layer.Vector 层类可以与一个 OpenLayers.StyleMap 实例相关联,该实例确定如果只有一个 OpenLayers.Style,则层的默认样式,或者如果包含多个 OpenLayers.Style,则可以应用于每个渲染意图的样式集。以自己的方式,每个 OpenLayers.Style 实例都可以以两种形式使用:

  • 具有作为应用于特征的默认样式的符号化器散列

  • 与它关联一些 OpenLayers.Rule 实例

这里我们来到了本菜谱的主要概念,即规则

规则不过是一个过滤器(具体来说是 OpenLayers.Filter)和符号化器之间的连接,如果过滤器匹配特征,则应用符号化器。

这简单的事情为我们提供了很多灵活性和样式化特征的强大功能。除了可以使用具有属性替换的符号化器之外,我们还可以使用 OpenLayers 提供的过滤器集合:比较过滤器、空间过滤器或逻辑过滤器。

本菜谱的目标是加载一个包含欧洲国家的 GML 文件,并根据它们的 AREA 属性对其进行样式化,如下面的截图所示:

定义自定义规则以样式化特征

如何做...

  1. 一旦创建了包含 OpenLayers 依赖项的 HTML 文件,请添加 div 元素以容纳地图:

    <div id="ch07_custom_rules" style="width: 100%; height: 95%;"></div>
    
    
  2. 在 JavaScript 代码部分,初始化地图,将 OpenStreetMap 作为底图,并将地图中心定位在所需位置:

    <script type="text/javascript">
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch07_custom_rules");
    
        var osm = new OpenLayers.Layer.OSM();
        map.addLayer(osm);
    
        map.setCenter(new OpenLayers.LonLat(40,50).transform(new OpenLayers.Projection("EPSG:4326"), 
        new OpenLayers.Projection("EPSG:900913")), 3);
    
    
  3. 现在,定义五个不同的规则,根据特征的 AREA 属性来样式化元素。以下代码包含检查值是否小于 10,000 的规则:

        var aRule = new OpenLayers.Rule({
            filter: new OpenLayers.Filter.Comparison({
                type: OpenLayers.Filter.Comparison.LESS_THAN,
                property: "AREA",
                value: 10000
            }),
            symbolizer: {
                fillColor: "#EBC137",
                fillOpacity: 0.5, 
                strokeColor: "black"
            }
        });
    
    
  4. 以下代码包含检查值是否在 10,000 到 25,000 之间的规则:

        var bRule = new OpenLayers.Rule({
            filter: new OpenLayers.Filter.Logical({
                type: OpenLayers.Filter.Logical.AND,
                filters: [
                    new OpenLayers.Filter.Comparison({
                        type: OpenLayers.Filter.Comparison.GREATER_THAN,
                        property: "AREA",
                        value: 10000
                    }),
                    new OpenLayers.Filter.Comparison({
                        type: OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO,
                        property: "AREA",
                        value: 25000
                    })
                ]
            }),
            symbolizer: {
                fillColor: "#E38C2D",
                fillOpacity: 0.7, 
                strokeColor: "black"
            }
        });
    
    
  5. 检查值是否在 25,000 到 50,000 之间的规则:

        var cRule = new OpenLayers.Rule({
            filter: new OpenLayers.Filter.Logical({
                type: OpenLayers.Filter.Logical.AND,
                filters: [
                    new OpenLayers.Filter.Comparison({
                        type: OpenLayers.Filter.Comparison.GREATER_THAN,
                        property: "AREA",
                        value: 25000
                    }),
                    new OpenLayers.Filter.Comparison({
                        type: OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO,
                        property: "AREA",
                        value: 50000
                    })
                ]
            }),
            symbolizer: {
                fillColor: "#DB4C2C",
                fillOpacity: 0.7, 
                strokeColor: "black"
            }
        });
    
    
  6. 检查值是否在 50,000 到 100,000 之间的规则:

        var dRule = new OpenLayers.Rule({
            filter: new OpenLayers.Filter.Logical({
                type: OpenLayers.Filter.Logical.AND,
                filters: [
                    new OpenLayers.Filter.Comparison({
                        type: OpenLayers.Filter.Comparison.GREATER_THAN,
                        property: "AREA",
                        value: 50000
                    }),
                    new OpenLayers.Filter.Comparison({
                        type: OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO,
                        property: "AREA",
                        value: 100000
                    })
                ]
            }),
            symbolizer: {
                fillColor: "#771E10",
                fillOpacity: 0.7, 
                strokeColor: "black"
            }
        });
    
    
  7. 最后,检查值是否大于 100,000 的规则:

        var eRule = new OpenLayers.Rule({
            filter: new OpenLayers.Filter.Comparison({
                type: OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO,
                property: "AREA",
                value: 100000
            }),
            symbolizer: {
                fillColor: "#48110C",
                fillOpacity: 0.7, 
                strokeColor: "black"
            }
        });
    
    
  8. 使用之前定义的常规规则创建样式:

        var style = new OpenLayers.Style();
        style.addRules([aRule, bRule, cRule, dRule, eRule]);
    
    
  9. 最后,创建一个矢量层,加载 GML 文件并使用之前的样式:

        map.addLayer(new OpenLayers.Layer.Vector("World Cities (GeoJSON)", {
            protocol: new OpenLayers.Protocol.HTTP({
                url: "http://localhost:8080/openlayers-cookbook/recipes/data/europe.gml",
                format: new OpenLayers.Format.GML()
            }),
            styleMap: new OpenLayers.StyleMap(style),
            strategies: [new OpenLayers.Strategy.Fixed()]
        }));
    </script>
    
    

它是如何工作的...

如配方开头所述,OpenLayers.Style实例接受一组OpenLayers.Rule实例来样式化特征。

给定一个规则,所有匹配指定OpenLayers.Filter的特征都将使用指定的符号化哈希进行样式化,并且多亏了过滤器,我们有足够的灵活性来创建比较或逻辑过滤器。

在代码中,我们创建了五个过滤器。让我们描述其中两个。

aRule规则由一个比较过滤器组成,匹配所有具有小于 10,000 的AREA属性值的特征:

    var aRule = new OpenLayers.Rule({
        filter: new OpenLayers.Filter.Comparison({
            type: OpenLayers.Filter.Comparison.LESS_THAN,
            property: "AREA",
            value: 10000
        }),
        symbolizer: {
            fillColor: "#EBC137",
            fillOpacity: 0.5, 
            strokeColor: "black"
        }
    });

bRule使用更复杂的规则。在这种情况下,它是一个由两个比较过滤器组成的逻辑AND过滤器。它匹配所有特征,检查它们的AREA属性是否大于 10,000 且小于或等于 25,000:

    var bRule = new OpenLayers.Rule({
        filter: new OpenLayers.Filter.Logical({
            type: OpenLayers.Filter.Logical.AND,
            filters: [
                new OpenLayers.Filter.Comparison({
                    type: OpenLayers.Filter.Comparison.GREATER_THAN,
                    property: "AREA",
                    value: 10000
                }),
                new OpenLayers.Filter.Comparison({
                    type: OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO,
                    property: "AREA",
                    value: 25000
                })
            ]
        }),
        symbolizer: {
            fillColor: "#E38C2D",
            fillOpacity: 0.7, 
            strokeColor: "black"
        }
    });

一旦我们创建了所有所需的规则,我们就可以创建一个OpenLayers.Style实例:

    var style = new OpenLayers.Style();
    style.addRules([aRule, bRule, cRule, dRule, eRule]);

然后将其应用于矢量层:

    map.addLayer(new OpenLayers.Layer.Vector("World Cities (GeoJSON)", {
        protocol: new OpenLayers.Protocol.HTTP({
            url: "http://localhost:8080/openlayers-cookbook/recipes/data/europe.gml",
            format: new OpenLayers.Format.GML()
        }),
        styleMap: new OpenLayers.StyleMap(style),
        strategies: [new OpenLayers.Strategy.Fixed()]
    }));

注意

我们创建了一个OpenLayers.StyleMap实例,只传递一个样式,而不是每个期望的渲染意图的样式。这意味着层中不会有渲染意图,或者换句话说,所有渲染意图都将使用相同的样式渲染。

因为矢量层必须从我们的服务器上的 GML 文件中读取数据,所以我们使用了OpenLayers.Protocol.HTTP实例,该实例从指定的 URL 加载文件,并使用OpenLayers.Format.GML格式的实例来读取它。

最后,为了使地图视口居中,我们需要转换坐标。

因为地图的基本层是 OpenStreetMap,这使得地图的投影变为 EPSG:900913,而我们在指定中心位置时使用的是 EPSG:4326。因此,我们需要进行转换:

  map.setCenter(new OpenLayers.LonLat(40,50).transform(new OpenLayers.Projection("EPSG:4326"), 
    new OpenLayers.Projection("EPSG:900913")), 3);

还有更多...

在我们的代码中,我们使用以下句子创建了样式:

    var style = new OpenLayers.Style();
    style.addRules([aRule, bRule, cRule, dRule, eRule]);

但是OpenLayers.Style构造函数可以接受两个参数:一个符号化哈希,用作默认样式,以及一组选项,其中我们需要指定实例属性。考虑到这一点,我们也可以这样实例化样式:

    var style = new OpenLayers.Style({
        our_default_style
    }, {
        rules: [aRule, bRule, cRule, dRule, eRule]
    });

参见

  • 使用唯一值规则进行样式化配方

  • 使用符号化进行特征样式化配方

  • 使用 StyleMap 改进样式以及替换特征属性的配方

样式化聚类特征

当处理大量的特征点时,通常使用聚类策略来避免点重叠并提高渲染性能。

在这个配方中,我们将展示如何轻松地使用聚类策略对矢量层进行样式化:

样式化聚类要素

我们的数据层将读取包含世界上一些城市的 GeoJSON 文件。样式将具有以下特征:

  • 对于每个聚类,我们将显示包含的要素数量

  • 点半径和边框将取决于包含的要素数量,包含的要素越多,半径就越大

如何做到这一点...

  1. 开始添加地图的div元素:

    <div id="ch07_cluster_number_style" style="width: 100%; height: 95%;"></div>
    
    
  2. 实例化一个OpenLayers.Map实例:

    <script type="text/javascript">
        // Create the map using the specified DOM element
        var map = new OpenLayers.Map("ch07_cluster_number_style");
    
    
  3. 将 OpenStreetMap 作为基本图层并居中视口:

        var osm = new OpenLayers.Layer.OSM();
        map.addLayer(osm);
    
        map.setCenter(new OpenLayers.LonLat(0,20).transform(new OpenLayers.Projection("EPSG:4326"), 
        new OpenLayers.Projection("EPSG:900913")), 2);
    
    
  4. 从 GeoJSON 文件加载数据并应用所需的样式:

        var cities = new OpenLayers.Layer.Vector("World Cities (GeoJSON)", {
            protocol: new OpenLayers.Protocol.HTTP({
                url: "http://localhost:8080/openlayers-cookbook/recipes/data/world_cities.json",
                format: new OpenLayers.Format.GeoJSON()
            }),
            strategies: [new OpenLayers.Strategy.Fixed(), new OpenLayers.Strategy.Cluster({distance: 25})],
            styleMap: new OpenLayers.StyleMap({
                'default': new OpenLayers.Style({
                    strokeWidth: '${strokeFunction}',
                    strokeOpacity: 0.5,
                    strokeColor: "#88aaaa",
                    fillColor: "#99CC55",
                    fillOpacity: 0.5,
                    pointRadius: '${radiusfunction}',
                    label: "${count}",
                    fontColor: "#ffffff"
                }, {
                    context: {
                        strokeFunction: function(feature) {
                            var count = feature.attributes.count;
                            var stk = Math.max(0.1 * count, 1);
                            return stk;
                        },
                        radiusFunction: function(feature) {
                            var count = feature.attributes.count;
                            var radius = Math.max(0.60 * count, 7);
                            return radius;
                        }
                    }
                })
            })
        });
        map.addLayer(cities);
    </script>
    
    

它是如何工作的...

在创建地图实例并添加基本图层后,我们已将视口居中。注意我们如何将坐标从 EPSG:4326(纬度/经度)转换为地图使用的 EPSG:900913(隐式用于 OpenStreetMap 图层):

    map.setCenter(new OpenLayers.LonLat(0,20).transform(new OpenLayers.Projection("EPSG:4326"), 
    new OpenLayers.Projection("EPSG:900913")), 2);

接下来,我们添加了矢量图层:

    var cities = new OpenLayers.Layer.Vector("World Cities (GeoJSON)", {
        protocol: new OpenLayers.Protocol.HTTP({
            url: "http://localhost:8080/openlayers-cookbook/recipes/data/world_cities.json",
            format: new OpenLayers.Format.GeoJSON()
        }),
        strategies: [new OpenLayers.Strategy.Fixed(), new OpenLayers.Strategy.Cluster({distance: 25})],
        styleMap: new OpenLayers.StyleMap({
            'default': ...
        })
    });

为了从我们的服务器加载 GeoJSON 文件,我们使用了OpenLayers.Protocol.HTTP实例和OpenLayers.Format.GeoJSON格式来读取它。

对于图层策略,我们指定了OpenLayers.Strategy.Fixed以加载内容一次,以及OpenLayers.Strategy.Cluster({distance: 25})来分组要素。属性distance设置了定义两个要素必须进入同一聚类的像素距离。

在这个阶段,在继续食谱之前,我们需要描述聚类过程是如何工作的。

当图层即将渲染时,聚类算法会检查每个要素是否与其他要素过于接近。对于过于接近的要素集,会创建一个新的点(聚类)并渲染。这样可以在地图上大大减少需要绘制的点数。此外,每个聚类点要素将包含它所代表的要素集的引用,以及一个包含要素数量的count属性。

返回到我们的代码,让我们看看应用于图层的样式,这是食谱中最重要的事情。

首先,我们为default渲染意图设置了样式:

        styleMap: new OpenLayers.StyleMap({
            'default': ...
        })

这意味着如果我们使用一些控制来更改图层的渲染意图为不同于默认值,样式可能不同。

注意

如果我们不直接传递样式实例来创建OpenLayers.StyleMap实例,即不指定渲染意图,那么样式将适用于任何渲染意图:new OpenLayers.StyleMap(our_style_here)

现在,让我们看看为图层定义的OpenLayers.Style实例:

new OpenLayers.Style({ 
    strokeWidth: '${strokeFunction}', 
    strokeOpacity: 0.5, 
    strokeColor: "#88aaaa", 
    fillColor: "#99CC55", 
    fillOpacity: 0.5, 
    pointRadius: '${radiusfunction}', 
    label: "${count}", 
    fontColor: "#ffffff" 
}, { 
    context: { 
        strokeFunction: function(feature) { 
            var count = feature.attributes.count; 
            var stk = Math.max(0.1 * count, 1); 
            return stk; 
        }, 
        radiusFunction: function(feature) { 
            var count = feature.attributes.count; 
            var radius = Math.max(0.60 * count, 7); 
            return radius; 
        } 
    } 
})

构造函数接收两个参数:一个符号化哈希,它定义了样式属性,以及一组选项。

在符号化哈希中,我们使用了属性替换功能:

    strokeWidth: '${strokeFunction}', 
    ...
    pointRadius: '${radiusfunction}', 
    label: "${count}", 
    ....

count属性来自聚类点要素的属性,正如我们之前解释的那样。

另一方面,strokeFunctionradiusFunction不是属性,而是在OpenLayers.Style选项的context属性中定义的函数。所有符号化属性都是针对context对象进行评估的。因此,每当图层即将渲染时,每个函数都会接收到一个特征引用。

radiusFunction的情况下,它根据count属性计算点的半径,返回count的 60%或7之间的最大值:

        radiusFunction: function(feature) { 
            var count = feature.attributes.count; 
            var radius = Math.max(0.60 * count, 7); 
            return radius; 
        } 

如我们所见,上下文的使用足够强大,允许我们根据其他特征属性动态设置样式属性。

参见

  • 使用 StyleMap 和替换特征属性来改进样式的配方

  • 定义自定义规则以样式化特征的配方

  • 在第三章的使用聚类策略配方中,矢量图层

第八章. 基础之外

在本章中,我们将介绍:

  • 使用投影

  • 使用OpenLayers.Request请求远程数据

  • 创建自定义控件

  • 创建自定义渲染器

  • 选择与线相交的特征

  • 使用图像图层制作动画

简介

OpenLayers 是一个庞大且复杂的框架。没有其他选项可以用于一个允许处理许多 GIS 标准、从许多不同的数据源读取、在不同的浏览器技术上进行渲染等的框架。这种力量是有代价的。

OpenLayers 的实现试图尽可能减少对外部库的依赖。这意味着,OpenLayers 需要实现许多在其他项目中可以找到的功能:DOM 元素的操纵、AJAX 请求等。

本章展示了这些功能中的一些,以及我们在日常工作中可能需要的一些其他可能的常见需求,这些需求在其他章节中没有解释,例如创建图层动画或实现自定义控件。因此,本章更适合有经验的 JavaScript 程序员。

使用投影

与其他 JavaScript 地图库相比,OpenLayers 允许使用大量的投影。

通常,我们指定地图所需的投影。稍后当向地图添加矢量图层时,我们需要指定图层的投影,以便 OpenLayers 将图层投影中的特征转换为地图投影。

但是,默认情况下,OpenLayers 在投影方面有一个很大的限制:我们只能使用EPSG:4326EPSG:900913。为什么?因为投影之间的转换不是一项简单的任务,而且还有其他优秀的项目可以做到这一点。

因此,当我们想要使用 EPSG:4326 和 EPSG:900913 之外的投影时,OpenLayers 使用Proj4js 库(trac.osgeo.org/proj4js)。

注意

关于投影的教学超出了本书的范围。EPSG 代码只是对大量可用投影进行分类和识别的一种标准化方式。EPSG:4326 对应于 WGS84(世界大地测量系统),而 EPSG:900913 是球面墨卡托投影,因其被用于谷歌地图而广受欢迎。

让我们看看如何将 Proj4js 与 OpenLayers 集成,以及如何轻松地使用它。想法是创建一个显示地图和文本区域的程序,该文本区域将显示点击位置的坐标:

使用投影

准备工作

我们必须将一些可用的 Proj4js 文件放置在我们的 Web 应用程序目录中。为此,请执行以下步骤:

  • 访问 Proj4js 项目的网页并下载分发 ZIP 文件(对于这个食谱,我们使用了download.osgeo.org/proj4js/proj4js-1.1.0.zip

  • 解压缩下载的文件,并将proj4js-compressed.js文件和defs文件夹复制到您的网络应用程序文件夹中

如何操作...

  1. 创建一个 HTML 文件并添加 OpenLayers 依赖项。作为一个依赖项,还包括Proj4js库:

    <script type="text/javascript" src="img/proj4js-compressed.js"></script>
    
    
  2. 现在添加文本区域和地图的代码:

    <textarea id="textarea" name="textarea" data-dojo-type="dijit.form.SimpleTextarea" rows="4" cols="80"></textarea>
    <br/><br/>
    <div id="ch08_projections" style="width: 100%; height: 85%;"></div>
    
    
  3. 在 JavaScript 部分,创建一个新的控制来管理点击事件:

    <script type="text/javascript">
        // Create the click control
        OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, {    
    
            defaultHandlerOptions: {
                'single': true,
                'double': false,
                'pixelTolerance': 0,
                'stopSingle': false,
                'stopDouble': false
            },
    
            initialize: function(options) {
                this.handlerOptions = OpenLayers.Util.extend({}, this.defaultHandlerOptions);
                OpenLayers.Control.prototype.initialize.apply(this, arguments); 
                this.handler = new OpenLayers.Handler.Click(
                this, {
                    'click': this.trigger
                }, 
                this.handlerOptions);
            }, 
    
    
  4. trigger函数上,添加以下代码以在textarea对象中转换并显示坐标:

            trigger: function(e) {
                var lonlatS = map.getLonLatFromViewPortPx(e.xy);
                var lonlatT1 = lonlatS.clone().transform( map.getProjectionObject(), new OpenLayers.Projection("EPSG:41001") );
                var lonlatT2 = lonlatS.clone().transform( map.getProjectionObject(), new OpenLayers.Projection("EPSG:4326") );
    
                var message = "Click at: \n"+
                    "Lon: " + lonlatS.lon + " , Lat: "+lonlatS.lat + " ("+map.getProjection()+")\n" +
                    "Lon: " + lonlatT2.lon + " , Lat: "+lonlatT2.lat + " (EPSG:4326) \n" +
                    "Lon: " + lonlatT1.lon + " , Lat: "+lonlatT1.lat + " (EPSG:41001) \n";
    
                dijit.byId("textarea").set('value', message);
            },
    
            CLASS_NAME: "OpenLayers.Control.Click"
        });
    
    
  5. 创建地图实例,添加基本图层,并定位视口:

        var map = new OpenLayers.Map("ch08_projections");
        var osm = new OpenLayers.Layer.OSM();
        map.addLayer(osm);
        map.setCenter(new OpenLayers.LonLat(0,0), 2);
    
    
  6. 最后,创建一个新的点击控制实例并将其添加到地图中:

        var click = new OpenLayers.Control.Click();
        map.addControl(click);
        click.activate();
    </script>
    
    

它是如何工作的...

当 Proj4js 代码可用时,OpenLayers 会内部使用 Proj4js 代码。因此,作为 OpenLayers 开发者,我们不需要直接使用 Proj4js API,唯一的要求是在我们的应用程序中添加 Proj4js 依赖项:

<script type="text/javascript" src="img/proj4js-compressed.js"></script>

当用户在地图上的某个位置点击时,点击控制(我们稍后将看到)执行trigger函数。e变量包含所有点击事件的信息,包括像素的 xy 位置。

        trigger: function(e) {
            var lonlatS = map.getLonLatFromViewPortPx(e.xy);
            var lonlatT1 = lonlatS.clone().transform( map.getProjectionObject(), new OpenLayers.Projection("EPSG:41001") );
            var lonlatT2 = lonlatS.clone().transform( map.getProjectionObject(), new OpenLayers.Projection("EPSG:4326") );
            ...
            ...

注意

确保您使用的投影定义在defs文件夹中定义。否则,您需要创建一个新文件,其中包含以 proj4 表示法的转换。

给定一个OpenLayers.LonLat实例,我们可以使用transform()方法在投影之间进行转换。

注意

我们始终可以使用transform()方法,但如果不包含 Proj4js 依赖项,它们将仅在 EPSG:4326 和 EPSG:900913 之间进行转换。

多亏了OpenLayers.Map.getLonLatFromViewPortPx()方法,我们可以从OpenLayers.Pixel转换到OpenLayers.LonLat实例。

因为转换方法会修改当前实例,所以我们使用clone()方法创建一个新的实例以避免修改源变量。

到目前为止,trigger方法可以构建一个消息字符串并将其放置在文本区域中。

最后,让我们简要描述在食谱中使用的点击控制。

第一步是定义新的控制作为OpenLayers.Control类的子类:

    OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, {

控制将使用OpenLayers.Handler,因此在这里我们将定义一些选项:

        defaultHandlerOptions: {
            'single': true,
            'double': false,
            'pixelTolerance': 0,
            'stopSingle': false,
            'stopDouble': false
        },

initialize方法负责初始化控制实例。首先,我们创建一组选项,作为先前定义的对象和用户作为参数传递的选项的组合(使用OpenLayers.Util.extend()方法):

        initialize: function(options) {
            this.handlerOptions = OpenLayers.Util.extend({}, this.defaultHandlerOptions);
            OpenLayers.Control.prototype.initialize.apply(this, arguments); 
            this.handler = new OpenLayers.Handler.Click(
            this, {
                'click': this.trigger
            }, 
            this.handlerOptions);
        }, 

我们已经初始化了一个OpenLayers.Handler.Click实例,以便每次检测到用户按下鼠标按钮时执行trigger监听器函数。

最后,作为一个好的实践,我们设置CLASS_NAME属性,使用一个字符串来标识我们的新控制类:

        CLASS_NAME: "OpenLayers.Control.Click"
    });

参见

  • 在第一章的“使用地图选项进行游戏”食谱中,网络地图基础

  • 在第三章的“程序化创建要素”食谱中,处理矢量图层

使用 OpenLayers.Request 检索远程数据

数据是网络地图应用的基础。我们可以在地图上添加栅格或矢量图层,这将加载图像或矢量信息。

在矢量图层的情况下,多亏了 OpenLayers.ProtocolOpenLayers.Format 子类,我们可以配置图层从不同的来源和不同的格式加载数据。

无论如何,可能会有这样的情况,我们需要自己请求数据,读取特定格式,并添加功能。我们正在讨论如何进行异步 JavaScript 调用。

这个菜谱展示了我们如何使用辅助类 OpenLayers.Request 从远程服务器异步请求数据。

这里,我们将请求一个返回随机 x 和 y 值的 URL,我们将这些值作为地图上的点要素进行处理。

注意

OpenLayers 是为 GIS 网络开发者设计的框架,因此它被设计为独立于其他项目,例如 jQuery 和 Dojo,它们提供了请求远程数据和实现自己的功能。

如何做到这一点...

  1. 创建包含 OpenLayers 库依赖关系的 HTML 文件后,添加一个 div 元素来容纳地图:

    <div id="ch08_requesting" style="width: 100%; height: 95%;"></div>
    
    
  2. 在 JavaScript 部分,创建地图实例,添加基本图层,并定位视口:

        var map = new OpenLayers.Map("ch08_requesting");
        var osm = new OpenLayers.Layer.OSM();
        map.addLayer(osm);
    
        // Certer viewport
        map.setCenter(new OpenLayers.LonLat(0,0), 2);
    
    
  3. 创建一个矢量图层并将其添加到地图中:

        var vectorLayer = new OpenLayers.Layer.Vector("Points");
        map.addLayer(vectorLayer);
    
    
  4. 最后,向返回一组随机 x 和 y 值的 points.php 工具代码发起请求:

        OpenLayers.Request.GET({
            url: "utils/points.php",
            params: {
                num: 100
            },
            success: function(response) {
                var format = new OpenLayers.Format.JSON();
    
                var points = format.read(response.responseText);
                for(var i=0; i< points.length; i++) {
                    var p = new OpenLayers.Geometry.Point(points[i].x, points[i].y);
                    p.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));
    
                    var f = new OpenLayers.Feature.Vector(p);
                    vectorLayer.addFeatures([f]);
                }
            },
            failure: function(response) {
                alert("Sorry, there was an error requesting data !!!");
            }
        });
    
    

它是如何工作的...

在 JavaScript 中,XMLHttpRequest 对象允许我们与服务器端进行通信。

注意

关于如何使用 XMLHttpRequest 的更多信息可以在 acuriousanimal.com/blog/2011/01/27/working-with-the-javascript-xmlhttprequest-objectdeveloper.mozilla.org/en/AJAX/Getting_Started 找到。

由于浏览器之间的兼容性问题,OpenLayers 使用了一个跨浏览器的 W3C 兼容版本的 XMLHttpRequest 对象,并通过包装实现了 OpenLayers.Request 类。

OpenLayers.Request 类实现了 HTTP 方法:GET, POST, PUT, DELETE, HEADOPTIONS,并且被其他 OpenLayers 类用于从/向远程服务器(如 OpenLayers.Protocol.HTTP)获取/发送数据。

注意

HTTP 简介可以在:developer.mozilla.org/en/HTTP 找到。

在这个菜谱中,我们使用了 OpenLayers.Request.GET 方法,并带有以下参数:

  • url: 它是我们将要请求的 URL

  • params: 它是一组我们可以发送到 GET 请求中的选项参数

  • success: 它是一个在成功请求 URL 时将被执行的回调函数

  • failure: 它是一个在发生任何问题时将被执行的回调函数

在我们的代码中,我们通过传递一个 num 参数请求 utils/points.php,这等同于请求 URL utils/points.php?num=100

    OpenLayers.Request.GET({
        url: "utils/points.php",
        params: {
            num: 100
        },
        success: function(response) {
            ...
        },
        failure: function(response) {
            ...
        }
    });

如果由于某种原因请求失败,将执行 failure 方法并显示一个警告消息。另一方面,如果请求成功,我们将读取返回的响应并将点特征添加到地图中。

points.php 脚本根据 num 参数返回一个随机的 x 和 y 值,编码为 JSON 数组。调用响应的内容不过是一段需要解释的文本,我们可以在响应的 responseText 属性中找到它。

要将 JSON 字符串转换为 JavaScript 对象,我们可以使用 OpenLayers.Format.JSON 类:

            var format = new OpenLayers.Format.JSON();            
            var points = format.read(response.responseText);

最后,对于每个读取的对象,我们读取 x 和 y 值并创建一个点特征:

            for(var i=0; i< points.length; i++) {
                var p = new OpenLayers.Geometry.Point(points[i].x, points[i].y);
                p.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));

                var f = new OpenLayers.Feature.Vector(p);
                vectorLayer.addFeatures([f]);
            }

注意

PHP 脚本返回的 x 和 y 值范围从 -180 到 180(x 轴)和 -80 到 80(y 轴)。因此,我们将坐标从 EPSG:4326 转换为 EPSG:900913,这是地图的基本图层投影。

还有更多...

OpenLayers.Request 是一个功能强大的类,允许使用几乎任何 HTTP 方法。例如,除了 GET 方法外,我们还可以使用 POST 方法向服务器发送数据。

注意

如果你打算在你的应用程序中广泛使用 AJAX,务必了解跨域请求(XDR)同源策略en.wikipedia.org/wiki/Same_origin_policy)的限制。

最后,仔细查看 OpenLayers.Request 类的选项。你可以找到选项,例如 async 来指定请求是否必须同步执行,user/password 来对使用基本身份验证的服务器进行请求,或者 headers 来设置请求的 HTTP 头。

参见

  • 在第三章 Working with Vector LayersReading features using Protocols directly 节中,Working with Vector Layers,介绍了如何直接使用协议读取特征。

  • 在第三章 Working with Vector LayersReading and creating features from a WKT 节中,Working with Vector Layers,介绍了如何从 WKT 读取和创建特征。

创建自定义控件

OpenLayers 提供了大量的控件,可以满足广泛的需求。不幸的是,我们可能需要为构建新的网络应用程序而创建一个新的控件,或者扩展一个现有的控件:

创建自定义控件

在本食谱中,我们将创建一个新的名为 Cross 的控件。该控件将显示一个类似于古代战机的目标选择器的十字准线符号,如图中所示,它将显示所指向的位置。此外,该控件将允许注册点击事件,这些事件将返回当前位置。

如何做到这一点...

  1. 创建一个 HTML 文件并添加 OpenLayers 依赖项,然后包含我们新控件的代码:

    <script type="text/javascript" src="img/crossControl.js"></script>
    
    
  2. 接下来,添加两个所需的 CSS 类以控制:

    <style>
        .olControlCross {
            width: 48px;
            height: 48px;
            background: url('./recipes/data/target.png') no-repeat;
        }
        .olControlCrossText {
            position: relative;
            top: -10px;
            width: 200px;
            color: black;
        }
    </style>
    
    
  3. 现在,添加一个 div 元素来包含地图:

    <div id="ch08_drawing_cross" style="width: 100%; height: 95%;"></div>
    
    
  4. 在 JavaScript 代码中,创建地图实例并添加一个基本图层:

        var map = new OpenLayers.Map("ch08_drawing_cross");
        var layer = new OpenLayers.Layer.WMS( "OpenLayers WMS",
        "http://vmap0.tiles.osgeo.org/wms/vmap0", {layers: 'basic'} );
        map.addLayer(layer);
    
        // Certer viewport
        map.setCenter(new OpenLayers.LonLat(0,0), 2);
    
    
  5. 创建交叉控件,将其添加到地图中,并激活它:

        var crossControl = new OpenLayers.Control.Cross({
            eventListeners: {
                "crossClick": function(event) {
                    var lonlat = event.lonlat;
                    var message = "Clicked on: " + lonlat.lon + " / " + lonlat.lat;
                    alert(message);
                }
            }
        });
        map.addControl(crossControl);
        crossControl.activate();
    
    
  6. 现在,我们将逐步描述我们创建的新控制的源代码。首先,创建一个新的 crossControl.js 文件,并开始应用关于控制的最佳实践,编写描述:

    /** 
     * Class: OpenLayers.Control.Cross 
     * The Cross control renders a cross in the middle of the map. 
     * 
     * Inherits from: 
     *  - <OpenLayers.Control> 
     */ 
    
    
  7. 接下来,创建一个新的 OpenLayers.Control.Cross 类,作为 OpenLayers.Control: 的子类。

    OpenLayers.Control.Cross = OpenLayers.Class(OpenLayers.Control, {
    
    
  8. 定义新控制的一组属性和方法。第一步是初始化一个数组,包含我们的控制可以发出的集合事件:

        /** 
         * crossClick event is triggered when the cross is clicked by the mouse. 
         */ 
        EVENT_TYPES: ["crossClick"], 
    
    
  9. 接下来,有一个 size 属性,它用于知道图像控制的大小,并且需要计算确切的控制位置:

        /** 
         * Parameter: size 
         * {OpenLayers.Size} with the desired dimension for the image 
         */ 
        size: null, 
    
    
  10. 最后一个属性用于存储用作标签的 DOM 元素的引用,以显示当前控制目标的位置:

        /** 
         * Parameter: element 
         * {DOMElement} for the label shown by the control 
         */ 
        element: null, 
    
    
  11. 一旦我们定义了类中使用的所有必需属性,我们需要初始化控制。再次强调,按照 OpenLayers 的约定在源代码中进行注释是一个好习惯:

        /** 
         * Constructor: OpenLayers.Control.Cross 
         * Draw a cross in the middle of the map. 
         * 
         * Parameters: 
         * options - {Object} An optional object whose properties will be used 
         *     to extend the control. 
         */ 
        initialize: function(options) { 
            // Concatenate events specific to measure with those from the base 
            this.EVENT_TYPES = 
            OpenLayers.Control.Cross.prototype.EVENT_TYPES.concat( 
                OpenLayers.Control.prototype.EVENT_TYPES); 
    
            if(!options) { 
                options = {}; 
            }        
            if(!options.size) { 
                options.size = new OpenLayers.Size(48, 48); 
            } 
            OpenLayers.Control.prototype.initialize.apply(this, [options]); 
        }, 
    
    
  12. 接下来,我们需要实现 draw 方法,该方法在控制准备在页面上显示时被调用,并负责设置所需的 DOM 元素以渲染控制。第一步涉及计算控制的位置,即地图的中间位置:

        /** 
         * Method: draw 
         * 
         * Returns: 
         * {DOMElement} 
         */    
        draw: function() { 
    
            // Compute center position 
            var position = new OpenLayers.Pixel( 
                (this.map.div.offsetWidth - this.size.w) / 2, 
                (this.map.div.offsetHeight - this.size.h) / 2 
                ); 
    
    
  13. 然后,我们可以调用超类的 draw 方法来绘制控制。这将初始化 this.div 属性(从 OpenLayers.Control 继承而来)与将包含控制的 DOM 元素。默认情况下,CSS 类 olControlCross 被添加到 div 元素中,因此我们可以轻松地对其进行样式化:

            OpenLayers.Control.prototype.draw.apply(this, [position]); 
    
    
  14. 在此之后,我们可以创建一个新的 div 元素用于标签,该标签将显示当前目标的位置。这是通过使用 OpenLayers.Util.createDiv 方法完成的。此外,由于 OpenLayers.Element.addClass 方法,我们设置了 CSS 类 olControlCrossText 到标签,以便用户可以对其进行样式化:

            // Create location label element 
            this.element = OpenLayers.Util.createDiv(null); 
            OpenLayers.Element.addClass(this.element, "olControlCrossText"); 
    
    
  15. 计算当前的 OpenLayers.LonLat 位置并设置标签文本。我们省略了 computeLonLat 函数的代码,该函数可以在控制类中找到:

            var lonlat = this.computeLonLat();(); 
            this.element.innerHTML = lonlat.lon + " / " + lonlat.lat; 
    
    
  16. 将标签元素添加到主控制元素中:

            this.div.appendChild(this.element);
    
    
  17. 作为最后一步,我们注册了两个监听器。首先,一个监听 this.div 元素以检测鼠标点击控制:

            // Listen for event in the control's div 
            OpenLayers.Event.observe(this.div, 'click', OpenLayers.Function.bind(this.onClick, this));
    
    
  18. 第二步,添加一个监听地图的 move 事件,以便我们可以更新控制的位置标签:

            // Register event for map's move event. 
            this.map.events.register("move", this, this.onMove); 
    
    
  19. 最后,返回对 this.div 元素的引用:

            return this.div; 
        }, 
    
    
  20. 接下来是两个监听器的代码。onMove 方法在地图每次移动时更新标签的文本(目标位置):

        /** 
         * Updates the location text. 
         */ 
        onMove: function (event) { 
            var lonlat = this.computeLonLat(); 
            this.element.innerHTML = lonlat.lon + " / " + lonlat.lat; 
        }, 
    
    
  21. 当在控制上发生鼠标点击时,onClick 函数被执行。其责任是触发 crossClick 事件,以便任何外部监听器都可以被通知:

        /** 
         * Fires a crossClick event. 
         */ 
        onClick: function (event) { 
            var lonlat = this.computeLonLat(); 
            this.events.triggerEvent("crossClick", { 
                lonlat: lonlat 
            }); 
        }, 
    
    
  22. 这是计算 OpenLayers.LonLat 的辅助函数的代码,从当前控制器的像素位置:

        /** 
         * Computes the control location. 
         * 
         * Returns: 
         * {<OpenLayers.LonLat>} 
         */ 
        computeLonLat: function() { 
            var pixel = this.position.clone(); 
            pixel.x += this.size.w/2; 
            pixel.y += this.size.h/2; 
            return this.map.getLonLatFromPixel(pixel); 
        }, 
    
    
  23. 最后,但同样重要的是,我们必须设置属性 CLASS_NAME,它是一个字符串,用于标识控制名称。按照惯例,它是控制的整个命名空间:

        CLASS_NAME: "OpenLayers.Control.Cross" 
    }); 
    
    

它是如何工作的...

除了我们创建的基础图层外,程序没有太多神秘之处,还有一个交叉控件:

    var crossControl = new OpenLayers.Control.Cross({
        eventListeners: {
            "crossClick": function(event) {
                var lonlat = event.lonlat;
                var message = "Clicked on: " + lonlat.lon + " / " + lonlat.lat;
                alert(message);
            }
        }
    });

注意

我们通过在crossClick事件上注册监听函数来初始化交叉控制,该事件在每次点击交叉图像时都会触发。

还有更多...

注意我们如何创建新的控件类:

OpenLayers.Control.Cross = OpenLayers.Class(OpenLayers.Control, { 
    ...
});

使用OpenLayers.Class可以轻松创建新类或子类。它需要两个参数:

  • 一个具有类定义的对象,该定义将扩展源类

在我们的代码中,我们通过对象字面量中定义的第二个参数中的属性和函数扩展了OpenLayers.Control类。

此外,任何类都必须使用initialize方法进行初始化。通常要执行的操作的顺序是:

  1. EVENT_TYPES控制数组的数组与基础OpenLayers.Control类中定义的数组合并。这意味着我们正在扩展基础事件类型,以包含新交叉控制中定义的集合:

        initialize: function(options) { 
            // Concatenate events specific to measure with those from the base 
            this.EVENT_TYPES = 
            OpenLayers.Control.Cross.prototype.EVENT_TYPES.concat( 
                OpenLayers.Control.prototype.EVENT_TYPES);
    
    
  2. 如果在构造函数中未定义实例属性,则为它们设置默认值:

            if(!options) { 
                options = {}; 
            }        
            if(!options.size) { 
                options.size = new OpenLayers.Size(48, 48); 
            } 
    
    
  3. 调用超类构造函数。一旦我们初始化了子类,我们需要初始化超类。这是一个自下而上的初始化:

            OpenLayers.Control.prototype.initialize.apply(this, [options]); 
        }, 
    
    

参见

  • 在第五章的添加控件配方中,添加和删除控件

  • 在第四章的监听非 OpenLayers 事件配方中,处理事件

  • 在第二章的添加 WMS 图层配方中,添加栅格图层

创建自定义渲染器

当与矢量图层一起工作时,样式是一个强大的功能,它为我们提供了很多可能性:填充颜色和透明度、描边颜色、标签和文字颜色等等。但是,如果我们需要更多呢?

每个OpenLayers.Layer.Vector实例都包含一个渲染器,该渲染器负责使用浏览器中可用的最佳技术渲染图层特征(如点、路径和多边形)在地图上。这些可以是许多现代浏览器(如 Firefox 或 Chrome)中可用的 HTML5 Canvas 元素(en.wikipedia.org/wiki/Canvas_element),SVG(en.wikipedia.org/wiki/Scalable_Vector_Graphics),或 VML(en.wikipedia.org/wiki/Vector_Markup_Language)。

OpenLayers.Layer.Vector被初始化时,OpenLayers 会寻找最佳可用的渲染引擎,并创建一个OpenLayers.Renderer实例,该实例将渲染地图上的特征。

本配方旨在展示我们如何创建一个新的渲染器来改进特征的可视化。

创建自定义渲染器

上一张截图显示了某些样式化的点几何特征,并使用默认的 OpenLayers 渲染器进行渲染。

以下截图显示了使用我们新的渲染器实现渲染的相同特征:

创建自定义渲染器

注意

我们将扩展OpenLayers.Renderer.Canvas,以改进可视化。这个渲染器使用 HTML5 Canvas 元素。这意味着新的渲染器只能在 HTML5 兼容的浏览器上工作。您可以在html5test.com检查您的浏览器是否支持 canvas 元素:

如何做到这一点...

  1. 创建包含 OpenLayers 依赖项的 HTML 文件后,第一步是包含具有新渲染器类实现的文件:

    <script type="text/javascript" src="img/gradientRenderer.js"></script>
    
    
  2. 现在,像往常一样,你可以添加将包含地图的div元素:

    <div id="ch08_renderer" style="width: 100%; height: 95%;"></div>
    
    
  3. 在 JavaScript 部分,添加以下代码以初始化地图并添加一个基本图层:

        var map = new OpenLayers.Map("ch08_renderer");    
        var osm = new OpenLayers.Layer.OSM();
        map.addLayer(osm);
    
    
  4. 将地图的视口居中:

        var center = new OpenLayers.LonLat(-80,40);
        center.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));
        map.setCenter(center, 5);
    
    
  5. 使用基于特征的POP_RANK属性的唯一值规则创建一个StyleMap实例:

        var styles = {
            7: { pointRadius: 5, label: "${POP_RANK}", fillColor: "#FFF8DC", fillOpacity: 0.6},
            6: { pointRadius: 8, label: "${POP_RANK}", fillColor: "#FFE4C4", fillOpacity: 0.6},
            5: { pointRadius: 11, label: "${POP_RANK}", fillColor: "#DEB887", fillOpacity: 0.6},
            4: { pointRadius: 14, label: "${POP_RANK}", fillColor: "#DAA520", fillOpacity: 0.7},
            3: { pointRadius: 16, label: "${POP_RANK}", fillColor: "#CD853F", fillOpacity: 0.8},
            2: { pointRadius: 19, label: "${POP_RANK}", fillColor: "#A0522D", fillOpacity: 0.9},
            1: { pointRadius: 22, label: "${POP_RANK}", fillColor: "#B22222", fillOpacity: 1.0}
        };
    
        var styleMap = new OpenLayers.StyleMap();
        styleMap.addUniqueValueRules("default", "POP_RANK", styles);
    
    
  6. 创建矢量图层并将其添加到地图中:

        var vectorLayer = new OpenLayers.Layer.Vector("Cities", {
            styleMap: styleMap,
            renderers: ["Gradient"],
            protocol: new OpenLayers.Protocol.HTTP({
                url: "http://localhost:8080/openlayers-cookbook/recipes/data/world_cities.json",
                format: new OpenLayers.Format.GeoJSON()
            }),
            strategies: [new OpenLayers.Strategy.Fixed()]
        });
        map.addLayer(vectorLayer);
    
    
  7. 让我们看看OpenLayers.Renderer.Gradient的实现,它美化了以漂亮的渐变样式渲染的点特征。开始创建一个名为gradientRenderer.js的 JavaScript 文件,我们之前已经在主程序中包含了它。遵循良好的实践,我们在文件中开始注释:

    /** 
     * Class: OpenLayers.Renderer.Gradient 
     * Improved canvas based rendered to draw points using gradient. 
     * 
     * Inherits: 
     *  - <OpenLayers.Renderer.Canvas> 
     */ 
    
    
  8. 现在,创建一个名为OpenLayers.Renderer.Canvas的子类,命名为OpenLayers.Renderer.Gradient

    OpenLayers.Renderer.Gradient = OpenLayers.Class(OpenLayers.Renderer.Canvas, {
    
    
  9. 在新的 OpenLayers 类中要实现的第一种方法是initialize()方法:

        /** 
         * Constructor: OpenLayers.Renderer.Gradient 
         * 
         * Parameters: 
         * containerID - {<String>} 
         * options - {Object} Optional properties to be set on the renderer. 
         */ 
        initialize: function(containerID, options) { 
            OpenLayers.Renderer.Canvas.prototype.initialize.apply(this, arguments); 
        }, 
    
    
  10. 接下来,我们实现drawPoint()方法,该方法继承自OpenLayers.Renderer.Canvas类,负责渲染图层中的点几何特征。现在,该方法接收三个参数:几何对象、要应用的样式以及特征标识属性:

        /** 
         * Method: drawPoint 
         * This method is only called by the renderer itself. 
         * 
         * Parameters: 
         * geometry - {<OpenLayers.Geometry>} 
         * style    - {Object} 
         * featureId - {String} 
         */ 
        drawPoint: function(geometry, style, featureId) { 
    
    
  11. geometry参数中,使用从OpenLayers.Renderer.Canvas类继承的getLocalXY()方法计算点的确切像素位置。

            var pt = this.getLocalXY(geometry); 
            var p0 = pt[0]; 
            var p1 = pt[1]; 
    
            if(!isNaN(p0) && !isNaN(p1)) {  
                if(style.fill !== false) { 
                    this.setCanvasStyle("fill", style); 
    
    
  12. 使用fillColorfillOpacity属性创建一个字符串,用于应用渐变的颜色:

                    // Create color from fillColor and fillOpacity properties. 
                    var color = style.fillColor; 
                    color += "ff"; 
                    color = color.replace("#", "0x"); 
    
                    var colorRGBA = 'rgba(' + 
                    ((color >> 24) & 0xFF) + ',' + 
                    ((color >> 16) & 0xFF) + ',' + 
                    ((color >>  8) & 0xFF) + ',' + 
                    style.fillOpacity + ')'; 
    
    
  13. 然后创建一个以特征位置为中心、半径值由特征样式指定的 canvas 渐变:

                    var gradient = this.canvas.createRadialGradient(p0, p1, 0, p0, p1, style.pointRadius); 
    
    
  14. 定义必要的步骤,以便渐变从白色渐变到之前创建的 RGB 颜色:

                    gradient.addColorStop(0, '#FFFFFF'); 
                    gradient.addColorStop(0.9, colorRGBA); 
                    gradient.addColorStop(1, 'rgba(1,255,0,0)'); 
    
                    this.canvas.fillStyle = gradient;            
                    this.canvas.fillRect(0, 0, this.root.width, this.root.height); 
    
                    this.canvas.fill(); 
                } 
            } 
        }, 
    
    
  15. 最后,通过设置CLASS_NAME属性来识别新的类:

        CLASS_NAME: "OpenLayers.Renderer.Gradient " 
    });
    
    

它是如何工作的...

这个菜谱的重要点在于我们为矢量图层指定的一个属性,即renderers

renderers属性允许我们指定图层可以使用的OpenLayers.Renderer集合。通常这个属性从不使用,并且默认值是:renderers: ['SVG', 'VML', 'Canvas']。这意味着图层可以使用的支持的渲染器实例是OpenLayers.Renderer.SVG, OpenLayers.Renderer.VMLOpenLayers.Renderer.Canvas

对于这个菜谱,我们创建了OpenLayers.Renderer.Gradient类,我们将在后面描述。设置renderers: ["Gradient"]意味着我们只想允许图层与OpenLayers.Renderer.Gradient实例一起工作。

让我们更详细地描述如何初始化矢量图层:

  var vectorLayer = new OpenLayers.Layer.Vector("Cities", {
        styleMap: styleMap,
        renderers: ["Gradient"],
        protocol: new OpenLayers.Protocol.HTTP({
            url: "http://localhost:8080/openlayers-cookbook/recipes/data/world_cities.json",
            format: new OpenLayers.Format.GeoJSON()
        }),
        strategies: [new OpenLayers.Strategy.Fixed()]
    });

除了渲染器之外,我们还使用了一个OpenLayers.Protocol.HTTP实例和一个OpenLayers.Format.GeoJSON实例,来加载包含世界上一些城市的 GeoJSON 文件。文件中的特征包括POP_RANK属性等。

多亏了OpenLayers.Strategy.Fixed策略实例,图层只通过之前的协议加载数据源一次。我们不需要在地图缩放时每次都加载文件。

最后,但同样重要的是,我们已经将styleMap属性设置为一个先前创建的OpenLayers.StyleMap实例:

    var styleMap = new OpenLayers.StyleMap();
    styleMap.addUniqueValueRules("default", "POP_RANK", styles);

这个样式图是通过基于POP_RANK属性的唯一值规则功能定义的。该属性从17取值,因此我们为每个可能的值定义了一个符号化哈希样式,通过调整半径和填充颜色属性:

    var styles = {
        7: { pointRadius: 5, label: "${POP_RANK}", fillColor: "#FFF8DC", fillOpacity: 0.6},
        6: { pointRadius: 8, label: "${POP_RANK}", fillColor: "#FFE4C4", fillOpacity: 0.6},
        5: { pointRadius: 11, label: "${POP_RANK}", fillColor: "#DEB887", fillOpacity: 0.6},
        4: { pointRadius: 14, label: "${POP_RANK}", fillColor: "#DAA520", fillOpacity: 0.7},
        3: { pointRadius: 16, label: "${POP_RANK}", fillColor: "#CD853F", fillOpacity: 0.8},
        2: { pointRadius: 19, label: "${POP_RANK}", fillColor: "#A0522D", fillOpacity: 0.9},
        1: { pointRadius: 22, label: "${POP_RANK}", fillColor: "#B22222", fillOpacity: 1.0}
    };

对于主程序,没有更多要评论的,除了我们如何使地图视口居中的方式。

因为基本图层是OpenStreetMap,默认使用 EPSG:900913 投影,而我们指定了 EPSG:4326 投影的中心,所以我们需要将坐标转换到适当的地图投影。

还有更多...

通常,这个initialize方法会先调用其父类的initialize方法,然后设置实例的具体属性。

在这种情况下,我们的类没有特定的初始化属性,因此实现这个方法并不是严格必要的,但作为一个例子(以及良好的实践),我们已经编写了对父类调用的代码:

    initialize: function(containerID, options) {
        OpenLayers.Renderer.Canvas.prototype.initialize.apply(this, arguments); 
    }, 

OpenLayers.Renderer.Canvas类中遵循的渲染图层特征的过程稍微复杂一些,但可以总结如下:

  • 对于每个特征,类都会检查其几何形状

  • 根据几何类型,类会调用专门设计用于渲染点、线或多边形的不同方法

因为我们的渲染器是为了美化点而实现的,所以我们只重写了drawPoint方法,该方法负责渲染点几何形状。

我们在这里定义的渲染器使用 HTML5 canvas 元素,因此菜谱的主要部分与这项技术相关。

注意

关于 HTML5 canvas 元素的信息可以在互联网上找到。我们想指向 Mozilla 项目中的这个教程:developer.mozilla.org/en/Canvas_tutorial

一个很好的练习是创建这个渲染器的 SVG 版本。这样,就可以在更多浏览器中渲染渐变点。

参见

  • 创建自定义控件菜谱

  • 在第七章的“样式化特征”中的使用符号化样式化特征菜谱

  • 在第七章的“样式化特征”中的定义自定义规则以样式化特征菜谱

选择与线相交的特征

在处理矢量层中的特征时,一个常见的操作是选择特征,当然,OpenLayers 有一些特征选择控件。

OpenLayers.Control.SelectFeature控件特别有用作为选择控件,因为选择是在客户端进行的,也就是说,选择是通过浏览器中加载的特征进行的。没有向 WFS 服务器发出请求。

OpenLayers.Control.SelectFeature控件可以以不同的方式工作。我们可以通过点击来选择一个特征,或者我们可以画一个框来选择所有包含的特征。

相反,它不允许选择与路径相交的特征。

在这个菜谱中,我们将看到如何扩展OpenLayers.Control.SelectFeature控件以允许通过绘制路径选择特征。

如何做到这一点...

  1. 创建一个 HTML5 文件并添加 OpenLayers 库依赖项。现在,包括新的OpenLayers.Control.SelectFeature控件的代码:

    <script type="text/javascript" src="img/selectFeaturePath.js"></script>
    
    
  2. body部分中,添加一个div元素来容纳地图实例:

    <div id="ch08_selecting" style="width: 100%; height: 95%;"></div>
    
    
  3. 接下来,将 JavaScript 代码放置在文档的head元素中的script元素内:

    <script type="text/javascript">
    
    
  4. 创建地图实例,添加基础图层,并居中视口:

        var map = new OpenLayers.Map("ch08_selecting");
        var osm = new OpenLayers.Layer.OSM();
        map.addLayer(osm);
    
        // Certer viewport
        var center = new OpenLayers.LonLat(25,50);
        center.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));
        map.setCenter(center, 4);
    
    
  5. 现在,创建一个加载欧洲国家 GML 文件的矢量图层:

        var vectorLayer = new OpenLayers.Layer.Vector("Europe", {
            protocol: new OpenLayers.Protocol.HTTP({
                url: "http://localhost:8080/openlayers-cookbook/recipes/data/europe.gml",
                format: new OpenLayers.Format.GML()
            }),
            strategies: [new OpenLayers.Strategy.Fixed()]
        });
        map.addLayer(vectorLayer);
    
    
  6. 然后,创建我们新的OpenLayers.Control.SelectFeaturePath控件实例,将其添加到地图中并激活它:

        var sp = new OpenLayers.Control.SelectFeaturePath(vectorLayer);
        map.addControl(sp);
        sp.activate();  
    </script>
    
    
  7. 现在,我们将看到新的SelectFeaturePath控制器的实现。创建一个selectFeaturePath.js文件并添加一些控制描述:

    /**
     * Class: OpenLayers.Control.SelectFeaturePath
     * The SelectFeaturePath control selects vector features from a given layer 
     * that intersects with a path.
     *
     * Inherits from:
     *  - <OpenLayers.Control.SelectFeature>
     */
    
    
  8. 创建新的类:

    OpenLayers.Control.SelectFeaturePath = OpenLayers.Class(OpenLayers.Control.SelectFeature, {
    
    
  9. 实现负责初始化控件的initialize方法。注意我们如何调用父类的initialize方法来初始化父类:

        /**
         * Constructor: OpenLayers.Control.SelectFeaturePath
         * Create a new control for selecting features using 
         * an OpenLayers.Handler.Path handler.
         *
         * Parameters:
         * layers - {<OpenLayers.Layer.Vector>}, or an array of vector layers. The
         *     layer(s) this control will select features from.
         * options - {Object} 
         */
        initialize: function(layers, options) {
            OpenLayers.Control.SelectFeature.prototype.initialize.apply(this, arguments);
    
            this.box = true;                    
            this.handlers.box = new OpenLayers.Handler.Path(this, {
                done: this.selectPath
            });
        },
    
    
  10. 实现selectPath方法。此方法选择与创建的线相交的特征:

        /**
         * Method: selectPath
         * Callback from the handlers.box set up when <path> selection is done.
         * Select those features that intersect with the path.
         *
         * Parameters:
         * path - {<OpenLayers.Geometry.LineString>}  
         */
        selectPath: function(path) {
            // If multiple is false, first deselect currently selected features
            if (!this.multipleSelect()) {
                this.unselectAll();
            }
    
            // Consider we want multiple selection
            var prevMultiple = this.multiple;
            this.multiple = true;
            var layers = this.layers || [this.layer];
            var layer;
            for(var l=0; l<layers.length; ++l) {
                layer = layers[l];
                for(var i=0, len = layer.features.length; i<len; ++i) {
                    var feature = layer.features[i];
                    // Check if the feature is displayed
                    if (!feature.getVisibility()) {
                        continue;
                    }
    
                    if (this.geometryTypes == null || OpenLayers.Util.indexOf(
                        this.geometryTypes, feature.geometry.CLASS_NAME) > -1) {
                        if (path.intersects(feature.geometry)) {
                            if (OpenLayers.Util.indexOf(layer.selectedFeatures, feature) == -1) {
                                this.select(feature);
                            }
                        }
                    }
                }
            }
            this.multiple = prevMultiple;        
        },
    
    
  11. 最后,为类创建一个唯一的标识符:

        CLASS_NAME: "OpenLayers.Control.SelectFeaturePath"
    });
    
    

它是如何工作的...

主程序没有太多神秘之处。我们创建了一个地图,添加了一个矢量图层,然后添加了我们的自定义SelectFeaturePath控件:

    var sp = new OpenLayers.Control.SelectFeaturePath(vectorLayer);
    map.addControl(sp);
    sp.activate(); 

注意我们是如何将坐标转换以使地图视口居中的。这是因为我们正在指定 EPSG:4326 坐标系中的位置,而地图使用的是 EPSG:900913 的基础图层:

    var center = new OpenLayers.LonLat(25,50);
    center.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));
    map.setCenter(center, 4);

因为我们的控件是OpenLayers.Control.SelectFeature控件的扩展,所以在进入代码描述之前简要描述它很重要。

正如我们在菜谱开头所评论的,此控件在客户端工作,也就是说,没有向数据源发出请求。控件遍历图层(或图层集)中的所有特征,并选择符合标准的特征。

默认情况下,在实例化时未指定任何选项,控制允许我们通过点击它来选择要素。我们还可以设置属性,例如hover属性,允许我们突出显示鼠标所指的要素,或者在这里很重要的box属性,允许通过绘制框来选择要素。

这样做是因为控制内部使用了一个OpenLayers.Handler.Box处理器的实例,它负责绘制一个框并返回边界框的坐标,以便控制可以检查哪些要素在框内。

扩展我们的控制并允许通过绘制路径来选择要素的想法很简单,我们不再使用OpenLayers.Handler.Box处理器,而是将使用OpenLayers.Handler.Path处理器。

好的,我们已经了解了我们需要的基础知识。让我们去看看我们做得怎么样。

我们使用OpenLayers.Class方法创建了新的OpenLayers.Control.SelectFeaturePath类。这个类允许合并两个对象的属性和方法:

OpenLayers.Control.SelectFeaturePath = OpenLayers.Class(OpenLayers.Control.SelectFeature, { 
    ...
    ...
});

在上一行中,我们正在合并OpenLayers.Control.SelectFeature类的属性和方法与第二个参数中定义的属性,该参数是一个字面量表示法中的对象。

initialize()方法中,设置从超类继承的boxhandlers.box属性非常重要。将handlers.box设置为OpenLayers.Handler.Path的新实例使控制使用路径处理器而不是框处理器来选择要素。

设置为truebox属性表示我们想要使用处理器而不是简单地点击来选择要素:

    initialize: function(layers, options) {
        OpenLayers.Control.SelectFeature.prototype.initialize.apply(this, arguments);

        this.box = true;                    
        this.handlers.box = new OpenLayers.Handler.Path(this, {
            done: this.selectPath
        });
    },

使用名为handlers.box的属性来指定路径处理器并不是最清晰的方法,但相比之下,这是最简单的方法。OpenLayers.Control.SelectFeature类使用这个属性来获取适当的处理器以进行选择。

在前面的代码中,当路径处理器初始化时,我们为done事件设置了一个监听函数,该事件在路径处理器完成绘制线条时触发。

selectPath方法负责找出哪些要素与路径相交,并更改其渲染样式以突出显示它们。

注意,监听函数接收一个包含创建的路径的OpenLayers.Geometry.LineString实例:

    selectPath: function(path) { 
        // If multiple is false, first deselect currently selected features 
        if (!this.multipleSelect()) { 
            this.unselectAll(); 
        } 

因为控制可以与多个图层一起工作,所以我们需要遍历所有图层以及每个图层上的所有要素:

        // Consider we want multiple selection 
        var prevMultiple = this.multiple; 
        this.multiple = true; 
        var layers = this.layers || [this.layer]; 
        var layer; 
        for(var l=0; l<layers.length; ++l) { 
            layer = layers[l]; 
            for(var i=0, len = layer.features.length; i<len; ++i) { 
                var feature = layer.features[i]; 
                // Check if the feature is displayed 
                if (!feature.getVisibility()) { 
                    continue; 
                } 

在检查要素是否可见之后,我们可以使用intersects方法检查要素是否与路径相交:

                if (this.geometryTypes == null || OpenLayers.Util.indexOf( 
                    this.geometryTypes, feature.geometry.CLASS_NAME) > -1) { 
                    if (path.intersects(feature.geometry)) { 
                        if (OpenLayers.Util.indexOf(layer.selectedFeatures, feature) == -1) { 
                            this.select(feature); 
                        } 
                    } 
                } 
            } 
        } 
        this.multiple = prevMultiple;        
    }, 

注意

OpenLayers.Util.indexOf(array, object)函数返回一个对象在数组中找到的索引。

要更改要素的渲染样式,我们只需调用从OpenLayers.Control.SelectFeature类继承的select方法。

注意

向量图层可以使用不同的样式渲染特征,称为渲染意图:默认、选择或临时是我们可以使用的基本渲染意图。

参见

  • 创建自定义渲染器的食谱

  • 在第七章“特征样式”的“使用 StyleMap 和渲染意图”食谱中,样式特征

使用图像图层制作动画

当处理地理信息时,其空间中的几何表示并不是唯一重要的事情。日复一日,时间正成为需要考虑的新维度。

这样,可视化必须显示数据随时间的变化:城市人口、国家边界、道路建设等等。

有许多方法可以动画化数据随时间的变化,但正如往常一样,我们使用 Web 技术,可以分为两组:基于服务器端和基于客户端的解决方案。

对于服务器端解决方案,我们可以在 WMS 和 WFS 标准中找到TIME参数。它允许我们请求特定时间或时间范围内的栅格或矢量数据。

服务器端解决方案意味着客户端必须在我们想要显示不同区间数据时每次都请求服务器。

对于客户端,一个简单的解决方案是在内存中保存所有数据,并且只显示我们感兴趣的区间对应的数据。

在本食谱中,我们将展示如何轻松地在客户端创建动画。

我们将从NEXTRADen.wikipedia.org/wiki/NEXRAD)加载一些图像,展示不同时间点的降雨演变(如图下截图所示),并且我们将通过简单地显示或隐藏图像来创建动画:

使用图像图层制作动画

如何做到这一点...

  1. 创建一个新的 HTML 文件,并添加 OpenLayers 依赖项。在body部分开始添加播放按钮和滑块所需的元素:

    <table>
        <tr>
            <td>
                Animation:
            </td>
            <td>
                <div id="animSlider" dojoType="dijit.form.HorizontalSlider" value="0" minimum="0" maximum="100" intermediateChanges="true"
                     showButtons="false" style="width:300px;" onChange="animation">
                    <div dojoType="dijit.form.HorizontalRule" container="bottomDecoration" count=11 style="height:5px;"></div>
                </div> 
            </td>
            <td>
                <div dojoType="dijit.form.ToggleButton" iconClass="dijitCheckBoxIcon" onChange="animateAction">Play</div>
            </td>
        </tr>
    </table>
    
    

    注意

    除了 OpenLayers,我们还使用 Dojo Toolkit 框架(dojotoolkit.org)来创建更吸引人的用户界面。学习 Dojo 超出了本书的范围,而且,了解它也不是理解本食谱所必需的。

  2. 接下来,添加div元素来包含地图:

    <div id="ch08_animating_raster" style="width: 100%; height: 100%;"></div>
    
    
  3. 现在,在头部部分添加以下 JavaScript 代码。开始初始化地图实例,添加一个基本图层,并定位视口:

    <script type="text/javascript">
        var map = new OpenLayers.Map("ch08_animating_raster");
        var wms = new OpenLayers.Layer.WMS("OpenLayers WMS Basic", "http://labs.metacarta.com/wms/vmap0",
        {
            layers: 'basic'
        });
        map.addLayer(wms);
    
        // Center the view
        map.setCenter(new OpenLayers.LonLat(-85, 40), 4);
    
    
  4. 现在,我们将创建一些栅格图像图层,将它们添加到地图中,并将它们存储在数组中以控制它们的可见性:

        var img_extent = new OpenLayers.Bounds(-131.0888671875, 30.5419921875, -78.3544921875, 53.7451171875);
        var img_size = new OpenLayers.Size(780, 480);
    
        var img_ulr = image = null;
        var imgArray = [];
        for(var i=1; i<=32; i++) {
            index = (i<10) ? "0"+i : i;
            img_url = "http://localhost:8080/openlayers-cookbook/recipes/data/radar/nexrad"+index+".png";        
            image = new OpenLayers.Layer.Image("Image Layer", img_url, img_extent, img_size, {
                isBaseLayer: false,
                alwaysInRange: true, // Necessary to always draw the image
                visibility: false
            });
            imgArray.push(image);
            map.addLayer(image);
        }
        imgArray[0].setVisibility(true);
    
    
  5. 以下代码控制滑块小部件的变化:

        var currentIndex = 0;
        function animation(value){
            imgArray[currentIndex].setVisibility(false);
            currentIndex = Math.floor(value * 31 / 100);
            imgArray[currentIndex].setVisibility(true);
        }
    
    
  6. 最后,以下代码控制当点击播放按钮时的自动动画:

        var interval = null;
        function animateAction(checked) {
            if(checked) {
                interval = setInterval(function() {
                    var v = dijit.byId('animSlider').get('value');
                    v = (v>=100) ? 0 : (v+1);
                    dijit.byId('animSlider').set('value', v);
                    animation(v);
                },50);
            } else {
                clearInterval(interval);
            }
        }
    </script>
    
    

它是如何工作的...

如本食谱开头所述,想法是在客户端动画化一些天气雷达。因此,我们从服务器加载一系列图像,为每个图像创建一个图层。

在这里使用OpenLayers.Layer.Image类来保存每个图像作为一个单独的层。其构造函数需要五个参数:

  • name: 层的名称

  • url: 图像必须获取的 URL

  • extent: 图像在地图内的范围

  • size: 像素大小

  • options: 要传递给层的选项集

注意

extentsize参数用于计算图像的分辨率。

在我们的情况下,所有图像层将具有相同的extentsize

    var img_extent = new OpenLayers.Bounds(-131.0888671875, 30.5419921875, -78.3544921875, 53.7451171875);
    var img_size = new OpenLayers.Size(780, 480);

在创建所有图像层的循环中,我们动态设置img_url变量的值,并将一些选项传递给OpenLayers.Layer.Image构造函数:

        image = new OpenLayers.Layer.Image("Image Layer", img_url, img_extent, img_size, {
            isBaseLayer: false,
            alwaysInRange: true, // Necessary to always draw the image
            visibility: false
        });
        imgArray.push(image);

通过将isBaseLayer属性设置为false,我们指定我们的层不是基础层,它将作为覆盖层。此外,我们将visibility属性设置为false以最初隐藏层。稍后,我们将设置第一个图像层为可见层:

    imgArray[0].setVisibility(true);

alwaysInRange属性是从超类OpenLayers.Layer继承的,在此情况下特别有用。我们希望我们的图像层在任何缩放级别都可见。我们不希望 OpenLayers 计算正确的分辨率;给定图像层的范围和大小,层必须显示。因此,将alwaysInRange设置为true使层始终可见。

接下来,我们将看到如何自动自动化动画。当按下播放按钮时执行animateAction。它是一个切换按钮,因此根据其状态,布尔参数checked如果被选中则为true,如果没有选中则为false

    function animateAction(checked) {
        if(checked) {

如果播放按钮被选中,则我们创建一个间隔,每 50 毫秒执行一次给定的匿名函数,实际上增加了滑块的值并调用animation函数:

            interval = setInterval(function() {
                var v = dijit.byId('animSlider').get('value');
                v = (v>=100) ? 0 : (v+1);
                dijit.byId('animSlider').set('value', v);
                animation(v);
            },50);
        } else {

如果按钮未选中,则我们移除间隔引用以停止执行:

            clearInterval(interval);
        }

注意

间隔超时在 JavaScript 中用于创建动画和延迟。

可以在www.w3schools.com/jsref/met_win_setinterval.asp找到 JavaScript 中关于间隔的良好解释。

最后,让我们看看animation函数,它负责更改层可见性和创建动画效果。

所有层都添加到地图中,并存储在imgArray中。全局currentIndex变量用于保存当前可见层。动画函数执行三件事:

  • 隐藏当前可见层

  • 给定一个从 0 到 100 的数值value,计算层数组索引,该索引从 0 到 31

  • 显示新的当前层

    var currentIndex = 0;
    function animation(value){
        imgArray[currentIndex].setVisibility(false);
        currentIndex = Math.floor(value * 31 / 100);
        imgArray[currentIndex].setVisibility(true);
    }

那就是全部!一旦将层集加载到客户端,使用任何现代浏览器,动画的性能都足够好。

这只是一个示例,所以有很多地方可以改进。例如,考虑如何实现一个情况,即我们有一个远程服务器,上面有数百张图片需要顺序加载。在这种情况下,我们不能一次性加载所有图片,因为这可能会在浏览器中引起内存不足的问题。

假设我们有一千张图片需要动画化,我们可以实施一些缓冲策略。给定一个包含十张图片的缓冲区,我们可以从服务器加载前十张图片,然后对它们进行动画处理,当动画到达最后加载的图片时,再从服务器加载下十张图片。

正如我们所见,这些情况超出了本书的范围,不仅与 OpenLayers 有关,还与软件架构和设计有关。

参见

  • 在第二章的创建图像图层配方中,添加栅格图层

  • 在第二章的改变图层不透明度配方中,添加栅格图层

posted @ 2025-10-23 15:13  绝不原创的飞龙  阅读(4)  评论(0)    收藏  举报