import Feature from 'ol/Feature.js';
import Map from 'ol/Map.js';
import Point from 'ol/geom/Point.js';
import Polyline from 'ol/format/Polyline.js';
import VectorSource from 'ol/source/Vector.js';
import View from 'ol/View.js';
import XYZ from 'ol/source/XYZ.js';
import {
Circle as CircleStyle,
Fill,
Icon,
Stroke,
Style,
} from 'ol/style.js';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer.js';
import {getVectorContext} from 'ol/render.js';
const key = 'Get your own API key at https://www.maptiler.com/cloud/';
const attributions =
'<a href="https://www.maptiler.com/copyright/" target="_blank">© MapTiler</a> ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap contributors</a>';
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 XYZ({
attributions: attributions,
url: 'https://api.maptiler.com/maps/hybrid/{z}/{x}/{y}.jpg?key=' + key,
tileSize: 512,
// 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 CircleStyle({
radius: 7,
fill: new Fill({color: 'black'}),
stroke: new Stroke({
color: 'white',
width: 2,
const vectorLayer = new VectorLayer({
source: new VectorSource({
features: [routeFeature, geoMarker, startMarker, endMarker],
style: function (feature) {
return styles[feature.get('type')];
map.addLayer(vectorLayer);
const speedInput = document.getElementById('speed');
const startButton = document.getElementById('start-animation');
let animating = false;
let distance = 0;
let lastTime;
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 currentCoordinate = route.getCoordinateAt(
distance > 1 ? 2 - distance : distance
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();
startButton.textContent = 'Stop Animation';
vectorLayer.on('postrender', moveFeature);
// hide geoMarker and trigger map render through change event
geoMarker.setGeometry(null);
function stopAnimation() {
animating = false;
startButton.textContent = 'Start Animation';
// Keep marker at current animation position
geoMarker.setGeometry(position);
vectorLayer.un('postrender', moveFeature);
startButton.addEventListener('click', function () {
if (animating) {
stopAnimation();
} else {
startAnimation();
<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>
<div id="map" class="map"></div>
<label for="speed">
speed:
<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="./resources/elm-pep.js"></script>
<script type="module" src="main.js"></script>
</body>
</html>
package.json
"name": "feature-move-animation",
"dependencies": {
"ol": "7.3.0"
"devDependencies": {
"vite": "^3.2.3"
"scripts": {
"start": "vite",
"build": "vite build"