【基于Leaflet和Canvas绘图的前端大量栅格数据渲染】
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个部分:
- 计算出渲染范围,左下角和右上角屏幕坐标
- 计算出每一个坐标的渲染起点
- 计算出每一个坐标的渲染宽高
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_contentapp.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在三维地图实现热力图
【基于Leaflet和Canvas绘图的前端大量栅格数据渲染】相关推荐
- leaflet地图原理_基于 leaflet 的标注绘图
标注功能的背景 现在我们需要做一个标注工具,在一个视频上面绘制选框,把某个区域标记下来,接下来,把我们选框的内容坐标数据存储下来,这样作为一个分析的数据源,结合视频文件去分析选框区域内的内容情况. 结 ...
- h5学习笔记之canvas绘图(1)
一.canvas标签的说明 在设置cancas大小的时候,必须在标签内写,如果在css内写会发生变化 默认宽度300px,默认高度 150px 背景颜色红色 的一个长方形 <canvas wid ...
- [转]基于Prototype,利用Canvas绘图实现的web流程图设计器(原型)
基于Prototype,利用Canvas绘图实现的web流程图设计器(原型) 关键字: javascript prototype script.aculo.us canvas 流程图 web画线 刚才 ...
- 如何使用canvas绘图
这里是修真院前端小课堂,每篇分享文从 [背景介绍][知识剖析][常见问题][解决方案][编码实战][扩展思考][更多讨论][参考文献] 八个方面深度解析前端知识/技能,本篇分享的是: [如何使用can ...
- H5 canvas 绘图
H5的canvas绘图技术 canvas元素是HTML5中新添加的一个元素,该元素是HTML5中的一个亮点.Canvas元素就像一块画布,通过该元素自带的API结合JavaScript代码可以绘制各种 ...
- Canvas绘图在微信小程序中的应用:生成个性化海报
Canvas绘图在微信小程序中的应用:生成个性化海报 如极客时间的一些实现案例: 基础语法 Canvas本质是一个可以使用脚本(通常为JavaScript)来绘制图形的 HTML 元素,默认大小为30 ...
- 前端实现可绘制的canvas画布_前端图形学基础(五)——Canvas状态管理
点击右上角的关注,不定期前端干货分享!! 欢迎来到我的前端图形学系列文章: 前端图形学基础(一)--Canvas基础入门 前端图形学基础(二)--Canvas基础 前端图形学基础(三)--Canvas ...
- Canvas绘图基础(一)
简单图形绘制 矩形:描边与填充 Canvas的API提供了三个方法,分别用于矩形的清除.描边及填充 clearRect(double x, double y, double w, double h) ...
- HTML5 学习总结(四)——canvas绘图、WebGL、SVG
目录 一.Canvas 1.1.创建canvas元素 1.2.画线 1.3.绘制矩形 1.4.绘制圆弧 1.5.绘制图像 1.6.绘制文字 1.7.随机颜色与简单动画 二.WebGL 2.1.HTML ...
最新文章
- 【生活随想】实习结束以及开始校园招聘
- P1634 禽兽的传染病
- 蓝牙管理软件_Mac蓝牙检测软件----BlueSense
- 梯度方向直方图和物体检测Histogram of Oriented Gradients and Object Detection
- Stanford UFLDL教程 Softmax回归
- 【LeetCode】LC1672. 最富有客户的资产总量
- 【最详细】数据结构(C语言版 第2版)第八章课后习题答案 严蔚敏 等 编著
- 男生的哪个“不要”是真的不要?
- 放弃深度学习?我承认是因为线性代数
- 深度学习常用性能评价指标
- SVM支持向量和逻辑回归的decision_function用法详解
- 搭建Hadoop环境(超详细)
- Office之下拉字体选项名称为英文
- 英语十大词性之四 - 副词
- mysql for mac 重置密码_Mac版,mysql重置密码
- php htmlpurifier,htmlpurifierPHP过滤库
- android 中获取logcat缓冲区信息
- 运用京东云代码托管、云编译、云部署等产品进行蓝绿部署简单实践
- gerber 文件格式 [一]
- 禾川科技科创板上市破发:大跌17% 公司市值近30亿