Marker Animation

Demonstrates how to move a feature along a line.

This example shows how to use postcompose and vectorContext to animate a (marker) feature along a line. In this example an encoded polyline is being used.

<!DOCTYPE html>
<html>
  <head>
    <title>Marker Animation</title>
    <link rel="stylesheet" href="https://openlayers.org/en/v5.2.0/css/ol.css" type="text/css">
    <!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
    <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList,URL"></script>

  </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>
    <script>
      import Feature from 'ol/Feature.js';
      import Map from 'ol/Map.js';
      import View from 'ol/View.js';
      import Polyline from 'ol/format/Polyline.js';
      import Point from 'ol/geom/Point.js';
      import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer.js';
      import BingMaps from 'ol/source/BingMaps.js';
      import VectorSource from 'ol/source/Vector.js';
      import {Circle as CircleStyle, Fill, Icon, Stroke, Style} from 'ol/style.js';

      // This long string is placed here due to jsFiddle limitations.
      // It is usually loaded with AJAX.
      var polyline = [
        '[email protected]`[email protected]}@[email protected]??aC^[email protected][email protected]@b[wFdE??wFfE}N',
        'fIoGxB_I\\gG}@eHoCyTmPqGaBaHOoD\\??yVrGotA|N??o[N_STiwAtEmHGeHcAkiA}^',
        'aMyBiHOkFNoI`CcVvM??gG^[email protected]??eCcA]OoL}DwFyCaCgCcCwDcGwHsSoX??wI_E',
        '[email protected][email protected]\\[email protected]@{vA}[email protected]}{@iRaqE{[email protected]_T{]_',
        '[email protected]{PmhEwaA{[email protected]]wQeEgtAsZ}LiCarAkVwI}D??_}[email protected]',
        '[email protected][email protected]@gV}TiYs[uTwXoNmT{[email protected]]{[email protected][email protected]_G}YsFw]k',
        '[email protected][email protected][email protected]|@[email protected][email protected]@[email protected]@[email protected]@{[email protected]',
        '[email protected]@[email protected][email protected]@[email protected]@{[email protected]}[email protected]@[email protected]~C{[email protected]',
        '[email protected]@kmBS{[email protected]_~QHeU`IuyDrC_}@[email protected]?qMbD}{AIkeAgB',
        'k_A_A{[email protected]@qH{[email protected]@qH{`@[email protected]@kL{[email protected]@ymBgwE}[email protected]__',
        '[email protected]@[email protected][email protected]}|[email protected]^eaC}L{dAaJ_aAiOyjByH{nAuYu`GsAw',
        '[email protected]{[email protected]}@[email protected]`CkiAbFkhBlTgdDdPyiB`W}xDnSa}DbJyhCrX',
        'itAhT}[email protected]}[email protected][email protected]}A~JovAxCqW~WanB`XewBbK{_A`K}[email protected]',
        'xBycBeCauBoF}}@[email protected][email protected]@cDs[eRaiBkQstAsQkcByNma',
        '[email protected][email protected]@eZcDwjBoGw`BoMegBaU_`[email protected][email protected]_',
        '[email protected]@_eC_UmlB}MmaBeWkkDeHwqAoX}~DcBsZmLcxBqOwqE_DkyAuJmrJ\\o',
        '~CfIewG|[email protected]}RorAoVajA_nAodD{[y`[email protected]@[email protected]{dAm',
        '[email protected]|@ojBwzDaaJsmBwbEgdCsrFqhAihDquAi`[email protected]}[email protected][email protected]_',
        'lKszAu|OmaA{wKm}@clHs_A_rEahCssKo\\[email protected]_wAyTwpBmPc|BwZknF',
        'oFscB_GsaDiZmyMyLgtHgQonHqT{hKaPg}[email protected][email protected]`EuiBudIabB{hF{[email protected]',
        'w`[email protected]~BkoAi}[email protected]@}`@oaXi_C}[email protected]|[email protected]]kgPcaAu}SkDw',
        '[email protected]\\[email protected]@siO{[ol\\kCmjMe\\[email protected]}EqiBaCg}',
        '@[email protected][email protected]I`[email protected]{[email protected]{[email protected]_yCu\\wyCwy',
        'A{[email protected]}rO{{[email protected]_bAumFo}DgqA_uByi',
        '@swC~AkzDlhA}xEvcBa}[email protected]@`rAo|@~bBq{@``[email protected]@[email protected]@nfC_eC',
        '|[email protected]}@``Fi~FpnAooC|[email protected]}[email protected]}[email protected]',
        '[email protected]{gCwGkgCc[[email protected][[email protected]@[email protected]@[email protected]@`[email protected]',
        '[email protected]`ExGuaBdEmbBpBssArAuqBBg}@[email protected]{AkB{[email protected]_bYmC}[email protected]@sPq_BuJ_',
        '[email protected]{X_{[email protected]{[email protected]@[email protected][email protected]@XcQ|@oNdCo',
        'SfFwXhEmOnLi\\lbAulB`[email protected]|[email protected]@[email protected]@bqC}{BhwDgcD`[email protected]@??bL{G|[email protected]@',
        'oS~][email protected]|[email protected]}Jv}[email protected]{[email protected]_]`|[email protected]@wSb{@[email protected]`RooQ~e',
        '[upZbuIolI|[email protected]@nMmJ|OeJn^{[email protected]@[email protected]@kAp~BkBxO{@|QsAfY',
        'gEtYiGd]}[email protected]`[email protected]@vgK}cJnSoSzQkVvUm^[email protected]`[email protected]\\[email protected]~k',
        'Dyq[[email protected]@[email protected]@[email protected]@neB}uBhqEesFjoGeyHtCoD|D}Ed|@ctAbIuOzqB',
        '_}D~NgY`\\[email protected][[email protected]{Cw`G`[email protected]{AdjAwzBh{C}`[email protected]@}[email protected]{[email protected]??jI',
        '[email protected]`CuOlC}YnAcV`@_^[email protected]}@[email protected]^uCkZiGk\\yGeY}[email protected][uWi[[email protected]',
        '[email protected]@[email protected]}qAwHkGi{@[email protected]_{B}IsJ',
        'uEeFymAssAkdAmhAyTcVkFeEoKiH}[email protected]@[email protected]@[email protected]@[email protected]@}EsFmG}Jk^[email protected][email protected]',
        '[email protected]@[email protected]@[email protected]@??kF}D??OL'
      ].join('');

      var route = /** @type {module:ol/geom/LineString~LineString} */ (new Polyline({
        factor: 1e6
      }).readGeometry(polyline, {
        dataProjection: 'EPSG:4326',
        featureProjection: 'EPSG:3857'
      }));

      var routeCoords = route.getCoordinates();
      var routeLength = routeCoords.length;

      var routeFeature = new Feature({
        type: 'route',
        geometry: route
      });
      var geoMarker = new Feature({
        type: 'geoMarker',
        geometry: new Point(routeCoords[0])
      });
      var startMarker = new Feature({
        type: 'icon',
        geometry: new Point(routeCoords[0])
      });
      var endMarker = new Feature({
        type: 'icon',
        geometry: new Point(routeCoords[routeLength - 1])
      });

      var 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 CircleStyle({
            radius: 7,
            fill: new Fill({color: 'black'}),
            stroke: new Stroke({
              color: 'white', width: 2
            })
          })
        })
      };

      var animating = false;
      var speed, now;
      var speedInput = document.getElementById('speed');
      var startButton = document.getElementById('start-animation');

      var vectorLayer = new VectorLayer({
        source: new VectorSource({
          features: [routeFeature, geoMarker, startMarker, endMarker]
        }),
        style: function(feature) {
          // hide geoMarker if animation is active
          if (animating && feature.get('type') === 'geoMarker') {
            return null;
          }
          return styles[feature.get('type')];
        }
      });

      var center = [-5639523.95, -3501274.52];
      var map = new Map({
        target: document.getElementById('map'),
        loadTilesWhileAnimating: true,
        view: new View({
          center: center,
          zoom: 10,
          minZoom: 2,
          maxZoom: 19
        }),
        layers: [
          new TileLayer({
            source: new BingMaps({
              imagerySet: 'AerialWithLabels',
              key: 'Your Bing Maps Key from http://www.bingmapsportal.com/ here'
            })
          }),
          vectorLayer
        ]
      });

      var moveFeature = function(event) {
        var vectorContext = event.vectorContext;
        var frameState = event.frameState;

        if (animating) {
          var elapsedTime = frameState.time - now;
          // here the trick to increase speed is to jump some indexes
          // on lineString coordinates
          var index = Math.round(speed * elapsedTime / 1000);

          if (index >= routeLength) {
            stopAnimation(true);
            return;
          }

          var currentPoint = new Point(routeCoords[index]);
          var feature = new Feature(currentPoint);
          vectorContext.drawFeature(feature, styles.geoMarker);
        }
        // tell OpenLayers to continue the postcompose animation
        map.render();
      };

      function startAnimation() {
        if (animating) {
          stopAnimation(false);
        } else {
          animating = true;
          now = new Date().getTime();
          speed = speedInput.value;
          startButton.textContent = 'Cancel Animation';
          // hide geoMarker
          geoMarker.setStyle(null);
          // just in case you pan somewhere else
          map.getView().setCenter(center);
          map.on('postcompose', moveFeature);
          map.render();
        }
      }


      /**
       * @param {boolean} ended end of animation.
       */
      function stopAnimation(ended) {
        animating = false;
        startButton.textContent = 'Start Animation';

        // if animation cancelled set the marker at the beginning
        var coord = ended ? routeCoords[routeLength - 1] : routeCoords[0];
        /** @type {module:ol/geom/Point~Point} */ (geoMarker.getGeometry())
          .setCoordinates(coord);
        //remove listener
        map.un('postcompose', moveFeature);
      }

      startButton.addEventListener('click', startAnimation, false);
    </script>
  </body>
</html>