当前位置: 首页 > news >正文

leaflet,canvas渲染目标,可加载大批量数据

基于Leaflet-CanvasMarker: 在Canvas上绘制Marker,而不是每个marker插件一个dom节点,极大地提高了渲染效率。主要代码参考自 https://github.com/eJuke/Leaflet.Canvas-Markers,不过此插件有些Bug,github国内不方便,作者也不维护了,所以在gitee上新建一个仓库进行维护。icon-default.png?t=N7T8https://gitee.com/panzhiyue/Leaflet-CanvasMarker 修改的canvas渲染marker

参数

collisionFlg

  • 类型:boolean
  • 默认值:false

配置是否启用碰撞检测,即重叠图标只显示一个

moveReset

  • 类型:boolean
  • 默认值:false

在move时是否刷新地图

zIndex

  • 类型:number
  • 默认值:null

Leaflet.Marker对象zIndex的默认值

opacity

  • 类型:number
  • 默认值:1

图层的不透明度,0(完全透明)-1(完全不透明)

Leaflet.Marker扩展参数

zIndex

  • 类型:number

显示顺序

Leaflet.Icon扩展参数

rotate

  • 类型:number

旋转角度:Math.PI/2

方法

  • addLayer(marker):向图层添加标记。
  • addLayers(markers):向图层添加标记。
  • removeLayer(marker, redraw):从图层中删除一个标记。redraw为true时删除后直接重绘,默认为true
  • redraw() : 重绘图层
  • addOnClickListener(eventHandler):为所有标记添加通用点击侦听器
  • addOnHoverListener(eventHandler):为所有标记添加悬停监听器
  • addOnMouseDownListener(eventHandler):为所有标记添加鼠标按下监听器
  • addOnMouseUpListener(eventHandler):为所有标记添加鼠标松开监听器

使用:
/*** 加载marker 点位** */export const useLoadMarker = () => {let MapCanvasLayer = null;// 添加10W个数据点let addLargeNumberMarker = () => {console.time("加载渲染canvas目标");// 创建图层MapCanvasLayer = L.canvasMarkerLayer({collisionFlg: false,moveReset: false,userDrawFunc: function (layer, marker, pointPos, size) {// console.log("lllllllllllll", layer);const ctx = layer._ctx;ctx.beginPath();ctx.arc(pointPos.x, pointPos.y, size[0] / 2, 0, 2 * Math.PI);ctx.fillStyle = "rgba(255,12,0,0.4)";ctx.fill();ctx.closePath();},}).addTo(window.MapLeaflet);let icon = L.icon({iconUrl: new URL("../assets/marker.jpg", import.meta.url).href,iconSize: [20, 20],iconAnchor: [10, 9],});// 定义Markerlet markers = [];for (var i = 0; i < 80000; i++) {let marker = L.marker([39.26203 + Math.random() * 1.2, 122.58546 + Math.random() * 2], {icon: L.icon({iconSize: [20, 20],iconAnchor: [10, 9],}),zIndex: 2,riseOnHover: true,});marker.bindTooltip("我是浮动出来的标记" + i, {//添加提示文字permanent: false, //是永久打开还是悬停打开direction: "top", //方向}); //在图层打开markers.push(marker);}// 把marker添加到图层MapCanvasLayer.addLayers(markers);console.timeEnd("加载渲染canvas目标");//定义事件MapCanvasLayer.addOnClickListener(function (e, data) {console.log(data);});MapCanvasLayer.addOnHoverListener(function (e, data) {console.log(data[0].data);});};return {addLargeNumberMarker,};
};

修改版本v1.0,可以自定义画marker,使用canvas画marrker再添加到canvas上

import rbush from "rbush"; //https://www.5axxw.com/wiki/content/7wjc4t
/*** @typedef {Object} MarkerData marker的rubsh数据* @property {Number} MarkerData.minX  marker的经度* @property {Number} MarkerData.minY  marker的纬度* @property {Number} MarkerData.maxX  marker的经度* @property {Number} MarkerData.maxY  marker的纬度* @property {L.Marker} MarkerData.data  marker对象* @example* let latlng=marker.getLatlng();* let markerData={*      minX:latlng.lng,*      minY:latlng.lat,*      maxX:latlng.lng,*      maxY:latlng.lat,*      data:marker* }*//*** @typedef {Object} MarkerBoundsData marker的像素边界rubsh数据* @property {Number} MarkerBoundsData.minX  marker的左上角x轴像素坐标* @property {Number} MarkerBoundsData.minY  marker的左上角y轴像素坐标* @property {Number} MarkerBoundsData.maxX  marker的右下角x轴像素坐标* @property {Number} MarkerBoundsData.maxY  marker的右下角y轴像素坐标* @property {L.Marker} MarkerBoundsData.data  marker对象* @example* let options = marker.options.icon.options;* let minX, minY, maxX, maxY;* minX = pointPos.x - ((options.iconAnchor && options.iconAnchor[0]) || 0);* maxX = minX + options.iconSize[0];* minY = pointPos.y - ((options.iconAnchor && options.iconAnchor[1]) || 0);* maxY = minY + options.iconSize[1];** let markerBounds = {*     minX,*     minY,*     maxX,*     maxY* };*//*** 用于在画布而不是DOM上显示标记的leaflet插件。使用单页1.0.0及更高版本。* 已修改源码适配项目需求* 注意:开启碰撞检测 图标重叠合并 L.icon({ iconAnchor: [10, 9],}); 必须要配置iconAnchor属性*/
export var CanvasMarkerLayer = (L.CanvasMarkerLayer = L.Layer.extend({options: {zIndex: null, //图层dom元素的堆叠顺序collisionFlg: false, //碰撞检测,使用canvas画marker时 不建议开启碰撞会影响性能moveReset: false, //在move时是否刷新地图opacity: 1, //图层透明度userDrawFunc: null, //改源码:自定义canvas画图标},//Add event listeners to initialized section.initialize: function (options) {L.setOptions(this, options);this._onClickListeners = [];this._onHoverListeners = [];this._onMouseDownListeners = [];this._onMouseUpListeners = [];/*** 所有marker的集合* @type {rbush<MarkerData>}*/this._markers = new rbush();this._markers.dirty = 0; //单个插入/删除this._markers.total = 0; //总数/*** 在地图当前范围内的marker的集合* @type {rbush<MarkerData>}*/this._containMarkers = new rbush();/*** 当前显示在地图上的marker的集合* @type {rbush<MarkerData>}*/this._showMarkers = new rbush();/*** 当前显示在地图上的marker的范围集合* @type {rbush<MarkerBoundsData>}*/this._showMarkerBounds = new rbush();},setOptions: function (options) {L.setOptions(this, options);return this.redraw();},/*** 重绘*/redraw: function () {return this._redraw(true);},/*** 获取事件对象** 表示给map添加的监听器* @return {Object} 监听器/函数键值对*/getEvents: function () {var events = {viewreset: this._reset,zoom: this._onZoom,moveend: this._reset,click: this._executeListeners,mousemove: this._executeListeners,mousedown: this._executeListeners,mouseup: this._executeListeners,};if (this._zoomAnimated) {events.zoomanim = this._onAnimZoom;}if (this.options.moveReset) {events.move = this._reset;}return events;},/*** 添加标注* @param {L/Marker} layer 标注* @return {Object} this*/addLayer: function (layer, redraw = true) {if (!(layer.options.pane == "markerPane" && layer.options.icon)) {console.error("Layer isn't a marker");return;}layer._map = this._map;var latlng = layer.getLatLng();L.Util.stamp(layer);this._markers.insert({minX: latlng.lng,minY: latlng.lat,maxX: latlng.lng,maxY: latlng.lat,data: layer,});this._markers.dirty++;this._markers.total++;var isDisplaying = this._map.getBounds().contains(latlng);if (redraw == true && isDisplaying) {this._redraw(true);}return this;},/*** 添加标注数组,在一次性添加许多标注时使用此函数会比循环调用marker函数效率更高* @param {Array.<L/Marker>} layers 标注数组* @return {Object} this*/addLayers: function (layers, redraw = true) {console.time("canvas-layer渲染时间")layers.forEach((layer) => {this.addLayer(layer, false);});if (redraw) {this._redraw(true);}console.timeEnd("canvas-layer渲染时间");return this;},/*** 删除标注* @param {*} layer 标注* @param {boolean=true} redraw 是否重新绘制(默认为true),如果要批量删除可以设置为false,然后手动更新* @return {Object} this*/removeLayer: function (layer, redraw = true) {var self = this;//If we are removed point// 改掩码if (layer && layer["minX"]) layer = layer.data;var latlng = layer.getLatLng();var isDisplaying = self._map.getBounds().contains(latlng);var markerData = {minX: latlng.lng,minY: latlng.lat,maxX: latlng.lng,maxY: latlng.lat,data: layer,};self._markers.remove(markerData, function (a, b) {return a.data._leaflet_id === b.data._leaflet_id;});self._markers.total--;self._markers.dirty++;if (isDisplaying === true && redraw === true) {self._redraw(true);}return this;},/*** 清除所有*/clearLayers: function () {this._markers = new rbush();this._markers.dirty = 0; //单个插入/删除this._markers.total = 0; //总数this._containMarkers = new rbush();this._showMarkers = new rbush();this._showMarkerBounds = new rbush();this._redraw(true);},/*** 继承L.Layer必须实现的方法** 图层Dom节点创建添加到地图容器*/onAdd: function (map) {this._map = map;if (!this._container) this._initCanvas();if (this.options.pane) this.getPane().appendChild(this._container);else map._panes.overlayPane.appendChild(this._container);this._reset();},/*** 继承L.Layer必须实现的方法** 图层Dom节点销毁*/onRemove: function (map) {if (this.options.pane) this.getPane().removeChild(this._container);else map.getPanes().overlayPane.removeChild(this._container);},/*** 绘制图标* @param {L/Marker} marker 图标* @param {L/Point} pointPos 图标中心点在屏幕上的像素位置*/_drawMarker: function (marker, pointPos) {var self = this;//创建图标缓存if (!this._imageLookup) this._imageLookup = {};//没有传入像素位置,则计算marker自身的位置if (!pointPos) {pointPos = self._map.latLngToContainerPoint(marker.getLatLng());}// 改源码:添加构造方法userDrawFunc--canvas图标// S 配置是否启用碰撞检测,即重叠图标只显示一个,使用canvas画marker时 不建议开启碰撞会影响性能let options = marker.options.icon.options;let minX, minY, maxX, maxY;minX = pointPos.x - ((options.iconAnchor && options.iconAnchor[0]) || 0);maxX = minX + options.iconSize[0];minY = pointPos.y - ((options.iconAnchor && options.iconAnchor[1]) || 0);maxY = minY + options.iconSize[1];let markerBounds = {minX,minY,maxX,maxY,};if (this.options.collisionFlg == true) {if (this._showMarkerBounds.collides(markerBounds)) {return;} else {this._showMarkerBounds.insert(markerBounds);let latlng = marker.getLatLng();this._showMarkers.insert({minX,minY,maxX,maxY,lng: latlng.lng,lat: latlng.lat,data: marker,});}}// E 配置是否启用碰撞检测,即重叠图标只显示一个,使用canvas画marker时 不建议开启碰撞会影响性能// console.log("图标------", marker.options.icon.options);if (marker.options.icon.options.iconUrl && !!marker.options.icon.options.iconUrl) {// 使用icon的marker执行//图标图片地址var iconUrl = marker.options.icon.options.iconUrl;//已经有canvas_img对象,表示之前已经绘制过,直接使用,提高渲染效率if (marker.canvas_img) {self._drawImage(marker, pointPos);} else {//图标已经在缓存中if (self._imageLookup[iconUrl]) {marker.canvas_img = self._imageLookup[iconUrl][0];//图片还未加载,把marker添加到预加载列表中if (self._imageLookup[iconUrl][1] === false) {self._imageLookup[iconUrl][2].push([marker, pointPos]);} else {//图片已经加载,则直接绘制self._drawImage(marker, pointPos);}} else {//新的图片//创建图片对象var i = new Image();i.src = iconUrl;marker.canvas_img = i;//Image:图片,isLoaded:是否已经加载,[[marker,pointPos]]:预加载列表self._imageLookup[iconUrl] = [i, false, [[marker, pointPos]]];//图片加载完毕,循环预加列表,绘制图标i.onload = function () {self._imageLookup[iconUrl][1] = true;self._imageLookup[iconUrl][2].forEach(function (e) {self._drawImage(e[0], e[1]);});};}}} else if (this.options.userDrawFunc && typeof this.options.userDrawFunc === "function") {// 使用canvas-userDrawFunc的marker执行const size = marker.options.icon.options.iconSize;this.options.userDrawFunc(this, marker, pointPos, size);}},/*** 绘制图标* @param {L/Marker} marker 图标* @param {L/Point} pointPos 图标中心点在屏幕上的像素位置*/_drawImage: function (marker, pointPos) {var options = marker.options.icon.options;this._ctx.save();this._ctx.globalAlpha = this.options.opacity;this._ctx.translate(pointPos.x, pointPos.y);this._ctx.rotate(options.rotate);this._ctx.drawImage(marker.canvas_img,-((options.iconAnchor && options.iconAnchor[0]) || 0),-((options.iconAnchor && options.iconAnchor[1]) || 0),options.iconSize[0],options.iconSize[1]);this._ctx.restore();},/*** 重置画布(大小,位置,内容)*/_reset: function () {var topLeft = this._map.containerPointToLayerPoint([0, 0]);L.DomUtil.setPosition(this._container, topLeft);var size = this._map.getSize();this._container.width = size.x;this._container.height = size.y;this._update();},/*** 重绘画布* @param {boolean} clear 是否清空*/_redraw: function (clear) {console.time("canvas一次重绘时间");this._showMarkerBounds = new rbush();this._showMarkers = new rbush();var self = this;//清空画布if (clear) this._ctx.clearRect(0, 0, this._container.width, this._container.height);if (!this._map || !this._markers) return;var tmp = [];//如果单个插入/删除的数量超过总数的10%,则重建查找以提高效率if (self._markers.dirty / self._markers.total >= 0.1) {self._markers.all().forEach(function (e) {tmp.push(e);});self._markers.clear();self._markers.load(tmp);self._markers.dirty = 0;tmp = [];}//地图地理坐标边界var mapBounds = self._map.getBounds();//适用于runsh的边界对象var mapBoxCoords = {minX: mapBounds.getWest(),minY: mapBounds.getSouth(),maxX: mapBounds.getEast(),maxY: mapBounds.getNorth(),};//查询范围内的图标self._markers.search(mapBoxCoords).forEach(function (e) {//图标屏幕坐标var pointPos = self._map.latLngToContainerPoint(e.data.getLatLng());var iconSize = e.data.options.icon.options.iconSize;var adj_x = iconSize[0] / 2;var adj_y = iconSize[1] / 2;var newCoords = {minX: pointPos.x - adj_x,minY: pointPos.y - adj_y,maxX: pointPos.x + adj_x,maxY: pointPos.y + adj_y,data: e.data,pointPos: pointPos,};tmp.push(newCoords);});// console.log("说有目标---", tmp);//需要做碰撞检测则降序排序,zIndex值大的优先绘制;不需要碰撞检测则升序排序,zIndex值的的后绘制tmp.sort((layer1, layer2) => {let zIndex1 = layer1.data.options.zIndex ? layer1.data.options.zIndex : 1;let zIndex2 = layer2.data.options.zIndex ? layer2.data.options.zIndex : 1;return (-zIndex1 + zIndex2) * (this.options.collisionFlg ? 1 : -1);}).forEach((layer) => {//图标屏幕坐标var pointPos = layer.pointPos;self._drawMarker(layer.data, pointPos);});//Clear rBush & Bulk Load for performancethis._containMarkers.clear();this._containMarkers.load(tmp);if (this.options.collisionFlg != true) {this._showMarkers = this._containMarkers;}console.timeEnd("canvas一次重绘时间");return this;},/*** 初始化容器*/_initCanvas: function () {this._container = L.DomUtil.create("canvas", "leaflet-canvas-icon-layer leaflet-layer");if (this.options.zIndex) {this._container.style.zIndex = this.options.zIndex;}var size = this._map.getSize();this._container.width = size.x;this._container.height = size.y;this._ctx = this._container.getContext("2d");var animated = this._map.options.zoomAnimation && L.Browser.any3d;L.DomUtil.addClass(this._container, "leaflet-zoom-" + (animated ? "animated" : "hide"));},/*** 添加click侦听器*/addOnClickListener: function (listener) {this._onClickListeners.push(listener);},/*** 添加hover侦听器*/addOnHoverListener: function (listener) {this._onHoverListeners.push(listener);},/*** 添加mousedown侦听器*/addOnMouseDownListener: function (listener) {this._onMouseDownListeners.push(listener);},/*** 添加mouseup侦听器*/addOnMouseUpListener: function (listener) {this._onMouseUpListeners.push(listener);},/*** 执行侦听器*/_executeListeners: function (event) {if (!this._showMarkers) return;var me = this;var x = event.containerPoint.x;var y = event.containerPoint.y;if (me._openToolTip) {me._openToolTip.closeTooltip();delete me._openToolTip;}var ret = this._showMarkers.search({minX: x,minY: y,maxX: x,maxY: y,});if (ret && ret.length > 0) {me._map._container.style.cursor = "pointer";if (event.type === "click") {var hasPopup = ret[0].data.getPopup();if (hasPopup) ret[0].data.openPopup();me._onClickListeners.forEach(function (listener) {listener(event, ret);});}if (event.type === "mousemove") {var hasTooltip = ret[0].data.getTooltip();if (hasTooltip) {me._openToolTip = ret[0].data;ret[0].data.openTooltip();}me._onHoverListeners.forEach(function (listener) {listener(event, ret);});}if (event.type === "mousedown") {me._onMouseDownListeners.forEach(function (listener) {listener(event, ret);});}if (event.type === "mouseup") {me._onMouseUpListeners.forEach(function (listener) {listener(event, ret);});}} else {me._map._container.style.cursor = "";}},/*** 地图Zoomanim事件监听器函数* @param {Object} env {center:L.LatLng,zoom:number}格式的对象*/_onAnimZoom(ev) {this._updateTransform(ev.center, ev.zoom);},/*** 地图修改zoom事件监听器函数*/_onZoom: function () {this._updateTransform(this._map.getCenter(), this._map.getZoom());},/*** 修改dom原始的transform或position* @param {L/LatLng} center 中心点* @param {number} zoom 地图缩放级别*/_updateTransform: function (center, zoom) {var scale = this._map.getZoomScale(zoom, this._zoom),position = L.DomUtil.getPosition(this._container),viewHalf = this._map.getSize().multiplyBy(0.5),currentCenterPoint = this._map.project(this._center, zoom),destCenterPoint = this._map.project(center, zoom),centerOffset = destCenterPoint.subtract(currentCenterPoint),topLeftOffset = viewHalf.multiplyBy(-scale).add(position).add(viewHalf).subtract(centerOffset);if (L.Browser.any3d) {L.DomUtil.setTransform(this._container, topLeftOffset, scale);} else {L.DomUtil.setPosition(this._container, topLeftOffset);}},/*** 更新渲染器容器的像素边界(用于以后的定位/大小/剪裁)子类负责触发“update”事件。*/_update: function () {var p = 0,size = this._map.getSize(),min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round();this._bounds = new L.Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round());this._center = this._map.getCenter();this._zoom = this._map.getZoom();this._redraw();},/*** 设置图层透明度* @param {Number} opacity 图层透明度*/setOpacity(opacity) {this.options.opacity = opacity;return this._redraw(true);},
}));export var canvasMarkerLayer = (L.canvasMarkerLayer = function (options) {return new L.CanvasMarkerLayer(options);
});

相关文章:

  • 配电室数据中心巡检3d可视化搭建的详细步骤
  • OCC介绍及框架分析
  • 在vue和uniapp中使用 websocket并封装js
  • Android GLES渲染——渲染回读
  • MFC序列号输入框
  • 一套轻量、安全的问卷系统基座,提供面向个人和企业的一站式产品级解决方案
  • K-Means 算法详解
  • 游戏中的寻路算法研究
  • 解决内核模块加载使用-f参数无法加载的问题
  • 为什么要学Java?
  • Linux驱动开发(二)--字符设备驱动开发提升 LED驱动开发实验
  • 18个机器学习核心算法模型总结
  • 2025计算机毕业设计选题题目推荐-毕设题目汇总大全
  • 智慧校园综合管理系统:打造高效智慧的学校管理平台
  • 契约锁电子签章平台 add 远程命令执行漏洞复现(XVE-2023-23720)
  • 【159天】尚学堂高琪Java300集视频精华笔记(128)
  • 【399天】跃迁之路——程序员高效学习方法论探索系列(实验阶段156-2018.03.11)...
  • 【node学习】协程
  • 【从零开始安装kubernetes-1.7.3】2.flannel、docker以及Harbor的配置以及作用
  • Android优雅地处理按钮重复点击
  • CentOS从零开始部署Nodejs项目
  • echarts的各种常用效果展示
  • Java 网络编程(2):UDP 的使用
  • Java知识点总结(JavaIO-打印流)
  • php中curl和soap方式请求服务超时问题
  • TypeScript迭代器
  • use Google search engine
  • uva 10370 Above Average
  • Yii源码解读-服务定位器(Service Locator)
  • 闭包--闭包作用之保存(一)
  • 对JS继承的一点思考
  • 基于axios的vue插件,让http请求更简单
  • 免费小说阅读小程序
  • 让你的分享飞起来——极光推出社会化分享组件
  • 如何在GitHub上创建个人博客
  • 视频flv转mp4最快的几种方法(就是不用格式工厂)
  • 第二十章:异步和文件I/O.(二十三)
  • 蚂蚁金服CTO程立:真正的技术革命才刚刚开始
  • ​ ​Redis(五)主从复制:主从模式介绍、配置、拓扑(一主一从结构、一主多从结构、树形主从结构)、原理(复制过程、​​​​​​​数据同步psync)、总结
  • # 数论-逆元
  • #我与Java虚拟机的故事#连载08:书读百遍其义自见
  • ( 10 )MySQL中的外键
  • (12)Hive调优——count distinct去重优化
  • (2)leetcode 234.回文链表 141.环形链表
  • (20)目标检测算法之YOLOv5计算预选框、详解anchor计算
  • (52)只出现一次的数字III
  • (8)Linux使用C语言读取proc/stat等cpu使用数据
  • (env: Windows,mp,1.06.2308310; lib: 3.2.4) uniapp微信小程序
  • (zhuan) 一些RL的文献(及笔记)
  • (附源码)计算机毕业设计ssm基于Internet快递柜管理系统
  • (十)Flink Table API 和 SQL 基本概念
  • (学习日记)2024.03.25:UCOSIII第二十二节:系统启动流程详解
  • (一) storm的集群安装与配置
  • (转)原始图像数据和PDF中的图像数据
  • (最优化理论与方法)第二章最优化所需基础知识-第三节:重要凸集举例