Set up timeline animation controls
This example demonstrates how to set up timeline animation controls to allow the user to control playback and view the current animation progress. This allows you to fully customize your map's timeline controls by responding to events triggered by the MapsGL map controller and timeline.
timeline-controls.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>MapsGL SDK - Set up timeline animation controls</title>
<meta name="description" content="Set up map timeline animation controls to allow the user to control playback and view the current animation progress." />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://api.mapbox.com/mapbox-gl-js/v2.8.0/mapbox-gl.css" rel="stylesheet" />
<script defer src="https://api.mapbox.com/mapbox-gl-js/v2.8.0/mapbox-gl.js"></script>
<link href="https://cdn.aerisapi.com/sdk/js/mapsgl/latest/aerisweather.mapsgl.css" rel="stylesheet" />
<script defer src="https://cdn.aerisapi.com/sdk/js/mapsgl/latest/aerisweather.mapsgl.js"></script>
<style>
body, html {
margin: 0;
padding: 0;
}
#map {
height: 100vh;
width: 100%;
}
#controls {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 8px;
position: absolute;
top: 10px;
left: 10px;
z-index: 1;
background: rgba(255, 255, 255, 0.8);
padding: 10px;
border-radius: 5px;
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
font-size: 12px;
}
#controls h3 {
margin: 0 0 6px;
font-size: 14px;
}
#controls a {
color: #333;
display: block;
margin: 0 4px;
text-decoration: none;
font-weight: bold;
}
#controls a:hover {
text-decoration: underline;
}
#controls .divider {
margin: 0 4px;
background: #ccc;
width: 1px;
height: 16px;
}
#controls .time {
font-weight: normal;
}
#controls .loader {
display: none;
height: 20px;
}
.loader .ring {
display: inline-block;
position: relative;
width: 20px;
height: 20px;
}
.loader .ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 18px;
height: 18px;
margin: 1px;
border: 2px solid #333;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #333 transparent transparent transparent;
}
.loader .ring div:nth-child(1) {
animation-delay: -0.45s;
}
.loader .ring div:nth-child(2) {
animation-delay: -0.3s;
}
.loader .ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div id="map"></div>
<div id="controls">
<button class="btn-play">Play</button>
<button class="btn-pause">Pause</button>
<div class="divider"></div>
<input id="timeline-slider" type="range" min="0" max="1" value="1" step="0.01">
<div class="time">0.0</div>
<div class="loader">
<div class="ring">
<div></div><div></div><div></div><div></div>
</div>
</div>
</div>
<script>
function formatDate(date) {
return `${date.toLocaleDateString('en', { weekday: 'short' })} ${date.toLocaleTimeString('en')}`;
}
window.addEventListener('load', () => {
// Create the Mapbox map instance
mapboxgl.accessToken = 'MAPBOX_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v11',
center: [-85.5, 40],
zoom: 3,
projection: 'mercator'
});
const account = new aerisweather.mapsgl.Account('CLIENT_ID', 'CLIENT_SECRET');
const controller = new aerisweather.mapsgl.MapboxMapController(map, { account });
// Set up animation controls
const controls = {
time: document.querySelector('#controls .time'),
play: document.querySelector('#controls .btn-play'),
pause: document.querySelector('#controls .btn-pause'),
slider: document.getElementById('timeline-slider'),
loader: document.querySelector('#controls .loader')
};
function updateTimelineLabel() {
controls.slider.value = controller.timeline.position;
controls.time.innerHTML = `${formatDate(controller.timeline.currentDate)}`;
}
// Hide/show the loading indicator when any map layer is loading data
controller.on('load:start', () => {
controls.loader.style.display = 'block';
});
controller.on('load:complete', () => {
controls.loader.style.display = 'none';
});
// Set up timeline controls
controls.pause.disabled = true;
controller.timeline.on('play', () => {
controls.play.textContent = 'Stop';
controls.pause.textContent = 'Pause';
controls.pause.disabled = false;
}).on('stop', () => {
controls.play.textContent = 'Play';
controls.pause.textContent = 'Pause';
controls.pause.disabled = true;
}).on('pause', () => {
controls.pause.textContent = 'Resume';
}).on('resume', () => {
controls.pause.textContent = 'Pause';
});
controls.play.addEventListener('click', (e) => {
e.preventDefault();
if (controller.timeline.isActive) {
controller.timeline.stop();
} else {
controller.timeline.play();
}
});
controls.pause.addEventListener('click', (e) => {
e.preventDefault();
if (controller.timeline.isPaused) {
controller.timeline.resume();
} else {
controller.timeline.pause();
}
});
controls.slider.addEventListener('input', (e) => {
controller.timeline.goTo(controls.slider.value);
});
// Set up the timeline slider and time display
controller.timeline.on('advance', ({ position, date }) => {
controls.slider.value = position;
controls.time.innerHTML = `${formatDate(date)}`;
});
controller.timeline.on('range:change', () => {
updateTimelineLabel();
});
controls.slider.addEventListener('input', (e) => {
controller.timeline.pause();
controller.timeline.goTo(controls.slider.value);
});
updateTimelineLabel();
controller.on('load', () => {
let beforeLayerId;
// Find the Mapbox layer id to insert the weather layer before by matching against a
// particular Mapbox layer id value and type. Here we look for the first admin boundary
// layer to insert weather layers below.
const mapboxLayers = controller.map.getStyle().layers;
mapboxLayers.forEach((layer) => {
if (!beforeLayerId && layer.type === 'line') {
if (/^admin-1/.test(layer.id)) {
beforeLayerId = layer.id;
}
}
});
controller.addWeatherLayer('temperatures', null, beforeLayerId);
});
});
</script>
</body>
</html>