0%

OpenLayers6实例分析:Dynamic Data(动态数据)

按照课程顺序,应该先分析Dynamic Data这个demo,对应如下:

定义基本结构

先把地图基本结构展示出来:

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
<!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>Dynamic Data(动态数据)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="map" class="map"></div>
<script type="text/javascript">
// 定义一个使用OSM源的瓦片图层
let tileLayer = new ol.layer.Tile({
source: new ol.source.OSM()
});
// 初始化地图
let map = new ol.Map({
layers: [tileLayer],
target: 'map',
view: new ol.View({
center: [0, 0],
zoom: 2
})
});
</script>
</body>
</html>

定义圆圈样式

接下来是定义几种不同的样式,如下图所示,普通圆点的边框为红色,填充为黄色,打头第一个圆点的边框为蓝色,填充为黑色。

通过ol.style来定义这几种不同的样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// imageStyle:每个普通圆
// headInnerImageStyle:第一个的圆的内圈
// headOuterImageStyle:第一个圆的外圈
let imageStyle = new ol.style.Style({
image: new ol.style.Circle({
radius: 5,
fill: new ol.style.Fill({ color: 'yellow' }),
stroke: new ol.style.Stroke({ color: 'red', width: 1 })
})
});

let headInnerImageStyle = new ol.style.Style({
image: new ol.style.Circle({
radius: 2,
fill: new ol.style.Fill({ color: 'blue' })
})
});

let headOuterImageStyle = new ol.style.Style({
image: new ol.style.Circle({
radius: 5,
fill: new ol.style.Fill({ color: 'black' })
})
});

postrender事件

事件


在实例中不断运动的动画是利用ol.layer.Tilepostrender事件回调来完成的,通过官网查询我们可以发现ol.layer.Tile有如下几个事件:

如标红所示,postrender事件是在layer渲染完成后触发,并且带有矢量上下文,所以是可以利用矢量上下文来自由绘制Canvas的。

矢量上下文

在OpenLayers6中,ol.render.getVectorContext()是可以获取事件的矢量上下文的,API和源码如下图所示:




很明显可以得知,输入参数是个event,得到的是矢量上下文,也就是一个CanvasImmediateRenderer对象。该对象有四个方法drawCircledrawFeaturedrawGeometrysetStyle



在这个demo中利用到了最后的两个方法drawGeometrysetStyle,熟悉Canvas的同学们应该都知道,Canvas在绘制时需要先设置绘制的各种参数,再进行绘制,该对象中的这几个方法的思路与Canvas很相似,所以再demo中是先setStyle,后给全部圆点drawGeometry,然后再setStyle,再单独给第一个点drawGeometry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tileLayer.on('postrender', event => {
// 获取矢量上下文和当前帧
let vectorContext = ol.render.getVectorContext(event);
let frameState = event.frameState;
// 省略
// 中间省略圆点位置计算过程
// 省略
vectorContext.setStyle(imageStyle);
vectorContext.drawGeometry(new ol.geom.MultiPoint(coordinates));
// 单独给取出第一个点
let headPoint = new ol.geom.Point(coordinates[coordinates.length - 1]);
vectorContext.setStyle(headOuterImageStyle);
vectorContext.drawGeometry(headPoint);
vectorContext.setStyle(headInnerImageStyle);
vectorContext.drawGeometry(headPoint);
map.render();
});

render

在绘制完成以后,调取map对象的render方法,绘制下一帧。同样当下一帧绘制完成以后,又会触发 postrender的事件继续绘制然后render。由于每一帧的frameState.time不同,所以圆点的位置也不同,最终实现了动画的效果。

圆点位置计算

前面写的都是OpenLayers相关的内容,在圆点位置计算方面,就是一个纯数学过程了。如下所示,最开始是先定义了几个变量。在这几个变量中,最关键的是theta,它代表的是当前帧的第一个点的弧度,说起来比较拗口,其实是先用event.frameState.time求出当前帧的时间戳,然后除以omegaTheta(旋转一个周期的时间长度),得到的就是当前帧是第多少个周期,再乘2 * Math.PI,就得到了当前帧的弧度。

1
2
3
4
5
6
7
8
9
const n = 200; // 旋转点的个数
const omegaTheta = 60000; // 旋转一个周期用的时间,ms
// 几个长度 m
const R = 7e6; // 类似公转
const r = 2e6; // 类似自转
const p = 2e6; // 一个基本长度常量

let theta = 2 * Math.PI * frameState.time / omegaTheta; // 计算当前帧弧度
let coordinates = []; // 存放每个点位置的数组

在demo代码中的数组是像下面这样循环出来的,首先t不难理解,theta是当前帧的弧度,加上2 * Math.PI * i / n以后,也就是每一个点都偏移一定的弧度,形成一条连线。

1
2
3
4
5
6
for (let i = 0; i < n; ++i) {
let t = theta + 2 * Math.PI * i / n;
let x = (R + r) * Math.cos(t) + p * Math.cos((R + r) * t / r);
let y = (R + r) * Math.sin(t) + p * Math.sin((R + r) * t / r);
coordinates.push([x, y]);
}

这一块代码最难理解的地方是xy,所以可以把他分开来理解,先只取xy的前半部分:

1
2
3
4
5
6
for (let i = 0; i < n; ++i) {
let t = theta + 2 * Math.PI * i / n;
let x = (R + r) * Math.cos(t); // 只保留前半部分
let y = (R + r) * Math.sin(t); // 只保留前半部分
coordinates.push([x, y]);
}

结果的到的图片如下所示,可以发现,圆点排列组合成了一个很大的圆,类似于一种公转的效果:

接下来,再只取xy的后半部分:

1
2
3
4
5
6
for (let i = 0; i < n; ++i) {
let t = theta + 2 * Math.PI * i / n;
let x = p * Math.cos((R + r) * t / r); // 只保留后半部分
let y = p * Math.sin((R + r) * t / r); // 只保留后半部分
coordinates.push([x, y]);
}

得到的结果如下所示,跟保留前半部分不同的是,圆点虽然也排列成了圆圈,但是半径比较小,类似于一种自转的效果。所以当两者一结合,就会组合出demo中所示的混乱旋转的效果。

源码

最后贴出代码:

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
<!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>Dynamic Data(动态数据)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="map" class="map"></div>
<script type="text/javascript">
// 定义一个使用OSM源的瓦片图层
let tileLayer = new ol.layer.Tile({
source: new ol.source.OSM()
});
// 初始化地图
let map = new ol.Map({
layers: [tileLayer],
target: 'map',
view: new ol.View({
center: [0, 0],
zoom: 2
})
});
// 定义几种不同的样式
// imageStyle:每个普通圆
// headInnerImageStyle:开头的圆的内圈
// headOuterImageStyle:开头圆的外圈
let imageStyle = new ol.style.Style({
image: new ol.style.Circle({
radius: 5,
fill: new ol.style.Fill({ color: 'yellow' }),
stroke: new ol.style.Stroke({ color: 'red', width: 1 })
})
});

let headInnerImageStyle = new ol.style.Style({
image: new ol.style.Circle({
radius: 2,
fill: new ol.style.Fill({ color: 'blue' })
})
});

let headOuterImageStyle = new ol.style.Style({
image: new ol.style.Circle({
radius: 5,
fill: new ol.style.Fill({ color: 'black' })
})
});
// 定义几个常量
const n = 200; // 200个点
const omegaTheta = 60000; // 旋转时间,ms
// 几个长度 m
const R = 7e6;
const r = 2e6;
const p = 2e6;
// 回调
tileLayer.on('postrender', event => {
// 获取矢量上下文和当前帧
let vectorContext = ol.render.getVectorContext(event);
let frameState = event.frameState;
let theta = 2 * Math.PI * frameState.time / omegaTheta;
console.log(frameState.time, theta)
let coordinates = [];
for (let i = 0; i < n; ++i) {
let t = theta + 2 * Math.PI * i / n;
let x = (R + r) * Math.cos(t) + p * Math.cos((R + r) * t / r);
let y = (R + r) * Math.sin(t) + p * Math.sin((R + r) * t / r);
// var x = (R + r) * Math.cos(t)
// var y = (R + r) * Math.sin(t)
// var x = p * Math.cos((R + r) * t / r);
// var y = p * Math.sin((R + r) * t / r);
coordinates.push([x, y]);
}
vectorContext.setStyle(imageStyle);
vectorContext.drawGeometry(new ol.geom.MultiPoint(coordinates));
let headPoint = new ol.geom.Point(coordinates[coordinates.length - 1]);
vectorContext.setStyle(headOuterImageStyle);
vectorContext.drawGeometry(headPoint);
vectorContext.setStyle(headInnerImageStyle);
vectorContext.drawGeometry(headPoint);
map.render();
});
map.render();
</script>
</body>

</html>
👆 全文结束,棒槌时间到 👇