leaflet流星线的实现以及参数配置

发布时间 2023-12-19 17:14:37作者: 天涯何处归一

最近项目里面需要实现一个效果,在leaflet里面实现流星线,查了很多资料都没有我想要的,最后找到一个流动线(具体连接已经找不到了)在此基础上改动实现了该效果!

html文件

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Leaflet 流星线</title>
    <link rel="stylesheet" href="./leaflet/leaflet.css">
    <style>
        html,
        body {
            height: 100%;
            margin: 0;
        }

        .leaflet-container {
            height: 100vh;
            width: 100;
            max-width: 100%;
            max-height: 100%;
        }
        .my-div-icon {
            background-color: rgb(10, 214, 214);
            border-radius: 50%;
            width: 10px;
            height: 10px;
        }
    </style>
</head>

<body>
    <div id="map"></div>
    <script src="/leaflet/leaflet.js"></script>
    <script src="/leaflet/migrationLayer.js"></script>

    <script>
        var url = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
        var tilemap = new L.TileLayer(url);
        var mapcenter = new L.latLng(39.5, 116.89);
        var map = new L.Map("map", {    //"map"为需要插入地图的div的id
            CRS: 'Simple',   
            center: mapcenter,   
            zoom: 6,           
            layers: [tilemap],  
            minZoom: 0,         
            maxZoom: 16,        
            opacity: 0.8,       
        });
            const getRandomCoordinates = () => {
                var latitude = Math.random() * 45; // 纬度范围 -90 到 90
                var longitude = Math.random() * 150; // 经度范围 -180 到 180
                return [longitude, latitude];
            }
            const getRandomHexColor = () => {
                var letters = '0123456789ABCDEF';
                var color = '#';
                for (var i = 0; i < 6; i++) {
                    color += letters[Math.floor(Math.random() * 16)];
                }
                return color;
            }

            var data = []
                for (let i = 0; i < 10; i++) {
                    data.push({
                        color: getRandomHexColor(),  //流星颜色
                        from: getRandomCoordinates(),
                        to: getRandomCoordinates(),
               label: [A,B], beArrow: false, // 是否展示箭头(牵引箭头和结束箭头) bePulse: true,// 是否展示 扩散圆(结束点的圆) arrowSize: 0.1, // 箭头大小 }) } var migrationLayer = new L.migrationLayer({ map: map, data: data, pulseRadius:5, pulseColor: '#f00f00', pulseBorderWidth:1, starWidth: 4, //流星宽度 arcWidth: 2,// 弧线宽度 arcLabel:false, // 是否展示label arcAlpha: 0.5,//弧线透明度 arcStrokeColor: '#f00f00', // 弧线颜色 arcLabelFont:'10px sans-serif', } ); migrationLayer.addTo(map); migrationLayer.onClick((data) => { // 监听点击弧线事件 console.log('当前点击的弧线是', data) }) </script> </body> </html>

  

其次就是 migrationLayer.js 的代码实现:
(function (window) {
    var utils = {
        // color:rgb或rgba格式
        // opacity: 透明度
        calculateColor: function (color, opacity) {
            if (color.indexOf('#') === 0) {
                var color16 = color.slice(1);
                var r = parseInt(color16.slice(0, 2), 16);
                var g = parseInt(color16.slice(2, 4), 16);
                var b = parseInt(color16.slice(4), 16);
                return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
            } else if (/^rgb\(/.test(color)) {
                return color.replace(/rgb/, 'rgba').replace(')', ",") +
                    opacity + ')';
            } else {
                return color.split(',').splice(0, 3).join(',') +
                    opacity + ')';
            }
        }
    };
    var arrayUtils = {
        forEach: function (arr, cb, scope) {
            if (typeof Array.prototype.forEach === 'function') {
                arr.forEach(cb, scope);
            } else {
                for (var i = 0, len = arr.length; i < len; i++) {
                    cb.apply(scope, [arr[i], i, arr]);
                }
            }
        },
        map: function (arr, cb, scope) {
            if (typeof Array.prototype.map === 'function') {
                return arr.map(cb, scope);
            } else {
                var mapped = [];
                for (var i = 0, len = arr.length; i < len; i++) {
                    mapped[i] = cb.apply(scope, [arr[i], i, arr]);
                }
                return mapped;
            }
        }
    };

    var Marker = (function () {
        var M = function (options) {
            this.x = options.x;
            this.y = options.y;
            this.rotation = options.rotation;
            this.style = options.style;
            this.color = options.color;
            this.size = options.size;
            this.borderWidth = options.borderWidth;
            this.borderColor = options.borderColor;
        };

        M.prototype.draw = function (context) {
            context.save();
            context.translate(this.x, this.y);
            context.rotate(this.rotation);

            context.lineWidth = this.borderWidth || 0;
            context.strokeStyle = this.borderColor || '#000';
            context.fillStyle = this.color || '#000';
            context.beginPath();
            if (this.style === 'circle') {
                context.arc(0, 0, this.size, 0, Math.PI * 2, false);
            } else if (this.style === 'arrow') {
                context.moveTo(-this.size, -this.size);
                context.lineTo(this.size, 0);
                context.lineTo(-this.size, this.size);
                context.lineTo(-this.size / 4, 0);
                context.lineTo(-this.size, -this.size);
            }
            context.closePath();
            context.stroke();
            context.fill();
            context.restore();
        };

        return M;
    })();

    var Arc = (function () {
        var A = function (options) {
            var startX = options.startX,
                startY = options.startY,
                endX = options.endX,
                endY = options.endY;
            //两点之间的圆有多个,通过两点及半径便可以定出两个圆,根据需要选取其中一个圆
            var L = Math.sqrt(Math.pow(startX - endX, 2) + Math.pow(startY - endY, 2));
            var m = (startX + endX) / 2; // 横轴中点
            var n = (startY + endY) / 2; // 纵轴中点
            var factor = 1.5;

            var centerX = (startY - endY) * factor + m;
            var centerY = (endX - startX) * factor + n;

            var radius = Math.sqrt(Math.pow(L / 2, 2) + Math.pow(L * factor, 2));
            var startAngle = Math.atan2(startY - centerY, startX - centerX);
            var endAngle = Math.atan2(endY - centerY, endX - centerX);

            // this.L = L;
            this.x = centerX;
            this.y = centerY;
            this.startX = centerX;
            this.startY = centerY;
            this.endX = endX;
            this.endY = endY;
            this.centerX = centerX;
            this.centerY = centerY;
            this.startAngle = startAngle;
            this.endAngle = endAngle;
            this.startLabel = options && options.labels && options.labels[0],
            this.endLabel = options && options.labels && options.labels[1],
            this.radius = radius;
            this.lineWidth = options.width || 1;
            this.strokeStyle = options.color || '#000';
            this.arcAlpha = options.arcAlpha || .2;
            this.arcStrokeColor = options.arcStrokeColor || '#000';
            this.label = options.label;
            this.font = options.font;
        };

        A.prototype.draw = function (context) {
            context.save();
            context.lineWidth = this.lineWidth;
            context.strokeStyle = this.strokeStyle;
            context.globalAlpha  = this.arcAlpha;
            context.arcStrokeColor  = this.arcStrokeColor;
            context.shadowColor = this.strokeStyle;
            // context.shadowBlur = this.shadowBlur || 0;

            context.beginPath();
            context.arc(this.centerX, this.centerY, this.radius, this.startAngle, this.endAngle, false);
            context.stroke();
            context.restore();

            context.save();
            context.fillStyle = this.strokeStyle;
            if (this.label) {
                context.font = this.font;
                if (this.startLabel) {
                    var x = this.startX - 15
                    var y = this.startY + 5
                    context.fillText(this.startLabel, x, y);
                }
                if (this.endLabel) {
                    var x = this.endX - 15;
                    var y = this.endY - 5;
                    context.fillText(this.endLabel, x, y);
                }
            }
            context.restore();
        };

        return A;
    })();

    var Pulse = (function () {
        function P(options) {
            this.x = options.x;
            this.y = options.y;
            this.maxRadius = options.radius;
            this.color = options.color;
            this.lineWidth = options.borderWidth;
            this.r = 0;
            this.factor = 2 / options.radius;
        };

        P.prototype.draw = function (context) {
            var vr = 0.5;
            this.r += vr;
            context.save();
            context.translate(this.x, this.y);
            var strokeColor = this.color;
            strokeColor = utils.calculateColor(strokeColor, 1 - this.r / this.maxRadius);
            context.strokeStyle = strokeColor;
            context.shadowColor = strokeColor;
            context.lineWidth = this.lineWidth;
            context.beginPath();
            context.arc(0, 0, this.r, 0, Math.PI * 2, false);
            context.stroke();
            context.restore();

            if (Math.abs(this.maxRadius - this.r) < 0.8) {
                this.r = 0;
            }
        }

        return P;
    })();

    var Spark = (function () {
        var S = function (options) {
            var startX = options.startX,
                startY = options.startY,
                endX = options.endX,
                endY = options.endY;

            //两点之间的圆有多个,通过两点及半径便可以定出两个圆,根据需要选取其中一个圆
            var L = Math.sqrt(Math.pow(startX - endX, 2) + Math.pow(startY - endY, 2));
            var m = (startX + endX) / 2; // 横轴中点
            var n = (startY + endY) / 2; // 纵轴中点
            var factor = 1.5;

            var centerX = (startY - endY) * factor + m;
            var centerY = (endX - startX) * factor + n;

            var radius = Math.sqrt(Math.pow(L / 2, 2) + Math.pow(L * factor, 2));
            var startAngle = Math.atan2(startY - centerY, startX - centerX);
            var endAngle = Math.atan2(endY - centerY, endX - centerX);

            // 保证Spark的弧度不超过Math.PI
            if (startAngle * endAngle < 0) {
                if (startAngle < 0) {
                    startAngle += Math.PI * 2;
                    endAngle += Math.PI * 2;
                } else {
                    endAngle += Math.PI * 2;
                }
            }
            this.beArrow = options.beArrow;
            this.arrowSize = options.arrowSize || 3;
            this.tailPointsCount = 20; // 拖尾点数
            this.centerX = centerX;
            this.centerY = centerY;
            this.startAngle = startAngle;
            this.endAngle = endAngle;
            this.radius = radius;
            this.lineWidth = options.width || 0;
            this.starWidth = options.starWidth;
            this.strokeStyle = options.color || '#fff';
            this.factor = 2 / this.radius;
            this.deltaAngle = (80 / Math.min(this.radius, 10000)) / this.tailPointsCount;
            this.trailAngle = this.startAngle;
            this.arcAngle = this.startAngle;

            this.animateBlur = true;

            this.marker = new Marker({
                x: 50,
                y: 80,
                rotation: 50 * Math.PI / 180,
                style: 'arrow',
                color:  this.strokeStyle,
                size: this.arrowSize,
                borderWidth: 5,
                borderColor: this.strokeStyle
            });
        };

        S.prototype.drawArc = function (context, strokeColor, lineWidth, startAngle, endAngle) {
            context.save();
            context.lineWidth = lineWidth;  // 流星宽度
            context.strokeStyle = strokeColor;
            context.shadowColor = this.strokeStyle;
            context.lineCap = "round";
            context.beginPath();
            context.arc(this.centerX, this.centerY, this.radius, startAngle, endAngle, false);
            context.stroke();
            context.restore();
        };

        S.prototype.draw = function (context) {
            var endAngle = this.endAngle;
            // 匀速
            var angle = this.trailAngle + this.factor;
            var strokeColor = this.strokeStyle;
            if (this.animateBlur) {
                this.arcAngle = angle;
            }
            this.trailAngle = angle;
            strokeColor = utils.calculateColor(strokeColor, 0.1);

            //拖尾阴影
            // this.drawArc(context, strokeColor, this.lineWidth, this.startAngle, this.arcAngle);

            // 拖尾效果
            var count = this.tailPointsCount;
            for (var i = 0; i < count; i++) {
                var arcColor = utils.calculateColor(this.strokeStyle, 0.3 - 0.3 / count * i);
                var tailLineWidth = 5;
                if (this.trailAngle - this.deltaAngle * i > this.startAngle) {
                    this.drawArc(context, arcColor,
                        this.starWidth,
                        this.trailAngle - this.deltaAngle * i,
                        this.trailAngle
                    );
                }
            }

            context.save();
            context.translate(this.centerX, this.centerY);
            if( this.beArrow ){
                this.marker.x = Math.cos(this.trailAngle) * this.radius;
                this.marker.y = Math.sin(this.trailAngle) * this.radius;
                this.marker.rotation = this.trailAngle + Math.PI / 2;
                this.marker.draw(context);
            }
            context.restore();

            if ((endAngle - this.trailAngle) * 180 / Math.PI < 0.5) {
                this.trailAngle = this.startAngle;
                this.animateBlur = false;
            }
        };

        return S;
    })();


    var Migration = (function () {
        var M = function (options) {
            this.data = options.data;
            this.store = {
                arcs: [],
                markers: [],
                pulses: [],
                sparks: []
            };
            this.playAnimation = true;
            this.started = false;
            this.context = options.context;
            this.style = options.style;
            this.init();
        };

        M.prototype.init = function () {
            this.updateData(this.data);
        };
        /*
         * Shape 必须拥有draw方法
        */
        M.prototype.add = function (Shape) {

        };
        M.prototype.remove = function () {

        };
        M.prototype.clear = function () {
            this.store = {
                arcs: [],
                markers: [],
                pulses: [],
                sparks: []
            };
            // 更新状态
            this.playAnimation = true;
            this.started = false;
            // 清除绘画实例,如果没有这个方法,多次调用start,相当于存在多个动画队列同时进行
            window.cancelAnimationFrame(this.requestAnimationId);
        };
        /*
         * 更新数据
        */
        M.prototype.updateData = function (data) {
            if (!data || data.length === 0) {
                return;
            }
            this.clear();
            this.data = data;
            if (this.data && this.data.length > 0) {
                arrayUtils.forEach(this.data, function (element) {
                    var arc = new Arc({
                        startX: element.from[0],
                        startY: element.from[1],
                        endX: element.to[0],
                        endY: element.to[1],
                        labels: element.labels,
                        label: this.style.arc.label,
                        font: this.style.arc.font,
                        width: this.style.arc.width,
                        arcAlpha: this.style.arc.arcAlpha,
                        color: this.style.arc.arcStrokeColor,
                    });
                    var marker = new Marker({
                        x: element.to[0],
                        y: element.to[1],
                        rotation: arc.endAngle + Math.PI / 2,
                        style: 'arrow',
                        color: this.style.arc.arcStrokeColor,
                        size: 6,
                        borderWidth: 0,
                        borderColor: this.style.arc.arcStrokeColor,
                    });
                    var pulse = new Pulse({
                        x: element.to[0],
                        y: element.to[1],
                        radius: this.style.pulse.radius,
                        color: this.style.pulse.color || element.color,
                        borderWidth: this.style.pulse.borderWidth
                    });
                    var spark = new Spark({
                        startX: element.from[0],
                        startY: element.from[1],
                        endX: element.to[0],
                        endY: element.to[1],
                        width: 5,
                        starWidth: this.style.star.width,
                        color: element.color,
                        beArrow: element.beArrow,
                        arrowSize: element.arrowSize,
                    });

                    this.store.arcs.push(arc);
                    element.beArrow && this.store.markers.push(marker);  // 结束点箭头
                    element.bePulse && this.store.pulses.push(pulse);  // 结束闪烁点
                    this.store.sparks.push(spark);
                }, this);
            }
        };
        /*
        */
        M.prototype.start = function (canvas) {
            var that = this;
            if (!this.started) {
                (function drawFrame() {
                    that.requestAnimationId = window.requestAnimationFrame(drawFrame, canvas);
                    if (that.playAnimation) {
                        canvas.width += 1;
                        canvas.width -= 1;
                        for (var p in that.store) {
                            var shapes = that.store[p];
                            for (var i = 0, len = shapes.length; i < len; i++) {
                                shapes[i].draw(that.context);
                            }
                        }
                    }
                })();
                this.started = true;
            }
        };
        M.prototype.play = function () {
            this.playAnimation = true;
        };
        M.prototype.pause = function () {
            this.playAnimation = false;
        };
        return M;
    })();

    L.MigrationLayer = L.Class.extend({
        options: {
            map: {},
            data: {},
            pulseRadius: 25,
            pulseBorderWidth: 3,
            arcWidth: 1,
            arcLabel: true,
            arcAlpha: 1,
            arcLabelFont: '15px sans-serif',
            Marker: {},
            Spark: {}

        },
        _setOptions: function (obj, options) {
            if (!obj.hasOwnProperty('options')) {
                obj.options = obj.options ? L.Util.create(obj.options) : {};
            }
            for (var i in options) {
                obj.options[i] = options[i];
            }
            return obj.options;
        },
        initialize: function (options) {
            this._setOptions(this, options);
            this._map = this.options.map || {};
            this._data = this.options.data || {};
            this._style = {
                pulse: {
                    radius: this.options.pulseRadius,
                    borderWidth: this.options.pulseBorderWidth,
                    color: this.options.pulseColor,
                },
                arc: {
                    width: this.options.arcWidth,
                    label: this.options.arcLabel,
                    font: this.options.arcLabelFont,
                    arcAlpha: this.options.arcAlpha,
                    arcStrokeColor: this.options.arcStrokeColor,
                },
                star:{
                    width:  this.options.starWidth,
                },
            } || {};
            this._show = true;
            this._init();
        },
        _init: function () {
            var container = L.DomUtil.create('div', 'leaflet-ODLayer-container');
            container.style.position = 'absolute';
            container.style.width = this._map.getSize().x + "px";
            container.style.height = this._map.getSize().y + "px";

            this.container = container;

            this.canvas = document.createElement('canvas');
            this.context = this.canvas.getContext('2d');
            container.appendChild(this.canvas);
            this._map.getPanes().overlayPane.appendChild(container);
            if (!this.migration) {
                var data = this._convertData();
                this.migration = new Migration({
                    data: data,
                    context: this.context,
                    style: this._style
                });
            }
        },
        _resize: function () {
            var bounds = this._map.getBounds();
            var topleft = bounds.getNorthWest();
            var topLeftscreen = this._map.latLngToContainerPoint(topleft);
            //当地图缩放或者平移到整个地图的范围小于外层容器的范围的时候,要对this.container进行上下平移操作,反之则回归原位
            if (topLeftscreen.y > 0) {
                this.container.style.top = -topLeftscreen.y + 'px';
            } else {
                this.container.style.top = '0px';
            }

            var containerStyle = window.getComputedStyle(this._map.getContainer());
            this.canvas.setAttribute('width', parseInt(containerStyle.width, 10));
            this.canvas.setAttribute('height', parseInt(containerStyle.height, 10));
        },
        _convertData: function () {
            var bounds = this._map.getBounds();

            if (this._data && bounds) {
                var data = arrayUtils.map(this._data, function (d) {

                    var fromPixel = this._map.latLngToContainerPoint(new L.LatLng(d.from[1], d.from[0]));
                    var toPixel = this._map.latLngToContainerPoint(new L.LatLng(d.to[1], d.to[0]));
                    return {
                        from: [fromPixel.x, fromPixel.y],
                        to: [toPixel.x, toPixel.y],
                        labels: d.labels,
                        value: d.value,
                        color: d.color,
                        beArrow: d.beArrow,
                        arrowSize: d.arrowSize,
                        bePulse: d.bePulse,
                    }
                }, this);

                return data;
            }
        },
        _bindMapEvents: function () {
            var that = this;
            this._map.on('moveend', function () {
                that.migration.play();
                that._draw();
            });
            this._map.on('zoomstart ', function () { that.container.style.display = 'none' });
            this._map.on('zoomend', function () {
                if (that._show) {
                    that.container.style.display = ''
                    that._draw();
                }
            });
        },
        _draw: function () {
            var bounds = this._map.getBounds();
            if (bounds && this.migration.playAnimation) {
                this._resize();
                this._transform();
                var data = this._convertData();
                this.migration.updateData(data);
                this.migration.start(this.canvas);
            }
        },
        _transform: function () {
            var bounds = this._map.getBounds();
            var topLeft = this._map.latLngToLayerPoint(bounds.getNorthWest());
            L.DomUtil.setPosition(this.container, topLeft);
        },
        addTo: function () {
            this._bindMapEvents();
            var bounds = this._map.getBounds();
            if (bounds && this.migration.playAnimation) {
                this._resize();
                this._transform();

                var data = this._convertData();
                this.migration.updateData(data);
                this.migration.start(this.canvas);
            }
        },
        onClick: function(callBack){
            const isPointInArc = (x, y,arc) => {
                this.context.beginPath();
                this.context.lineWidth = (arc.lineWidth>6)?arc.lineWidth:12;  // 值越大越好触发点击事件(方便选中线)
                this.context.arc(arc.centerX, arc.centerY, arc.radius, arc.startAngle, arc.endAngle, false);
                return this.context.isPointInStroke(x, y);
            }
            let that = this;
            this.canvas.addEventListener('click', function(event) {
                var rect = that.canvas.getBoundingClientRect();
                var x = event.clientX - rect.left;
                var y = event.clientY - rect.top;
                that.migration.store.arcs.forEach((circle, index) => {
                    if (isPointInArc(x, y, circle)) {
                        console.log('Arc clicked: ' + index);
                        callBack(circle);
                    }
                });
            })
        },
        setData: function (data) {
            this._data = data;
            this._draw();
        },
        hide: function () {
            this.container.style.display = 'none';
            this._show = false;
        },
        show: function () {
            this.container.style.display = '';
            this._show = true;
        },
        play: function () {
            this.migration.play();
        },
        pause: function () {
            this.migration.pause();
        },
        destroy: function () {
            this.migration.clear();
            //移除dom
            this.container.parentNode.removeChild(this.container);
            //移除事件监听
            this._map.clearAllEventListeners();
            this.mapHandles = [];
        }

    });
    L.migrationLayer = function (options) {
        return new L.MigrationLayer(options)
    }
})(window)