# 1. hello ol
其实学习的最好方式应该是官方文档,但可能会受限于个人的知识储备问题,“吸收”到的知识也会有所差别。
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.4.3/css/ol.css" type="text/css">
<style>
.map {
height: 400px;
width: 100%;
}
</style>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.4.3/build/ol.js"></script>
<title>OpenLayers example</title>
</head>
<body>
<h2>My Map</h2>
<div id="map" class="map"></div>
<script type="text/javascript">
/*
* 地图表现:必备三要素,
* 图层(Layer)
* 视图(View)
* 目标容器(target)
*
* 核心类:Map、Layer、Source、View
* 渲染方式:ol3中有Canvas、WebGL、DOM
* ol5中删除了DOM渲染方式,Canvas(由ol.renderer.Map实现)、
* WebGL(由ol.renderer.Layer实现)
*/
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.fromLonLat([37.41, 8.82]),
zoom: 4
})
});
</script>
</body>
</html>
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
这是官网上的quickstart
的中代码:
- 采用传统的CDN的方式引入ol;
- 创建了一个id为map的div元素,作为地图的容器,并通过class指定元素大小;
- 再使用ol中Map类的构造器创建地图,配置参数中必须配置三个参数才能显示地图。其中
target
指定页面中的容器标签;layers
配置地图的图层;view
可以指定地图的中心位置和地图的缩放级别,还可以配置地图的投影等。
ol
中没有在view里面配置投影的,默认使用的是Web墨卡托投影(EPSG:3857
),投影相关的方法在ol.proj
的命名空间下。fromLonLat
方法是将经纬度的地理坐标转换为投影坐标,默认的目标投影是EPSG:3857
。
# 2. 开发方式
除了上述的传统的直接使用CND
引入ol
的开发方式外,目前在前端开发最常用的还是安装npm
包的形式。
需要安装Nodejs环境
前端工程化解决方案有很多,像Webpack
、Parcel
(opens new window)等。ol
官方的教程使用的是Parcel
。
下面我们使用Parcel
工具来手动配置一个工程化的示例:
安装
Parcel
#npm npm install -g parcel-bundler #yarn yarn global add parcel-bundler
1
2
3
4
5创建项目目录,目录名称为
pracelol
mkdir pracelol && cd pracelol
1初始化项目,生成包管理文件
package.json
,安装ol
npm init -y npm install ol
1
2创建
index.html
文件和main.js
文件:index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script type="module" src="main.js"></script> </head> <body> <div id="map" style="width: 100%;height: 400px;"></div> </body> </html>
1
2
3
4
5
6
7
8
9
10
11
12
13script
标签中添加type
属性,值为module
,Parcel
会将该标签引用的JS文件转码为ES5。mian.js
import 'ol/ol.css'; import Map from 'ol/Map'; import View from 'ol/View'; import OSM from 'ol/source/OSM'; import TileLayer from 'ol/layer/Tile'; var map = new Map({ layers: [ new TileLayer({source: new OSM()}) ], view: new View({ center: [0, 0], zoom: 4 }), target: 'map' });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16在
package.json
文件中配置脚本命令:{ "name": "pracelol", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev":"parcel index.html" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "ol": "^6.7.0" } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15运行
npm run dev
命令,会执行parcel index.html
将index.html
文件作为入口文件进行打包编译,会看到项目中做出了一个dist
目录。并启动一个地址为http://localhost:1234
的Web服务。效果:
# 3. 源码解析
Map.js
/**
* @module ol/Map
*/
import PluggableMap from './PluggableMap.js';
import {defaults as defaultControls} from './control.js';
import {defaults as defaultInteractions} from './interaction.js';
import {assign} from './obj.js';
import CompositeMapRenderer from './renderer/Composite.js';
/**
* @classdesc
* The map is the core component of OpenLayers. For a map to render, a view,
* one or more layers, and a target container are needed:
*
* import Map from 'ol/Map';
* import View from 'ol/View';
* import TileLayer from 'ol/layer/Tile';
* import OSM from 'ol/source/OSM';
*
* var map = new Map({
* view: new View({
* center: [0, 0],
* zoom: 1
* }),
* layers: [
* new TileLayer({
* source: new OSM()
* })
* ],
* target: 'map'
* });
*
* The above snippet creates a map using a {@link module:ol/layer/Tile} to
* display {@link module:ol/source/OSM~OSM} OSM data and render it to a DOM
* element with the id `map`.
*
* The constructor places a viewport container (with CSS class name
* `ol-viewport`) in the target element (see `getViewport()`), and then two
* further elements within the viewport: one with CSS class name
* `ol-overlaycontainer-stopevent` for controls and some overlays, and one with
* CSS class name `ol-overlaycontainer` for other overlays (see the `stopEvent`
* option of {@link module:ol/Overlay~Overlay} for the difference). The map
* itself is placed in a further element within the viewport.
*
* Layers are stored as a {@link module:ol/Collection~Collection} in
* layerGroups. A top-level group is provided by the library. This is what is
* accessed by `getLayerGroup` and `setLayerGroup`. Layers entered in the
* options are added to this group, and `addLayer` and `removeLayer` change the
* layer collection in the group. `getLayers` is a convenience function for
* `getLayerGroup().getLayers()`. Note that {@link module:ol/layer/Group~Group}
* is a subclass of {@link module:ol/layer/Base}, so layers entered in the
* options or added with `addLayer` can be groups, which can contain further
* groups, and so on.
*
* @api
*/
class Map extends PluggableMap {
/**
* @param {import("./PluggableMap.js").MapOptions} options Map options.
*/
constructor(options) {
options = assign({}, options);
if (!options.controls) {
options.controls = defaultControls();
}
if (!options.interactions) {
options.interactions = defaultInteractions();
}
super(options);
}
createRenderer() {
return new CompositeMapRenderer(this);
}
}
export default Map;
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
我们可以在ol.map
源码的注释中知道map
是Opnelayer
中核心的组件,一个map
必须要有一个View
实例、一个或多个图层layers
和一个用于确定页面渲染DOM
节点的id
的target
,这是地图表现的必备三要素(view、layers、target)。
地图初始化后,在id为target
属性指定的DOM
节点为容器生成了一系列的地图表现相关的标签。
在target
元素的位置内创建地图视口容器class
属性为ol-viewport
,可以通过getViewport()
方法获取该节点。另外在ol-viewport
里面创建用于图层渲染的ol-layer
节点、用于在地图上添加注记图标的ol-overlaycontainer
节点和用于展示地图控件的ol-overlaycontainer-stopevent
节点。
map
:Map.targetol.viewport
:Map.view 视图ol-layers
:Map.getLayers() 图层组(集合)ol-layer
:图层,根据渲染方式创建Canvas元素- canvas :画布
ol-overlaycontainer
:Map.getOverlays() 内容叠加层ol-overlaycontainer-stopevent
:Map.getControls() 控件层
PC端页面视口的大小就是浏览器的大小,但这里
ol-viewport
的宽高大小设置都为100%作为地图的视口,是最近的父辈元素的容器大小,即target
属性指定的DOM
元素的大小。
图层组Layers
是以图层数组的形式存储,与其他地图API
不同,ol
中没有必须的底图basemap
,所有的图层都按照加载的顺序叠加显示,先添加的在下面,从底向上排列。
在ol.Map
源码中,Map构造器作为主入口,接受参数,判断是否使用默认的控件和交互控件,其余渲染流程都在父类PluggableMap
中。主要渲染流程如下:
配置参数option,解析控件、交互组件、键盘事件DOM对象、叠加层和图层数组
const optionsInternal = createOptionsInternal(options); /** * @param {MapOptions} options Map options. * @return {MapOptionsInternal} Internal map options. */ function createOptionsInternal(options) { ... return { controls: controls, //控件 interactions: interactions, //交互组件 keyboardEventTarget: keyboardEventTarget, //键盘事件dom对象 overlays: overlays, //叠加层 values: values //图层数组 }; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18构建页面DOM元素,
ol-viewport
和子容器ol-overlaycontainer
、ol-overlaycontainer-stopevent
/** * @private * @type {!HTMLElement} */ this.viewport_ = document.createElement('div'); this.viewport_.className = 'ol-viewport' + ('ontouchstart' in window ? ' ol-touch' : ''); this.viewport_.style.position = 'relative'; this.viewport_.style.overflow = 'hidden'; this.viewport_.style.width = '100%'; this.viewport_.style.height = '100%'; /** * @private * @type {!HTMLElement} */ this.overlayContainer_ = document.createElement('div'); this.overlayContainer_.style.position = 'absolute'; this.overlayContainer_.style.zIndex = '0'; this.overlayContainer_.style.width = '100%'; this.overlayContainer_.style.height = '100%'; this.overlayContainer_.className = 'ol-overlaycontainer'; this.viewport_.appendChild(this.overlayContainer_); /** * @private * @type {!HTMLElement} */ this.overlayContainerStopEvent_ = document.createElement('div'); this.overlayContainerStopEvent_.style.position = 'absolute'; this.overlayContainerStopEvent_.style.zIndex = '0'; this.overlayContainerStopEvent_.style.width = '100%'; this.overlayContainerStopEvent_.style.height = '100%'; this.overlayContainerStopEvent_.className = 'ol-overlaycontainer-stopevent'; this.viewport_.appendChild(this.overlayContainerStopEvent_); /** * 绑定浏览器事件 */ this.mapBrowserEventHandler_ = new MapBrowserEventHandler(this, options.moveTolerance); const handleMapBrowserEvent = this.handleMapBrowserEvent.bind(this); for (const key in MapBrowserEventType) { this.mapBrowserEventHandler_.addEventListener(MapBrowserEventType[key], handleMapBrowserEvent); } /** * @private * @type {HTMLElement|Document} */ this.keyboardEventTarget_ = optionsInternal.keyboardEventTarget; /** * @private * @type {?Array<import("./events.js").EventsKey>} */ this.keyHandlerKeys_ = null; const handleBrowserEvent = this.handleBrowserEvent.bind(this); this.viewport_.addEventListener(EventType.CONTEXTMENU, handleBrowserEvent, false); this.viewport_.addEventListener(EventType.WHEEL, handleBrowserEvent, PASSIVE_EVENT_LISTENERS ? {passive: false} : false);
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创建瓦片队列,添加图层、视图、SIZE、TARGET变化的处理事件
/** * @private * @type {TileQueue} */ this.tileQueue_ = new TileQueue( this.getTilePriority.bind(this), this.handleTileChange_.bind(this)); this.addEventListener(getChangeEventType(MapProperty.LAYERGROUP), this.handleLayerGroupChanged_); this.addEventListener(getChangeEventType(MapProperty.VIEW), this.handleViewChanged_); this.addEventListener(getChangeEventType(MapProperty.SIZE), this.handleSizeChanged_); this.addEventListener(getChangeEventType(MapProperty.TARGET), this.handleTargetChanged_);
1
2
3
4
5
6
7
8
9
10
11
12解析控件参数,并绑定事件监听
this.controls.forEach( /** * @param {import("./control/Control.js").default} control Control. * @this {PluggableMap} */ function(control) { control.setMap(this); }.bind(this)); this.controls.addEventListener(CollectionEventType.ADD, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function(event) { event.element.setMap(this); }.bind(this)); this.controls.addEventListener(CollectionEventType.REMOVE, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function(event) { event.element.setMap(null); }.bind(this));
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解析交互参数,添加事件监听
`` this.interactions.forEach( /** * @param {import("./interaction/Interaction.js").default} interaction Interaction. * @this {PluggableMap} */ function(interaction) { interaction.setMap(this); }.bind(this)); this.interactions.addEventListener(CollectionEventType.ADD, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function(event) { event.element.setMap(this); }.bind(this)); this.interactions.addEventListener(CollectionEventType.REMOVE, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function(event) { event.element.setMap(null); }.bind(this));
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解析叠加层,添加事件监听
this.overlays_.forEach(this.addOverlayInternal_.bind(this)); this.overlays_.addEventListener(CollectionEventType.ADD, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function(event) { this.addOverlayInternal_(/** @type {import("./Overlay.js").default} */ (event.element)); }.bind(this)); this.overlays_.addEventListener(CollectionEventType.REMOVE, /** * @param {import("./Collection.js").CollectionEvent} event CollectionEvent. */ function(event) { const overlay = /** @type {import("./Overlay.js").default} */ (event.element); const id = overlay.getId(); if (id !== undefined) { delete this.overlayIdIndex_[id.toString()]; } event.element.setMap(null); }.bind(this));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22因为layer、target和view是构建map的必备要素,所以一定会触发
handleLayerGroupChanged_
、handleTargetChanged_
和handleViewChanged_
事件,从而执行this.render()
函数,最后执行渲染的主函数renderFrame_()
。handleLayerGroupChanged_() { console.log("handleLayerGroupChanged_") if (this.layerGroupPropertyListenerKeys_) { this.layerGroupPropertyListenerKeys_.forEach(unlistenByKey); this.layerGroupPropertyListenerKeys_ = null; } const layerGroup = this.getLayerGroup(); if (layerGroup) { this.layerGroupPropertyListenerKeys_ = [ listen( layerGroup, ObjectEventType.PROPERTYCHANGE, this.render, this), listen( layerGroup, EventType.CHANGE, this.render, this) ]; } this.render(); } render() { console.log("render"); if (this.renderer_ && this.animationDelayKey_ === undefined) { this.animationDelayKey_ = requestAnimationFrame(this.animationDelay_); } } this.animationDelay_ = function() { console.log("animationDelay_") this.animationDelayKey_ = undefined; this.renderFrame_(Date.now()); }.bind(this); renderFrame_(time) { console.log("renderFrame_") const size = this.getSize(); const view = this.getView(); const previousFrameState = this.frameState_; /** @type {?FrameState} */ let frameState = null; if (size !== undefined && hasArea(size) && view && view.isDef()) { const viewHints = view.getHints(this.frameState_ ? this.frameState_.viewHints : undefined); const viewState = view.getState(); frameState = { animate: false, coordinateToPixelTransform: this.coordinateToPixelTransform_, declutterItems: previousFrameState ? previousFrameState.declutterItems : [], extent: getForViewAndSize(viewState.center, viewState.resolution, viewState.rotation, size), index: this.frameIndex_++, layerIndex: 0, layerStatesArray: this.getLayerGroup().getLayerStatesArray(), pixelRatio: this.pixelRatio_, pixelToCoordinateTransform: this.pixelToCoordinateTransform_, postRenderFunctions: [], size: size, tileQueue: this.tileQueue_, time: time, usedTiles: {}, viewState: viewState, viewHints: viewHints, wantedTiles: {} }; } this.frameState_ = frameState; this.renderer_.renderFrame(frameState); if (frameState) { if (frameState.animate) { this.render(); } Array.prototype.push.apply(this.postRenderFunctions_, frameState.postRenderFunctions); if (previousFrameState) { const moveStart = !this.previousExtent_ || (!isEmpty(this.previousExtent_) && !equals(frameState.extent, this.previousExtent_)); if (moveStart) { this.dispatchEvent( new MapEvent(MapEventType.MOVESTART, this, previousFrameState)); this.previousExtent_ = createOrUpdateEmpty(this.previousExtent_); } } const idle = this.previousExtent_ && !frameState.viewHints[ViewHint.ANIMATING] && !frameState.viewHints[ViewHint.INTERACTING] && !equals(frameState.extent, this.previousExtent_); if (idle) { this.dispatchEvent(new MapEvent(MapEventType.MOVEEND, this, frameState)); clone(frameState.extent, this.previousExtent_); } } //派发图层渲染的postrender事件 this.dispatchEvent(new MapEvent(MapEventType.POSTRENDER, this, frameState)); this.postRenderTimeoutHandle_ = setTimeout(this.handlePostRender.bind(this), 0); } /** * @protected */ handlePostRender() { console.log("handlePostRender"); const frameState = this.frameState_; // Manage the tile queue // Image loads are expensive and a limited resource, so try to use them // efficiently: // * When the view is static we allow a large number of parallel tile loads // to complete the frame as quickly as possible. // * When animating or interacting, image loads can cause janks, so we reduce // the maximum number of loads per frame and limit the number of parallel // tile loads to remain reactive to view changes and to reduce the chance of // loading tiles that will quickly disappear from view. const tileQueue = this.tileQueue_; if (!tileQueue.isEmpty()) { let maxTotalLoading = this.maxTilesLoading_; let maxNewLoads = maxTotalLoading; if (frameState) { const hints = frameState.viewHints; if (hints[ViewHint.ANIMATING] || hints[ViewHint.INTERACTING]) { const lowOnFrameBudget = !IMAGE_DECODE && Date.now() - frameState.time > 8; maxTotalLoading = lowOnFrameBudget ? 0 : 8; maxNewLoads = lowOnFrameBudget ? 0 : 2; } } if (tileQueue.getTilesLoading() < maxTotalLoading) { tileQueue.reprioritize(); // FIXME only call if view has changed tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); } } if (frameState && this.hasListener(RenderEventType.RENDERCOMPLETE) && !frameState.animate && !this.tileQueue_.getTilesLoading() && !this.getLoading()) { //派发图层渲染的rendercomplete事件 this.renderer_.dispatchRenderEvent(RenderEventType.RENDERCOMPLETE, frameState); } const postRenderFunctions = this.postRenderFunctions_; for (let i = 0, ii = postRenderFunctions.length; i < ii; ++i) { postRenderFunctions[i](this, frameState); } postRenderFunctions.length = 0; }
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# 参考文章
[1] Openlayers源码阅读 https://blog.csdn.net/u013240519/article/details/104997512