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

基于Leaflet-CanvasMarker: 在Canvas上绘制Marker,而不是每个marker插件一个dom节点,极大地提高了渲染效率。主要代码参考自 https://github.com/eJuke/Leaflet.Canvas-Markers,不过此插件有些Bug,github国内不方便,作者也不维护了,所以在gitee上新建一个仓库进行维护。https://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):为所有标记添加鼠标松开监听器
使用:
javascript 复制代码
/**
 * 加载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],
        });

        // 定义Marker
        let 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上

javascript 复制代码
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 performance
        this._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);
});
相关推荐
羊子雄起4 天前
leaflet矢量瓦片vetorgrid显示聚合和图标裁剪显示不全的问题
javascript·leaflet·vectorgrid·显示不全
ikgade23 天前
Leaflet 接入天地图服务
javascript·html·leaflet·天地图
Q行天下2 个月前
leaflet加载GeoServer的WMS地图服务.md
wms·geoserver·leaflet
Modify_QmQ2 个月前
leaflet【十】实时增加轨迹点轨迹回放效果实现
vue3·leaflet·轨迹回放·实时增加轨迹
小纯洁w4 个月前
Uniapp 使用 Leaflet
uni-app·leaflet
Modify_QmQ4 个月前
Leaflet【六】绘制交互图形、测量、经纬度展示
gis·vue3·leaflet·地图交互·距离测量、面积测量
哪有人敲代码不戴头盔的6 个月前
leaflet加载wms服务实现wms交互
leaflet
Sail_Again6 个月前
leaflet知识点:地图窗格panes的应用
leaflet
曾经的你d9 个月前
【vue+leaflet】vue项目中使用leaflet绘制室内平面图、leaflet.pm在平面图中绘制点、线、面图层(一)
vue.js·leaflet·leaflet.pm