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

2024-06-20 15:52

本文主要是介绍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);
});

这篇关于leaflet,canvas渲染目标,可加载大批量数据的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1078575

相关文章

最好用的WPF加载动画功能

《最好用的WPF加载动画功能》当开发应用程序时,提供良好的用户体验(UX)是至关重要的,加载动画作为一种有效的沟通工具,它不仅能告知用户系统正在工作,还能够通过视觉上的吸引力来增强整体用户体验,本文给... 目录前言需求分析高级用法综合案例总结最后前言当开发应用程序时,提供良好的用户体验(UX)是至关重要

Oracle Expdp按条件导出指定表数据的方法实例

《OracleExpdp按条件导出指定表数据的方法实例》:本文主要介绍Oracle的expdp数据泵方式导出特定机构和时间范围的数据,并通过parfile文件进行条件限制和配置,文中通过代码介绍... 目录1.场景描述 2.方案分析3.实验验证 3.1 parfile文件3.2 expdp命令导出4.总结

更改docker默认数据目录的方法步骤

《更改docker默认数据目录的方法步骤》本文主要介绍了更改docker默认数据目录的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1.查看docker是否存在并停止该服务2.挂载镜像并安装rsync便于备份3.取消挂载备份和迁

不删数据还能合并磁盘? 让电脑C盘D盘合并并保留数据的技巧

《不删数据还能合并磁盘?让电脑C盘D盘合并并保留数据的技巧》在Windows操作系统中,合并C盘和D盘是一个相对复杂的任务,尤其是当你不希望删除其中的数据时,幸运的是,有几种方法可以实现这一目标且在... 在电脑生产时,制造商常为C盘分配较小的磁盘空间,以确保软件在运行过程中不会出现磁盘空间不足的问题。但在

如何用Java结合经纬度位置计算目标点的日出日落时间详解

《如何用Java结合经纬度位置计算目标点的日出日落时间详解》这篇文章主详细讲解了如何基于目标点的经纬度计算日出日落时间,提供了在线API和Java库两种计算方法,并通过实际案例展示了其应用,需要的朋友... 目录前言一、应用示例1、天安门升旗时间2、湖南省日出日落信息二、Java日出日落计算1、在线API2

Java如何接收并解析HL7协议数据

《Java如何接收并解析HL7协议数据》文章主要介绍了HL7协议及其在医疗行业中的应用,详细描述了如何配置环境、接收和解析数据,以及与前端进行交互的实现方法,文章还分享了使用7Edit工具进行调试的经... 目录一、前言二、正文1、环境配置2、数据接收:HL7Monitor3、数据解析:HL7Busines

Mybatis拦截器如何实现数据权限过滤

《Mybatis拦截器如何实现数据权限过滤》本文介绍了MyBatis拦截器的使用,通过实现Interceptor接口对SQL进行处理,实现数据权限过滤功能,通过在本地线程变量中存储数据权限相关信息,并... 目录背景基础知识MyBATis 拦截器介绍代码实战总结背景现在的项目负责人去年年底离职,导致前期规

Redis KEYS查询大批量数据替代方案

《RedisKEYS查询大批量数据替代方案》在使用Redis时,KEYS命令虽然简单直接,但其全表扫描的特性在处理大规模数据时会导致性能问题,甚至可能阻塞Redis服务,本文将介绍SCAN命令、有序... 目录前言KEYS命令问题背景替代方案1.使用 SCAN 命令2. 使用有序集合(Sorted Set)

SpringBoot整合Canal+RabbitMQ监听数据变更详解

《SpringBoot整合Canal+RabbitMQ监听数据变更详解》在现代分布式系统中,实时获取数据库的变更信息是一个常见的需求,本文将介绍SpringBoot如何通过整合Canal和Rabbit... 目录需求步骤环境搭建整合SpringBoot与Canal实现客户端Canal整合RabbitMQSp

MyBatis框架实现一个简单的数据查询操作

《MyBatis框架实现一个简单的数据查询操作》本文介绍了MyBatis框架下进行数据查询操作的详细步骤,括创建实体类、编写SQL标签、配置Mapper、开启驼峰命名映射以及执行SQL语句等,感兴趣的... 基于在前面几章我们已经学习了对MyBATis进行环境配置,并利用SqlSessionFactory核