Skip to main content

Animating Google Maps Directions with Multiple Waypoints in JavaScript

The Google Maps API offers a wealth of functionality. Even when it doesn't directly offer a specific piece of functionality, you can usually create what you're looking for with the tools provided.

Here I am going to retrieve directions from the directions API using a bunch of geographical coordinates and then animate a polyline on to a map.

First, we need some coordinates for our route.

const directions = [
    { lat: 50.262950, lng: -5.050700 },
    { lat: 51.507351, lng: -0.127758 },
    { lat: 52.205338, lng: 0.121817 },
    { lat: 52.486244, lng: -1.890401 },
    { lat: 52.954784, lng: -1.158109 },
    { lat: 53.383060, lng: -1.464800 },
    { lat: 53.480759, lng: -2.242631 },
    { lat: 53.799690, lng: -1.549100 }
];

Now, one caveat of the directions API is that it has a rate limit of 25 waypoints. Anything above that becomes quite costly. So even though there are not more than 25 waypoints in the array above, there is a chance you will need to chunk your coordinates into multiple calls to the API to avoid the limit. So let's build that.

// Break up our coordinates into chunks of 25 to avoid rate limits
let chunks = [];
let chunkSize = 25;

directions.forEach((waypoint, i) => {

let pi = Math.floor(i / (chunkSize - 1));
chunks[pi] = chunks[pi] || [];

if (chunks[pi].length === 0 && pi !== 0) {
    // make new chunks origin the same as last chunks destination
    chunks[pi] = [directions[i - 1]];
}

chunks[pi].push(waypoint);

});

There are plenty of ways to chunk an array, but in this instance we need the last value of the previous chunk to also be the first value of the next chunk, so that our directions are continuous.

Now that we have our coordinates in chunks we can start building out our map. Make sure that your function name matches the callback you set when including the Google Maps API in your HTML and don't forget to enable the Google Maps Directions API for your API key.

// map callback once maps api loaded
function initMap() {

let map = new google.maps.Map(document.getElementById('map'), {
    center: { lat: 54.4916899, lng: -2.211134 },
    zoom: 6,
    scrollwheel: true
});

}

Note: The rest of our code will need to be inside the initMap() function to work properly.

Now we need to run through our chunks and construct our individual requests to the directions service.

// build requests for api
let requests = [];

chunks.forEach((chunk) => {

    let origin = chunk[0];
    let destination = chunk[chunk.length - 1];

    // build waypoints without origin/destination
    let waypoints = chunk.slice(1, -1).map(waypoint => {
    return {
        location: new google.maps.LatLng(waypoint.lat, waypoint.lng),
        stopover: false
    }
    });

    requests.push({
    travelMode: 'DRIVING',
    origin: new google.maps.LatLng(origin.lat, origin.lng),
    destination: new google.maps.LatLng(destination.lat, destination.lng),
    waypoints: waypoints
    });

});

Each request to the directions service needs an origin, a destination and any waypoints in between.

Now, because the directions service is asynchronous and we have multiple requests to make, we will need to make our function that retrieves the directions return a promise, so that we can deal with the results once they have all been retrieved.

// return promise for each directions request
function buildRoute(requests) {

    let directionsService = new google.maps.DirectionsService();

    return Promise.all(requests.map((request) => {
    
    return new Promise(function(resolve) {
        directionsService.route(request, function(result, status) {
        if (status == google.maps.DirectionsStatus.OK) {
            return resolve(result.routes[0].legs[0]);
        }
        });
    });

    }));
}

Here we have mapped out our requests to multiple promises, which will only be resolved once all requests have been returned. Top tip! You will notice that we have used the legs value from the response instead of overview_path, which also includes coordinates. This is because overview_path is actually smoothed and not an accurate set of coordinates, so our polyline would not snap to the roads. So for a more accurate route, it is better to use legs.

Once we have of our directions we will be able to draw our polyline. Let's build out the animation for this.

// build up a polyline of our route
function animatePath(map, pathCoords) {

    let speed = 1000; // higher = slower/smoother
    
    let route = new google.maps.Polyline({
    path: [],
    geodesic: true,
    strokeColor: '#FF0000',
    strokeOpacity: 1.0,
    strokeWeight: 3,
    editable: false,
    map: map
    });

    // break into chunks for animation
    let chunk = Math.ceil(pathCoords.length / speed);
    let totalChunks = Math.ceil(pathCoords.length / chunk);
    let i = 1;

    function step() {

    // redraw polyline with bigger chunk
    route.setPath(pathCoords.slice(0, i * chunk));
    i++;

    if (i <= totalChunks) {
        window.requestAnimationFrame(step);
    }

    }

    window.requestAnimationFrame(step);

}

Here we setup our polyline with no path and then continously build up our path with bigger chunks of the coordinates array that we will be creating in a moment. The speed value essentially divides the number of paths. The higher this value is, the slower and more smoother the animation will be. We could add more calculation to this for the animation to work out distances and make the animation more fluid, but I found this to be sufficient.

There are other examples of this same code using setTimeout() or setInterval(), but these can become very laggy very fast, especially on bigger routes. requestAnimationFrame() is more performant and allows the browser to be more efficient with it.

Almost there! Now we need to send our array of requests to the directions service using the asynchronous function we built earlier and then animate our route with the results that are returned.

// wait for directions to be returned, then animate the route
buildRoute(requests).then((results) => {

    // flatten all paths into one set of coordinates
    let coords = results.flatMap((result) => {
    return result.steps.flatMap(step => step.path);
    });

    // finally animate path of our route
    animatePath(map, coords);

});

Inside of the legs array that we returned earlier from the directions service are multiple steps, each with multiple paths. So we need to map and flatten these paths included in each request to get our full and final array of coordinates for our polyline. Then it is just a case of passing this to our animation function and they will animate nicely onto our map.

There are of course many other things you can do with the Google Maps API, such as markers and bounding boxes to make this more polished. But this covers drawing a polyline on to a map using the results from the directions service.