0

So, I'm getting addresses from my firestore database, converting them to coordinates, then trying to add markers based on each set of coordinates. I'm able to add markers outside the loop, but not inside.

Thank you

    //Convert addresses to coords
    //Fetch all addresses from db
    db.collection('specials').get()
    .then(snapshot => {
        snapshot.forEach(special => {
            //Set location
            var location = special.data().address;

            //Get geocode info
            axios.get('https://maps.googleapis.com/maps/api/geocode/json?components=country:NZ|country:AU',{
                params:{
                    address:location, 
                    key:'************************************',  
                }
            })
            .then(function(response){
                //Geometry
                markerLat = response.data.results[0].geometry.location.lat;
                markerLng = response.data.results[0].geometry.location.lng; 

                //console.log("Lat:" + markerLat + " Lng:" + markerLng);

                //Doesn't work inside the loop
                markers.push({
                    coords:{lat: markerLat, lng: markerLng}
                });

            })
        });
    })

    //Works outside the loop
    markers.push({
        coords:{lat: -36.8585, lng: 174.7833}
    });
James
  • 57
  • 4
  • 2
    Could you clarify your problem? What do you mean when you say it "doesn't work inside the loop"? How are you testing it? Are you sure you are accounting for the asynchronous nature of the geocoder? Please provide a [mcve] that demonstrates your issue. – geocodezip Mar 15 '20 at 13:28
  • @James: does my answer help in any way? Did you make progress? – Frank van Puffelen Mar 18 '20 at 15:28

1 Answers1

4

My first guess is that you're using markers outside of the loop, for example in a call to the maps API. That won't work, as the data is loaded from Firestore asynchronously, and the data isn't available yet at that point.

The easiest way to understand how asynchronous loading works is with some well placed logging statements:

console.log("1. Before calling database");
db.collection('specials').get()
.then(snapshot => {
    console.log("2. Got database results, calling geocode API");
    snapshot.forEach(special => {
        var location = special.data().address;
        axios.get('https://maps.googleapis.com/maps/api/geocode/json?components=country:NZ|country:AU',{
            params:{ address:location,  key:'********' }
        })
        .then(function(response){
            console.log("3. Got geocode result");
        })
    });
})
console.log("4. After calling database");

Now when you run this code, the logging output will be:

  1. Before calling database

  2. After calling database

  3. Got database results, calling geocode API

  4. Got geocode result

  5. Got geocode result

...

This is probably not what you expected, as the code doesn't execute in the order in which you have it in your file. But it is completely working as intended, due to the nature of asynchronous APIs. And, it explains why locations from the database aren't added to maps, if your call to maps is near logging statement 4.: by the time that runs, no data has been loaded from the database yet, and the geocoding hasn't been done yet either.

Any code that needs the data from the database should be inside the then callback. To make things more complex: since you also want all geolookups to have been completed, you only want to add the markers to maps, once all the nested then() calls have happened.

To do this you can use Promise.all, which resolves a single then() once multiple other then() calls have been resolved.

Combining all of this, leads to code that should look something like this:

//Fetch all addresses from db
db.collection('specials').get()
.then(snapshot => {
    // Get all addresses from the documents
    return snapshot.docs.map(doc => doc.data().address);
})
.then(addresses => {
    // Geocode all addresses, to get coordinates
    return Promise.all(addresses.map(location => {
        return axios.get('https://maps.googleapis.com/maps/api/geocode/json?components=country:NZ|country:AU',{
            params:{
                address:location, 
                key:'************************************',  
            }
        })
    });
})
.then(locations => {
    // Convert all geometry into markers
    return locations.map(response => {
        markerLat = response.data.results[0].geometry.location.lat;
        markerLng = response.data.results[0].geometry.location.lng; 

        return  {
            coords:{lat: markerLat, lng: markerLng}
        });
    });
})
.then(markers => {
    // TODO: call the maps API and add the markers
});

If your code is running in a modern JavaScript environment, you can use async/await to make the above read a bit more normally.

//Fetch all addresses from db
let snapshot = await db.collection('specials').get()

// Get all addresses from the documents
let addresses = snapshot.docs.map(doc => doc.data().address);

// Geocode all addresses, to get coordinates
let locations = await Promise.all(addresses.map(location => {
    return axios.get('https://maps.googleapis.com/maps/api/geocode/json?components=country:NZ|country:AU',{
        params:{
            address:location, 
            key:'************************************',  
        }
    })
});

// Convert all geometry into markers
let markers = locations.map(response => {
    markerLat = response.data.results[0].geometry.location.lat;
    markerLng = response.data.results[0].geometry.location.lng; 

    return  {
        coords:{lat: markerLat, lng: markerLng}
    });
});

// TODO: call the maps API and add the markers

As you can see here the structure is mostly the same, we mostly just undented the then blocks by using the await keyword.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807