javascriptgoogle-maps-api-3batch-processingmap-directionsgoogle-elevation-api

Inaccurate Google Maps Elevation Service response when splitting a too large path


This is a bit of a question with some level of detail to it, so let me first explain the situation, then my implementation and last the question so you understand best.

As of April 4 an update is added and the issues are narrowed down to one pending issue, see the bottom of this question for the up to date info.

TLDR;

I have a long route returned from Google Maps Directions API and want an Elevation chart for that route. Too bad it doesn't work because it's requested via GET and the URL maximum length is 2.048 chars which get exceeded. I splitted the requests; guaranteed the correct processing order using Promises; but Evelation data isn't always complete for full route, isn't always displayed in the correct order, doesn't always follow the given path and inter elevation location spans over several km's sometimes.

Introduction;

Trying to create an elevation chart for a Google Maps DirectionsService response I'm facing an issue with too long routes (this doesn't seem to be related to distance, rather than number of LatLngs per overview_path). This is caused by the fact the ElevationService is requested via GET and a maximum length of an URL is 2048 chars. This problem is described on SO here as well.

Implementation;

I figured I would be smarter than Google (not really, but at least trying to find a way to work around it), to split the path returned by the DirectionsService (overview_path property) into batches and concatenate the results (elevations returned by the ElevationService method getElevationsAlongPath).

While the ElevationService work asynchronously I make sure the requests are all processed in the correct order with help of other SO-users first using setTimout and now working with Promises.

My code

var maxBatchSize = 200;
var currentBatch = 0;
var promise = Promise.resolve();
var totalElevationBatches = Math.ceil(directions.routes[0].overview_path.length / maxBatchSize);
var batchSize =  Math.ceil(directions.routes[0].overview_path.length / totalElevationBatches);

while(currentBatch < totalElevationBatches) {
    promise = addToChain(promise, currentBatch, batchSize);
    currentBatch++;
}

promise.then(function() {
    drawRouteElevationChart(); // this uses the routeElevations to draw an AreaChart
});

function getRouteElevationChartDataBatchPromise(batch, batchSize) {
    return new Promise(function(resolve, reject) {
        var elevator = new google.maps.ElevationService();
        var thisBatchPath = [];

        for (var j = batch * batchSize; j < batch * batchSize + batchSize; j++) {
            if (j < directions.routes[0].overview_path.length) {
                thisBatchPath.push(directions.routes[0].overview_path[j]);
            } else {
                break;
            }
        }

        elevator.getElevationAlongPath({
            path: thisBatchPath,
            samples: 512
        }, function (elevations, status) {
            if (status != google.maps.ElevationStatus.OK) {
                if(status == google.maps.ElevationStatus.OVER_QUERY_LIMIT) {
                    console.log('Over query limit, retrying in 250ms');

                    resolve(setTimeout(function() {
                        getRouteElevationChartDataBatchPromise(batch, batchSize);

                    }, 250));
                } else {
                    reject(status);
                }
            } else {
                routeElevations = routeElevations.concat(elevations);
                resolve();
            }
        });
    });
}

function addToChain(chain, batch, batchSize){
    return chain.then(function(){
        console.log('Promise add to chain for batch: ' + batch);
        return getRouteElevationChartDataBatchPromise(batch, batchSize);
    });
}

Side note;

I'm also batching the DirectionService's request to address the 8 waypoint limitation the service has but I can confirm this is not the issue since I'm also facing the issue with 8 or fewer waypoints.

Problem;

The problems I'm facing are:

elevation data not following the overview_path it was told to follow

I figured batching the ElevationService using Promises (and before timing with setTimtout) would solve all my problems but the only problem I solved is not exceeding the 2.048 char request URL and facing the above described new issues.

Help is really appreciated

Also I would like to put a 250 rep. bounty on this question right ahead but that's impossible at this moment. So please feel free to reply as I can later add the bounty and award it to the answer that solves the issues described. A 250 rep. bounty has been awarded to show my appreciation for you to point me in the right direction.

Thanks for reading and replying!

Updated at April 4 leaving 1 pending issue (for as far as I can tell at the moment)

Problem with elevations in random order tackled down

I've been able to tackle some of the problems when I was noticing inconsistent behavior in the directions results. This was caused for an obvious reason: the asynchronous calls weren't "Promised" to be scheduled so some of the times the order was correct, most of the times it wasn't. I didn't noticed this at first because the markers were displayed correctly (cached).

Problem with inter elevation distance tackled down

The div displaying the elevation data was only a 300px wide and containing many datapoints. By such a small width I was simply unable to hover over enough points causing to trigger elevation points which lie further apart from each other.

Problem with elevation data not showing along the route

Somehow somewhere down the line I've also solved this issue but I'm not sure if the bigger width or "Promising" the directions order has solved this.

Pending issue: elevation data is not always complete

The only remaining issue is that elevation data is not always covering the full path. I believe this is because an error in the Promising logic because logging some messages in the console tells me the elevation chart is drawn at a point where not all Promise-then's have completed and I think this is caused by refiring a batched call when an Over Query Limit error is returned by the Google Maps API.

How can I refire the same chain when an Over Query Limit error is returned? I've tried not to resolve the same function again, but just fire the setTimeout(...), but then the Promise doesn't seem to resolve the refired batch at the moment it is no longer getting an Over Query Limit. Currently this is how I've set it up (for both directions and elevation):

function getRouteElevationChartDataBatchPromise(batch, batchSize) {
    return new Promise(function(resolve, reject) {
        var elevator = new google.maps.ElevationService();
        var thisBatchPath = [];

        for (var j = batch * batchSize; j < batch * batchSize + batchSize; j++) {
            if (j < directions.routes[0].overview_path.length) {
                thisBatchPath.push(directions.routes[0].overview_path[j]);
            } else {
                break;
            }
        }

        elevator.getElevationAlongPath({
            path: thisBatchPath,
            samples: 512
        }, function (elevations, status) {
            if (status != google.maps.ElevationStatus.OK) {
                if(status == google.maps.ElevationStatus.OVER_QUERY_LIMIT) {
                    console.log('ElevationService: Over Query Limit, retrying in 200ms');

                    resolve(setTimeout(function() {
                        getRouteElevationChartDataBatchPromise(batch, batchSize);

                    }, 200));
                } else {
                    reject(status);
                }
            } else {
                console.log('Elevations Count: ' + elevations.length);
                routeElevations = routeElevations.concat(elevations);
                resolve();
            }
        });
    });
}

Solution

  • The last remaining issue has also been solved with the help of this SO question: How to re-run a javascript promise when failed?. So if jfriend00 replies to this question I can award the bounty to him, since that's the trick that helped me out in the end.

    To be sure the function resolves at status OK, retries at OVER_QUERY_LIMIT and reject at any other status I had to put the Promise logic within a function and call that function, like so:

    function getRouteElevationChartDataBatchPromise(batch, batchSize) {
        return new Promise(function(resolve, reject) {
            function run(batch, batchSize) {
                var elevator = new google.maps.ElevationService();
                var thisBatchPath = [];
    
                for (var j = batch * batchSize; j < batch * batchSize + batchSize; j++) {
                    if (j < directions.routes[0].overview_path.length) {
                        thisBatchPath.push(directions.routes[0].overview_path[j]);
                    } else {
                        break;
                    }
                }
    
                elevator.getElevationAlongPath({
                    path: thisBatchPath,
                    samples: 512
                }, function (elevations, status) {
                    if(status == google.maps.ElevationStatus.OK) {
                        routeElevations = routeElevations.concat(elevations);
                        resolve();
                    } else if (status == google.maps.ElevationStatus.OVER_QUERY_LIMIT) {                        
                        setTimeout(function () {
                            run(batch, batchSize);
                        }, 200);
                    } else {
                        reject(status);
                    }
                });
            }
    
            run(batch, batchSize);
        });
    }