gif:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js地图-leaflet</title>
<link href="https://cdn.bootcss.com/leaflet/1.3.1/leaflet.css" rel="stylesheet">
<style type="text/css">
.m-map { 486px;height: 302px;}
.leaflet-control{display: none;}
.leaflet-right{direction: none;}
</style>
</head>
<body>
<div id="map" class="m-map"></div>
<script src="https://cdn.bootcss.com/leaflet/1.3.1/leaflet.js"></script>
<script src="src.js"></script>
<script type="text/javascript">
var map = L.map('map').setView([51.505, -0.09], 1);;
var positron = L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
}).addTo(map);
L.marker([51.5, -0.09]).addTo(map)
.bindPopup('这里是伦敦')
.openPopup();
var data = [{"from":[-0.09,51.505],"to":[116.3,39.9],"labels":["伦敦","北京"],"color":"#ff3a31"}];
var migrationLayer = new L.migrationLayer({
map: map,
data: data,
pulseRadius:30,
pulseBorderWidth:3,
arcWidth:1,
arcLabel:true,
arcLabelFont:'10px sans-serif',
}
);
migrationLayer.addTo(map);
</script>
</body>
</html>
src.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.startX = startX;
this.startY = startY;
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.label = options.label;
this.font = options.font;
this.shadowBlur = options.shadowBlur;
};
A.prototype.draw = function (context) {
context.save();
context.lineWidth = this.lineWidth;
context.strokeStyle = this.strokeStyle;
context.shadowColor = this.strokeStyle;
context.shadowBlur = this.shadowBlur || 2;
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.shadowBlur = 5;
this.lineWidth = options.borderWidth;
this.r = 0;
this.factor = 2 / options.radius;
};
P.prototype.draw = function (context) {
// var vr = (this.maxRadius - this.r) * this.factor;
var vr = 0.5;
this.r += vr;
// this.shadowBlur = Math.floor(this.r);
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.shadowBlur = this.shadowBlur;
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.tailPointsCount = 50; // 拖尾点数
this.centerX = centerX;
this.centerY = centerY;
this.startAngle = startAngle;
this.endAngle = endAngle;
this.radius = radius;
this.lineWidth = options.width || 1;
this.strokeStyle = options.color || '#fff';
this.factor = 2 / this.radius;
this.deltaAngle = (80 / Math.min(this.radius, 400)) / 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: 'rgb(255, 255, 255)',
size: 3,
borderWidth: 0,
borderColor: this.strokeStyle
});
};
S.prototype.drawArc = function (context, strokeColor, lineWidth, startAngle, endAngle) {
context.save();
context.lineWidth = lineWidth;
// context.lineWidth = 5;
context.strokeStyle = strokeColor;
context.shadowColor = this.strokeStyle;
// context.shadowBlur = 5;
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,
tailLineWidth - tailLineWidth / count * i,
this.trailAngle - this.deltaAngle * i,
this.trailAngle
);
}
}
context.save();
context.translate(this.centerX, this.centerY);
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,
this.style.arc.width,
color: element.color
});
var marker = new Marker({
x: element.to[0],
y: element.to[1],
rotation: arc.endAngle + Math.PI / 2,
style: 'arrow',
color: element.color,
size: 4,
borderWidth: 0,
borderColor: element.color
});
var pulse = new Pulse({
x: element.to[0],
y: element.to[1],
radius: this.style.pulse.radius,
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],
15,
color: element.color
});
this.store.arcs.push(arc);
this.store.markers.push(marker);
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,
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
},
arc: {
this.options.arcWidth,
label: this.options.arcLabel,
font: this.options.arcLabelFont
}
} || {};
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);
//this._resize();
if (!this.migration) {
var data = this._convertData();
this.migration = new Migration({
data: data,
context: this.context,
style: this._style
});
//this._draw();
//this._bindMapEvents();
}
},
_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
}
}, this);
return data;
}
},
_bindMapEvents: function () {
var that = this;
// window.onresize = function () {
// that._resize();
// };
// this._map.on('movestart ', function () {
// that.migration.pause();
// });
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);
}
},
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)