通过在ol.source.ImageCanvas中获取VectorContext对象高效率绘制海量要素

使用OpenLayers构建项目时,有时会遇到一些性能优化的问题,比如大量要素的绘制。OpenLayers为绘制海量的点要素提供了一些手段,比如版本6之前的ol.WebGLMap,6之后的ol.layer.WebGLPoints。但是当我们需要绘制海量的其他类型要素(LineStringPolygon)时,貌似没有比较合适的方案。

本文通过对VectorContext对象的研究和对源码的分析,实现了在ol.source.ImageCanvas中获得VectorContext对象,并使用VectorContext高效绘制海量LineString

本文的方式只能在node编程环境中使用,因为OpenLayers官方没有将ol.format等工具类的接口在ol.js中暴露出来,无法引用。


本文分析部分较长,只需要代码的请直拉到底。最后的Demo随机生成了10万条三个顶点的折线,绘制在地图上,渲染的时间大约需要1.3s,如果采用ol.source.Vectorol.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:inversePixelTransformcoordinateToPixelTransform相乘得到的。

    inversePixelTransformpixelTransform进行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源码:



  
    
    Using OpenLayers with Webpack
    
    
  
  
   

    
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就能看到。

你可能感兴趣的:(Openlayers,开发高级技巧)