使用OpenLayers構建項目時,有時會遇到一些性能優化的問題,比如大量要素的繪製。OpenLayers爲繪製海量的點要素提供了一些手段,比如版本6之前的ol.WebGLMap,6之後的ol.layer.WebGLPoints。但是當我們需要繪製海量的其他類型要素(LineString、Polygon)時,貌似沒有比較合適的方案。
本文通過對VectorContext對象的研究和對源碼的分析,實現了在ol.source.ImageCanvas中獲得VectorContext對象,並使用VectorContext高效繪製海量LineString。
本文的方式只能在node編程環境中使用,因爲OpenLayers官方沒有將ol.format等工具類的接口在ol.js中暴露出來,無法引用。
本文分析部分較長,只需要代碼的請直拉到底。最後的Demo隨機生成了10萬條三個頂點的折線,繪製在地圖上,渲染的時間大約需要1.3s,如果採用ol.source.Vector和ol.layer.Vector效率則低得多。
問題:
有關canvasFunction:OpenLayers爲了方便使用canvas API在地圖上繪製圖形,提供了一個ol.source.ImageCanvas類型的source,在這個source的canvasFunction屬性定義的函數中,可以在canvas上實時繪製圖形,通過返回這個canvas作爲source的數據,並使用ol.layer.Image類的對象,將這個canvas作爲一個地圖的圖層渲染到地圖上。
上圖是對這個 canvasFunction的描述。之前的博文《OpenLayers 5 使用turf.js渲染克里金插值計算的等值面》中提到的Trojx同學的方案就是用了這個東西來渲染克里金插值的結果。
canvasFunction的缺點就是需要根據extent, resolution, pixelRatio, size, projection這幾個參數自己來進行繪製座標的計算,略麻煩。因爲是在canvas上繪圖,座標系統只能使用屏幕座標,所以如果繪製海量要素的話,座標轉換工作的量非常大,而且還有個很大的問題——處理不了視圖旋轉。
於是想到了——能不能在ImageCanvas中使用VectorContext呢?
分析:
- 首先去看一下源碼中VectorContext有關的代碼,針對render事件(prerender、postrender),通過ol.render.getVectorContext(event)可以獲取一個VectorContext的句柄,那麼就看一下這裏需要哪些東西來取得這個句柄,定位到源碼:
可以看到,最終使用的是一個CanvasImmediateRenderer的構造函數來生產的這個對象,需要的參數有:
其中context可以通過canvas計算得到, pixelRatio, extent都已經有了,需要計算的是transform,rotation, squaredTolerance, userTransform這幾個參數。 - transform:可以通過分析源碼,利用ol的內部類型transform來實現;
首先看它的計算,是由一個transform組件中的multiply函數將兩個transform:inversePixelTransform和coordinateToPixelTransform相乘得到的。
inversePixelTransform由pixelTransform進行makeInverse運算得到;
pixelTransform由原始的transform進行座標映射得到;
coordinateToPixelTransform由原始的transform進行座標映射得到;
通過ol.transform的源碼可以知道,實現座標映射的API叫做composeTransform,於是可以在源碼裏搜索調用這個API的地方,發現與我們目標相關聯的有以下兩個地方:
ol/renderer/canvas/ImageLayer.js 中:
ol/renderer/Map.js 中:
所以這個問題就可以得到解決了。 - rotation:可以通過當前視圖的rotation屬性獲取;
- squaredTolerance, userTransform:可以通過resolution, pixelRatio, projection這三個參數計算獲得;
解決方案:
實現一個全局函數,傳入canvas以及canvasFunction的幾個參數,返回一個VectorContext對象(函數中有對全局對象map的調用,如想移植,可以增加一個參數view用作計算)
代碼如下:
function getCanvasVectorContext(canvas, extent, resolution, pixelRatio, size, projection) {
canvas.width = size[0] * pixelRatio;
canvas.height = size[1] * pixelRatio;
let width = Math.round(size[0] * pixelRatio);
let height = Math.round(size[1] * pixelRatio);
let context = canvas.getContext('2d');
let coordinateToPixelTransform = createTransform();
let pixelTransform = createTransform();
let inversePixelTransform = createTransform();
let rotation = map.getView().getRotation();
let center = map.getView().getCenter();
composeTransform(coordinateToPixelTransform,
size[0] / 2, size[1] / 2,
1 / resolution, -1 / resolution,
-rotation,
-center[0], -center[1]);
composeTransform(pixelTransform,
size[0] / 2, size[1] / 2,
1 / pixelRatio, 1 / pixelRatio,
rotation,
-width / 2, -height / 2
);
makeInverse(inversePixelTransform, pixelTransform);
const transform = multiplyTransform(inversePixelTransform.slice(), coordinateToPixelTransform);
const squaredTolerance = getSquaredTolerance(resolution, pixelRatio);
let userTransform;
const userProjection = getUserProjection();
if (userProjection) {
userTransform = getTransformFromProjections(userProjection, projection);
}
return new CanvasImmediateRenderer(
context, pixelRatio, extent, transform,
rotation, squaredTolerance, userTransform);
}
之後在ImageCanvas中就可以調用這個函數獲得VectorCanvas的句柄, 就可以像在render事件回調函數中一樣繪製要素了,不需要手動進行座標轉換:
var canvas = document.createElement('canvas');
var canvasLayer = new ImageLayer({
source: new ImageCanvasSource({
canvasFunction: (extent, resolution, pixelRatio, size, projection) => {
var vc = getCanvasVectorContext(canvas, extent, resolution, pixelRatio, size, projection)
//使用VectorContext對象繪製要素數組
randomFeatures.forEach(item => {
vc.drawFeature(item, lineStyle)
})
console.log(new Date().getTime());
return canvas;
},
projection: 'EPSG:4326'
})
})
全部Demo源碼:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Using OpenLayers with Webpack</title>
<link rel="stylesheet" href="https://openlayers.org/en/latest/css/ol.css" type="text/css">
<style>
html, body {
margin: 0;
height: 100%;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map" class="map"></div>
<script src="./svg.bundle.js"></script>
</body>
</html>
import { Map, View } from 'ol';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import LineString from 'ol/geom/LineString';
import ImageLayer from 'ol/layer/Image';
import ImageCanvasSource from 'ol/source/ImageCanvas';
import VectorSource from 'ol/source/Vector';
import VectorLayer from 'ol/layer/Vector';
import Feature from 'ol/Feature';
import Style from 'ol/style/Style';
import Stroke from 'ol/style/Stroke';
import {
create as createTransform,
multiply as multiplyTransform,
compose as composeTransform,
makeInverse
} from 'ol/transform';
import CanvasImmediateRenderer from 'ol/render/canvas/Immediate';
import { getSquaredTolerance } from 'ol/renderer/vector';
import { getUserProjection, getTransformFromProjections } from 'ol/proj';
var layer = new VectorLayer({
source: new VectorSource()
})
var tile = new TileLayer({
source: new OSM()
})
var map = new Map({
layers: [tile
],
target: 'map',
view: new View({
projection: 'EPSG:4326',
center: [104, 30],
zoom: 1
})
});
var randomFeatures = [];
for (var i = 0; i < 100000; i++) {
var anchor = new Feature({
geometry: new LineString([[Math.random()*180, Math.random()*160-80], [Math.random()*180, Math.random()*160-80], [Math.random()*180, Math.random()*160-80]])
});
randomFeatures.push(anchor)
}
console.log(new Date().getTime());
var lineStyle = new Style({
stroke: new Stroke({
color: [255, 0, 0, 0.5],
width: 0.1
})
});
function getCanvasVectorContext(canvas, extent, resolution, pixelRatio, size, projection) {
canvas.width = size[0] * pixelRatio;
canvas.height = size[1] * pixelRatio;
let width = Math.round(size[0] * pixelRatio);
let height = Math.round(size[1] * pixelRatio);
let context = canvas.getContext('2d');
let coordinateToPixelTransform = createTransform();
let pixelTransform = createTransform();
let inversePixelTransform = createTransform();
let rotation = map.getView().getRotation();
let center = map.getView().getCenter();
composeTransform(coordinateToPixelTransform,
size[0] / 2, size[1] / 2,
1 / resolution, -1 / resolution,
-rotation,
-center[0], -center[1]);
composeTransform(pixelTransform,
size[0] / 2, size[1] / 2,
1 / pixelRatio, 1 / pixelRatio,
rotation,
-width / 2, -height / 2
);
makeInverse(inversePixelTransform, pixelTransform);
const transform = multiplyTransform(inversePixelTransform.slice(), coordinateToPixelTransform);
const squaredTolerance = getSquaredTolerance(resolution, pixelRatio);
let userTransform;
const userProjection = getUserProjection();
if (userProjection) {
userTransform = getTransformFromProjections(userProjection, projection);
}
return new CanvasImmediateRenderer(
context, pixelRatio, extent, transform,
rotation, squaredTolerance, userTransform);
}
var canvas = document.createElement('canvas');
var canvasLayer = new ImageLayer({
source: new ImageCanvasSource({
canvasFunction: (extent, resolution, pixelRatio, size, projection) => {
var vc = getCanvasVectorContext(canvas, extent, resolution, pixelRatio, size, projection)
randomFeatures.forEach(item => {
vc.drawFeature(item, lineStyle)
})
console.log(new Date().getTime());
return canvas;
},
projection: 'EPSG:4326'
})
})
map.addLayer(canvasLayer);
我在企鵝家的課堂和CSDN學院都開通了《OpenLayers實例詳解》課程,歡迎報名學習。搜索關鍵字OpenLayers就能看到。