Sunday, April 29, 2018

Javascript coding challenge - async callback/variable scope

The Problem

My son asked about this ECMAScript/Javascript code, where he was using Google's geocoding and mapping APIs to populate a map with markers.

<script type="text/javascript">
function test() {
    var locations = [
        ['100 N Main St Wheaton, IL 60187', 'Place A'],
        ['200 W Apple Dr Winfield, IL 60190', 'Place B']
    ];
    var map = new google.maps.Map(document.getElementById('map'), {
        zoom: 10,
        center: new google.maps.LatLng(41.876905, -88.101131),
        mapTypeId: google.maps.MapTypeId.ROADMAP
    });
    var infowindow = new google.maps.InfoWindow();
    var marker, i;
    var geocoder = new google.maps.Geocoder();
    for (i = 0; i < locations.length; i++) {
        geocoder.geocode({
            'address': locations[i][0]
        }, function(results, status) {
            marker = new google.maps.Marker({
                position: results[0].geometry.location,
                map: map,
                title: locations[i][1]
            });
        });
    }
}


It was broken, being unable to find locations[i][1] to assign to title.

My response:

So, what would I do?

Study the documentation further on Markers and Geocoding.

The second one tells me my understanding/recollection of scope of anonymous functions was wrong. Variable resultsMap is passed as a parameter to geocodeAddress and then it calls geocoder.geocode with an anonymous callback function that uses that variable. However, that variable will never change. So too for you, locations will never change. So, the real problem you have is that the value of i changed. When i hit the end of the for loop, i was = locations.length and that would put it outside the range of valid indexes for locations and it would choke. The async execution is what is causing the problem.

Google's examples all make only one mark, so they can hard code what they want.

So, options: (1) don't initially set the title of the marker, but come back later and do it, (2) specify a title variable that doesn't change, or (3) force waiting on async execution so i doesn't change.

(1) Now, can we count on Javascript not doing out of order execution of the callbacks? I don't think so. Otherwise, you could append the markers to an array and assume they are in order of the locations used in calls to geocode(). If that were true then you could come back and add the titles later.

Is there something returned in the results of geocode that would help us index into the locations array? Reading through https://developers.google.com/maps/documentation/javascript/geocoding doesn't reveal anything. I was hopeful that placeId might work, if it were an arbitrary field because you can pass the value in to the geocode call and you get it back out in results. However, Google has reserved the values for their own meaning. And, the address you send in to geocode is not necessarily the same one you get out of results.formatted_address.

(2) I imagine, with some effort, one could use dynamic code generation (the program writes code and then runs it) to define fixed variables for each of the values of locations, such that you have something like: location1 = locations[0], location2 = locations[1], etc. and then those variables could be referenced in the callback function. The eval() function is used to dynamically evaluate code. Even better might be just using the constant value in the callback function definition. So it would be something like this inside the for (i=0 loop:

var dynamicCode = 'geocoder.geocode(
 { \'address\': ' + locations[i][0] + '},
 function(results,status) { ' +
   'marker = new google.maps.Marker({ position: results[0].geometry.location,' +
   'map:map, title: \'' + locations[i][1] + ' }); });'
alert('about to execute this code:\n' + dynamicCode)
eval(dynamicCode)

You could embed newlines \n into that dynamic code if you want to make it look prettier, but it is not necessary. On the other hand, spaces could be trimmed out too. So, that is one solution.

(3) Another solution, waiting for callback to complete. Now, the modern way to deal with this is to use Javascript Promises. However, there would need to be support for this from the geocode library, where the callback comes from. Reviewing the reference documentation on geocode, does not reveal any support for this. So, a more hacky approach involves a lock. It would look like this:

var lock;
for (i = 0; i < locations.length; i++) {
    lock = 1; // lock set for this loop iteration
    geocoder.geocode({
      'address': locations[i][0]
      }, function(results, status) {
         marker = new google.maps.Marker({
            position: results[0].geometry.location,
            map: map,
            title: locations[i][1]
         });
        lock = 0; //
    });
    while (lock == 1) {
    await wait(100); // wait 100 milliseconds and check again
}
So, the lock is set going into the call to geocode() and only in the callback function is the lock unset. Polling for the lock to be unset happens every 100 ms, but a shorter time interval may make sense.