通過在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源碼:

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章