0

I've run into this problem with Leaflet Map. I am trying to loop over some addresses, make a call for each loop to get the lat-long, plot on mymap, and group the markers to bound them using fitBounds

createRequest(projectUrl, getAddresses, handleErrors, projectInit)
    .then( (jsonAddresses) =>{
        // jsonAddresses = [{building_num: 100, street_name: 'main', ... }, {}, ...]

        let state_province = ''
        let markers        = []
        let projectsGroup  = L.featureGroup().addTo(mymap)   // Creating an empty group to add markers to later on
        
       
        for (let address of jsonAddresses )
        {
            
            let theAddress  = address.building_num + ' ' +  
                              address.street_direction + ' ' + 
                              address.street_name + ' ' + 
                              address.street_type + ' ' + 
                              address.civic_address_city + ' '+
                              address.state_province + ' ' +
                              address.country

            addressInit = {'address': theAddress ,
                           'buidlingNum': address.building_num,
                           'streetName': address.street_name,
                           'city': address.civic_address_city
                          }

            // Using geoApify API to get lat-long for each address
            geoCodingUrl = `${geoapifyURL}${theAddress}&apiKey=${GEOAPIFY_API_KEY}`

            // createRequest makes asynchronous call and returns a json response
            createRequest(geoCodingUrl, getLatLongGeoApify, handleErrors, addressInit )
            .then( (response) => {
                latLongAddress = response
                latLongAddress.address = address  // address comes from the for-loop

                latLongs.push(latLongAddress)   // latLongAddress is a list of objects [{<lat>,<lon>}, {},...] returned by createRequest 
                return latLongs
            })
            .then( (result)=> {
                let marker = L.marker( [latitude,longitude] ).addTo(mymap)
                marker.addTo(projectsGroup)

                markers.push(marker)

                for (let project of result)  // project represents the lat-long of the project, e.g. project = [<lat>,<lon>]
                {
                    marker.setLatLng( [project.lat, project.lon] ).bindPopup("Try!") // Setting the marker coordinates
                    marker.addTo(projectsGroup)   // Adding the marker to the projectGroup
                }
                mymap.setZoom(12)
                
                
            })
            
        }
        
        // test_marker is to create another marker that cannot fit in the 
        // default view, which necessitates fitBounds to work properly
        const test_marker = L.marker( [48.354495, -122.0905781] ).addTo(mymap)  
        test_marker.addTo(projectsGroup)
        
        
        mymap.fitBounds(projectsGroup.getBounds() )

        
        return latLongs

    })
    
});

When I look at my markers in the console, it looks like an empty list, but when I open it, there are 4 markers in it. I know this must have to do with asynchronous nature of the call and the fact that the console is live, so initially it does not have any data and then it receives the data. But, I cannot figure out how I can add markers to the featureGroup in the async-proper way so I can bound them

4
  • 1
    Code is a bit complex to understand what you are trying to do. Please add comments in the code at critical steps about what the function of the step.
    – TomazicM
    Jul 7, 2020 at 5:26
  • I don't understand what do you mean by "bound them". Is it about binding a popup or similar, or about doing a fitBounds? Jul 7, 2020 at 8:17
  • @IvanSanchez I meant fitBounds
    – EarlyCoder
    Jul 7, 2020 at 14:08
  • @TomazicM I did add a few lines of comments in my edit. You are correct, one's code is usually hard to understand without good comments
    – EarlyCoder
    Jul 7, 2020 at 14:24

1 Answer 1

2

If I have understood this right:

  • You pull a list of addresses from somewhere called projectUrl
  • For each of those addresses you perform a call to a Geocoding API (geoapify in your specific case)
    • Each of those geocoding calls is asynchronous
  • You want to have a L.Marker for the result of each of those geocoding calls (i.e. one L.Marker per address)
    • (Your current code seems to suggest that you want a marker for every geocoding result for every address, and I'm gonna think that's not desired)
  • You want to do something (i.e. fitBounds) once all the markers are ready (i.e. when all the geocoding requests have been performed and handled)
  • You are using Promises (even if you're not aware) since .then(callback) is several times in your code.

For this scenario, the trick is to leverage Promise.all to trigger a function call when all geocoding requests are done.

I see two main approaches here, depending on what each promise shall resolve to: a lat-lng or a marker.

So we can do something like:

createRequest(projectUrl, /* stuff */).then((jsonAddresses) => {
    let projectsGroup = L.featureGroup().addTo(mymap); // Creating an empty group to add markers to later on
    let requests = [];
    
    for (let address of jsonAddresses) {
        let geocodingAddress = address.street + address.city; /* Real code is a bit more complex */
        geoCodingUrl = `${geoapifyURL}${theAddress}&apiKey=${GEOAPIFY_API_KEY}`;

        let request = createRequest(geoCodingUrl, /* stuff */)
            .then((geojson) => {
                // geoapify seems to return a GeoJSON FeatureCollection of Points, so fetch the first
                if (geojson.features.length === 0) { 
                    // Sanity check: no geocoding results means no marker, and resolving to 'undefined'.
                    return;
                }
                let marker = L.marker( L.GeoJSON.coordsToLatLng(geojson.features[0].coordinates) );
                marker.addTo(projectsGroup);
                
                // Returning the marker means that the Promise stored in this 'request' variable will resolve to a marker.
                return marker;
            });
            
        requests.push(request);
    }

    Promise.all(requests).then(fulfilledStuff=>{
        console.log(fulfilledStuff);    // Shall be an array of L.Marker, since each Promise resolved to a Marker
        mymap.fitBounds(projectsGroup.getBounds());
    });
});

Or

createRequest(projectUrl, /* stuff */).then((jsonAddresses) => {
    let projectsGroup = L.featureGroup().addTo(mymap); // Creating an empty group to add markers to later on
    let requests = [];
    
    for (let address of jsonAddresses) {
        let geocodingAddress = address.street + address.city; /* Real code is a bit more complex */
        geoCodingUrl = `${geoapifyURL}${theAddress}&apiKey=${GEOAPIFY_API_KEY}`;

        let request = createRequest(geoCodingUrl, /* stuff */)
            .then((geojson) => {
                if (geojson.features.length === 0) { return; }
                return L.GeoJSON.coordsToLatLng(geojson.features[0].coordinates);
            });
            
        requests.push(request);
    }

    Promise.all(requests).then(fulfilledStuff=>{
        console.log(fulfilledStuff);    // Shall be an array of L.LatLngs, since each Promise resolved to a L.LatLngs
        fulfilledStuff
            .filter((item)=>!!item) // Filter out 'undefined' geocoding results
            .forEach((latLng) => {
                (L.marker(latLng)).addTo(projectsGroup);
            });
        mymap.fitBounds(projectsGroup.getBounds());
    });
});

Or we can get even fancier and do something like

createRequest(projectUrl, /* stuff */).then((jsonAddresses) => {
    let projectsGroup = L.featureGroup().addTo(mymap); // Creating an empty group to add markers to later on
    
    // jsonAddresses is an Array, right? Applying Array.prototype.map we can convert that into an Array of Promises.
    var promises = jsonAddresses.map((address)=>{
        let geocodingAddress = address.street + address.city; /* Real code is a bit more complex */
        geoCodingUrl = `${geoapifyURL}${theAddress}&apiKey=${GEOAPIFY_API_KEY}`;
        return createRequest(geoCodingUrl, /* stuff */)
            .then((geojson) => {
                if (geojson.features.length === 0) { return; }
                return (L.marker(L.GeoJSON.coordsToLatLng(geojson.features[0].coordinates))).addTo(projectsGroup);
            });
    })

    Promise.all(promises).then(fulfilledStuff=>{
        console.log(fulfilledStuff);    // Shall be an array of L.Markers
        mymap.fitBounds(projectsGroup.getBounds());
    });
});

Note that this sample code is missing code paths for handling most geocoding errors (what if the network is down for one of the geocoding operations?).

In any case, please do read the documentation about Promises, be aware if what you have is a Promise, an Array of Promises, an Object, an Array of Objects or whatever, and be aware of what your Promises resolve to (which is the return value of the callback passed to the last chained .then()).

1
  • Great! I was aware of the promises, but I totally forgot about Promise.all! Thank you so much Ivan
    – EarlyCoder
    Jul 7, 2020 at 18:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.