上篇在地图上绘制了动态的飞机航线,于是我想着,能不能加个飞机的图标跟着航线飞行。
在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>