四十二、openlayers官网示例Flight Animation扩展——在地图上绘制飞机航线、飞机随航线飞行效果

 

上篇在地图上绘制了动态的飞机航线,于是我想着,能不能加个飞机的图标跟着航线飞行。

在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>

最近更新

  1. TCP协议是安全的吗?

    2024-06-09 03:02:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-09 03:02:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-09 03:02:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-09 03:02:02       20 阅读

热门阅读

  1. SELinux:安全增强型Linux

    2024-06-09 03:02:02       10 阅读
  2. 嵌入式C中Hex与Bin文件对比分析

    2024-06-09 03:02:02       11 阅读
  3. 数据结构学习笔记-二叉树

    2024-06-09 03:02:02       9 阅读
  4. Python使用Flask构建简单的web应用

    2024-06-09 03:02:02       11 阅读
  5. 深度学习 - softmax交叉熵损失计算

    2024-06-09 03:02:02       10 阅读
  6. 代码杂谈 之 pyspark如何做相似度计算

    2024-06-09 03:02:02       13 阅读
  7. B3928 [GESP202312 四级] 田忌赛马

    2024-06-09 03:02:02       9 阅读
  8. 发布处理方案 —— 前台项目构建与发布处理

    2024-06-09 03:02:02       9 阅读
  9. Tomcat 启动闪退问题解决方法

    2024-06-09 03:02:02       6 阅读