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 split the requests; guaranteed the correct processing order using Promises; but Elevation 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 kilometres 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
).
LatLng
per batch and check
how many batches are required to process the full path (totalBatches = overview_path.length / maxBatchSize
);batchSize = Math.ceil(overview_path.length / totalBatches)
).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:
LatLng
's from the
overview_path
provided in a given batch (see screenshot);LatLng
s per batch.I figured batching the ElevationService using Promises (and before timing with setTimeout) 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.
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 data points. 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();
}
});
});
}
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);
});
}