--- title: 快速起步 date: 2021-09-08 author: ac tags: - OpenLayers categories: - GIS --- ## 1. hello ol > 其实学习的最好方式应该是官方文档,但可能会受限于个人的知识储备问题,“吸收”到的知识也会有所差别。 ```html OpenLayers example

My Map

``` 这是官网上的`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 Document
``` > `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. 效果: image-20210911112328085 ## 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`节点为容器生成了一系列的地图表现相关的标签。 image-20201102105547517 在`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} */ 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