原文链接及内容

运行界面

此示例展示了维也纳的补贴光伏设施的开放数据。不同的样式函数用于显示聚类、单个要素、聚类的凸包以及重叠要素的展开视图。鼠标悬停在聚类上时显示其凸包。单击聚类将视图缩放至其包含的要素的范围。单击非常靠近的要素组成的聚类会显示这些要素的扩展视图,沿聚类周围的圆形排列。

根据光伏装置的功率不同,要素的样式也有所不同。

注:

  • convex hull of a cluster中的convex hull是凸包,指一个点集的最小凸多边形,能够包含所有点且边界为凸形。常用于描述点集的外围轮廓。这里用到了一个用于绘制二维点集凸包的第三方算法库monotone-chain-convex-hull
  • cluster这里可以翻译为簇或聚类。

main.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
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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
// 绘制二维点集凸包用到的算法
import monotoneChainConvexHull from 'monotone-chain-convex-hull';
import Feature from 'ol/Feature.js';
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import {createEmpty, extend, getHeight, getWidth} from 'ol/extent.js';
import GeoJSON from 'ol/format/GeoJSON.js';
import LineString from 'ol/geom/LineString.js';
import Point from 'ol/geom/Point.js';
import Polygon from 'ol/geom/Polygon.js';
import TileLayer from 'ol/layer/Tile.js';
import VectorLayer from 'ol/layer/Vector.js';
import {fromLonLat} from 'ol/proj.js';
import Cluster from 'ol/source/Cluster.js';
import ImageTile from 'ol/source/ImageTile.js';
import VectorSource from 'ol/source/Vector.js';
import CircleStyle from 'ol/style/Circle.js';
import Fill from 'ol/style/Fill.js';
import Icon from 'ol/style/Icon.js';
import Stroke from 'ol/style/Stroke.js';
import Style from 'ol/style/Style.js';
import Text from 'ol/style/Text.js';

const circleDistanceMultiplier = 1;
const circleFootSeparation = 28;
const circleStartAngle = Math.PI / 2;

const convexHullFill = new Fill({
color: 'rgba(255, 153, 0, 0.4)',
});
const convexHullStroke = new Stroke({
color: 'rgba(204, 85, 0, 1)',
width: 1.5,
});
const outerCircleFill = new Fill({
color: 'rgba(255, 153, 102, 0.3)',
});
const innerCircleFill = new Fill({
color: 'rgba(255, 165, 0, 0.7)',
});
const textFill = new Fill({
color: '#fff',
});
const textStroke = new Stroke({
color: 'rgba(0, 0, 0, 0.6)',
width: 3,
});
const innerCircle = new CircleStyle({
radius: 14,
fill: innerCircleFill,
});
const outerCircle = new CircleStyle({
radius: 20,
fill: outerCircleFill,
});
const darkIcon = new Icon({
src: 'data/icons/emoticon-cool.svg',
});
const lightIcon = new Icon({
src: 'data/icons/emoticon-cool-outline.svg',
});

/**
* 单个要素的样式, 适合具有1个要素的聚类或聚类圆
* @param {Feature} clusterMember 聚类中某个要素
* @return {Style} 聚类中成员位置的图标样式
*/
function clusterMemberStyle(clusterMember) {
return new Style({
geometry: clusterMember.getGeometry(),
image: clusterMember.get('LEISTUNG') > 5 ? darkIcon : lightIcon,
});
}

let clickFeature, clickResolution;
/**
* 该样式用于具有彼此太接近的要素的聚类,在单击时激活。
* @param {Feature} cluster 具有重叠成员的聚类
* @param {number} resolution 当前视图的分辨率
* @return {Array<Style>|null} 用于呈现聚类成员的展开视图的样式。
*/
function clusterCircleStyle(cluster, resolution) {
if (cluster !== clickFeature || resolution !== clickResolution) {
return null;
}
const clusterMembers = cluster.get('features');
const centerCoordinates = cluster.getGeometry().getCoordinates();
return generatePointsCircle(
clusterMembers.length,
cluster.getGeometry().getCoordinates(),
resolution,
).reduce((styles, coordinates, i) => {
const point = new Point(coordinates);
const line = new LineString([centerCoordinates, coordinates]);
styles.unshift(
new Style({
geometry: line,
stroke: convexHullStroke,
}),
);
styles.push(
clusterMemberStyle(
new Feature({
...clusterMembers[i].getProperties(),
geometry: point,
}),
),
);
return styles;
}, []);
}

/**
* 参考的Leaflet的实现:
* https://github.com/Leaflet/Leaflet.markercluster/blob/31360f2/src/MarkerCluster.Spiderfier.js#L55-L72
* 将点围绕聚类中心排列成一个圆圈,并从中心指向每个点的一条线。
* @param {number} count 聚类成员的数量
* @param {Array<number>} clusterCenter 聚类的中心坐标
* @param {number} resolution 当前地图视图的分辨率
* @return {Array<Array<number>>} 表示聚类成员的坐标数组。
*/
function generatePointsCircle(count, clusterCenter, resolution) {
const circumference =
circleDistanceMultiplier * circleFootSeparation * (2 + count);
let legLength = circumference / (Math.PI * 2); //通过周长计算半径
const angleStep = (Math.PI * 2) / count;
const res = [];
let angle;

legLength = Math.max(legLength, 35) * resolution; // 到达聚类图标之外的最小距离

for (let i = 0; i < count; ++i) {
// 顺时针方向,像螺旋一样。
angle = circleStartAngle + i * angleStep;
res.push([
clusterCenter[0] + legLength * Math.cos(angle),
clusterCenter[1] + legLength * Math.sin(angle),
]);
}

return res;
}

let hoverFeature;
/**
* 聚类的凸包样式,鼠标悬停时激活。
* @param {Feature} cluster 聚类要素
* @return {Style|null} 聚类凸包的 Polygon 样式。
*/
function clusterHullStyle(cluster) {
if (cluster !== hoverFeature) {
return null;
}
const originalFeatures = cluster.get('features');
const points = originalFeatures.map((feature) =>
feature.getGeometry().getCoordinates(),
);
return new Style({
geometry: new Polygon([monotoneChainConvexHull(points)]),
fill: convexHullFill,
stroke: convexHullStroke,
});
}

function clusterStyle(feature) {
const size = feature.get('features').length;
if (size > 1) {
return [
new Style({
image: outerCircle,
}),
new Style({
image: innerCircle,
text: new Text({
text: size.toString(),
fill: textFill,
stroke: textStroke,
}),
}),
];
}
const originalFeature = feature.get('features')[0];
return clusterMemberStyle(originalFeature);
}

const vectorSource = new VectorSource({
format: new GeoJSON(),
url: 'data/geojson/photovoltaic.json',
});

const clusterSource = new Cluster({
attributions:
'Data: <a href="https://www.data.gv.at/auftritte/?organisation=stadt-wien">Stadt Wien</a>',
distance: 35,
source: vectorSource,
});

// 显示悬停聚类的凸包的图层。
const clusterHulls = new VectorLayer({
source: clusterSource,
style: clusterHullStyle,
});

// 显示聚类和单个要素的图层。
const clusters = new VectorLayer({
source: clusterSource,
style: clusterStyle,
});

// 显示重叠聚类成员的展开视图的图层。
const clusterCircles = new VectorLayer({
source: clusterSource,
style: clusterCircleStyle,
});

const raster = new TileLayer({
source: new ImageTile({
attributions:
'Base map: <a target="_blank" href="https://basemap.at/">basemap.at</a>',
url: 'https://maps{1-4}.wien.gv.at/basemap/bmapgrau/normal/google3857/{z}/{y}/{x}.png',
}),
});

const map = new Map({
layers: [raster, clusterHulls, clusters, clusterCircles],
target: 'map',
view: new View({
center: [0, 0],
zoom: 2,
maxZoom: 19,
extent: [
...fromLonLat([16.1793, 48.1124]),
...fromLonLat([16.5559, 48.313]),
],
showFullExtent: true,
}),
});

map.on('pointermove', (event) => {
clusters.getFeatures(event.pixel).then((features) => {
if (features[0] !== hoverFeature) {
// 鼠标悬停(聚类)时显示凸包。
hoverFeature = features[0];
clusterHulls.setStyle(clusterHullStyle);
// 更改鼠标光标样式以指示聚类可单击。
map.getTargetElement().style.cursor =
hoverFeature && hoverFeature.get('features').length > 1
? 'pointer'
: '';
}
});
});

map.on('click', (event) => {
clusters.getFeatures(event.pixel).then((features) => {
if (features.length > 0) {
const clusterMembers = features[0].get('features');
if (clusterMembers.length > 1) {
// 计算聚类成员的范围。
const extent = createEmpty();
clusterMembers.forEach((feature) =>
extend(extent, feature.getGeometry().getExtent())
);
const view = map.getView();
const resolution = map.getView().getResolution();
if (
view.getZoom() === view.getMaxZoom() ||
(getWidth(extent) < resolution && getHeight(extent) < resolution)
) {
// 显示聚类成员的展开视图。
clickFeature = features[0];
clickResolution = resolution;
clusterCircles.setStyle(clusterCircleStyle);
} else {
// 缩放至聚类成员的范围。
view.fit(extent, {duration: 500, padding: [50, 50, 50, 50]});
}
}
}
});
});

界面布局文件index.html代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dynamic clusters</title>
<link rel="stylesheet" href="node_modules/ol/ol.css">
<style>
.map {
width: 100%;
height: 400px;
}
</style>
</head>
<body>
<div id="map" class="map"></div>

<script type="module" src="main.js"></script>
</body>
</html>