0%

OpenLayers6实例分析:Marker Animation(标记动画)

分析 Marker Animation 这个 demo,官网介绍是:

This example shows how to use postrender events and a vector context to animate a marker feature along a line. In this example an encoded polyline is being used.
此示例展示了如何使用 postrender 事件和矢量上下文使一个标记要素沿着线移动。在这个实例中使用了一个经过编码的折线。

Marker Animation

定义基本结构

先展示地图基本结构,地图的样式使用的是AerialWithLabelsOnDemand(按需加载的带标签的航拍图)。在这里的 vectorLayer是动画显示的图层,要素和动画逻辑都在此层上执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/css/ol.css"
type="text/css"
/>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/build/ol.js"></script>
<title>Marker Animation(标记动画)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="map" class="map"></div>
<label for="speed">
速度:&nbsp;
<input id="speed" type="range" min="10" max="999" step="10" value="60" />
</label>
<button id="start-animation">开始动画</button>
<script>
// 基本地图设置
let center = [-5639523.95, -3501274.52];
let map = new ol.Map({
target: document.getElementById("map"),
view: new ol.View({
center: center,
zoom: 10,
minZoom: 2,
maxZoom: 19
}),
layers: [
new ol.layer.Tile({
source: new ol.source.BingMaps({
imagerySet: "AerialWithLabelsOnDemand",
// 需要填写必应地图的开发者key,申请网址http://www.bingmapsportal.com/
key: "Your Bing Maps Key from http://www.bingmapsportal.com/ here"
})
}),
vectorLayer
]
});
</script>
</body>
</html>

设置 vectorLayer

加载数据并生成要素

猜测这个 demo 可能是 OpenLayers 官方摘自jsFiddle,jsFiddle 加载外部数据时有限制,所以使用字符串数组 join 一下合并成长字符串当数据源。

使用ol.format.Polyline新建一个 openlayers 折线polyline,使用readGeometry方法读取polyline,并把投影从 4326 转到 3857 的 web 标准格式,返回了一个几何体对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 通常使用AJAX方式加载数据,但由于jsFiddle的限制,所以这里使用长字符串来替代
const polyline = [
"hldhx@lnau`BCG_EaC??cFjAwDjF??uBlKMd@}@z@??aC^yk@z_@se@b[wFdE??wFfE}N",
"fIoGxB_I\\gG}@eHoCyTmPqGaBaHOoD\\??yVrGotA|N??o[N_STiwAtEmHGeHcAkiA}^",
"aMyBiHOkFNoI`CcVvM??gG^gF_@iJwC??eCcA]OoL}DwFyCaCgCcCwDcGwHsSoX??wI_E",
"kUFmq@hBiOqBgTwS??iYse@gYq\\cp@ce@{vA}s@csJqaE}{@iRaqE{lBeRoIwd@_T{]_",
"Ngn@{PmhEwaA{SeF_u@kQuyAw]wQeEgtAsZ}LiCarAkVwI}D??_}RcjEinPspDwSqCgs@",
"sPua@_OkXaMeT_Nwk@ob@gV}TiYs[uTwXoNmT{Uyb@wNg]{Nqa@oDgNeJu_@_G}YsFw]k",
"DuZyDmm@i_@uyIJe~@jCg|@nGiv@zUi_BfNqaAvIow@dEed@dCcf@r@qz@Egs@{Acu@mC",
"um@yIey@gGig@cK_m@aSku@qRil@we@{mAeTej@}Tkz@cLgr@aHko@qOmcEaJw~C{w@ka",
"i@qBchBq@kmBS{kDnBscBnFu_Dbc@_~QHeU`IuyDrC_}@bByp@fCyoA?qMbD}{AIkeAgB",
"k_A_A{UsDke@gFej@qH{o@qGgb@qH{`@mMgm@uQus@kL{_@yOmd@ymBgwE}x@ouBwtA__",
"DuhEgaKuWct@gp@cnBii@mlBa_@}|Asj@qrCg^eaC}L{dAaJ_aAiOyjByH{nAuYu`GsAw",
"Xyn@ywMyOyqD{_@cfIcDe}@y@aeBJmwA`CkiAbFkhBlTgdDdPyiB`W}xDnSa}DbJyhCrX",
"itAhT}x@bE}Z_@qW_Kwv@qKaaAiBgXvIm}A~JovAxCqW~WanB`XewBbK{_A`K}fBvAmi@",
"xBycBeCauBoF}}@qJioAww@gjHaPopA_NurAyJku@uGmi@cDs[eRaiBkQstAsQkcByNma",
"CsK_uBcJgbEw@gkB_@ypEqDoqSm@eZcDwjBoGw`BoMegBaU_`Ce_@_uBqb@ytBwkFqiT_",
"fAqfEwe@mfCka@_eC_UmlB}MmaBeWkkDeHwqAoX}~DcBsZmLcxBqOwqE_DkyAuJmrJ\\o",
"~CfIewG|YibQxBssB?es@qGciA}RorAoVajA_nAodD{[y`AgPqp@mKwr@ms@umEaW{dAm",
"b@umAw|@ojBwzDaaJsmBwbEgdCsrFqhAihDquAi`Fux@}_Dui@_eB_u@guCuyAuiHukA_",
"lKszAu|OmaA{wKm}@clHs_A_rEahCssKo\\sgBsSglAqk@yvDcS_wAyTwpBmPc|BwZknF",
"oFscB_GsaDiZmyMyLgtHgQonHqT{hKaPg}Dqq@m~Hym@c`EuiBudIabB{hF{pWifx@snA",
"w`GkFyVqf@y~BkoAi}Lel@wtc@}`@oaXi_C}pZsi@eqGsSuqJ|Lqeb@e]kgPcaAu}SkDw",
"zGhn@gjYh\\qlNZovJieBqja@ed@siO{[ol\\kCmjMe\\isHorCmec@uLebB}EqiBaCg}",
"@m@qwHrT_vFps@kkI`uAszIrpHuzYxx@e{Crw@kpDhN{wBtQarDy@knFgP_yCu\\wyCwy",
"A{kHo~@omEoYmoDaEcPiuAosDagD}rO{{AsyEihCayFilLaiUqm@_bAumFo}DgqA_uByi",
"@swC~AkzDlhA}xEvcBa}Cxk@ql@`rAo|@~bBq{@``Bye@djDww@z_C_cAtn@ye@nfC_eC",
"|gGahH~s@w}@``Fi~FpnAooC|u@wlEaEedRlYkrPvKerBfYs}Arg@m}AtrCkzElw@gjBb",
"h@woBhR{gCwGkgCc[wtCuOapAcFoh@uBy[yBgr@c@iq@o@wvEv@sp@`FajBfCaq@fIipA",
"dy@ewJlUc`ExGuaBdEmbBpBssArAuqBBg}@s@g{AkB{bBif@_bYmC}r@kDgm@sPq_BuJ_",
"s@{X_{AsK_d@eM{d@wVgx@oWcu@??aDmOkNia@wFoSmDyMyCkPiBePwAob@XcQ|@oNdCo",
"SfFwXhEmOnLi\\lbAulB`X_d@|k@au@bc@oc@bqC}{BhwDgcD`l@ed@??bL{G|a@eTje@",
"oS~]cLr~Bgh@|b@}Jv}EieAlv@sPluD{z@nzA_]`|KchCtd@sPvb@wSb{@ko@f`RooQ~e",
"[upZbuIolI|gFafFzu@iq@nMmJ|OeJn^{Qjh@yQhc@uJ~j@iGdd@kAp~BkBxO{@|QsAfY",
"gEtYiGd]}Jpd@wRhVoNzNeK`j@ce@vgK}cJnSoSzQkVvUm^rSgc@`Uql@xIq\\vIgg@~k",
"Dyq[nIir@jNoq@xNwc@fYik@tk@su@neB}uBhqEesFjoGeyHtCoD|D}Ed|@ctAbIuOzqB",
"_}D~NgY`\\um@v[gm@v{Cw`G`w@o{AdjAwzBh{C}`Gpp@ypAxn@}mAfz@{bBbNia@??jI",
"ab@`CuOlC}YnAcV`@_^m@aeB}@yk@YuTuBg^uCkZiGk\\yGeY}Lu_@oOsZiTe[uWi[sl@",
"mo@soAauAsrBgzBqgAglAyd@ig@asAcyAklA}qAwHkGi{@s~@goAmsAyDeEirB_{B}IsJ",
"uEeFymAssAkdAmhAyTcVkFeEoKiH}l@kp@wg@sj@ku@ey@uh@kj@}EsFmG}Jk^_r@_f@m",
"~@ym@yjA??a@cFd@kBrCgDbAUnAcBhAyAdk@et@??kF}D??OL"
].join("");

let route = new ol.format.Polyline({
factor: 1e6
// 返回一个geometry
}).readGeometry(polyline, {
// 数据源的投影格式和生成要素的投影格式
dataProjection: "EPSG:4326",
featureProjection: "EPSG:3857"
});
// 获取路线的坐标
let routeCoords = route.getCoordinates();
let routeLength = routeCoords.length;
// 每个要素分别设置一个type,用于设置样式
// 路线的要素
let routeFeature = new ol.Feature({
type: "route",
geometry: route
});
// 运动点的要素
let geoMarker = new ol.Feature({
type: "geoMarker",
geometry: new ol.geom.Point(routeCoords[0])
});
// 开始点标志的要素
let startMarker = new ol.Feature({
type: "icon",
geometry: new ol.geom.Point(routeCoords[0])
});
// 结束点标志的要素
let endMarker = new ol.Feature({
type: "icon",
geometry: new ol.geom.Point(routeCoords[routeLength - 1])
});

获取到了路线的geometry以后,新建一个ol.Feature对象,对象中定义一个属性typeroute,最后返回一个路线的要素routeFeature。同样的方式生成运动点的要素(typegeoMarker)、开始点标志的要素startMarkertypeicon)、结束点标志的要素endMarkertypeicon),此处的type主要是为应用不同的style做的准备。

生成样式和图层

定义一个styles对象,键名为route的时路线的样式,键名为icon的起始点和终止点的图标样式,键名为geoMarker为运动点的样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 对应各type的样式
let styles = {
route: new ol.style.Style({
stroke: new ol.style.Stroke({
width: 6,
color: [237, 212, 0, 0.8]
})
}),
icon: new ol.style.Style({
image: new ol.style.Icon({
anchor: [0.5, 1],
src: "../data/icon.png"
})
}),
geoMarker: new ol.style.Style({
image: new ol.style.Circle({
radius: 7,
fill: new ol.style.Fill({ color: "black" }),
stroke: new ol.style.Stroke({
color: "white",
width: 2
})
})
})
};
// 是否处于运动状态的标志
let animating = false;
// 移动速度,当前时间戳
let speed, now;
// 速度输入框和开始按钮
let speedInput = document.getElementById("speed");
let startButton = document.getElementById("start-animation");
// 添加这些要素的图层
let vectorLayer = new ol.layer.Vector({
source: new ol.source.Vector({
features: [routeFeature, geoMarker, startMarker, endMarker]
}),
style: feature => {
// 当正在运动时隐藏初始位置标志点
if (animating && feature.get("type") === "geoMarker") {
return null;
}
// 根据类型返回样式
return styles[feature.get("type")];
}
});

接着设置一个运动状态的标志animatingfalse,然后在添加vectorLayer图层的时候,把routeFeaturegeoMarkerstartMarkerendMarker添加到features中。

有一个关键的地方就是在设置style的时候使用了函数返回,对应不同的type使用之前预定义好的不同的样式。如果animatingtrue(正在运动)并且typegeoMarker(初始位置标志点),就返回null(隐藏初始位置标志点)。

按钮点击的回调函数

startAnimation

button 按钮上绑定的是startAnimation回调函数,这个函数名义上叫start,但实际上这个同时控制开始和取消动画。该函数执行时,如果animatingtrue,就去执行停止动画的函数stopAnimation

如果animatingfalse,就去执行else中的代码,最关键的地方是给vectorLayer添加了postrender回调,执行moveFeature,形成动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @description: 按钮点击的回调函数
* @param {type} null
* @return: null
*/
let startAnimation = () => {
if (animating) {
// 如果animating为true就执行停止动画的函数
stopAnimation(false);
} else {
// 把animating改为true,设置时间戳,取到初始速度,把按钮文字设为取消动画
animating = true;
now = new Date().getTime();
speed = speedInput.value;
startButton.textContent = "取消动画";
// 隐藏标记
geoMarker.setStyle(null);
// 防止平移了地图,重新设置地图中心
map.getView().setCenter(center);
// 添加postrender回调
vectorLayer.on("postrender", moveFeature);
// 渲染地图
map.render();
}
};
// 给按钮绑定点击事件
startButton.addEventListener("click", startAnimation, false);

最后给按钮绑定click事件,回调函数为startAnimation

stopAnimation

停止动画的回调函数stopAnimation接收一个ended参数,这个函数的关键就是对ended的判断。如果endedture的时候,也就是动画正常运行完,这时就把运动点放到最后一点;反之,就是动画并没有运行完而是被中途取消,就把运动点重置在起点处。

最后,解绑vectorLayerspostrender回调,取消动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @description: 停止动画
* @param {Boolean} ended 为true的时候也就是正常结束,所以标记在最后;为false的时候是主动取消,所以标记在最前。
* @return: null
*/
let stopAnimation = ended => {
// 把aniamting设为false,把按钮文字改为开始动画
animating = false;
startButton.textContent = "开始动画";

// 如果点击取消动画,在开始的地方设置标记
// 如果ended为true,就把点放到最后一个;如果ended为false,就把点放第一个。
let coord = ended ? routeCoords[routeLength - 1] : routeCoords[0];
// 给geoMaker设为coords
let geometry = geoMarker.getGeometry();
geometry.setCoordinates(coord);
// 解绑postrender事件监听
vectorLayer.un("postrender", moveFeature);
};

移动要素的回调函数

最核心的移动要素的函数moveFeature如下,获取矢量上下文和当前帧以后,判断只有在animatingtrue的时候,才执行运动的代码。控制速度是利用动画运行时间elapsedTime和速度speed计算得到每次运动该跳过的索引数,同时,最近的一个点拿出来使用drawFeature函数绘制在地图上,这样在不断render的过程中就形成了动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 移动要素的回调函数
let moveFeature = event => {
let vectorContext = ol.render.getVectorContext(event);
let frameState = event.frameState;
// 当animating为true的时候才执行
if (animating) {
let elapsedTime = frameState.time - now;
// 这里提升速度的技巧时在lineString的坐标上跳过几个索引
let index = Math.round((speed * elapsedTime) / 1000);
// 当索引大过总数的时候就结束
if (index >= routeLength) {
stopAnimation(true);
return;
}
// 把最近的一个点拿出来做成要素,形成动画效果
let currentPoint = new ol.geom.Point(routeCoords[index]);
let feature = new ol.Feature(currentPoint);
vectorContext.drawFeature(feature, styles.geoMarker);
}
// 继续渲染动画
map.render();
};

全部代码

全部代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/css/ol.css"
type="text/css"
/>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/build/ol.js"></script>
<title>Marker Animation(标记动画)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="map" class="map"></div>
<label for="speed">
速度:&nbsp;
<input id="speed" type="range" min="10" max="999" step="10" value="60" />
</label>
<button id="start-animation">开始动画</button>
<script>
// 通常使用AJAX方式加载数据,但由于jsFiddle的限制,所以这里使用长字符串来替代
const polyline = [
"hldhx@lnau`BCG_EaC??cFjAwDjF??uBlKMd@}@z@??aC^yk@z_@se@b[wFdE??wFfE}N",
"fIoGxB_I\\gG}@eHoCyTmPqGaBaHOoD\\??yVrGotA|N??o[N_STiwAtEmHGeHcAkiA}^",
"aMyBiHOkFNoI`CcVvM??gG^gF_@iJwC??eCcA]OoL}DwFyCaCgCcCwDcGwHsSoX??wI_E",
"kUFmq@hBiOqBgTwS??iYse@gYq\\cp@ce@{vA}s@csJqaE}{@iRaqE{lBeRoIwd@_T{]_",
"Ngn@{PmhEwaA{SeF_u@kQuyAw]wQeEgtAsZ}LiCarAkVwI}D??_}RcjEinPspDwSqCgs@",
"sPua@_OkXaMeT_Nwk@ob@gV}TiYs[uTwXoNmT{Uyb@wNg]{Nqa@oDgNeJu_@_G}YsFw]k",
"DuZyDmm@i_@uyIJe~@jCg|@nGiv@zUi_BfNqaAvIow@dEed@dCcf@r@qz@Egs@{Acu@mC",
"um@yIey@gGig@cK_m@aSku@qRil@we@{mAeTej@}Tkz@cLgr@aHko@qOmcEaJw~C{w@ka",
"i@qBchBq@kmBS{kDnBscBnFu_Dbc@_~QHeU`IuyDrC_}@bByp@fCyoA?qMbD}{AIkeAgB",
"k_A_A{UsDke@gFej@qH{o@qGgb@qH{`@mMgm@uQus@kL{_@yOmd@ymBgwE}x@ouBwtA__",
"DuhEgaKuWct@gp@cnBii@mlBa_@}|Asj@qrCg^eaC}L{dAaJ_aAiOyjByH{nAuYu`GsAw",
"Xyn@ywMyOyqD{_@cfIcDe}@y@aeBJmwA`CkiAbFkhBlTgdDdPyiB`W}xDnSa}DbJyhCrX",
"itAhT}x@bE}Z_@qW_Kwv@qKaaAiBgXvIm}A~JovAxCqW~WanB`XewBbK{_A`K}fBvAmi@",
"xBycBeCauBoF}}@qJioAww@gjHaPopA_NurAyJku@uGmi@cDs[eRaiBkQstAsQkcByNma",
"CsK_uBcJgbEw@gkB_@ypEqDoqSm@eZcDwjBoGw`BoMegBaU_`Ce_@_uBqb@ytBwkFqiT_",
"fAqfEwe@mfCka@_eC_UmlB}MmaBeWkkDeHwqAoX}~DcBsZmLcxBqOwqE_DkyAuJmrJ\\o",
"~CfIewG|YibQxBssB?es@qGciA}RorAoVajA_nAodD{[y`AgPqp@mKwr@ms@umEaW{dAm",
"b@umAw|@ojBwzDaaJsmBwbEgdCsrFqhAihDquAi`Fux@}_Dui@_eB_u@guCuyAuiHukA_",
"lKszAu|OmaA{wKm}@clHs_A_rEahCssKo\\sgBsSglAqk@yvDcS_wAyTwpBmPc|BwZknF",
"oFscB_GsaDiZmyMyLgtHgQonHqT{hKaPg}Dqq@m~Hym@c`EuiBudIabB{hF{pWifx@snA",
"w`GkFyVqf@y~BkoAi}Lel@wtc@}`@oaXi_C}pZsi@eqGsSuqJ|Lqeb@e]kgPcaAu}SkDw",
"zGhn@gjYh\\qlNZovJieBqja@ed@siO{[ol\\kCmjMe\\isHorCmec@uLebB}EqiBaCg}",
"@m@qwHrT_vFps@kkI`uAszIrpHuzYxx@e{Crw@kpDhN{wBtQarDy@knFgP_yCu\\wyCwy",
"A{kHo~@omEoYmoDaEcPiuAosDagD}rO{{AsyEihCayFilLaiUqm@_bAumFo}DgqA_uByi",
"@swC~AkzDlhA}xEvcBa}Cxk@ql@`rAo|@~bBq{@``Bye@djDww@z_C_cAtn@ye@nfC_eC",
"|gGahH~s@w}@``Fi~FpnAooC|u@wlEaEedRlYkrPvKerBfYs}Arg@m}AtrCkzElw@gjBb",
"h@woBhR{gCwGkgCc[wtCuOapAcFoh@uBy[yBgr@c@iq@o@wvEv@sp@`FajBfCaq@fIipA",
"dy@ewJlUc`ExGuaBdEmbBpBssArAuqBBg}@s@g{AkB{bBif@_bYmC}r@kDgm@sPq_BuJ_",
"s@{X_{AsK_d@eM{d@wVgx@oWcu@??aDmOkNia@wFoSmDyMyCkPiBePwAob@XcQ|@oNdCo",
"SfFwXhEmOnLi\\lbAulB`X_d@|k@au@bc@oc@bqC}{BhwDgcD`l@ed@??bL{G|a@eTje@",
"oS~]cLr~Bgh@|b@}Jv}EieAlv@sPluD{z@nzA_]`|KchCtd@sPvb@wSb{@ko@f`RooQ~e",
"[upZbuIolI|gFafFzu@iq@nMmJ|OeJn^{Qjh@yQhc@uJ~j@iGdd@kAp~BkBxO{@|QsAfY",
"gEtYiGd]}Jpd@wRhVoNzNeK`j@ce@vgK}cJnSoSzQkVvUm^rSgc@`Uql@xIq\\vIgg@~k",
"Dyq[nIir@jNoq@xNwc@fYik@tk@su@neB}uBhqEesFjoGeyHtCoD|D}Ed|@ctAbIuOzqB",
"_}D~NgY`\\um@v[gm@v{Cw`G`w@o{AdjAwzBh{C}`Gpp@ypAxn@}mAfz@{bBbNia@??jI",
"ab@`CuOlC}YnAcV`@_^m@aeB}@yk@YuTuBg^uCkZiGk\\yGeY}Lu_@oOsZiTe[uWi[sl@",
"mo@soAauAsrBgzBqgAglAyd@ig@asAcyAklA}qAwHkGi{@s~@goAmsAyDeEirB_{B}IsJ",
"uEeFymAssAkdAmhAyTcVkFeEoKiH}l@kp@wg@sj@ku@ey@uh@kj@}EsFmG}Jk^_r@_f@m",
"~@ym@yjA??a@cFd@kBrCgDbAUnAcBhAyAdk@et@??kF}D??OL"
].join("");
// https://openlayers.org/en/latest/apidoc/module-ol_format_Polyline-Polyline.html
let route = new ol.format.Polyline({
factor: 1e6
// 返回一个geometry
}).readGeometry(polyline, {
// 数据源的投影格式和生成要素的投影格式
dataProjection: "EPSG:4326",
featureProjection: "EPSG:3857"
});
// 获取路线的坐标
let routeCoords = route.getCoordinates();
let routeLength = routeCoords.length;
// 每个要素分别设置一个type,用于设置样式
// 路线的要素
let routeFeature = new ol.Feature({
type: "route",
geometry: route
});
// 运动点的要素
let geoMarker = new ol.Feature({
type: "geoMarker",
geometry: new ol.geom.Point(routeCoords[0])
});
// 开始点标志的要素
let startMarker = new ol.Feature({
type: "icon",
geometry: new ol.geom.Point(routeCoords[0])
});
// 结束点标志的要素
let endMarker = new ol.Feature({
type: "icon",
geometry: new ol.geom.Point(routeCoords[routeLength - 1])
});
// 对应各type的样式
let styles = {
route: new ol.style.Style({
stroke: new ol.style.Stroke({
width: 6,
color: [237, 212, 0, 0.8]
})
}),
icon: new ol.style.Style({
image: new ol.style.Icon({
anchor: [0.5, 1],
src: "../data/icon.png"
})
}),
geoMarker: new ol.style.Style({
image: new ol.style.Circle({
radius: 7,
fill: new ol.style.Fill({ color: "black" }),
stroke: new ol.style.Stroke({
color: "white",
width: 2
})
})
})
};
// 是否出于运动状态的标志
let animating = false;
// 移动速度,当前时间戳
let speed, now;
// 速度输入框和开始按钮
let speedInput = document.getElementById("speed");
let startButton = document.getElementById("start-animation");
// 添加这些要素的图层
let vectorLayer = new ol.layer.Vector({
source: new ol.source.Vector({
features: [routeFeature, geoMarker, startMarker, endMarker]
}),
style: feature => {
// 当正在运动时隐藏初始位置标志点
if (animating && feature.get("type") === "geoMarker") {
return null;
}
// 根据类型返回样式
return styles[feature.get("type")];
}
});
// 基本地图设置
let center = [-5639523.95, -3501274.52];
let map = new ol.Map({
target: document.getElementById("map"),
view: new ol.View({
center: center,
zoom: 10,
minZoom: 2,
maxZoom: 19
}),
layers: [
new ol.layer.Tile({
source: new ol.source.BingMaps({
imagerySet: "AerialWithLabelsOnDemand",
key: "Your Bing Maps Key from http://www.bingmapsportal.com/ here"
})
}),
vectorLayer
]
});
// 移动要素的回调函数
let moveFeature = event => {
let vectorContext = ol.render.getVectorContext(event);
let frameState = event.frameState;
// 当animating为true的时候才执行
if (animating) {
let elapsedTime = frameState.time - now;
// 这里提升速度的技巧时在lineString的坐标上跳过几个索引
let index = Math.round((speed * elapsedTime) / 1000);
// 当索引大过总数的时候就结束
if (index >= routeLength) {
stopAnimation(true);
return;
}
// 把最近的一个点拿出来做成要素,形成动画效果
let currentPoint = new ol.geom.Point(routeCoords[index]);
let feature = new ol.Feature(currentPoint);
vectorContext.drawFeature(feature, styles.geoMarker);
}
// 继续渲染动画
map.render();
};
/**
* @description: 按钮点击的回调函数
* @param {type} null
* @return: null
*/
let startAnimation = () => {
if (animating) {
// 如果animating为true就执行停止动画的函数
stopAnimation(false);
} else {
// 把animating改为true,设置时间戳,取到初始速度,把按钮文字设为取消动画
animating = true;
now = new Date().getTime();
speed = speedInput.value;
startButton.textContent = "取消动画";
// 隐藏标记
geoMarker.setStyle(null);
// 防止平移了地图,重新设置地图中心
map.getView().setCenter(center);
// 添加postrender回调
vectorLayer.on("postrender", moveFeature);
// 渲染地图
map.render();
}
};

/**
* @description: 停止动画
* @param {Boolean} ended 为true的时候也就是正常结束,所以标记在最后;为false的时候是主动取消,所以标记在最前。
* @return: null
*/
let stopAnimation = ended => {
// 把aniamting设为false,把按钮文字改为开始动画
animating = false;
startButton.textContent = "开始动画";

// 如果点击取消动画,在开始的地方设置标记
// 如果ended为true,就把点放到最后一个;如果ended为false,就把点放第一个。
let coord = ended ? routeCoords[routeLength - 1] : routeCoords[0];
// 给geoMaker设为coords
let geometry = geoMarker.getGeometry();
geometry.setCoordinates(coord);
// 解绑postrender事件监听
vectorLayer.un("postrender", moveFeature);
};
// 给按钮绑定点击事件
startButton.addEventListener("click", startAnimation, false);
</script>
</body>
</html>
👆 全文结束,棒槌时间到 👇