meface/docs/article/gis/openlayers/11quicklystart.md

638 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 快速起步
date: 2021-09-08
author: ac
tags:
- OpenLayers
categories:
- GIS
---
## 1. hello ol
> 其实学习的最好方式应该是官方文档,但可能会受限于个人的知识储备问题,“吸收”到的知识也会有所差别。
```html
<!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>
```
这是官网上的`quickstart`的中代码:
1. 采用传统的CDN的方式引入ol
2. 创建了一个id为map的div元素作为地图的容器并通过class指定元素大小
3. 再使用ol中Map类的构造器创建地图配置参数中必须配置三个参数才能显示地图。其中`target`指定页面中的容器标签;`layers`配置地图的图层;`view`可以指定地图的中心位置和地图的缩放级别,还可以配置地图的投影等。
`ol`中没有在view里面配置投影的默认使用的是Web墨卡托投影(`EPSG:3857`),投影相关的方法在`ol.proj`的命名空间下。`fromLonLat`方法是将经纬度的地理坐标转换为投影坐标,默认的目标投影是`EPSG:3857`。
## 2. 开发方式
除了上述的传统的直接使用`CND`引入`ol`的开发方式外,目前在前端开发最常用的还是安装`npm`包的形式。
> 需要安装Nodejs环境
前端工程化解决方案有很多,像`Webpack`、[`Parcel`](https://parceljs.org/)等。`ol`官方的教程使用的是`Parcel`。
下面我们使用`Parcel`工具来手动配置一个工程化的示例:
1. 安装`Parcel`
```shell
#npm
npm install -g parcel-bundler
#yarn
yarn global add parcel-bundler
```
2. 创建项目目录,目录名称为`pracelol`
```shell
mkdir pracelol && cd pracelol
```
3. 初始化项目,生成包管理文件`package.json`,安装`ol`
```shell
npm init -y
npm install ol
```
4. 创建`index.html`文件和`main.js`文件:
`index.html`
```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>
```
> `script`标签中添加`type`属性,值为`module``Parcel`会将该标签引用的JS文件转码为ES5。
`mian.js`
```javascript
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'
});
```
5. 在`package.json`文件中配置脚本命令:
```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"
}
}
```
6. 运行`npm run dev`命令,会执行`parcel index.html`将`index.html`文件作为入口文件进行打包编译,会看到项目中做出了一个`dist`目录。并启动一个地址为`http://localhost:1234`的Web服务。
7. 效果:
<img src="./images/image-20210911112328085.png" alt="image-20210911112328085" style="zoom: 80%;" />
## 3. 源码解析
`Map.js`
```javascript
/**
* @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;
```
我们可以在`ol.map`源码的注释中知道`map`是`Opnelayer`中核心的组件,一个`map`必须要有一个`View`实例、一个或多个图层`layers`和一个用于确定页面渲染`DOM`节点的`id`的`target`这是地图表现的必备三要素view、layers、target
地图初始化后在id为`target`属性指定的`DOM`节点为容器生成了一系列的地图表现相关的标签。
<img src="./images/image-20201102105547517.png" alt="image-20201102105547517" style="zoom: 50%;" />
在`target`元素的位置内创建地图视口容器`class`属性为`ol-viewport`,可以通过`getViewport()`方法获取该节点。另外在`ol-viewport`里面创建用于图层渲染的`ol-layer`节点、用于在地图上添加注记图标的`ol-overlaycontainer`节点和用于展示地图控件的`ol-overlaycontainer-stopevent`节点。
- `map `Map.target
- `ol.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`中。主要渲染流程如下:
1. 配置参数option解析控件、交互组件、键盘事件DOM对象、叠加层和图层数组
```javascript
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 //图层数组
};
}
```
2. 构建页面DOM元素`ol-viewport`和子容器`ol-overlaycontainer`、`ol-overlaycontainer-stopevent`
```javascript
/**
* @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);
```
3. 创建瓦片队列添加图层、视图、SIZE、TARGET变化的处理事件
```javascript
/**
* @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_);
```
4. 解析控件参数,并绑定事件监听
```javascript
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));
```
5. 解析交互参数,添加事件监听
```javascript
`` 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));
```
6. 解析叠加层,添加事件监听
```javascript
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));
```
7. 因为layer、target和view是构建map的必备要素所以一定会触发`handleLayerGroupChanged_`、`handleTargetChanged_`和`handleViewChanged_`事件,从而执行`this.render()`函数,最后执行渲染的主函数`renderFrame_()`。
```javascript
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] Openlayers源码阅读 https://blog.csdn.net/u013240519/article/details/104997512