leaflet流星线的实现以及参数配置
最近项目里面需要实现一个效果,在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)