原文链接及内容

运行界面

这个例子展示了如何使用postrendervectorContext来制作飞行动画。具体地,使用到arc.js计算两个机场之间的大圆弧,然后使用postrender对飞行路径进行动画处理。航班数据由OpenFlights提供(使用了来自Mapbox.js文档中的简化数据集)。

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
import Feature from 'ol/Feature.js';
import LineString from 'ol/geom/LineString.js';
import Map from 'ol/Map.js';
import Stamen from 'ol/source/Stamen.js';
import VectorSource from 'ol/source/Vector.js';
import View from 'ol/View.js';
import {Stroke, Style} from 'ol/style.js';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer.js';
import {getVectorContext} from 'ol/render.js';
import {getWidth} from 'ol/extent.js';

const tileLayer = new TileLayer({
source: new Stamen({
layer: 'toner',
}),
});

const map = new Map({
layers: [tileLayer],
target: 'map',
view: new View({
center: [-11000000, 4600000],
zoom: 2,
}),
});

const style = new Style({
stroke: new Stroke({
color: '#EAE911',
width: 2,
}),
});

const flightsSource = new VectorSource({
attributions:
'Flight data by ' +
'<a href="https://openflights.org/data.html">OpenFlights</a>,',
loader: function () {
const url = 'data/openflights/flights.json';
fetch(url)
.then(function (response) {
return response.json();
})
.then(function (json) {
const flightsData = json.flights;
for (let i = 0; i < flightsData.length; i++) {
const flight = flightsData[i];
const from = flight[0];
const to = flight[1];

// create an arc circle between the two locations
const arcGenerator = new arc.GreatCircle(
{x: from[1], y: from[0]},
{x: to[1], y: to[0]}
);

const arcLine = arcGenerator.Arc(100, {offset: 10});
// paths which cross the -180°/+180° meridian are split
// into two sections which will be animated sequentially
const features = [];
arcLine.geometries.forEach(function (geometry) {
const line = new LineString(geometry.coords);
line.transform('EPSG:4326', 'EPSG:3857');

features.push(
new Feature({
geometry: line,
finished: false,
})
);
});
// add the features with a delay so that the animation
// for all features does not start at the same time
addLater(features, i * 50);
}
tileLayer.on('postrender', animateFlights);
});
},
});

const flightsLayer = new VectorLayer({
source: flightsSource,
style: function (feature) {
// if the animation is still active for a feature, do not
// render the feature with the layer style
if (feature.get('finished')) {
return style;
}
return null;
},
});

map.addLayer(flightsLayer);

const pointsPerMs = 0.02;
function animateFlights(event) {
const vectorContext = getVectorContext(event);
const frameState = event.frameState;
vectorContext.setStyle(style);

const features = flightsSource.getFeatures();
for (let i = 0; i < features.length; i++) {
const feature = features[i];
if (!feature.get('finished')) {
// only draw the lines for which the animation has not finished yet
const coords = feature.getGeometry().getCoordinates();
const elapsedTime = frameState.time - feature.get('start');
if (elapsedTime >= 0) {
const elapsedPoints = elapsedTime * pointsPerMs;

if (elapsedPoints >= coords.length) {
feature.set('finished', true);
}

const maxIndex = Math.min(elapsedPoints, coords.length);
const currentLine = new LineString(coords.slice(0, maxIndex));

// animation is needed in the current and nearest adjacent wrapped world
const worldWidth = getWidth(map.getView().getProjection().getExtent());
const offset = Math.floor(map.getView().getCenter()[0] / worldWidth);

// directly draw the lines with the vector context
currentLine.translate(offset * worldWidth, 0);
vectorContext.drawGeometry(currentLine);
currentLine.translate(worldWidth, 0);
vectorContext.drawGeometry(currentLine);
}
}
}
// tell OpenLayers to continue the animation
map.render();
}

function addLater(features, timeout) {
window.setTimeout(function () {
let start = Date.now();
features.forEach(function (feature) {
feature.set('start', start);
flightsSource.addFeature(feature);
const duration =
(feature.getGeometry().getCoordinates().length - 1) / pointsPerMs;
start += duration;
});
}, timeout);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Flight Animation</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>
<!-- Pointer events polyfill for old browsers, see https://caniuse.com/#feat=pointer -->
<script src="https://cdn.jsdelivr.net/npm/elm-pep@1.0.6/dist/elm-pep.js"></script>
<script src="https://api.mapbox.com/mapbox.js/plugins/arc.js/v0.1.0/arc.js"></script>
<script type="module" src="main.js"></script>
</body>
</html>