第3章 OpenLayers开发基础
3.1 OpenLayers简介
OpenLayers是一个模块化、高性能并且功能丰富的WebGIS客户端的JavaScript包,用于显示地图及空间数据,并与之进行交互,具有灵活的扩展机制。OpenLayer最初由MetaCarta公司开发,是一个完全免费、开源的JavaScript库,通过BSD License发行。目前,OpenLayers已经成为一个拥有众多开发者和帮助社区的成熟、流行的框架。
OpenLayers目前的最新版本为5.x,在默认情况下,使用经过性能优化的Canvas渲染器,同时也支持WebGL渲染器,可在支持HTML5和ECMAScript 5的浏览器上运行,包括Chrome、Firefox、Safari和Edge等,而对于较旧的浏览器和平台,如Internet Explorer(低至版本9)和Android 4.x,需要转换应用程序包(如使用Babel)并与polyfill捆绑在一起。
在地图数据以服务方式提供的前提下,OpenLayers实现访问空间数据的方法符合行业标准,支持各种公开的和私有的数据标准和资源。OpenLayers支持OGC制定的WMS、WFS等服务规范,可以通过远程服务的方式,将以OGC服务规范发布的地图数据加载到基于浏览器的OpenLayers客户端中显示。目前,OpenLayers所支持的数据格式有XML、JSON、GeoJSON、MVT、GML、GPX、KML、WFS、MVT、WKT(Well-Known Text)等,在其format名称空间下的各个类里实现了具体读/写这些数据格式的解析器。因此,基于OpenLayers能够利用的地图资源非常丰富,提供给用户最多的选择,包括公共地图服务,如OpenStreetMap、Google地图、Bing地图、Baidu地图等,OGC资源(如WMS、WMTS、WFS),其他矢量数据以及简单的图片等。
在采用JavaScript纯客户端开发的WebGIS项目中,OpenLayers是作为功能脚本库引用的,在HTML文档中调用OpenLayers提供的类,以及类的属性和方法,从而实现互联网地图发布与功能操作。目前,OpenLayers官方发布了OpenLayers 5.x版本,相较于OpenLayers3版本,除个别方法的命名以及调用方式有改变,绝大部分API都没有发生变化,即表现层未发生实质变化,着重改造内部实现,总体来说可概括为以下两点。
(1)OpenLayers 5的主旨是改进OpenLayers的开发人员或用户的使用体验,而JavaScript在使用和创建模块时的运行效果最佳。为此,OpenLayers 5采用了可向下兼容的ECMAScript6标准提供的内置模块功能将其代码重新设计为一套ES模块,完全消除了对Closure Com piler(闭包编译器)的依赖,并提高了与主流模块捆绑器的兼容性。
(2)OpenLayers 5较OpenLayers3的早期版本(v3.19之前版本),删除了落后的DOM渲染方式,目前提供HTML5标准的Canvas渲染和WebGL渲染两种模式的渲染器,显然只有那些支持Canvas的浏览器才能使用Canvas渲染器。同样,WebGL渲染器只能用于支持WebGL的设备和浏览器。
本书主要对OpenLayers 5的应用进行详细解析,希望能够帮助读者了解OpenLayers5,并掌握基于OpenLayers 5的WebGIS开发实践。图3-1为OpenLayers官网(http://www.openlayers.org/)提供的OpenLayers 5的系列资源,包括OpenLayers 5框架与API文档等。
图3-1 OpenLay ers官网
3.1.1 OpenLayers 5的体系架构
本节先介绍OpenLayers是什么、能做什么、有什么意义,接下来通过OpenLayers 5的体系架构进一步了解和认识OpenLayers 5。
MetaCarta公司设计OpenLayers的目的,就是为了能够在客户端更好地展现和操作地图。OpenLayers将抽象事物具体化为类,其核心类是Map、Layer、Source、View,几乎所有的动作都围绕这个核心类展开,从而实现地图加载和相关操作。OpenLayers 5的体系架构如图3-2所示。
图3-2 OpenLay ers 5的体系架构
由OpenLayers 5的体系架构可见,可把整个地图看成一个容器(Map),核心为地图图层(Layer)、对应图层的数据源(Source)与矢量图层样式(Style)、与地图表现相关的地图视图(View),除此之外,容器中还有一些特别的层和控件,如地图交互操作控件,以及绑定在Map和Layer上的一系列待请求的事件。底层是OpenLayers的数据源,即Image、GML、KML、JSON等OGC服务资源,均为source与format命名空间下的子类,这些数据经过Renderer渲染,显示在地图容器中的图层Layer上。其中,地图容器(Map)与图层(Layer)的渲染有Canvas、WebGL两种类型,分别由ol.renderer.Map与ol.renderer.Layer实现。
3.1.2 OpenLayers 5的工作原理
OpenLayers是实现WebGIS客户端的开发库,它提供了一整套JavaScript的API,具有良好的体系架构和实现机制。GIS的核心是空间数据,关键是如果将空间数据应用到各行业的具体业务中或人们的日常生活中,充分挖掘空间数据的价值。针对复杂的空间数据,OpenLayers 5如何将这些空间数据抽象为类?如何解析各种空间数据源?又是如何渲染并在客户端展示?地图加载后,OpenLayers 5是如何控制地图,实现地图的相关操作?下面从五个方面做简要分析。
1.数据组织
首先,我们来了解OpenLayers 5的数据结构与组织。从表现形态来看,空间数据的矢量数据由点、线、面三类要素构成,将这些要素对应到Web客户端表现时,需要抽象为相应的类,包括要素之间的关系。在OpenLayers 5中,矢量数据的抽象主要由ol.geom.Geometry基类下的几何对象子类实现。图3-3给出了Geometry基类及其子类的继承关系。
图3-3 Geometr y基类及其子类的继承关系
从图3-3可见,OpenLayers 5的Geometry基类与之前版本的OpenLayers基本上是一致的。几何对象子类,即Point与MultiPoint(点与多点)、LineString与MultiLineString(线与多线)、Polygon与MultiPolygon(区与多区)、LinearRing(线性环)、Circle(圆),均继承于SimpleGeometry子类,SimpleGeometry子类与GeometryCollection子类则继承于Geometry基类。其中,LinearRing子类只能作为区几何组成部分使用,GeometryCollection子类则为Geometry对象集合。在组织矢量要素时,通过Feature子类(ol.Feature)组织为要素,或者通过ol.Collection子类(Feature集合)组织为要素集合。但是在实现的细节方面,还是有许多的不同,比如在OpenLayers 3中,Polygon子类提供了一个静态函数circular,该函数是在椭球的表面生成一个近似的圆,而在OpenLayers 5中,则直接导出了生成圆的函数,同时把函数入口参数中的ol.Sphere去除了,而默认为采用Clarke 1866 Authalic Sphere,当然也提供了相应的扩展参数,调用者可根据椭球的不同而设置不同的椭球半径。从这些细节可以看出,OpenLayers 5在用户的体验上有不少的改进,更多的修改或者改进可以关注OpenLayers官网代码的更新日志。上述为矢量数据的基本结构与组织原理,这是GIS的基础。基于客户端的数据组织与渲染机制可将矢量数据渲染为矢量地图,并在客户端表现出来。
对于Web网页地图应用而言,除了矢量数据,还有瓦片数据,以及图片等各类数据,需要将这些数据统一在Web网页呈现。对于Web网页的各类数据展现,OpenLayers 5是如何组织与设计的呢?接下来,我们来进一步了解OpenLayers 5的数据组织与实现原理。
OpenLayers 5的地图数据通过图层(Layer)组织渲染,且通过数据源(Source)设置具体的地图数据来源。因此,Layer与Source是密切相关的对应关系,缺一不可。Layer可看成渲染地图的层容器,具体的数据需要通过Source设置。
地图数据根据数据源(Source)可分为Image、Tile、Vector三大类,对应设置到Image、Tile、Vector三大类图层(Layer)中。地图图层与数据源的关系如图3-4所示,其中,矢量图层Vector通过样式(Style)来设置矢量数据渲染的方式和外观。
图3-4 地图图层与数据源的关系
在数据源(Source)中,Image类为单一图像基类,其子类作为画布(canvas)元素、服务器图片、单个静态图片、WMS单一图像等的数据源;Tile类为瓦片抽象基类,其子类作为各类瓦片数据的数据源;Vector则为矢量基类,可直接实例化创建矢量数据的数据源(支持各种格式的矢量数据),其子类则为扩展的某类矢量数据的数据源。
ol.source.Vector是矢量数据源基类,为矢量图层提供具体的数据来源,包括直接组织或读取的矢量数据(Featrues)、远程数据源的矢量数据(即通过URL设置数据源路径)等。若为URL设置的矢量数据源,则通过解析器Format(即ol.format.Feature的子类)来解析XML、Text、JSON格式的各类矢量数据,如GML、KML、GPS、WFS、WKT、GeoJSON等地图数据。
2.数据解析
从上述的OpenLayers 5空间数据组织可知,地图数据的数据源可分为Image、Tile、Vector三大类。其中,Image为图片数据源,Tile为瓦片数据源,两者本质基本相同,均为图片或图片集。Vector为矢量基类,由其Format属性设置解析数据类型,即通过ol.format.Feature类的子类进行各种格式矢量数据的解析。
由OpenLayers 5的API可知,ol.format.Feature类用于读/写各种格式的数据,并创建了多种格式的子类,即各类数据解析器,包括XML、Text、JSON类型的各种格式数据解析器。
以GML数据解析为例,其实现原理为:先通过接口调用得到GML格式的文本数据,然后通过ol.format.GML类的读/写方法来解析这个文本数据,从而读取矢量要素(Feature)及其几何对象(Geometry)等,最后结合样式(Style)并通过相应的渲染器在客户端渲染出来。不管是什么格式的数据,最后解析得到基本的Point、LineString子类的Geometry对象,进行客户端渲染后就是在地图上看到的那些内容。
3.数据渲染
基于OpenLayers的地图整个表现过程为:先通过URL(服务地址/文件路径)调用数据,然后用各种格式的数据解析器解析数据,再用相应的渲染器在图层中进行渲染,最后结合相应的控件表现出来,成为一幅“动态”地图。
在OpenLayers 5中,渲染功能由渲染器(即Renderer相关类)实现,通过Map的Renderer属性设置渲染方式,然后根据渲染方式(Canvas、WebGL)、与图层类型(Image、Tile、Vector)匹配渲染器将图层数据渲染出来。Renderver相关类关系如图3-5所示。
图3-5 Renderer相关类关系
在此主要讨论渲染器Renderer相关类,了解OpenLayers 5的渲染机制。该机制与OpenLayers3基本一致,主要区别是去除了DOM渲染。图层数据主要由ol.renderer.Layer子类与相关子类负责渲染,即分别通过Canvas、WebGL两大渲染类型的相关子类实现。ol.layer.Image子类的图像数据基于ImageLayer渲染器渲染,ol.layer.Tile子类的瓦片数据基于TileLayer渲染器渲染,ol.layer.Vector子类的矢量数据则由VectorLayer渲染器渲染。其中,对于矢量数据的渲染,在VectorLayer渲染器中是通过ol.render.VectorContext的相关子类来实现的。
下面简要介绍矢量数据的渲染机制。矢量数据的最终渲染方法由VectorContext子类实现,支持Canvas、WebGL两种方式,其中WebGL渲染方式对矢量数据的支持有限且不支持矢量瓦片。对于矢量数据,在VectorContext子类中分别提供了各类数据的渲染方法,诸如基于Canvas的drawCircle、drawFeature、drawGeometry方法,以及基于WebGl的drawFeature、drawGeometry方法。矢量数据的渲染同时会结合样式(Style)进行,可通过setStyle方法设置样式(Style)。
另外,ol.FeatureOverlay图层加载的矢量数据渲染也是基于上述的矢量数据渲染机制实现的。在OpenLayers 5中,地图交互控制主要通过ol.interaction.Interaction的相关子类实现,其中交互式图形绘制涉及客户端绘制图形的渲染,由RenderBox类实现,也采用上述的矢量数据渲染机制,即使用drawAsync、drawPolygonGeometry方法进行渲染。
图像或瓦片数据在Web客户端仅仅是图片,不包含其他的几何信息和属性信息,均通过HTML中的canvas标签显示,即通过数据源对应的渲染器处理渲染。瓦片数据是将矢量或影像裁剪得到的多级图片集,在进行渲染时需要根据级、行、列获取对应的图片,并在Web客户端以网格的方式组织各级瓦片地图。
4.地图表现
数据在被渲染后是如何在Web网页显示的?即地图表示是怎么实现的?众所周知,Web客户端通常使用HTML来表现网页内容,通过行为控制(如JavaScript)实现动态交互效果。基于OpenLayers的地图应用也是一样的原理。
在前面提到,OpenLayers 5的核心是Map类、Layer类与Source类,结合地图视图类View、矢量样式类Style等渲染地图。Map类的实例就是内嵌于Web网页的交互式地图。因此,最关键的是Map类,首先要把这个类分析明白。地图表现,简单说就是向一个地图容器里面加载图层与控件。Map类就是地图容器。顾名思义,它就像一个容器,可以分门别类、按空间层次来存放内容。那么,这个容器是如何实现的?具体有什么功能呢?
在Web网页中,地图容器通过div层来表现,通过Map类的target属性关联作为地图容器的HTML元素——div层,即将此地图容器div层与JavaScript的map对象绑定,然后将layers(含对应数据源source)、controls等内容加载到Map中,表现为地图。对地图的渲染,必须由一个或多个图层类(Layers)、一个地图视图类(View)及一个目标容器(div层)实现,即在创建一个Map实例时必须设置其图层(layers)、视图(view)与目标容器(target),这也是地图显示必备的三要素。
Map函数声明格式为ol.Map(options)。例如,在HTML页面的body中创建一个ID为“map”的DIV层,并定义一个map对象的JS代码如下:
//实例化map对象加载地图 var map = new ol.Map({ targe t:′map′,//地图容器div层的ID //在地图容器中加载的图层 layers: [ //加载瓦片图层数据 new ol.layer.Tile({ titl e:″天地图矢量图层″, source: new ol.source.XYZ({ url: ″http://t0.tianditu.com/DataServer? T=vec_w&x={x}&y={y}&l={z}&tk=您的天地图密钥″, attributions: ″天地图的属性描述″, wrapX: false }) }), new ol.layer.Tile({ titl e:″天地图矢量注记图层″, source: new ol.source.XYZ({ url: ″http://t0.tianditu.com/DataServer? T=cva_w&x={x}&y={y}&l={z}&tk=您的天地图密钥″, attributions: ″天地图的属性描述″, wrapX: false }) })], //地图视图设置 view: new ol.View({ center: [0, 0], //地图初始中心点 zoom: 3 //地图初始显示级别 }) });
上述代码实现加载一个瓦片地图(即天地图)功能,并可以对地图进行平移、缩放操作。在实例化map对象时,仅设置了地图显示必备的三要素:图层(layers)、视图(view)和目标容器(target)。如果没有设置图层与数据源,则在客户端渲染一个空白的地图容器。Layer类(含数据源)提供地图数据支持,View类主要控制用户与地图的最基本的交互,支持缩放、平移,设置中心点、地图显示级别等基本交互动作。地图应用非常丰富,我们肯定不会满足于仅仅显示并缩放地图,还想让地图完全“动”起来,即添加很多可交互的功能。例如,我们还想在单击相应的兴趣点时,地图能与我们进行交互,弹出想要的信息框显示兴趣点的详细信息等。这些丰富的地图交互功能,就得依靠地图控件(Control)、地图交互功能(Interaction)、叠加层(Overlay)等各个功能类,并结合Map类的属性、方法、事件来实现。
那么,地图容器是如何将设置的图层数据渲染呈现在Web页面的?从Web页面表现角度出发,通过剖析其HTML结构就能明晰其设计原理。地图显示页面的HTML结构如图3-6所示,在调试模式下可清楚查看地图容器的页面元素及其内部的结构和内容。
图3-6 地图显示页面的HTML结构
在用户定义的地图容器(即ID为map的div层)中,OpenLayers 5内部创建了一个viewport容器(类名为ol-viewport的div层),地图的所有内容均在viewport容器中呈现,可通过Map类的getViewport()得到此容器。在viewport容器里,分别创建了如下三个关键的内容层,分别渲染呈现地图容器中的内容:
● 地图渲染层:根据地图渲染方式创建的Canvas元素,用于渲染地图。在默认情况下,地图基于Canvas方式渲染,需要注意的是webgl方式的渲染目前对矢量数据支持有限且不支持矢量瓦片。
● 内容叠加层:类名为ol-overlaycontainer的div层,装载叠加层(ol.Overlay)内容,如在地图上添加的图片、标注等。
● 地图控件层:类名为ol-overlaycontainer-stopevent的div层,装载显示地图控件或叠加内容。例如,上面代码在该层里创建默认加载的三个地图控件元素。
viewport容器及其包含的所有元素的样式信息均在OpenLayers 5的默认样式文件中定义,用户可通过修改默认样式文件来自定义新样式,从而改变某些容器或控件的展现效果。
从上述对Map类的分析可见,Map类作为地图容器,可以加载各种数据格式的图层。地图容器中加载的图层(Layers)以其加载的先后顺序表现,即按顺序分层叠加表现,如图3-7所示。这些Layers可以通过图层数组的方式存储,也可以通过ol.Collection以图层组(LayerGroup)方式来存储。
图3-7 地图容器加载的图层表现示意图
与OpenLayers3一样,OpenLayers 5的地图不需要一个必备的基底图层,所有图层按加载顺序叠加表现。每个图层都是相对独立的,可以分别进行控制。叠加图层的顺序很重要,按照地图容器添加图层的先后顺序,从底往上排列,即每次给地图容器添加的图层都放到现有图层的上方。在应用时要根据具体应用需求,对图层加载顺序进行合理设置,避免覆盖有效的地图信息;或者通过图层的可见属性、透明度属性等,根据应用需求显示/隐藏图层,或设置图层半透明表现等。
其中,对于地图控件、叠加层内容的表现,可以通过设置对应页面元素(即HTML元素)的z-index值,在地图上以z-index值为序进行叠加。
5.事件机制
从感官角度我们知道,地图加载后在Web页面中表现,并能够通过功能按钮等操作地图,实现地图的缩放、拖动,以及对地图要素的查询、分析等功能。这些功能是怎么实现呢?这就要归功于OpenLayers 5中的事件机制,通过各种事件对地图进行控制。
OpenLayers 5的事件封装和语法变化是公认的一大亮点。一方面,OpenLayers 5的事件基于NodeJS的组件机制进行封装,根据不同的应用需求封装不同类型的事件;另一方面,OpenLayer5的事件语法统一变更为最新的ES6标准规范。在OpenLayers 5中,ol.MapEvent为地图事件的基类,提供了内部的事件类型,如地图移动结束事件(moveend)、地图框架渲染事件(postrender)。地图浏览器事件类(ol.MapBrowserEvent)继承于ol.MapEvent类,是控制整个地图交互的核心,提供外部设备(如鼠标、触摸屏)与地图交互的主要事件类型,如单击(singleclick、click)、双击(dbclick)、指针移动(pointermove)、指针拖曳(pointerdrag)等事件,这些事件由Map类的对象操作,与Map类相关联。除了地图浏览器事件,OpenLayers 5中的对象均支持参数、属性变更事件,即change、propertychange事件,由对象事件类(ol.Object.Event)提供。Change事件是针对一个对象的结构要素、状态等变更的事件,如地图容器Map中对象的地图视图变更事件(change:view)、交互控件类的激活状态变更事件(change:active)等。除此之外,还有交互功能类事件、集合事件类事件等,这些事件将网页元素或DOM对象与功能控制的实现形成一个逻辑整体,很好地实现了用户与地图的交互功能。
● 地图事件类(ol.MapEvent):包含moveend、postrender事件。
● 地图浏览器事件类(ol.MapBrowserEvent):包含singleclick、click、dbclick、pointermove、pointerdrag事件。
● 对象事件类(ol.Object.Event):包含change、propertychange事件。
● 选择控件事件类(ol.interaction.Select.Event):包含select事件。
● 绘制控件事件类(ol.interaction.Draw.Event):包含drawstart、drawend事件。
● 修改控件事件类(ol.interaction.Modify.Event):包含modifystart、modifyend事件。
● 集合事件类(ol.Collection.Event):包含add、remove事件。
针对各类事件,OpenLayers 5提供了统一的事件监听方法,即通过on与once方法添加事件监听,通过un与unByKey方法移除事件监听。在使用这些事件时,均由关联的对象添加事件监听,事件触发后再通过自定义的事件回调函数处理。
开发Web端应用程序,通常要基于鼠标事件进行交互,如onmousedown、onmousemove onmouseout、onmouseover、onmouseup、onclick、ondbclick等事件。OpenLayers 5的事件机制与普通的Web端应用类似,只不过针对具体应用需求进行了事件封装,使得与地图的交互更为简便易用。目前,OpenLayers 5提供的事件都是Web端地图应用中最常用的事件类,但并不局限于这些事件,还可以充分发挥其优良的扩展性,针对具体功能应用需求进行相应扩展。