0%

OpenLayers6实例分析:Earthquake Clusters(地震点聚合)

分析 Earthquake Clusters 这个 demo,官网介绍是:

This example parses a KML file and renders the features as clusters on a vector layer. The styling in this example is quite involved. Single earthquake locations (rendered as stars) have a size relative to their magnitude. Clusters have an opacity relative to the number of features in the cluster, and a size that represents the extent of the features that make up the cluster. When clicking or hovering on a cluster, the individual features that make up the cluster will be shown.

To achieve this, we make heavy use of style functions.

本示例将解析 KML 文件并在矢量图层上将要素渲染成聚合点。在这个示例中的样式非常复杂。单个的地震点(被渲染成了星状)的大小与震级相关。聚合点的透明度与聚合点内要素的数量相关,并且聚合点的大小也代表着聚合点内要素的四至。当在聚合点上点击或者悬浮时,组成聚合点的各个要素就会被展示。

由于上述原因,我们大量使用了样式函数。

Earthquake Clusters

定义基本结构

地图的基本结构如下所示,raster 图层使用的是普通的 toner 碳粉地图,不一样的地方是 vector 矢量图层的定义。在 source 中使用了ol.source.Cluster,这个就是 OpenLayers 官方的点聚合源,distance 属性判定两个聚合点之间最小像素距离。在source 中的 source 属性里,ol.source.Vectorurl 属性是数据源的链接,format 属性是根据数据源格式来设定。

这里最关键的是 vectorstyle 属性,样式是从 styleFunction 函数中返回的,这是整个地震点聚合的核心。

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
<!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>Earthquake Clusters(地震点聚合)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="map" class="map"></div>
<script type="text/javascript">
// 矢量图层
vector = new ol.layer.Vector({
// 数据源使用的是ol.source.Cluster
source: new ol.source.Cluster({
// 两个聚合点之间的最小像素距离
distance: 40,
source: new ol.source.Vector({
url:
"https://openlayers.org/en/latest/examples/data/kml/2012_Earthquakes_Mag5.kml",
format: new ol.format.KML({
// 不从KML文件中提取样式
extractStyles: false
})
})
}),
// 样式从样式函数中返回
style: styleFunction
});
// 普通的碳粉地图
let raster = new ol.layer.Tile({
source: new ol.source.Stamen({
layer: "toner"
})
});

let map = new ol.Map({
layers: [raster, vector],
interactions: ol.interaction.defaults().extend([
new ol.interaction.Select({
condition: evt => {
return evt.type == "pointermove" || evt.type == "singleclick";
},
// 被选择到的要素的样式
style: selectStyleFunction
})
]),
target: "map",
view: new ol.View({
center: [0, 0],
zoom: 2
})
});
</script>
</body>
</html>

地图中还有一个不一样的地方是,数据源使用 KML 格式。KML 格式属于 XML 规范,文件里面是各种标签,笼统地说,在解析的时候标签名就是键名,标签里面的内容就是键值,如下所示,name 标签中存放就是地震震级、时间、地理位置等信息,Point 下的 coordinates 存放的是经纬度信息。

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
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://earth.google.com/kml/2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<Document>
<name>2012 Earthquakes, Magnitude 5</name>
<atom:author>
<atom:name>U.S. Geological Survey</atom:name>
</atom:author>
<atom:link href="http://earthquake.usgs.gov"/>
<Folder>
<name>Magnitude 5</name>
<Placemark id="2012 Jan 15 13:40:16.40 UTC">
<name>M 5.9 - 2012 Jan 15, SOUTH SHETLAND ISLANDS</name>
<magnitude>5.9</magnitude>
<Point>
<coordinates>-56.072,-60.975,0</coordinates>
</Point>
</Placemark>
<Placemark id="2012 Jan 19 06:48:48.75 UTC">
<name>M 5.9 - 2012 Jan 19, OFF W. COAST OF S. ISLAND, N.Z.</name>
<magnitude>5.9</magnitude>
<Point>
<coordinates>165.778,-46.686,0</coordinates>
</Point>
</Placemark>
...以下省略
</Folder>
</Document>
</kml>

普通地震点样式

未聚合的普通地震点是一个五角星,五角星的尺寸与该点的地震震级大小正相关,如图所示。

star

具体实现时,是先创建好地震点的 fillstroke 样式,再给每个点使用一个 createEarthquakeStyle 样式函数返回 style。通过裁剪字符串取到了地震震级后转换为 radius,震级越大,radius 越大。

最后返回ol.style.Style的时候,geometry中存放位置信息,image中存放一个ol.style.RegularShape对象。这个 RegularShape 对象是 OpenLayers 为了便于快速绘制规则图形而加入的,把 radiusearthquakeFillearthquakeStroke 传进去后就能返回一个完备的五角星 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 地震点fill样式
let earthquakeFill = new ol.style.Fill({
color: "rgba(255, 153, 0, 0.8)"
});
// 地震点stroke样式
let earthquakeStroke = new ol.style.Stroke({
color: "rgba(255, 204, 0, 0.2)",
width: 1
});
/**
* @description: 创建地震点的样式
* @param {Object} feature 地震点的要素
* @return: {Object} new ol.style.Style 地震点的样式
*/
const createEarthquakeStyle = feature => {
// name标签中存放的内容如下:<name>M 5.9 - 2012 Jan 19, OFF W. COAST OF S. ISLAND, N.Z.</name>
// 所以name存放的字符串如下:M 5.9 - 2012 Jan 19, OFF W. COAST OF S. ISLAND, N.Z.
let name = feature.get("name");
// magnitude被转换成浮点数如下:5.9
let magnitude = parseFloat(name.substr(2));
let radius = 5 + 20 * (magnitude - 5);
// 该要素对应五角星的样式
return new ol.style.Style({
geometry: feature.getGeometry(),
image: new ol.style.RegularShape({
// 内半径
radius1: radius,
// 外半径
radius2: 3,
// 5角形
points: 5,
// 旋转角度
angle: Math.PI,
// fill和stroke样式
fill: earthquakeFill,
stroke: earthquakeStroke
})
});
};

计算地震点聚合信息

先声明一个 maxFeatureCount 存放最多的聚合 cluster 中聚合地震点的数目,主要目的是用后后面计算聚合圆圈的透明度。获取到图层的的聚合点要素数组以后,迭代并在每个聚合点要素中获取原始的要素信息。

原始的要素信息主要有两个作用,一个是计算最大的聚合点中的原始要素数目,这个数目跟聚合点的透明度有关系,第二个是计算每个聚合点的四至,利用这个四至和 resolution(每个像素代表多少米),就能计算聚合点的半径大小。

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
// 最多的聚合数,便于计算聚合圆圈的透明度
let maxFeatureCount;
// 矢量图层
let vector = null;
/**
* @description: 计算聚合后的信息
* @param {type} resolution 分辨率,一个像素代表多少米
* @return: null
*/
const calculateClusterInfo = resolution => {
maxFeatureCount = 0;
// 获取图层中所有要素
let features = vector.getSource().getFeatures();
let feature, radius;
for (let i = features.length - 1; i >= 0; --i) {
feature = features[i];
// 获取第i个要素中的所有features
let originalFeatures = feature.get("features");
// 创建一个空的地图范围,四至,其实是一个数组
let extent = ol.extent.createEmpty();
// void 0 返回undefined
let j = void 0,
jj = void 0;
console.log(j, jj);
for (j = 0, jj = originalFeatures.length; j < jj; ++j) {
// 不断地循环让四至变大
ol.extent.extend(extent, originalFeatures[j].getGeometry().getExtent());
}
maxFeatureCount = Math.max(maxFeatureCount, jj);
// 半径是根据四至来算出来的
radius =
(0.25 * (ol.extent.getWidth(extent) + ol.extent.getHeight(extent))) /
resolution;
feature.set("radius", radius);
}
};

四至

四至是地理学的名词,笼统说就是包含一个要素的最小矩形范围,可以用一个数组来表示[minx, miny, maxx, maxy]

在迭代计算聚合点的四至时,是先ol.extent.createEmpty()创建了一个空的四至,再使用ol.extent.extend()使范围不断扩大,最终使 extent 中包含整个聚合点所有原始要素的四至。最后在计算半径的时候,获取 extent 的长度和宽度,除以 resolution 就能转变为像素数,就可以计算出像素半径。

styleFunction 与 selectStyleFunction

styleFunction

做好了前文的各项准备之后,就能构造矢量图层的样式返回函数 styleFunction,这个函数可以获取到两个入参 featureresolution

resolution 使用的时候首先判断和之前保存的 currentResolution 是否一致,也就是判断有没有缩小或者放大地图。如果不一致需要调用 calculateClusterInfo 函数。重新来计算聚合点的原始要素数目和四至,也就是聚合点的透明度和半径大小。

feature 使用的时候首先要判断聚合点要素的原始要素数目。如果等于 1 就代表只有一个原始要素,就正常返回 createEarthquakeStyle(originalFeature),也就是在地图上直接绘制一个五角星。

如果大于 1,就需要返回聚合点要素的样式,看代码可以发现,聚合点样式是使用imagetext 来构造。image 构造出了一个圆圈,半径直接取 calculateClusterInfo 函数设置给要素的 radiusrgba 颜色中的透明度是使用 maxFeatureCount 计算得出。text 是圆圈上的文字,文字内容是每个聚合点要素的原始要素数目。

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
// 文本fill样式
let textFill = new ol.style.Fill({
color: "#fff"
});
// 文本stroke样式
let textStroke = new ol.style.Stroke({
color: "rgba(0, 0, 0, 0.6)",
width: 3
});
// 保存上次调用时的resolution
let currentResolution;
/**
* @description: 普通样式的返回函数
* @param {type}
* @return:
*/
const styleFunction = (feature, resolution) => {
if (resolution != currentResolution) {
calculateClusterInfo(resolution);
currentResolution = resolution;
}
let style;
// 这里的fill颜色的透明度通过当前要素的features数目size与maxFeatureCount的比值计算得到,越少越透明
let size = feature.get("features").length;
if (size > 1) {
style = new ol.style.Style({
image: new ol.style.Circle({
radius: feature.get("radius"),
fill: new ol.style.Fill({
color: [255, 153, 0, Math.min(0.8, 0.4 + size / maxFeatureCount)]
})
}),
// 文本样式
text: new ol.style.Text({
text: size.toString(),
fill: textFill,
stroke: textStroke
})
});
} else {
// 如果只有一个要素,就返回普通地震点的样式
let originalFeature = feature.get("features")[0];
style = createEarthquakeStyle(originalFeature);
}
return style;
};

selectStyleFunction

这个 selectStyleFunction 样式函数中返回的是当鼠标悬浮或者单击聚合点时的样式。在 invisibleFill 能够发现其实是把聚合点圆圈和文本的透明度设置成了 0.01,接近透明。

聚合点中原始要素的显示,是迭代每一个原始要素,使用 createEarthquakeStyle 函数 push 出一个 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
25
26
27
28
29
// 隐藏状态时的fill样式
let invisibleFill = new ol.style.Fill({
color: "rgba(255, 255, 255, 0.01)"
});

/**
* @description: 地震聚合点被选中时的样式
* @param {Object} feature 要素
* @return: styles 被选中后的样式数组
*/
const selectStyleFunction = feature => {
// fill填充为透明
let styles = [
new ol.style.Style({
image: new ol.style.Circle({
radius: feature.get("radius"),
fill: invisibleFill
})
})
];
let originalFeatures = feature.get("features");
let originalFeature;
for (let i = originalFeatures.length - 1; i >= 0; --i) {
originalFeature = originalFeatures[i];
// 把每个feature点的样式push给数组
styles.push(createEarthquakeStyle(originalFeature));
}
return styles;
};

全部代码

最后贴存出全部代码:

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
<!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>Earthquake Clusters(地震点聚合)</title>
<style>
html,
body,
.map {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="map" class="map"></div>
<script type="text/javascript">
// 地震点fill样式
let earthquakeFill = new ol.style.Fill({
color: "rgba(255, 153, 0, 0.8)"
});
// 地震点stroke样式
let earthquakeStroke = new ol.style.Stroke({
color: "rgba(255, 204, 0, 0.2)",
width: 1
});
// 文本fill样式
let textFill = new ol.style.Fill({
color: "#fff"
});
// 文本stroke样式
let textStroke = new ol.style.Stroke({
color: "rgba(0, 0, 0, 0.6)",
width: 3
});
// 隐藏状态时的fill样式
let invisibleFill = new ol.style.Fill({
color: "rgba(255, 255, 255, 0.01)"
});
/**
* @description: 创建地震点的样式
* @param {Object} feature 地震点的要素
* @return: {Object} new ol.style.Style 地震点的样式
*/
const createEarthquakeStyle = feature => {
// name标签中存放的内容如下:<name>M 5.9 - 2012 Jan 19, OFF W. COAST OF S. ISLAND, N.Z.</name>
// 所以name存放的字符串如下:M 5.9 - 2012 Jan 19, OFF W. COAST OF S. ISLAND, N.Z.
let name = feature.get("name");
// magnitude被转换成浮点数如下:5.9
let magnitude = parseFloat(name.substr(2));
let radius = 5 + 20 * (magnitude - 5);
// 该要素对应五角星的样式
return new ol.style.Style({
geometry: feature.getGeometry(),
image: new ol.style.RegularShape({
radius1: radius,
radius2: 3,
points: 5,
angle: Math.PI,
fill: earthquakeFill,
stroke: earthquakeStroke
})
});
};
// 最多的聚合数,便于计算聚合圆圈的透明度
let maxFeatureCount;
// 矢量图层
let vector = null;
/**
* @description: 计算聚合后的信息
* @param {type} resolution 分辨率,一个像素代表多少米
* @return: null
*/
const calculateClusterInfo = resolution => {
maxFeatureCount = 0;
// 获取图层中所有要素
let features = vector.getSource().getFeatures();
let feature, radius;
for (let i = features.length - 1; i >= 0; --i) {
feature = features[i];
// 获取第i个要素中的所有features
let originalFeatures = feature.get("features");
// 创建一个空的地图范围,四至,其实是一个数组
let extent = ol.extent.createEmpty();
// void 0 返回undefined
let j = void 0,
jj = void 0;
console.log(j, jj);
for (j = 0, jj = originalFeatures.length; j < jj; ++j) {
// 不断地循环让四至变大
ol.extent.extend(
extent,
originalFeatures[j].getGeometry().getExtent()
);
}
// 取
maxFeatureCount = Math.max(maxFeatureCount, jj);
console.log(resolution);
// 半径是根据四至来算出来的
radius =
(0.25 *
(ol.extent.getWidth(extent) + ol.extent.getHeight(extent))) /
resolution;
feature.set("radius", radius);
}
};

// 保存上次调用时的resolution
let currentResolution;
/**
* @description: 普通样式的返回函数
* @param {type}
* @return:
*/
const styleFunction = (feature, resolution) => {
if (resolution != currentResolution) {
calculateClusterInfo(resolution);
currentResolution = resolution;
}
let style;
// 这里的fill颜色的透明度通过当前要素的features数目size与maxFeatureCount的比值计算得到,越少越透明
let size = feature.get("features").length;
if (size > 1) {
style = new ol.style.Style({
image: new ol.style.Circle({
radius: feature.get("radius"),
fill: new ol.style.Fill({
color: [
255,
153,
0,
Math.min(0.8, 0.4 + size / maxFeatureCount)
]
})
}),
// 文本样式
text: new ol.style.Text({
text: size.toString(),
fill: textFill,
stroke: textStroke
})
});
} else {
// 如果只有一个要素,就返回普通地震点的样式
let originalFeature = feature.get("features")[0];
style = createEarthquakeStyle(originalFeature);
}
return style;
};
/**
* @description: 地震聚合点被选中时的样式
* @param {Object} feature 要素
* @return: styles 被选中后的样式数组
*/
const selectStyleFunction = feature => {
// fill填充为透明
let styles = [
new ol.style.Style({
image: new ol.style.Circle({
radius: feature.get("radius"),
fill: invisibleFill
})
})
];
let originalFeatures = feature.get("features");
let originalFeature;
for (let i = originalFeatures.length - 1; i >= 0; --i) {
originalFeature = originalFeatures[i];
// 把每个feature点的样式push给数组
styles.push(createEarthquakeStyle(originalFeature));
}
return styles;
};
// 矢量图层
vector = new ol.layer.Vector({
// 数据源使用的是ol.source.Cluster
source: new ol.source.Cluster({
// 两个聚合点之间的最小像素距离
distance: 40,
source: new ol.source.Vector({
url:
"https://openlayers.org/en/latest/examples/data/kml/2012_Earthquakes_Mag5.kml",
format: new ol.format.KML({
// 不从KML文件中提取样式
extractStyles: false
})
})
}),
// 样式从样式函数中返回
style: styleFunction
});
// 普通的碳粉地图
let raster = new ol.layer.Tile({
source: new ol.source.Stamen({
layer: "toner"
})
});

let map = new ol.Map({
layers: [raster, vector],
interactions: ol.interaction.defaults().extend([
new ol.interaction.Select({
condition: evt => {
return evt.type == "pointermove" || evt.type == "singleclick";
},
// 被选择到的要素的样式
style: selectStyleFunction
})
]),
target: "map",
view: new ol.View({
center: [0, 0],
zoom: 2
})
});
</script>
</body>
</html>
👆 全文结束,棒槌时间到 👇