0

MY ANSWER BELOW HAS A WORKING CODE EXAMPLE AND AN EXPLANATION


I am trying to get my head around asynchronous programming by creating a Traveling Salesperson algorithm with the help of the Google maps API.

Here is how it should work:

  1. We have a list of address strings addresses
  2. For every combination of address [i] and [j] we calculate the distance, using google directionsService and we store this in a Javascript object (distanceTable).
  3. The tspSolver should run after we have all the distances (not implemented yet).

My naive code is below. As you can see there are several issues, all of which have to do with callbacks:

I pass directionResults as a callback to the directionsService. It correctly calculates the distance, but since I am no longer inside the loop to construct the distanceTable, I cannot store the result correctly. I commented out the section which should store it, but that obviously only works inside the loop.

I pass my tspSolver as a callback for allDistances. However I notice that it gets executed before the distances are calculated by directionResults. My guess is that I have to do some form of nesting of callbacks?

Who can help me make sense of this.

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

var addresses = ['Dam, Amsterdam','Spui, Amsterdam','Middenweg, Amsterdam'];
var distanceTable = {};
//all combinations of addresses with distances, for use in TSP algorithm
//{
//  addressA {
//    addressB: 2000
//    addressC: 2500
//  }
//  addressB {
//    addressC: 1800
//  }


function tspSolver(distanceTable) {
  console.log('Distances are there, now for some clever TSP algorithm')
  //this should only be executed after the distances are returned.
}

function allDistances(addresses, callback) {
  for(var i=0; i<addresses.length; ++i) {
    distanceTable[addresses[i]] = {};
    for(var j=i+1; j<addresses.length; ++j) {
      // Compose request for every pair of addresses (one way)
      var request = {
        origin: addresses[i],
        destination: addresses[j],
        travelMode: 'DRIVING'
      };
      console.log(request);
      gMap.directionsService.route(request, directionResults);
    }
  }
  callback(distanceTable)
}


function directionResults(result, status) {
  console.log("Receiving request for route");
  console.log(result);
  if (status == google.maps.DirectionsStatus.OK) {
    var totalDistance = 0;
    var legs = result.routes[0].legs;
    for(var i=0; i<legs.length; ++i) {
      totalDistance += legs[i].distance.value;
    }
    console.log(totalDistance);
    // I really want to add it to the distanceTable...
    //distanceTable[addresses[i]][addresses[j]] = totalDistance;
  }
}


//call function to start solving
function executeTSP() {
  allDistances(addresses, tspSolver);
}
Roy Prins
  • 2,790
  • 2
  • 28
  • 47

2 Answers2

1

I would assume you need to pass callback with request

Callback is the way to continue execution when you are programming in async.

Pseudo example:

function doAcync(callback)
{
 //gets result
 callback(result);
}

function one()
{
  doAcync(two);
}

function two(result)
{
  doAcync(three);
}

function three(result)
{
  // continue with your live
}
Matas Vaitkevicius
  • 58,075
  • 31
  • 238
  • 265
  • So what is the 'result' that gets passed to functions two and three? Sorry, but I am having some trouble getting my head around this. – Roy Prins Apr 17 '14 at 11:16
  • 1
    No worries most ppl do at first. result is what got retrieved from acyncronious call, it is sort of convention that if you are passing callback and function that you are passing callback to has return value, then callback function will be called with that return value. – Matas Vaitkevicius Apr 17 '14 at 11:31
  • I think your pattern would help me solve my second issue (the order of execution). I am not convinced it would help me to write the results into the distanceTable... – Roy Prins Apr 17 '14 at 12:45
  • I added another answer with the working code. I think it is not pretty, but it get the job done. I have to admit I did not use your pattern, but thanks for the help. – Roy Prins Apr 17 '14 at 14:06
1

The solution to my first problem lies in "closures inside loops". I am tracking my loop with a counter (rather 2 counters in my case). By the time the asynchronous function/closure is executed, the loop would have completed.

That would defeat all attempts to access the loop counters from within the closure: you would always get the maximum number.

The solution I chose was to wrap the closure in another function: a so called "anonymous wrapper". This wrapper hold a reference which is not affected by the loop.

Some better explanations can be found here: Mutable variable is accessible from closure. How can I fix this? And here: http://bonsaiden.github.io/JavaScript-Garden/#function.closures

To get the second problem (execution order) fixed, I resorted to doing a callback within the callback. Some admittedly ugly code keeps track that the callback is only called on the last iteration. I am sure there are more elegant ways to deal with this, but for now it works:

var directionsService;

function initialize() {
  directionsService = new google.maps.DirectionsService();
}

var addresses = ['Dam, Amsterdam','Spui, Amsterdam','Middenweg, Amsterdam'];
var distanceTable = {};
//all combinations of addresses with distances, for use in TSP algorithm
//{
//  addressA {
//    addressB: 2000
//    addressC: 2500
//  }
//  addressB {
//    addressC: 1800
//  }


function tspSolver(distances) {
  //this function is called as a callback from allDistances
  for(var i=0; i<addresses.length; ++i) {
    for(var j=i+1; j<addresses.length; ++j) {
      console.log(addresses[i]+' to '+addresses[j]+': '+distances[addresses[i]][addresses[j]])
    }
  }
}

function allDistances(addresses, callback) {
  for(var i=0; i<addresses.length; ++i) {
    distanceTable[addresses[i]] = {};
    for(var j=i+1; j<addresses.length; ++j) {
      var request = {
        origin: addresses[i],
        destination: addresses[j],
        travelMode: 'DRIVING'
      };
      console.log(request);
      //anonymous wrapper around closure to preserve the loop counters: i,j -> e,f
      (function(e,f) {directionsService.route(request, function(result,status) {
          if (status == google.maps.DirectionsStatus.OK) {
            //calculate totalDistance as sum of the legs and store the result
            var totalDistance = 0;
            var legs = result.routes[0].legs;
            for(var x=0; x<legs.length; ++x) {
              totalDistance += legs[x].distance.value;
            }
            distanceTable[addresses[e]][addresses[f]] = totalDistance;
          }
        //trigger the callback on the last iteration only
        if (e+2 == addresses.length)
          {
            callback(distanceTable)
          }
        });
      })(i,j);
      //end of wrapper
    }
  }
}

function executeTSP(){
  allDistances(addresses, tspSolver)
}
Community
  • 1
  • 1
Roy Prins
  • 2,790
  • 2
  • 28
  • 47