首页 > 技术文章 > OpenLayers 图层(Layers) 详解

javalinux 2021-09-16 15:14 原文

文中代码可以在 https://github.com/QingyaFan/data-visualization 找到。

本文更新的版本可以在我的微信公众号阅读:https://mp.weixin.qq.com/s/CZo7oC26sqX1UyYQoLF2hg

 

如果对现在的地图技术了解的少,看到地图,可能会觉得地图就是一张图片,这张图片可以缩放、移动。这种看法不能说不对,但只是表面现象,实际上地图是由一个或多个图层组成的,不同的图层存储不同类型的地物,如线状图层存储线状地物:道路、河流;点状图层存储POI信息:建筑、店铺等;面状地物存储诸如公园等有一定范围的地物。当然,也有可能不按照地物本身的几何特征划分图层的,比如现在的展示拥堵情况的图层,展示红绿灯位置的图层。

在 OpenLayers 中,图层是使用 layer 对象表示的,主要有 WebGLPoints Layer、热度图(HeatMap Layer)、图片图层(Image Layer)、切片图层(Tile Layer) 和 矢量图层(Vector Layer) 五种类型,它们都是继承 Layer 类的。

1、Layer 类
OpenLayers 初始化一幅地图(map),至少需要一个可视区域(view),一个或多个图层( layer), 和 一个地图加载的目标 HTML 标签(target),其中最重要的是图层( layer)。
在 这里 可以看到 layer 基类的定义,类的描述如下:

* @classdesc
* Base class from which all layer types are derived. This should only be instantiated
* in the case where a custom layer is be added to the map with a custom `render` function.
* Such a function can be specified in the `options` object, and is expected to return an HTML element.
*
* A visual representation of raster or vector map data.
* Layers group together those properties that pertain to how the data is to be
* displayed, irrespective of the source of that data.
1
2
3
4
5
6
7
8
layer 是各种图层的基类,只用于让子类型继承和实现,一般自身不会实例化。主要功能是对矢量数据和栅格数据的可视化。图层渲染结果的样式,主要与数据渲染方式有关,与数据源关系不大。

1.1 属性
初始化时,所有图层类型都具有的参数,如下:

source,指定了图层的数据来源,图层作用是以一定的样式渲染数据,source则指定了数据;
className,图层各个元素的样式;
opacity,透明度,默认为 1 ,即完全透明;
visible,是否可见;
zIndex,图层的叠加次序,默认是0,最底层,如果使用setMap方法添加的图层,zIndex值是Infinity,在最上层;
extent,图层渲染的区域,即浏览器窗口中可见的地图区域。extent 是一个矩形范围,格式是[number, number, number, number] 分别代表 [left, bottom, right, top] 。为了提升渲染效率和加载速度,extent范围之外的瓦片是不会请求的,当然也不会渲染;
minResolution,图层可见的最小分辨率;
maxResolution,图层可见的最大分辨率;
minZoom,图层可见的最小zoom level;
maxZoom,图层可见的最大zoom level
source是一个非常重要的参数,图层中渲染的数据来自于source参数指定的地址,可能是文件,可能是返回地理数据的网络API,不同的source对象类型不一样,source都有哪些可以参考这篇文章。zoom的边界情况也需要注意:是 (minZoom, maxZoom],图层可见的zoom level大于minZoom,小于等于maxZoom。这与resolution的情况刚好相反[minResolution, maxResolution)。

1.2 事件触发
有的同学问:我想在图层加载完成时,做一些事情,如何知道图层加载完成呢?图层初始化时,我们可以指定很多hook,用以当某些事件触发时做出一定的动作,这些事件中有一个postrender,会在图层渲染完成后触发,我们可以对这个事件传入回调。类似的事件还有prerender。

包含的方法其实没有什么好说的,一般就是对属性的getter和setter,详细的列表可以参考 这里

1.3 行为
我们看到了每种图层都有source可以让我们指定数据来源,那数据是如何变成我们看到的效果的?这就涉及到renderer了,每种图层类型都有一个隐式的属性:renderer_,这个我们从Layer基类的属性定义中可以看到:

/**
* @private
* @type {import("../renderer/Layer.js").default}
*/
this.renderer_ = null;
1
2
3
4
5
Layer基类还定义了相关方法:

/**
* Getthe renderer for this layer.
* @return {import("../renderer/Layer.js").default} The layer renderer.
*/
getRenderer() {
if (!this.renderer_) {
this.renderer_ = this.createRenderer();
}
return this.renderer_;
}
1
2
3
4
5
6
7
8
9
10
我们看到,如果图层的renderer_还没有得到初始化,会调用createRenderer方法初始化renderer_,在基类中是不做任何行为的,把初始化的细节下放到了各个具体的图层子类中实现,类似于Golang中的接口,C++中虚函数实现的多态。每种图层类型对应的renderer如下:

Heatmap,为了提升性能,使用了和WebGLPoints图层一样的渲染器:WebGLPointsLayerRenderer;
WebGLPointsLayer,渲染器为WebGLPointsLayerRenderer
ImageLayer,渲染器为CanvasImageLayerRenderer
TileLayer,渲染器为CanvasTileLayerRenderer
VectorLayer,渲染器为CanvasVectorLayerRenderer
VectorImageLayer,渲染器为CanvasVectorImageLayerRenderer
VectorTileLayer,渲染器为CanvasVectorTileLayerRenderer
以上各类图层使用的Renderer看来,openlayers当前(2020/04)主要使用H5的Canvas和WebGL进行渲染,目前来看,WebGL的比重会逐渐增加,从类似的mapboxgl.js或deck.gl可以看出来。

2、各种图层类型
从渲染发生的地方来看,openlayers的图层主要分为两类:Vector(矢量)和Raster(栅格),矢量图层是指在渲染发生在浏览器的图层,source返回的数据类型是矢量,如geojson的坐标串;栅格图层则是由服务器渲染,返回到浏览器的是一张张的瓦片图片,栅格图层主要是展示。
矢量图层类型有:

Graticule,地图上覆盖的网格标尺图层;
HeatMap,热力图;
Vector,矢量图
VectorImage,单张的矢量图层
VectorTile,矢量瓦片图层
WebGLPoints,WebGL渲染的海量点图层
栅格图层类型较为简单,只有Tile图层。

所有的图层都继承了 Layer 类,监听和触发的事件都在 ol.render.Event 中定义,共用的属性和状态都是在 layerbase 中定义的,它们除了从ol.layer.Layer 类继承而来的参数外,还定义了自己的属性和方法。下面我们来分别看看这几个图层类型。

注:不管使用什么图层类型,初始化 map 同时,如果不明确指定 control 对象,那么就会默认包含 缩放 和 鼠标拖拽 功能,关于这个 Control 对象,在后面的博客中会讲到,现在认为 Control 就是一个控制与地图交互的工具就好。

2.1 WebGLPoint Layer
WebGLPoint Layer 是由 WebGL 作为渲染引擎的点图层,众所周知,WebGL在渲染大量数据(>10k)效率明显优于Canvas或SVG,所以对于有大数据量前端渲染需求的,WebGL作为渲染引擎几乎是唯一的选择。以前openlayers一直没有webgl作为渲染引擎的图层类型,虽然openlayer自从3.x重构以来就一直将支持三维作为目标,但是进展较慢,对比隔壁mapboxgl.js,进度差的不是一点。严格来说,openlayers和leaflet是一个时代的产品,mapboxgl.js很早支持三维,且是leaflet的作者写的“下一代”前端地图可视化库。

WebGLPoint Layer本质上是矢量图层,在浏览器端渲染,然而,问题是:如果数据量较大,从服务器传来浏览器将会耗费很长时间,虽然只需要传输一次,虽然渲染快,但是用户感受到的是一直在等待。如果传输需要2分钟,渲染只需10ms,用户感知到的仍然是等了2分钟渲染。所以以当前的网速来看,可能更适合内网应用。当然,如果5G时代来的足够快,也可能真火了。

那么有的同学会问,我在服务器端渲染不比webgl性能高,它不香吗?当然香,但是服务器与客户端是1对多的关系,每个客户端都需要服务器渲染,并发量高了,服务器垮不垮?又有小伙伴说了,切片不就是解决这个问题的吗?对,但现在需求往往是样式随时会变,缓存了切片,样式一变,又要重新切,意义不大。矢量切片出现不就是这个问题的证据么?

实例
由于WebGL的优势是大数据量下的渲染性能,所以随意改变样式重新渲染代价贼低,对于海量数据的个性化渲染也成为了可能,WebGLPoint Layer的style也变成了一个非常实用的功能。先来个例子。

mounted() {
this.map = new Map({
layers: [
new TileLayer({
source: new OSM()
})
],
target: document.getElementById('map'),
view: new View({
center: [0, 0],
zoom: 2
})
});

let vectorSource = new Vector({
url: 'https://openlayers.org/en/latest/examples/data/geojson/world-cities.geojson',
format: new GeoJSON()
});

let pointLayer = new WebGLPointsLayer({
source: vectorSource,
style: {
"symbol": {
"symbolType": "circle",
"size": [
"interpolate",
[
"linear"
],
[
"get",
"population"
],
40000,
8,
2000000,
28
],
"color": "#006688",
"rotateWithView": false,
"offset": [
0,
0
],
"opacity": [
"interpolate",
[
"linear"
],
[
"get",
"population"
],
40000,
0.6,
2000000,
0.92
]
}
}
});

this.map.addLayer(pointLayer);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
图层中指定的数据源world-cities.geojson包含了19321个点要素,Style中指定的是根据每个点要素包含的人口数量属性决定要素的半径大小和要素展示的透明度。如果使用Canvas,性能会查差一些,虽然差的不多,但是随着数据量的继续变大,WebGL还是可以轻松应对,Canvas就到极限了。最终的渲染结果是这样的:

 

2.2 Heatmap Layer
将矢量数据渲染成热度图的类,继承了 ol.layer.Vector 类,ol.layer.Vector 继承了ol.layer.Layer 类, 额外的参数是 olx.layer.HeatmapOptions ,其定义如下:

/**
* @enum {string}
*/
ol.layer.HeatmapLayerProperty = {
BLUR: 'blur',
GRADIENT: 'gradient',
RADIUS: 'radius'
};
1
2
3
4
5
6
7
8
Heatmap 图层比起其它类型的图层多三个属性,常用的是 blur 和 radius,这两个属性什么作用呢,我们可以调整一下看看效果:

 

没错,blur 控制圆点的边缘,对边缘做模糊化; radius 则规定了圆点的半径。注:并不是点,而是圆。

实例
首先创建一个 heatmaplayer 对象:

var vector = new ol.layer.Heatmap({
source: new ol.source.KML({
url: 'data/kml/2012_Earthquakes_Mag5.kml',
projection: 'EPSG:3857',
extractStyles: false
}),
blur: parseInt(blur.value, 10),
radius: parseInt(radius.value, 10)
});
1
2
3
4
5
6
7
8
9
这里 heatmap 使用KML格式,本地文件data/kml/2012_Earthquakes_Mag5.kml 作为 heatmap 的来源,数据是2012年全球地震发生的位置和震级等简单的描述信息,然后将 heatmap 图层加到 map 中:

map = new ol.Map({ //初始化map
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.MapQuest({layer: 'sat'})
}),
heatmap
],
view: new ol.View({
center: ol.proj.transform([37.41, 8.82], 'EPSG:4326', 'EPSG:3857'),
zoom: 4
})
});
1
2
3
4
5
6
7
8
9
10
11
12
13
查看运行效果:

 

2.3 Image Layer
主要是指服务器端渲染的图像,可能是已经渲染好的图像,或者是每一次请求,都根据请求内容定制化地生成一幅图片,该图层类型支持任意的范围和分辨率。

实例
首先实例化一幅图片图层:

/**
* create an imageLayer
*/
var extent = [0, 0, 3264, 2448];
var projection = new ol.proj.Projection({
code: 'EPSG:4326',
extent: extent
}),
var imageLayer = new ol.layer.Image({
source: new ol.source.ImageStatic({
url: 'sample.jpg',
projection: projection,
imageExtent: extent
})
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
与 heatmap 一样,首先需要传入 URL 参数,即图片地址,这里可以是网络图片的地址,或者是本地的文件地址;然后需要传入参考坐标系 projection,code 是一个标识,可以是任何字符串,如果是EPSG:4326 或者是 EPSG:3857 ,那么就会使用这两个坐标系,如果不是,就使用默认的坐标系,extent 是一个矩形范围,上面已经提到;imageLayer 的第三个参数是 imageExtent 表示图片的尺寸,这里我们不能改变图片的原来的比例,图片只会根据原来的比例放大或缩小。

最后将 imageLayer 加到地图中:

map = new ol.Map({ //初始化map
target: 'map',
layers: [ imageLayer ],
view: new ol.View({
projection: projection,
center: ol.extent.getCenter(extent),
zoom: 2
})
});
1
2
3
4
5
6
7
8
9
效果如下:

 

放大之后感觉很像必应搜索的界面的感觉,有木有 _|:

 

2.4 Tile Layer
切片地图是比较常用的图层类型,切片的概念,就是利用网格将一幅地图切成大小相等的小正方形,如图:

 

这样就明白我们使用百度地图等地图时为什么网速慢时候,会一块一块的加载的原因了吧!对,因为是切片。当请求地图的时候,会请求视口(也就是浏览器可见的区域)可见的区域内包含的切片,其余的切片不会请求,这样就节省了网络带宽,而且一般这些切片都是预先切好的,且分为不同的缩放级别,根据不同的缩放级别分成不同的目录。如果将这些切片地图放到缓存中,那访问速度会更快。

继承了 ol.layer.Layer ,额外的参数是 olx.layer.TileOptions ,其定义如下:

/**
* @typedef {{brightness: (number|undefined),
* contrast: (number|undefined),
* hue: (number|undefined),
* opacity: (number|undefined),
* preload: (number|undefined),
* saturation: (number|undefined),
* source: (ol.source.Tile|undefined),
* visible: (boolean|undefined),
* extent: (ol.Extent|undefined),
* minResolution: (number|undefined),
* maxResolution: (number|undefined),
* useInterimTilesOnError: (boolean|undefined)}}
* @api
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可以看出,多出了 preload 和 useInterimTilesOnError 两个参数,preload 是在还没有将相应分辨率的渲染出来的时候,将低分辨率的切片先放大到当前分辨率(可能会有模糊),填充到相应位置,默认是 0,现在也就明白了当网速慢时,为什么地图会先是模糊的,然后再变清晰了吧,就是这个过程!useInterimTilesOnError是指当加载切片发生错误时,是否用一个临时的切片代替,默认值是 true。

实例
其实在 加载地图的例子 中,我们就是请求 MapQuest 的切片地图:

map = new ol.Map({ //初始化map
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.MapQuest({layer: 'sat'})
})
],
view: new ol.View({
center: ol.proj.transform([37.41, 8.82], 'EPSG:4326', 'EPSG:3857'),
zoom: 2
})
});
1
2
3
4
5
6
7
8
9
10
11
12
其中的 ol.layer.Tile 就是切片图层类型,来源是 MapQuest ,layer
是请求的图层的类型, MapQuest 有三种类型的图层:osm, sat 和 hyb ,osm 就是 OpenStreetMap 的缩写,是其提供的数据, sat 是卫星图,hyb 是两种类型的混合图层。

我们可以查看一下浏览器的网络请求内容:

 

这里是 Firefox 浏览器的 Firebug 网络请求面板,可见其请求的图片,是一块块的,且是基于一定的编号规则进行编号的。

2.5 Vector Layer
在 OpenLayers之使用Vector Layer 中曾经使用过,即矢量图层,矢量图层实际上是在客户端渲染的图层类型,服务器返回的数据或者文件会通过 OpenLayers 进行渲染,得到相应的矢量图层。

在客户端渲染的矢量数据图层,继承了 ol.layer.Layer ,额外的参数是 olx.layer.VectorOptions ,其定义如下:

> /**
* @typedef {{brightness: (number|undefined),
* contrast: (number|undefined),
* renderOrder: (function(ol.Feature, ol.Feature):number|null|undefined),
* hue: (number|undefined),
* minResolution: (number|undefined),
* maxResolution: (number|undefined),
* opacity: (number|undefined),
* renderBuffer: (number|undefined),
* saturation: (number|undefined),
* source: (ol.source.Vector|undefined),
* style: (ol.style.Style|Array.<ol.style.Style>|ol.style.StyleFunction|undefined),
* updateWhileAnimating: (boolean|undefined),
* updateWhileInteracting: (boolean|undefined),
* visible: (boolean|undefined)}}
* @api
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
相对于一般的图层,多出了 renderOrder、renderBuffer、style、updateWhileAnimating 和 updateWhileInteracting 五个参数。renderOrder 是指渲染地理要素时的顺序,一般情况下,在渲染之前,要素是基于一定规则排序的,而渲染就是根据这个顺序进行依次渲染的,这个参数便指定了这个排序规则,如果赋值为 null ,那么就不会对地理要素进行排序,渲染也不会有一定的顺序;renderBuffer 表示地图的视口区域的缓冲区;style 规定了矢量图层的样式,就是配色和形状等等;updateWhileAnimating 表示当有动画特效时,地理要素是否被重新创建,默认是 false,当设置为 true 时,可能会对性能有所影响;updateWhileInteracting 表示当 地理要素 交互时,是否会被重新渲染。

实例
首先创建一个 矢量图层:

vectorLayer = new ol.layer.Vector({ //初始化矢量图层
source: new ol.source.GeoJSON({
projection: 'EPSG:3857',
url: 'data/geojson/countries.geojson' //从文件加载边界等地理信息
}),
style: function(feature, resolution) {
style.getText().setText(resolution < 5000 ? feature.get('name') : ''); //当放大到1:5000分辨率时,显示国家名字
return [style];
}
});
1
2
3
4
5
6
7
8
9
10
服务器返回的 GeoJSON 格式的文件 data/geojson/countries.geojson 包含国家的边界数据,属于多边形类型,经过 OpenLayers 渲染之后得到结果如下:

 

可以看到蓝色的线为各个国家的边界,当鼠标在某个国家上方时,相应的区块会变红色,这是添加的事件,我们可以改变其样式,注意到 vectorlayer 相对于其他类型的图层,还包含了一个 style 参数,这个参数便是控制矢量图层的外观样式的,其定义如下:

/**
* 定义矢量图层
* 其中style是矢量图层的显示样式
*/
var style = new ol.style.Style({
fill: new ol.style.Fill({ //矢量图层填充颜色,以及透明度
color: 'rgba(255, 255, 255, 0.6)'
}),
stroke: new ol.style.Stroke({ //边界样式
color: '#319FD3',
width: 1
}),
text: new ol.style.Text({ //文本样式
font: '12px Calibri,sans-serif',
fill: new ol.style.Fill({
color: '#000'
}),
stroke: new ol.style.Stroke({
color: '#fff',
width: 3
})
})
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
style 是一个 ol.style.Style 类型,矢量图层是可以调节透明度的,如下:

fill: new ol.style.Fill({ //矢量图层填充颜色,以及透明度
color: 'rgba(255, 255, 255, 0.6)'
})
1
2
3
rgba 的最后一个变量就是控制透明度的变量,范围是 0~1,0 表示不透明,1 代表完全透明。因为这里主要讲 Layer,所以关于 ol.style.Style 其它的内容,这里就不多说了。

3、讨论
百度地图或高德地图提供的是什么layer类型呢?我们来分别看看它们在 Firefox 看到的网络请求。
百度地图:


高德地图:

 

通过上面的讨论,我们可以得出结论,它们都是提供的网络切片图层类型,而一些加载的地理要素,如酒店等,便是加载在一个矢量图层中的,所以说,它们是混杂着切片图层和矢量图层的。

4、总结
其实图层可以按照渲染的位置分为两类,一类是在服务器端渲染好,以图片形式返回浏览器的, imagelayer 和 tilelayer 都是属于这种类型;另一类是在浏览器渲染的图层类型,vectorlayer 和 heatmaplayer 就是这种类型。
OK,终于写完了,好累好累!

参考
https://www.omnisci.com/technical-glossary/cpu-vs-gpu
https://openlayers.org/en/latest/examples/webgl-points-layer.html

————————————————
版权声明:本文为CSDN博主「庆祝亚运会」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qingyafan/article/details/45398131

推荐阅读