上篇在地图上绘制了动态的飞机航线,于是我想着,能不能加个飞机的图标跟着航线飞行。
在iconfont上下载一个飞机的svg图形,放在public的data/icons下面
因为图标需要随着航线的方向飞行,需要根据航线调整角度,因此在加载数据源的时候需要计算一下角度,绑在每个feature上。
//计算角度
const rotation = calculateRotation(from, to);
features.push(
new Feature({
geometry: line,
finished: false,
rotation: rotation,
})
);
function calculateRotation(from, to) {
const dx = to[0] - from[0];
const dy = to[1] - from[1];
return Math.atan2(dy, dx);
}
在航线绘制完成之后,添加一个飞机动画开始执行的标识flight,给feature设置一个初始的index值
if (elapsedPoints >= coords.length) {
feature.set("finished", true);
feature.set("flight", true);
feature.set("index", 0);
}
animateFlights会一直执行,所以我们利用这个特点来绘制循环的动画。绘制线的思路是取坐标数组的第0个到第n个,每毫秒绘制不同的线。绘制点的思路则是直接取第n个点,每毫秒绘制不同的点,并且在n大于等于坐标数组之后又让n重新等于0,以此来实现循环的动画。
if (feature.get("flight")) {
const frameState = event.frameState;
const coords = feature.getGeometry().getCoordinates();
let index = feature.get("index");
index += step;
if (index >= coords.length - 1) {
index = 0;
}
if (index < 0) {
index = 0;
}
feature.set("index", index);
style.getImage().setRotation(feature.get("rotation"));
vectorContext.setStyle(style);
const currentPoint = new Point(coords[Math.floor(index)]);
// 在当前和最近相邻的包裹世界中需要动画
const worldWidth = getWidth(
map.getView().getProjection().getExtent()
);
const offset = Math.floor(map.getView().getCenter()[0] / worldWidth);
//直接用矢量上下文绘制线条
//在平铺地图上绘制线段时,需要考虑地图的无限水平滚动特性。通过平移和多次绘制线段,确保即使用户滚动地图,线段也能正确显示在地图的两端。这个方法处理了跨越地图边界的线段,避免了图形被截断的问题。
currentPoint.translate(offset * worldWidth, 0);
vectorContext.drawGeometry(currentPoint);
currentPoint.translate(worldWidth, 0);
vectorContext.drawGeometry(currentPoint);
}
完整代码:
<template>
<div class="box">
<h1>External map</h1>
<div id="map"></div>
</div>
</template>
<script>
import Feature from "ol/Feature.js";
import { LineString, Point, Polygon } from "ol/geom.js";
import Map from "ol/Map.js";
import StadiaMaps from "ol/source/StadiaMaps.js";
import VectorSource from "ol/source/Vector.js";
import View from "ol/View.js";
import { Stroke, Style, Icon, Circle as CircleStyle, Fill } 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";
var arc = require("arc");
export default {
name: "",
components: {},
data() {
return {
map: null,
extentData: "",
};
},
computed: {},
created() {},
mounted() {
const tileLayer = new TileLayer({
source: new StadiaMaps({
layer: "outdoors",
}),
});
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,
}),
image: new Icon({
anchor: [0.5, 0.5],
src: "data/icons/flight.svg",
rotation: -0.19931501061749937,
}),
});
const flightsSource = new VectorSource({
attributions:
"Flight data by " +
'<a href="https://openflights.org/data.html">OpenFlights</a>,',
loader: function () {
const url =
"https://openlayers.org/en/latest/examples/data/openflights/flights.json";
fetch(url)
.then(function (response) {
return response.json();
})
.then(function (json) {
let flightsData = json.flights;
flightsData = flightsData.splice(26, 100);
for (let i = 0; i < flightsData.length; i++) {
const flight = flightsData[i];
const from = flight[0];
const to = flight[1];
// 在两个位置之间创建一个圆弧
const arcGenerator = new arc.GreatCircle(
{ x: from[1], y: from[0] },
{ x: to[1], y: to[0] }
);
//生成100个点 offset是偏移量
const arcLine = arcGenerator.Arc(100, { offset: 10 });
//计算角度
const rotation = calculateRotation(from, to);
//穿过-180°/+180°子午线的路径是分开的
//分成两个部分,按顺序设置动画
const features = [];
arcLine.geometries.forEach(function (geometry) {
const line = new LineString(geometry.coords);
//将 line 对象的坐标系从 WGS84(EPSG:4326)转换为 Web Mercator 投影(EPSG:3857)
line.transform("EPSG:4326", "EPSG:3857");
features.push(
new Feature({
geometry: line,
finished: false,
rotation: rotation,
})
);
});
// 动画延迟:使用i * 50来设置延迟是为了确保每条路径的动画不会同时启动,这样可以产生连续动画的效果。
addLater(features, i * 50);
}
//tileLayer 图层每次完成渲染之后调用
tileLayer.on("postrender", animateFlights);
});
},
});
let flag = false;
const flightsLayer = new VectorLayer({
source: flightsSource,
style: function (feature) {
//等动画完毕再现在最终的线样式
if (feature.get("finished")) {
return [
new Style({
stroke: new Stroke({
color: "#EAE911",
width: 2,
}),
}),
new Style({
image: new Icon({
anchor: [0.5, 0.5],
src: "data/icons/flight.svg",
rotation: feature.get("rotation"),
}),
}),
];
}
return null;
},
});
map.addLayer(flightsLayer);
const pointsPerMs = 0.05;
let duration = 2000;
let step = 0.5;
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")) {
// 只画动画尚未完成的线
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);
feature.set("flight", true);
feature.set("index", 0);
}
const maxIndex = Math.min(elapsedPoints, coords.length);
const currentLine = new LineString(coords.slice(0, maxIndex));
// 在当前和最近相邻的包裹世界中需要动画
const worldWidth = getWidth(
map.getView().getProjection().getExtent()
);
const offset = Math.floor(
map.getView().getCenter()[0] / worldWidth
);
//直接用矢量上下文绘制线条
//在平铺地图上绘制线段时,需要考虑地图的无限水平滚动特性。通过平移和多次绘制线段,确保即使用户滚动地图,线段也能正确显示在地图的两端。这个方法处理了跨越地图边界的线段,避免了图形被截断的问题。
currentLine.translate(offset * worldWidth, 0);
vectorContext.drawGeometry(currentLine);
currentLine.translate(worldWidth, 0);
vectorContext.drawGeometry(currentLine);
}
}
if (feature.get("flight")) {
const frameState = event.frameState;
const coords = feature.getGeometry().getCoordinates();
let index = feature.get("index");
index += step;
if (index >= coords.length - 1) {
index = 0;
}
if (index < 0) {
index = 0;
}
feature.set("index", index);
style.getImage().setRotation(feature.get("rotation"));
vectorContext.setStyle(style);
const currentPoint = new Point(coords[Math.floor(index)]);
// 在当前和最近相邻的包裹世界中需要动画
const worldWidth = getWidth(
map.getView().getProjection().getExtent()
);
const offset = Math.floor(map.getView().getCenter()[0] / worldWidth);
//直接用矢量上下文绘制线条
//在平铺地图上绘制线段时,需要考虑地图的无限水平滚动特性。通过平移和多次绘制线段,确保即使用户滚动地图,线段也能正确显示在地图的两端。这个方法处理了跨越地图边界的线段,避免了图形被截断的问题。
currentPoint.translate(offset * worldWidth, 0);
vectorContext.drawGeometry(currentPoint);
currentPoint.translate(worldWidth, 0);
vectorContext.drawGeometry(currentPoint);
}
}
//告诉OpenLayers继续动画
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);
}
function calculateRotation(from, to) {
const dx = to[0] - from[0];
const dy = to[1] - from[1];
return Math.atan2(dy, dx);
}
},
methods: {},
};
</script>
<style lang="scss" scoped>
#map {
width: 100%;
height: 500px;
}
.box {
height: 100%;
}
</style>