【基于Leaflet和Canvas绘图的前端大量栅格数据渲染】

2023-11-19

1. 需求

有包含30万坐标点的json文件,每个坐标点包含经度、纬度、行值、列值、数值,现需要根据数值分级进行不同颜色的显示,并在地图的正确位置进行渲染。最终效果如下:
在这里插入图片描述

2. 环境和工具

2.1 使用Edge、Chrome

实测采用Chromium内核的浏览器在坐标计算和Canvas渲染速度上要快出非常多(对比Firefox),下图前7列为Firefox,最后一列为Edge,同样为7万个点,快了一倍不止。

在这里插入图片描述

2.2 PixJS的使用

Pixjs:HTML5创建引擎。最快、最灵活的 2D WebGL 渲染器。

Pixjs是一个2D的游戏引擎,它真的很快,非常快。本文只用到了最基础的两个功能,矩形绘制和图片绘制。渲染30万个矩形平均时间在400ms左右。

PixJS官网:https://pixijs.com/

PixJS手册中文网站:https://aitrade.ga/pixi.js-cn/index.html

3. 实现思路

3.1 地图引擎分析

Canvas绘图技术在地图上实现数据渲染,无非要解决两个问题。一是拖动跟随,二是缩放重绘。不同地图引擎在地图绘制上使用的技术和方法不同,比如Leaflet使用SVG,而ArcGIS使用WebGL。同时,他们对Canvas图层的处理方式也不同。

以Leaflet为例,在地图加载完成后,手动添加Canvas图层,并且在左上角绘制一个矩形。

在这里插入图片描述

当我们拖动地图一段距离后再次观察canvas的位置,发现它的定位并没有发生变化,但是却不在屏幕中心了。说明它跟随父级div的位置变动而变动,且起点坐标位于屏幕左上角。

在这里插入图片描述

了解canvas的变换规律,可以让我们在定位图片的过程中有的放矢。

3.2 渲染方式选择

一种方式是直接将栅格渲染至canvas中,在地图缩放后重新渲染。另一种方式是,先在离屏canvas中渲染出需要的效果,保存为webp格式或png格式的图片,之后的处理都在该图片上进行。这里采用方式二。

3.3 思路梳理

  • 初始化地图(监听地图移动事件)
  • 初始化PixJS(添加canvas)
  • 生成webp格式图片
  • 在地图上正确定位webp图
  • 响应地图拖动和缩放

4. 编码实现

4.1 监听地图事件

map.on('moveend', () => {
    // 获取地图拖动后,相对于地图原点坐标原点的偏移值
    const offset_x = map._mapPane._leaflet_pos.x;
    const offset_y = map._mapPane._leaflet_pos.y;

    // 获取地图相对于上次的偏移值‘,此时this.offset_x是上次的偏移值
    // 首次偏移后,偏移值‘ = 相对于地图原点坐标原点的偏移值
    if (this.offset_x) {
        this.offset_x_count = offset_x - this.offset_x;
    } else {
        this.offset_x_count = offset_x;
    }
    if (this.offset_y) {
        this.offset_y_count = offset_y - this.offset_y;
    } else {
        this.offset_y_count = offset_y;
    }

    // 记录本次偏移值
    this.offset_x = offset_x;
    this.offset_y = offset_y;
})

需要在地图初始化之后便开始记录偏移量(地图的拖动),map._mapPane._leaflet_pos.x获取的是始终是地图相对于初始化状态的偏移量,因此每次移动后的偏移增量需要额外计算。

4.2 初始化PixiJS

initPixjs() {
    let left = -this.map._mapPane._leaflet_pos.x + 'px';
    let top = -this.map._mapPane._leaflet_pos.y + 'px';

    let app = new PIXI.Application({
        width: document.body.clientWidth,
        height: document.body.clientHeight,
        backgroundAlpha: 0
    });
    
    this.pixjsApp = app;
    this.canvas = app.view;
    this.canvas.style.position = "absolute";
    this.canvas.style.top = top;
    this.canvas.style.left = left;

    let parent = document.getElementsByClassName("leaflet-pane leaflet-overlay-pane")[0];
    parent.appendChild(app.view);
}

pixi会自动生成canvas元素,我们需要将其添加到父级元素之下。在添加时,地图可能未移动过,也可能移动过,为了保证canvas起点位于屏幕左上角,在添加前获取地图的偏移值并将其负值作为canvas的绝对定位值。

4.3 经纬度转屏幕坐标

Leaflet并没有现成的方法将一个经纬度坐标转换到屏幕坐标,但是从文章:Leaflet 如何把一个坐标转换到屏幕上中我们可以得知,Leaflet 加载完以后,会有一个 map-pane 的div元素,里面包含了所有的图层,PixelOrigin 就是 map-pane 这个容器最左上角的位置。我们可以将某个经纬度转换为投影坐标,再减去像素原点的坐标,就能得到该点在屏幕中的具体位置,这也是为什么我们需要把canvas元素的起点固定在屏幕左上角,如此一来计算出来的屏幕坐标就是在canvas中的坐标。

/**
* 经纬度转屏幕坐标
* @param lon 经度
* @param lat 纬度
* @param zoom 缩放等级
* @returns {{x: number, y: number}} 屏幕坐标
*/
lngLatToScreen(lon, lat, zoom) {
    let point = this.$refs.map.CRS_4490.latLngToPoint(L.latLng({lon: lon, lat: lat}), zoom);
    let origin = this.$refs.map.map.getPixelOrigin();
    return {
        x: (point.x - origin.x),
        y: (point.y - origin.y)
    }
}
/**
* 全部栅格点坐标转换
* @param points 栅格点(行,列,积水值)
* @param bounds 显示范围
* @param zoom
*/
pointsToScreen(points, bounds, zoom) {
    const screen_left_bottom = this.lngLatToScreen(bounds.left_bottom[0], bounds.left_bottom[1], zoom);
    const screen_right_top = this.lngLatToScreen(bounds.right_top[0], bounds.right_top[1], zoom);

    // 计算每行/列总像素值,保存,作为渲染时画布的大小
    let row_pixels = (screen_right_top.x - screen_left_bottom.x);
    let col_pixels = (screen_left_bottom.y - screen_right_top.y);

    // 计算行像素步长和列像素步长
    let step_x = row_pixels / bounds.col_count; //在canvas的一行中,也就是x轴方向,每个栅格应该占有的像素 = 一行的总像素/总列数
    let step_y = col_pixels / bounds.row_count; //在canvas的一列中,也就是y轴方向,每个栅格应该占有的像素 = 一列的总像素/总行数

    // 渲染起点坐标
    let origin_x = screen_left_bottom.x;
    let origin_y = screen_right_top.y;
    this.origin_x = origin_x;
    this.origin_y = origin_y;

    // 计算所有栅格点相对于起点,在canvas中的渲染坐标
    let canvas_points = {};
    for (let i = 1; i <= 9; i++) {
        let part_result = [];
        let part_points = points['level' + i];
        for (let point of part_points) {
            // 行数确定y轴坐标,列数确定x轴坐标
            let x = point[1] * step_x;
            let y = (bounds.row_count - point[0] - 1) * step_y;
            part_result.push([x, y]);
        }
        canvas_points['level' + i] = part_result;
    }
    return {
        canvas_points: canvas_points,
        render_width: row_pixels,
        render_height: col_pixels,
        pixel_width: step_x,
        pixel_height: step_y,
        canvas_width: screen_right_top.x,
        canvas_height: screen_left_bottom.y,
        origin_x: origin_x,
        origin_y: origin_y
    }
},

坐标文件格式不同,处理的步骤便不同。这里主要关注以下3个部分:

  1. 计算出渲染范围,左下角和右上角屏幕坐标
  2. 计算出每一个坐标的渲染起点
  3. 计算出每一个坐标的渲染宽高

4.4 生成webp图

/**
* 生成离屏图webp图
* 在拖动时只需渲染webp图,缩放等级变化后需要重新生成
* @param zoom 缩放等级
* @param offset_x 地图x轴偏移量
* @param offset_y 地图y轴偏移量
* @returns webp图 Base64格式
*/
generatePic(zoom, offset_x, offset_y) {
    let result = this.pointsToScreen(this.points, this.bounds, zoom);
    let canvas_points = result['canvas_points'];
    let render_width = result['render_width'];
    let render_height = result['render_height'];
    let pixel_width = result['pixel_width'];
    let pixel_height = result['pixel_height'];
    let origin_x = result['origin_x'];
    let origin_y = result['origin_y'];

    const app = new PIXI.Application({
        width: render_width,
        height: render_height,
        backgroundAlpha: 0,
        preserveDrawingBuffer: true,
        autoDensity: true,
        autoStart: true,
    });

    const graphic = new PIXI.Graphics();
    app.stage.addChild(graphic);

    // console.time('渲染计时');
    for (let i = 1; i <= 9; i++) {
        let part_points = canvas_points['level' + i];
        graphic.beginFill(this.colors[i]);
        part_points.forEach(point => {
            graphic.drawRect(point[0], point[1], pixel_width, pixel_height)
        });
    }

    app.render();
    // console.timeEnd('渲染计时');

    this.imageBase64 = app.view.toDataURL('image/webp', 1.0);
    // 将图片加载到地图上
    this.addPicToMap(this.imageBase64, origin_x, origin_y, offset_x, offset_y);

    // pixjs使用后,销毁webgl_content
    app.renderer.gl.getExtension('WEBGL_lose_context').loseContext();
    app.destroy({
        removeView: true,
        stageOptions: {
            children: true
        }
    });

    return this.imageBase64;
}

这里使用PixiJS的Graphic类来绘制所有的矩形,这里的绘制并没有在屏幕中显示,而是在内存中进行。完成绘制后将canvas中的图像保存为base64编码,图片选择webp格式,相对于png格式它的文件体积更小,同时画质差距不大。离屏图的效果如下:

在这里插入图片描述

4.5 将webp图添加到地图中

addPicToMap(image_base64, origin_x, origin_y, offset_x, offset_y) {
    const pic = PIXI.Sprite.from(image_base64);
    pic.x = origin_x + offset_x;
    pic.y = origin_y + offset_y;
    this.pixjsPic = pic;
    this.pixjsApp.stage.addChild(pic);
}

前面我们提到,在添加图片之前,地图就可能移动过,因此图片在canvas中的位置也需要考虑地图的偏移,offset_x, offset_y参数在调用4.4函数时传入,其数值是当前地图偏移量。

4.6 响应地图事件

/**
 * 更新图片在canvas中的位置
* @param pic pixjs的Sprite类 包含一张图片
* @param x x轴偏移量
* @param y y轴偏移量
*/
changePicPosition(pic, x, y) {
    // 根据地图偏移数值设定图片位置
    pic.x = pic.x + x;
    pic.y = pic.y + y;
}
map.on('moveend', () => {
    if (!this.pixjsPic) {
        return;
    }
    // 1.若缩放等级变化,根据现有坐标点,重新生成图片并加载到地图上
    let sub = this.zoom - map.getZoom()
    if (sub !== 0) {
        this.generatePic(map.getZoom(), map._mapPane._leaflet_pos.x, map._mapPane._leaflet_pos.y);
        this.zoom = map.getZoom();
        return;
    }

    // 保持canvas在屏幕中央
    // canvas相对于图层的的绝对定位值变化方向,是地图偏移的反方向
    this.canvas.style.left = -this.offset_x + 'px';
    this.canvas.style.top = -this.offset_y + 'px';

    // 改变图片在canvas中的坐标
    // 图片坐标的变化方向,和地图相对于上次的偏移值‘方向相同
    this.changePicPosition(this.pixjsPic, this.offset_x_count, this.offset_y_count);
})

正常显示图片后,最后要做的就是在每次地图拖动结束后设置canvas的位置,同时也需要更新其中图片的位置,来保证1.canvas起点始终位于屏幕左上角 2.canvas中的图片始终与地图元素重合。

参考

arcgis 与 pixi.js 实现大数据量渲染 (一) - 简书

Leaflet中文文档

ArcGIS API实现气象数据可视化(esri公开课笔记)_跳墙网

ArcGIS API for Javascript 4.X扩展canvas在三维地图实现热力图

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【基于Leaflet和Canvas绘图的前端大量栅格数据渲染】 的相关文章

随机推荐

  • Java并发编程之设计模式

    同步模式之保护性暂停 1 定义 即 Guarded Suspension 用在一个线程等待另一个线程的执行结果 要点 有一个结果需要从一个线程传递到另一个线程 让他们关联同一个 GuardedObject 如果有结果不断从一个线程到另一个线
  • MIPI-DSI 协议深度解析-简单易懂协议详解

    1 MIPI MIPI 移动行业处理器接口 是 Mobile Industry Processor Interface 的缩写 MIPI 移动行 业处理器接口 是 MIPI 联盟发起的为移动应用处理器制定的开放标准 DCS DisplayC
  • vue项目history模式刷新404问题

    vue项目history模式部署到服务器后 根路径访问没有问题 但是进入其他功能再刷新页面就会出现404 因为你没在nginx或者apache配置上面加上重定向跳转 解决办法 只需要加上这段配置 nginx配置内容 location try
  • DNS详解

    1 背景 DNS 是 Domain Name System 的缩写 即域名系统 DNS可以理解是将域名 如 www baidu com 和IP地址进行相互映射的一个分布式数据库 人直接去记忆IP地址数串是相当困难的 像是电话号码 记忆常用的
  • VUE+js高德地图2.0API两幅地图联动同步缩放

  • 主机上连接到 vmware虚拟机的方式

    我们平时只有一台电脑 但是练习时需要有多台电脑 那么此时应该怎么办 答案是下载 vmware 可以在上面创建多台虚拟机 至于如何创建 不是本文的重点 下面先说说桥接是如何设置的 首先 进入配置好的虚拟机下面 选择 虚拟机 设置 网络适配器
  • NLP-实体&关系联合抽取-2021:GPLinker

    基础思路 关系抽取乍看之下是三元组 s p o s p o s p o 即subject pre
  • spi总线挂载多个设备的设备树描述

    内核版本 linux4 6 Vivado版本 201602 工程中用到很多spi控制的从设备 首先对FPGA工程的正确性验证一下 验证的设备树描述如下 spi e0006000 compatible xlnx zynq spi r1p6 r
  • 内存大页及基本块

    大页内存 透明大页和大页的关系 这两者最大的区别在于 标准大页管理是预分配的方式 而透明大页管理则是动态分配的方式 内存页的概念 内存页 page 内存的最小分配单元 page gt 一个应用程序至少要使用一个内存页 4096B 4k li
  • k8s学习(五)ReplicaSet的使用

    ReplicaSet ReplicaSet 的目的是维护一组在任何时候都处于运行状态的 Pod 副本的稳定集合 可确保指定数量的pod在任何设定的时间运行 因此 它通常用来保证给定数量的 完全相同的 Pod 的可用性 示例 1 nginx
  • C++基础之纯虚函数

    一 纯虚函数的定义 纯虚函数是一种特殊的虚函数 在许多情况下 在基类中不能对虚函数给出有意义的实现 而把它声明为纯虚函数 它的实现留给该基类的派生类去做 这就是纯虚函数的作用 C 中的纯虚函数 一般在函数名后使用 0作为此类函数的标志 前面
  • 4,引擎初始化--(5)初始化actor--6,生成PlayActor--(2)玩家登录--3,PostLogin()(学习资料来自于ue4游戏框架)

    一旦PlayerController生成出来 world关于玩家的网络的初始化工作全部完成 接下来 进行PostLogin 从而在玩家加入游戏时 能做一些设置工作 默认情况下 GameMode会为新生的PlayerController建立一
  • RFID技术在智慧图书馆盘点系统中的优势

    RFID射频识别及技术 作为一种新兴的非接触式的自动识别技术 其基本原理是电磁理论 因其操作便捷高效 无需人工干预 可在各种恶劣环境下 通过射频信号自动识别目标并获取相关数据 可识别高速运动中的物体并可同时识别多个标签 可以远距离识别 而不
  • _fseeki64在linux下的头文件,linux c 语言之--fseek(),fseeko(),fseeko64()讲解 (转载)

    转载 http blog csdn net lemoncyb article details 16841317 fseek 函数讲解 函数定义 int fseek FILE stream long offset int fromwhere
  • 查询目标服务器系统,查看目标服务器的操作系统

    查看目标服务器的操作系统 内容精选 换一换 云硬盘挂载至云服务器时 无法挂载 以下排查思路根据原因的出现概率进行排序 建议您从高频率原因往低频率原因排查 从而帮助您快速找到问题的原因 如果解决完某个可能原因仍未解决问题 请继续排查其他可能原
  • Linux-乌班图常用命令

    Linux提供了大量的命令 利用它可以有效地完成大量的工作 如磁盘操作 文件存取 目录操作 进程管理 文件权限设定等 所以 在Linux系统上工作离不开使用系统提供的命令 要想真正理解Linux系统 就必须从Linux命令学起 通过基础的命
  • Android框架体系架构的知识,值得收藏!

    一 概述 随着业务的发展 工程的逐渐增大与开发人员增多 很多工程都走向了模块化 组件化 插件化道路 来方便大家的合作开发与降低业务之间的耦合度 现在就和大家谈谈模块化的交互问题 首先看下模块化的几个优势 模块化的优势 结构清晰 业务独立 代
  • BUUCTF Web [极客大挑战 2019]Knife

    作者主页 士别三日wyx 此文章已录入专栏 网络攻防 持续更新热门靶场的通关教程 未知攻 焉知收 在一个个孤独的夜晚 你完成了几百个攻防实验 回过头来才发现 已经击败了百分之九十九的同期选手 极客大挑战 2019 Knife 一 题目简介
  • Android Context

    1 Context概念 Context 中文直译为 上下文 小学读语文的时候我们知道 有时候理解一个句子 需要看看上下文 这里上下文有时需要看看上下临接着的几段话就可以理解他的意思 有时候呢 我们需要把整篇文章都读取一遍才能知道他的意思 一
  • 【基于Leaflet和Canvas绘图的前端大量栅格数据渲染】

    1 需求 有包含30万坐标点的json文件 每个坐标点包含经度 纬度 行值 列值 数值 现需要根据数值分级进行不同颜色的显示 并在地图的正确位置进行渲染 最终效果如下 2 环境和工具 2 1 使用Edge Chrome 实测采用Chromi