Edit

本示例演示使用 ´postrender´ 来实现航班飞行动画.

本示例演示使用 postrendervectorContext 实现航班飞行动画。 使用 arc.js 计算两个机场之间的一个大的圆弧,然后 使用 postrender 实现动画显示航班飞行路线。 航班飞行数据由 OpenFlights() 提供。 The flight data is provided by OpenFlights (使用了来自 Mapbox.js 文档 的简化版数据集).

main.js
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
<!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="./resources/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>
package.json
{
  "name": "flight-animation",
  "dependencies": {
    "ol": "7.3.0"
  },
  "devDependencies": {
    "vite": "^3.2.3"
  },
  "scripts": {
    "start": "vite",
    "build": "vite build"
  }
}