angularopenlayersopenlayers-3openlayers-6angular-openlayers

How we can add multiple animated arrow icon on button click


We are trying to add multiple animated arrow icon on single line string on button click like as when we click start button then add one animated arrow icon on line string and when we click again start button then add new animated arrow icon on these line string and keep running previous arrow.

Click Here for Image

import Feature from 'ol/Feature';
import LineString from 'ol/geom/LineString';
import Map from 'ol/Map';
import Point from 'ol/geom/Point';
import Polyline from 'ol/format/Polyline';
import VectorSource from 'ol/source/Vector';
import View from 'ol/View';
import OSM from 'ol/source/OSM';
import {Icon, Stroke, Style} from 'ol/style';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';
import {getVectorContext} from 'ol/render';

const center = [-5639523.95, -3501274.52];
const map = new Map({
  target: document.getElementById('map'),
  view: new View({
    center: center,
    zoom: 10,
    minZoom: 2,
    maxZoom: 19,
  }),
  layers: [
    new TileLayer({
      source: new OSM(), 
    }),
  ],
});

// The polyline string is read from a JSON similiar to those returned
// by directions APIs such as Openrouteservice and Mapbox.
fetch('data/polyline/route.json').then(function (response) {
  response.json().then(function (result) {
    const polyline = result.routes[0].geometry;

    const route = new Polyline({
      factor: 1e6,
    }).readGeometry(polyline, {
      dataProjection: 'EPSG:4326',
      featureProjection: 'EPSG:3857',
    });

    const routeFeature = new Feature({
      type: 'route',
      geometry: route,
    });
    const startMarker = new Feature({
      type: 'icon',
      geometry: new Point(route.getFirstCoordinate()),
    });
    const endMarker = new Feature({
      type: 'icon',
      geometry: new Point(route.getLastCoordinate()),
    });
    const position = startMarker.getGeometry().clone();
    const geoMarker = new Feature({
      type: 'geoMarker',
      geometry: position,
    });

    const styles = {
      route: new Style({
        stroke: new Stroke({
          width: 6,
          color: [237, 212, 0, 0.8],
        }),
      }),
      icon: new Style({
        image: new Icon({
          anchor: [0.5, 1],
          src: 'data/icon.png',
        }),
      }),
      geoMarker: new Style({
        image: new Icon({
          src:
            'https://cdn1.iconfinder.com/data/icons/basic-ui-elements-color-round/3/19-32.png',
          rotation: getAngleAt(route, 0) + Math.PI / 2,
        }),
      }),
    };

    const vectorLayer = new VectorLayer({
      source: new VectorSource({
        features: [routeFeature, startMarker, endMarker],
      }),
      style: function (feature) {
        return styles[feature.get('type')];
      },
    });
    map.addLayer(vectorLayer);

    const vectorLayer1 = new VectorLayer({
      source: new VectorSource({
        features: [geoMarker],
      }),
      style: function (feature) {
        return styles[feature.get('type')];
      },
    });

    const speedInput = document.getElementById('speed');
    const startButton = document.getElementById('start-animation');
    let animating = false;
    let distance = 0;
    let lastTime;

    function getAngleAt(lineString, distance) {
      const length = lineString.getLength();
      const coordinates = lineString.getCoordinates();
      for (let i = 1, len = coordinates.length; i <= len; ++i) {
        if (
          new LineString(coordinates.slice(0, i + 1)).getLength() >=
          length * distance
        ) {
          return -Math.atan2(
            coordinates[i][1] - coordinates[i - 1][1],
            coordinates[i][0] - coordinates[i - 1][0]
          );
        }
      }
    }

    function moveFeature(event) {
      const speed = Number(speedInput.value);
      const time = event.frameState.time;
      const elapsedTime = time - lastTime;
      distance = (distance + (speed * elapsedTime) / 1e6) % 2;
      lastTime = time;
      const lineDistance = distance / 2 > 1 ? (2 - distance) / 2 : distance / 2;
      const direction = distance / 2 > 1 ? -Math.PI / 2 : Math.PI / 2;
      const currentCoordinate = route.getCoordinateAt(lineDistance);
      const angle = getAngleAt(route, lineDistance) + direction;
      styles.geoMarker.getImage().setRotation(angle);
      position.setCoordinates(currentCoordinate);
      const vectorContext = getVectorContext(event);
      vectorContext.setStyle(styles.geoMarker);
      vectorContext.drawGeometry(position);
      // tell OpenLayers to continue the postrender animation
      map.render();
    }

    function startAnimation() {
      animating = true;
      lastTime = Date.now();
      vectorLayer.on('postrender', moveFeature);
      // hide geoMarker and trigger map render through change event
      geoMarker.setGeometry(null);
    }


    startButton.addEventListener('click', function () {
      if (animating) {
        startAnimation();
      } else {
        map.addLayer(vectorLayer1);
        startAnimation();
      }
    });
  });
});

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Marker 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>
    <label for="speed">
      speed:&nbsp;
      <input id="speed" type="range" min="10" max="999" step="10" value="60">
    </label>
    <button id="start-animation">Start Animation</button>
    <!-- Pointer events polyfill for old browsers, see https://caniuse.com/#feat=pointer -->
    <script src="https://unpkg.com/elm-pep@1.0.6/dist/elm-pep.js"></script>
    <script type="module" src="main.js"></script>
  </body>
</html>

We try this code for add animated arrow icon on button click but we not getting any solution for how to add this.

Please see my image for what I want to.


Solution

  • The simplest way would be to add to an array of times and distances each time a new arrow is launched, then you can move each one on in a loop.

    const lastTimes = [];
    function moveFeature(event) {
      const speed = Number(speedInput.value);
      const time = event.frameState.time;
      for (let i = 0, ii = lastTimes.length; i < ii; ++i) {
        let {lastTime, distance} = lastTimes[i];
        const elapsedTime = time - lastTime;
        distance = (distance + (speed * elapsedTime) / 1e6) % 2;
        lastTime = time;
        lastTimes[i] = {lastTime, distance};
        const lineDistance = distance > 1 ? 2 - distance : distance;
        const direction = distance > 1 ? -Math.PI / 2 : Math.PI / 2;
        const currentCoordinate = route.getCoordinateAt(lineDistance);
        const angle = getAngleAt(route, lineDistance) + direction;
        styles.geoMarker.getImage().setRotation(angle);
        position.setCoordinates(currentCoordinate);
        const vectorContext = getVectorContext(event);
        vectorContext.setStyle(styles.geoMarker);
        vectorContext.drawGeometry(position);
      }
      // tell OpenLayers to continue the postrender animation
      map.render();
    }
    
    function startAnimation() {
      lastTimes.push({lastTime: Date.now(), distance: 0});
      if (!animating) {
        animating = true;
        //startButton.textContent = 'Stop Animation';
        vectorLayer.on('postrender', moveFeature);
        // hide geoMarker and trigger map render through change event
        geoMarker.setGeometry(null);
      }
    }
    

    https://codesandbox.io/s/feature-move-animation-forked-t2e6et

    You might also want to remove entries which have reach the end of the line?