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

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
OpenLayers是一个开源的JavaScript库,提供了丰富的地图功能和交互性,包括地图的展示、数据的加载与处理、视图的控制等。ol.source.xyz是OpenLayers用于加载XYZ瓦片图层的数据源。 XYZ瓦片是一种常见的地图数据切片方式,其X表示图层缩放级别,Y表示竖直方向的切片索引,Z表示水平方向的切片索引。ol.source.xyz可以通过指定瓦片图层的URL模板,从网络上加载XYZ瓦片数据,并在地图上进行展示。 在使用ol.source.xyz时,需要提供一个URL模板,以告诉OpenLayers如何获取瓦片数据。URL模板是一个包含占位符的字符串,OpenLayers会根据地图当前缩放级别、瓦片索引等参数替换占位符,从而构建出实际的瓦片图层URL。 例如,可以使用"https://example.com/tiles/{z}/{x}/{y}.png"作为URL模板,其"{z}"、"{x}"和"{y}"分别会被当前缩放级别、水平切片索引和竖直切片索引替换。这样OpenLayers就可以根据需要动态加载相应的瓦片数据。 除了URL模板,ol.source.xyz还可以通过属性进行配置,如最小缩放级别、最大缩放级别、瓦片大小等。这些属性可以根据具体需求进行设置,以便实现对瓦片图层的更精细的控制。 总之,ol.source.xyz是OpenLayers用于加载XYZ瓦片图层的数据源,提供了灵活可配置的方式用于加载和展示瓦片图层数据。使用ol.source.xyz可以方便地在地图上加载和呈现各种瓦片地图数据,为地图开发提供了丰富的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值