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);
});
相关推荐
Modify_QmQ1 天前
leaflet【十一】地图瓦片路径可视化
gis·vue3·leaflet·leafletmapblock
kongxx2 个月前
在Angular中使用Leaflet构建地图应用
leaflet
Jinuss2 个月前
源码分析之Leaflet中Marker
前端·leaflet
Jinuss2 个月前
源码分析之Leaflet中的LayerGroup
前端·leaflet
Jinuss3 个月前
源码分析之Leaflet中control模块Zoom类实现原理
前端·leaflet
Jinuss3 个月前
源码分析之Leaflet图层控制控件Control.Layers实现原理
源码·leaflet
Jinuss3 个月前
源码分析之Leaflet核心模块core中的Util工具方法
前端·leaflet
小金子J4 个月前
实现 Leaflet 多类型点位标记与聚合功能的实战经验分享
前端开发·leaflet·地理信息系统(gis)·地图聚合·地图标记
diygwcom5 个月前
leaflet手绘地图实现原理-可视化工具设计手绘地图
leaflet·手绘地图·自定义地图瓦片
duansamve7 个月前
WebGIS地图框架有哪些?
javascript·gis·openlayers·cesium·mapbox·leaflet·webgis