0%

OpenLayers6实例分析:Flight Animation(动态航线)

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

This example shows how to use postrender and vectorContext to animate flights. A great circle arc between two airports is calculated using arc.js and then the flight paths are animated with postrender. The flight data is provided by OpenFlights (a simplified data set from the Mapbox.js documentation is used).
此示例展示了如何使用 postrender 和 vectorContext 模拟航线动画。使用 arc.js 来计算了两个机场之间的弧线并使用 postrender 模拟航线路径动画。航线的数据来自于 OpenFlights(Mapbox.js 文档里面也使用了这个简单的数据)。

Flight Animation

定义基本结构

先展示地图基本结构,与之前的主要区别是引入了一个 arc.js,用来生成弧线:

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
<!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>
<script src="https://api.mapbox.com/mapbox.js/plugins/arc.js/v0.1.0/arc.js"></script>
<title>Flight Animation(航线动画)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="map" class="map"></div>
<script type="text/javascript">
let tileLayer = new ol.layer.Tile({
// 使用了Stamen源
source: new ol.source.Stamen({
layer: "toner" // 碳粉样式
})
});

let map = new ol.Map({
layers: [tileLayer],
target: "map",
view: new ol.View({
center: [0, 0],
zoom: 2
})
});
</script>
</body>
</html>

生成航线

生成航线的方式与常规一致,生成stylesourcelayer对象,然后再map.addLayer()把图层添加给地图。与之前 demo 的主要区别在于flightsSource中的loader属性:

关于loader介绍

The loader function used to load features, from a remote source for example. If this is not set and url is set, the source will create and use an XHR feature loader.

loader 函数被用来从远程资源处加载要素,如果 loader 属性没有设置但是 url 属性已设置,source 就会创建并使用一个 XHR 要素加载器。

在使用的时候如代码所示,就是在回调函数中:请求数据源=>处理数据=>设置逻辑。

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
let style = new ol.style.Style({
stroke: new ol.style.Stroke({
color: "#EAE911",
width: 2
})
});

let flightsSource = new ol.source.Vector({
wrapX: false,
attributions:
"Flight data by " +
'<a href="http://openflights.org/data.html">OpenFlights</a>,',
// 异步请求数据源
loader: () => {
// 请求数据源
let url =
"https://openlayers.org/en/latest/examples/data/openflights/flights.json";
fetch(url)
.then(response => {
return response.json();
})
// 处理数据
.then(json => {
let flightsData = json.flights;
for (let i = 0; i < flightsData.length; i++) {
// 分解出每个航线的起止点
let flight = flightsData[i];
let from = flight[0];
let to = flight[1];

// 用arcjs在起止点之间生成一个弧对象生成器
let arcGenerator = new arc.GreatCircle(
{ x: from[1], y: from[0] },
{ x: to[1], y: to[0] }
);
// 用弧对象生成器来生成弧线
let arcLine = arcGenerator.Arc(100, { offset: 10 });
if (arcLine.geometries.length === 1) {
// 利用弧对象生成一个LineString对象并转化到3857坐标系
let line = new ol.geom.LineString(arcLine.geometries[0].coords);
line.transform("EPSG:4326", "EPSG:3857");
// 把LineString对象转化为要素,finished设为false
let feature = new ol.Feature({
geometry: line,
finished: false
});
// 让不同的航线每隔50ms开始发射
addLater(feature, i * 50);
}
}
tileLayer.on("postrender", animateFlights);
});
}
});
// 创建一个绘制飞行轨迹的图层
let flightsLayer = new ol.layer.Vector({
source: flightsSource,
style: feature => {
// 如果这个要素的动画依然是活动的,就不用预定义的样式渲染这个要素
if (feature.get("finished")) {
return style;
} else {
return null;
}
}
});

map.addLayer(flightsLayer);

请求数据

请求数据时使用的是 ES6 中的fetch,使用Promise封装请求,请求到的数据格式如下所示,只截取了前三个示例,数据结构非常清晰,flights是一个航线数组,每个里面都包含起点和终点的经纬度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"flights": [
[
[43.449928, 39.956589],
[55.606186, 49.278728]
],
[
[55.34, 52.06],
[45.034689, 39.170539]
],
[
[55.012622, 82.650656],
[52.268028, 104.388975]
]
]
}

生成 LineString 对象

拿到数据之后利用arc.js来生成弧线,打印一下arcGeneratorarcLine,如下图所示,arcGenerator中包含了弧线的起点和终点的经纬度和屏幕坐标,arcLine生成了一个集合体对象。在arcLine生成的对象中可以看到,LineStringcoords是一个长度为 100 的数组,arcGenerator.Arc(100, { offset: 10 })中的 100 就是代表把弧线分成 100 个直线来实现。

arcLine生成的几何体对象转化为ol.geom.LineString对象并改为 3857 坐标系,接下来生成要素的时候同时赋值一个finished属性为false

然后执行addLater(feature, i * 50)tileLayer.on("postrender", animateFlights),这两句中的addLateranimateFlights两个函数是为了航线的动画实现而构建的。

核心动画逻辑函数

addLater

addLater是添加要素的方法,代码如下所示,利用setTimeOut分时间差逐次添加航线的轨迹,营造出一种航线非同步发出的的真实感。并给每个要素设置一个start起始时间,用来给后面的逻辑计算动画用时。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @description: 添加要素的函数,
* @param {type} feature 要素
* @param {Number} timeout 延迟时间
* @return: null
*/
let addLater = (feature, timeout) => {
window.setTimeout(() => {
// 设定一个start初始时间
feature.set("start", new Date().getTime());
flightsSource.addFeature(feature);
}, timeout);
};

animateFlights

animateFlights是动画执行逻辑的方法。如下所示,首先定义了一个常量pointsPerMs,这个常量的意义是没 ms 走过多少个点。前面已经把弧线分成了 100 段,pointsPerMs为 0.1 就是代表着 1ms 走 0.1 段,也就是 1s 走 100 段,所以动画正好 1s 完成。

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
const pointsPerMs = 0.1;
/**
* @description: 绘制航线动画的核心函数
* @param {type} event 回调参数
* @return: null
*/
let animateFlights = event => {
// 获取矢量上下文和当前帧,然后设定一个样式
let vectorContext = ol.render.getVectorContext(event);
let frameState = event.frameState;
vectorContext.setStyle(style);
// 获取source中的要素
let features = flightsSource.getFeatures();
for (let i = 0; i < features.length; i++) {
let feature = features[i];
// 如果这个要素的动画没有执行完再执行
if (!feature.get("finished")) {
let coords = feature.getGeometry().getCoordinates();
// 计算已执行时间
let elapsedTime = frameState.time - feature.get("start");
// 计算已执行的点数
let elapsedPoints = elapsedTime * pointsPerMs;
// 如果执行完成就设置finished为true
if (elapsedPoints >= coords.length) {
feature.set("finished", true);
}
// 创建当前帧的线
let maxIndex = Math.min(elapsedPoints, coords.length);
let currentLine = new ol.geom.LineString(coords.slice(0, maxIndex));

// 绘制
vectorContext.drawGeometry(currentLine);
}
}
// 继续渲染地图
map.render();
};

动画实现的方式也和之前几个实例基本一致,获取了矢量上下文和当前帧以后,先判断要素的finished属性是否为false,然后用当前帧时间和要素的start属性来计算动画已执行时间和已执行的段数(此时如果 100 段都执行完就设置finishedtrue),最后把当前帧的应该显示到的线段绘制出来并执行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
<!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>
<script src="https://api.mapbox.com/mapbox.js/plugins/arc.js/v0.1.0/arc.js"></script>
<title>Flight Animation(航线动画)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="map" class="map"></div>
<script type="text/javascript">
let tileLayer = new ol.layer.Tile({
source: new ol.source.Stamen({
layer: "toner" // 碳粉
})
});

let map = new ol.Map({
layers: [tileLayer],
target: "map",
view: new ol.View({
center: [0, 0],
zoom: 2
})
});

let style = new ol.style.Style({
stroke: new ol.style.Stroke({
color: "#EAE911",
width: 2
})
});

let flightsSource = new ol.source.Vector({
wrapX: false,
attributions:
"Flight data by " +
'<a href="http://openflights.org/data.html">OpenFlights</a>,',
// 异步请求数据源
loader: () => {
let url =
"https://openlayers.org/en/latest/examples/data/openflights/flights.json";
fetch(url)
.then(response => {
return response.json();
})
.then(json => {
let flightsData = json.flights;
for (let i = 0; i < flightsData.length; i++) {
// 分解出每个航线的起止点
let flight = flightsData[i];
let from = flight[0];
let to = flight[1];

// 用arcjs在起止点之间生成一个弧对象
let arcGenerator = new arc.GreatCircle(
{ x: from[1], y: from[0] },
{ x: to[1], y: to[0] }
);

let arcLine = arcGenerator.Arc(100, { offset: 10 });

if (arcLine.geometries.length === 1) {
// 利用弧对象生成一个LineString对象并转化到3857坐标系
let line = new ol.geom.LineString(
arcLine.geometries[0].coords
);
line.transform("EPSG:4326", "EPSG:3857");
// 把LineString对象转化为要素,finished设为false
let feature = new ol.Feature({
geometry: line,
finished: false
});
// 让不同的航线每隔50ms开始发射
addLater(feature, i * 50);
}
}
tileLayer.on("postrender", animateFlights);
});
}
});
// 创建一个绘制飞行轨迹的图层
let flightsLayer = new ol.layer.Vector({
source: flightsSource,
style: feature => {
// 如果这个要素的动画依然是活动的,就不用预定义的样式渲染这个要素
if (feature.get("finished")) {
return style;
} else {
return null;
}
}
});

map.addLayer(flightsLayer);

const pointsPerMs = 0.1;
/**
* @description: 绘制航线动画的核心函数
* @param {type} event 回调参数
* @return: null
*/
let animateFlights = event => {
// 获取矢量上下文和当前帧,然后设定一个样式
let vectorContext = ol.render.getVectorContext(event);
let frameState = event.frameState;
vectorContext.setStyle(style);
// 获取source中的要素
let features = flightsSource.getFeatures();
for (let i = 0; i < features.length; i++) {
let feature = features[i];
// 如果这个要素的动画没有执行完再执行
if (!feature.get("finished")) {
let coords = feature.getGeometry().getCoordinates();
// 计算已执行时间
let elapsedTime = frameState.time - feature.get("start");
// 计算已执行的点数
let elapsedPoints = elapsedTime * pointsPerMs;
// 如果执行完成就设置finished为true
if (elapsedPoints >= coords.length) {
feature.set("finished", true);
}
// 创建当前帧的线
let maxIndex = Math.min(elapsedPoints, coords.length);
let currentLine = new ol.geom.LineString(coords.slice(0, maxIndex));

// 绘制
vectorContext.drawGeometry(currentLine);
}
}
// 继续渲染地图
map.render();
};
/**
* @description: 添加要素的函数,
* @param {type} feature 要素
* @param {Number} timeout 延迟时间
* @return: null
*/
let addLater = (feature, timeout) => {
window.setTimeout(() => {
// 设定一个start初始时间
feature.set("start", new Date().getTime());
flightsSource.addFeature(feature);
}, timeout);
};
</script>
</body>
</html>
👆 全文结束,棒槌时间到 👇