Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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).

  • To get the best level of detail I query the ElevationService with 512 samples per batch;
  • and because the ElevationService spreads the samples over the length of the path I set up a maximum number of LatLng per batch and check how many batches are required to process the full path (totalBatches = overview_path.length / maxBatchSize);
  • and finally get an even spread for my directions result in an attempt to get an equal level of detail for the complete route (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:

  • Elevation data is not always following the full path of route, meaning the last elevation point in the chart is (far) from the end of the route;
  • Elevation data sometimes gets displayed in random order as if it seems the promises were still not waiting for the next task to execute;
  • Elevation data doensn't always follow the given LatLng's from the overview_path provided in a given batch (see screenshot);
  • Inter elevation distance data is a lot. Sometimes spans multiple km's while requesting for 512 samples for an evenly matched batch size with a maximum of 200 LatLngs per batch.

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();
            }
        });
    });
}
like image 655
Ben Fransen Avatar asked Mar 28 '16 16:03

Ben Fransen


1 Answers

Mmmh, how many points do you have to handle. Can you publish the path, so maybe some other can test it in their own apps. Did you try to reduce the path points with Douglas-Peuker or comparable methods. Did you try other apps like the free "Routeconverter" (which works with HGT) to see if you get better results. Do you need the elevation points direct/on the fly? Would using other free elevation services be an option. Perhaps you have to read the elevation points back to your route points, so that you can sort out unwanted points.

Only some thinking, in bad English - I'm afraid. Good luck, Reinhard

like image 195
ReFran Avatar answered Sep 24 '22 02:09

ReFran