概述
历史轨迹回放是GIS很常见的一个功能,本文结合turf.js
实现轨迹的展示与播放动画。
效果
实现功能
- 轨迹的展示;
- 轨迹的方向的箭头展示;
- 随着轨迹播放的小车,并调整方向与轨迹方向一致;
- 已播放路径的展示;
- 多条轨迹线的同时播放展示;
实现
代码语言:javascript复制const icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAMAAAC13D jAAADAFBMVEWs1eL/0T7 zTv lwD81FfXqSZNNA/84nXSixXJt5kjS2z/uCn8rBVxz yn09/mtymmvMavxrX9sxn/zTLnxmrJ9v/ uzNarMOHqsb1vGb9ynuDwdK7ycn7t0r85WpMpLuHrLn/4kDFmiRUaYL9xDT/0THlgAH01XH9rSz/1ULHqWP723N3ucv 2D/7wWeyoVTbs1S02uWP1OzIp1OidDBNjKe46vyYtKjit2n/zSzmmzncvWlzTAf/3UFrtMgwg6 4rJb ogGtkVr/swiSxtr/5j76xG7VsVOQrKxXXFnx0Wimwuola5T84Fztx5fglxw5PDPftVBXu9r9v0f/rgmWn5a3l1adbhP/0Tmftcbltljqu4WDdl3w6eGs5fmepJj2qSupiD2wvLN7pbXTtF//98f4pC321m rwsT63Wv9 vZXam/Cq4zFsIz/2zVxe3X xC1rwtqgdhvomjDtwXr/qwL/0mS/pGH31GPtvHn/0y3m07vByZXUqVf/0y smGv/80X900r/1D293OY1JgrnqVnx49LM5Ov5uFT/0Cr0oi/69vP9/PpkWCxrmbr/1znz1XjTrk7YtVt1udPdul 64u6bn5D/5i4aJSj/z3i4hSfm/v/322ry///vnir/0CzbjSTVrWSw2d328ej933r2v2j/1C//0C/kvjC/kkGukkCBmJeu2OK spbZ6vDT/f hutbCfgrz3HXxrTPFlkvtkATzhwDnp03RzLvvs0d3kZ6SsKj/yh6MsrmRqbD/2Y7zqSL/5n3/ryb/1CvlwmOOu8D5wlpoodNqvtz/ VCoiE1ZveCgilVovfO/j0SZj3KEm7VRscpWXmjOq1JNZFzv0njxxST3zSj/wCT/zSLekzDPy8T/yWv 4E56gXSjo5KrsJW4qI6jz9/swpDxu275zJf33LiWwszqy6Hx06zz0nD/pwr/7UsKDhADHDHrw00 FQD9yDcROFiXbSrtz2ysjizmsWuzkiW7lzRghaZkkKRgZ1dwm7322Wdgi4X/// RNRZLAAABAHRSTlP///8AU/cHJQAAAAlwSFlzAAAOwwAADsMBx2 oZAAAAB1pVFh0U29mdHdhcmUAAAAAAEFkb2JlIEltYWdlUmVhZHkGrQKXAAADD2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMTQgNzkuMTUxNDgxLCAyMDEzLzAzLzEzLTEyOjA5OjE1ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2NUZGMDA3MTYwREExMUVDOURGOEIxQTMwNjhFRTA1OCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2NUZGMDA3MDYwREExMUVDOURGOEIxQTMwNjhFRTA1OCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgV2luZG93cyI IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSIxREI5NjEzNURBODA0MkYzMzk5NDA4NzZFN0YzQkM2RCIgc3RSZWY6ZG9jdW1lbnRJRD0iMURCOTYxMzVEQTgwNDJGMzM5OTQwODc2RTdGM0JDNkQiLz4gPC9yZGY6RGVzY3JpcHRpb24 IDwvcmRmOlJERj4gPC94OnhtcG1ldGE IDw/eHBhY2tldCBlbmQ9InIiPz6ffRDqAAADEklEQVQ4jWP4jx/clstCF2L4/z/rxZMSKT09PckylqV6ICB5Z2bwsaVA4CZ5J4r//ZOZM0sqS0AgunoxWEvb1lLn0ln15eXlNqe0gGS5lkWijdZCIKi1eFI7q4Vt68aNN4SEhHh4XjtKRb8Eaune nrlOZ5ZvxQUFGZkH00GAQHPtwICkZGRGySUL3l63vP4M4PflgkIPnKLpbY 89Q dpfouYai/vEie4ik4DEJPdJKSnWl42NjbVYe3fJNMeZma3nCnf9 PEjE5Omqm6jWhZDcciWDMX L1fVweCV9hLtgweXpKunp6dP6NdfP3cqHwOfqLJNAyMj00cVlY8qeY5 DPNe18uoRqgergKDZVWdVZadnctA7MPTE71nr FjYBD9yebaxMioqanJVMCy8xtDF3PoGuEIRiwgwvXrhtmGDHx8ZhIBEboQsWWbcrYzpG3dx4ddC6Pr98i57e3NDFNdAvhtIUKqc3KsgFoUFglHaGpi06JkGufyaF3z1EcB/IpwLcVALRKLhJtUVLDoUVTaFScur/PIkCGAFablb84CiJZ MTEsWlyVfom2M9jZXbwI17I8MxekZS/QYbrYHfaTV8fQkGGNHdxhQC23QN5fY4nD 0p9RnZ20jouCO/PZ e0AgUyzhD7YW8kLS8uftEeFsjCe5YAtcxzPP9cNaJuWR0SmF9VNX9 Z f0VfZGk4FaTvuwKTZFRAhbMv31lYtqZZgXErY3r9/rixcS0E5P1wYCW/2zhXbi4uK8XDYNTI2NLOxy7L6KwATzrdT/eM31iqjAwECvI 4GIDBRZMKkKVOmWPP3/tE5EXQ6iPdT0UldX7m7cnerbjZu62ZYzPz6jBOP0mpzc/OAfMHdICD4u0UwCQgyLijnZ1/5Z2r/bsUbzSbVjwU3hFJlHwOzWDVzqbOQyer4 GlsMQ mAcH9tTFsD9c6AAFnrtPnD2wcReEF3Nzchw5p8Ox/Gg3OyLGVO2VlNTQ00sTEEjRAIIE7 MCBBDBQ2azvLsZU1tHhBgJLH7dB8j4C9GCydsgtRi8uAEXmog5wB7NZAAAAAElFTkSuQmCC'
const arrow = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAnElEQVQ4T63TsQ0CMQyF4f/NgMQQ0CBR0FIx190cFIiWhhFoKdgEiRUeSoF0gO8cjkub Evs2OLPpc9422dgBRwlNZn/BtjeApdOUJsh0QuuwKYWiYAFcAKWHaSR1EbpfAHlkO0ICdMJgV QXmAAmUu6v9IZA8wkPVKgtg7TF7H25t4UbN A9ahGmqqVD8AO2GdzUF45 I3ZJJb9JxbwRhEhB66xAAAAAElFTkSuQmCC'
class AnimationRoute {
constructor(map, json, play = true) {
this._map = map
this._json = json
this._play = play
this.init()
}
init() {
const that = this
that._index = 0
that._count = 1500
that._step = turf.length(that._json) / that._count
that._flag = 0
that._playId = 'play-' Date.now()
// 添加路径图层
that._map.addSource(that._playId, {
type: 'geojson',
data: that._json
})
that._map.addLayer({
id: that._playId,
type: 'line',
source: that._playId,
'layout': {
'line-cap': 'round',
'line-join': 'round'
},
'paint': {
'line-color': '#aaaaaa',
'line-width': 10
}
})
// 添加已播放路径
that._map.addSource(that._playId '-played', {
type: 'geojson',
data: that._json
})
that._map.addLayer({
id: that._playId '-played',
type: 'line',
source: that._playId '-played',
'layout': {
'line-cap': 'round',
'line-join': 'round'
},
'paint': {
'line-color': '#09801a',
'line-width': 10
}
})
// 添加路径上的箭头
that._map.loadImage(arrow, function(error, image) {
if (error) throw error
that._map.addImage(that._playId '-arrow', image)
that._map.addLayer({
'id': that._playId '-arrow',
'source': that._playId,
'type': 'symbol',
'layout': {
'symbol-placement': 'line',
'symbol-spacing': 50,
'icon-image': that._playId '-arrow',
'icon-size': 0.6,
'icon-allow-overlap': true
}
})
})
// 添加动态图标
that._map.loadImage(icon, function(error, image) {
if (error) throw error
that._map.addImage(that._playId '-icon', image)
that._map.addSource(that._playId '-point', {
'type': 'geojson',
'data': that._getDataByCoords()
})
that._map.addLayer({
'id': that._playId '-point',
'source': that._playId '-point',
'type': 'symbol',
'layout': {
'icon-image': that._playId '-icon',
'icon-size': 0.75,
'icon-allow-overlap': true,
'icon-rotation-alignment': 'map',
'icon-pitch-alignment': 'map',
'icon-rotate': 50
}
})
that._animatePath()
})
}
_animatePath() {
if(this._index > this._count) {
window.cancelAnimationFrame(this._flag)
} else {
const coords = turf.along(this._json, this._step * this._index).geometry.coordinates
// 已播放的线
const start = turf.along(this._json, 0).geometry.coordinates
this._map.getSource(this._playId '-played').setData(turf.lineSlice(start, coords, this._json))
// 车的图标位置
this._map.getSource(this._playId '-point').setData(this._getDataByCoords(coords))
// 计算旋转角度
const nextIndex = this._index === this._count ? this._count - 1 : this._index 1
const coordsNext = turf.along(this._json, this._step * nextIndex).geometry.coordinates
let angle = turf.bearing(
turf.point(coords),
turf.point(coordsNext)
) - 90
if(this._index === this._count) angle = 180
this._map.setLayoutProperty(this._playId '-point', 'icon-rotate', angle)
this._index
if(this._play) this._flag = requestAnimationFrame(() => {
this._animatePath()
})
}
}
_getDataByCoords(coords) {
if(!coords || coords.length !== 2) return null
return turf.point(coords, {
'label': this._formatDistance(this._step * this._index)
})
}
_formatDistance(dis) {
if(dis < 1) {
dis = dis * 1000
return dis.toFixed(0) '米'
} else {
return dis.toFixed(2) '千米'
}
}
destory() {
window.cancelAnimationFrame(this._flag)
if(this._map.getSource(this._playId '-point')) {
this._map.removeLayer(this._playId '-point')
// this._map.removeLayer(this._playId '-label')
this._map.removeSource(this._playId '-point')
}
if(this._map.getSource(this._playId)) {
this._map.removeLayer(this._playId)
this._map.removeSource(this._playId)
}
}
}
测试调用代码:
代码语言:javascript复制const route1 = {'type':'Feature','properties':{},'geometry':{'type':'LineString','coordinates':[[106.669,22.5785],[106.6374,22.5974],[106.6206,22.608],[106.6037,22.5553],[106.5784,22.4858],[106.5595,22.4373],[106.5637,22.3804],[106.5827,22.3298],[106.6543,22.313],[106.6859,22.2561],[106.7006,22.195],[106.688,22.1613],[106.6943,22.0897],[106.6964,22.018],[106.6838,21.9717],[106.7386,21.9864],[106.7554,22.0138],[106.8334,21.9759],[106.9008,21.9738],[106.9261,21.9422],[106.9767,21.9316],[107.0209,21.9485],[107.0609,21.919],[107.0125,21.8705],[107.0104,21.8305],[107.0609,21.8031],[107.1031,21.7862],[107.1473,21.7483],[107.2063,21.7125],[107.2611,21.6935],[107.2927,21.7251]]}}
new AnimationRoute(map, route1)
说明:如果为多个轨迹同时展示,多次调用new AnimationRoute
即可。