distancearcgis-js-apiesri-leaflet

Esri-Leaflet - Search within a distance


I need to design an application using a feature layer stored in ArcGIS online. Using a geocoder/search, I need to be able to enter an address and select a distance (1 block, 2 blocks, etc). The result will show the new point, a distance radius, and all points within the radius. I would also like to have a table of the results.

What I need is exactly like this app created by Derek Eder from DataMade: https://carto-template.netlify.app/, except mine needs the data stored in a secured ArcGIS layer. Can anyone point me to an example, tutorial, etc with an esri-leaflet implementation similar to this application? I have spent the past five days trying to convert the code, and I feel like I am getting no where.

Here is a link to guthub: https://github.com/datamade/searchable-map-template-carto

-------UPDATE-------

Seth - I can get the layer to display; however, the query to join the searched point with the layer does not work. I imagine I’m leaving something out, because the console error reads “token required”. See below:

    const radius = 1610;


    /**************************************************************************************************/
    // ArcGIS Authoization
    /**************************************************************************************************/

    $("#loginModal").modal({ backdrop: 'static', keyboard: false });

    // submit element of form
    var submitBtn = document.getElementById('btnArcGISOnline');

    // add event listener to form
    submitBtn.addEventListener('click', addServicesFromServer);

    // create map and set zoom level and center coordinates
    var map = L.map('mapCanvas', {
    }).setView([30.46258, -91.13171], 12);

    // set basemap to Esri Streets
    L.esri.basemapLayer('Streets').addTo(map);

    var layerurl = 'secure/layer/URL';

    var tokenUrl = 'https://www.arcgis.com/sharing/generateToken';
    
    // function to make request to server
    function serverAuth(server, username, password, callback) {
        L.esri.post(server, {
            username: username,
            password: password,
            f: 'json',
            expiration: 86400,
            client: 'referer',
            referer: window.location.origin
        }, callback);
    }
    
    // function to run when form submitted
    function addServicesFromServer(e) {
        // prevent page from refreshing
        e.preventDefault();

        // get values from form
        var username = document.getElementById('username').value;
        var password = document.getElementById('password').value;

        // generate token from server and add service from callback function
        serverAuth(tokenUrl, username, password, function (error, response) {
            if (error) {
                return;
            }

            // add layer to map
            var featureLayer = L.esri.featureLayer({
                url: layerurl,
                opacity: 1,
                token: response.token
            });
            featureLayer.addTo(map);
            $("#loginModal").modal("hide");

        }); // end serverAuth call
    } // end addServicesFromServer call        

    // HARNESS GEOCODER RESULTS
    let circle;

    // GeoSearch
    const search = L.esri.Geocoding.geosearch({
        useMapBounds: false,
        expanded: true,
        collapseAfterResult: false
    });
    search.addTo(map);      
        
    search.on("results", (results) => {
        if (results && results.latlng) {
            if (circle) {
                circle.remove();
            }
            circle = L.circle(results.latlng, { radius });
            circle.addTo(map);

            queryLayer(results.latlng);
        }
    });

    // SET UP QUERY FUNCTION
    function queryLayer(point) {
        const query = L.esri.query({ url: layerurl }).nearby(point, radius);
        query.run(function (error, featureCollection, response) {
            if (error) {
                console.log(error);
                return;
            }
            console.log(featureCollection.features);
            populateList(featureCollection.features);
        });
    }

    // WRITE RESULTS INTO A LIST
    function populateList(features) {
        const list = document.getElementById("results-list");
        let listItems = "";
        features.forEach((feature) => {
            listItems =
                listItems +
                `
                  <li>
                    Place: ${feature.properties?.Location} <br>
                    Lat: ${feature.properties?.Latitude} <br>
                    Lng: ${feature.properties?.Longitude} <br>
                  </li>
                `;

            list.innerHTML = listItems;
        });
    }

I attempted to pass the token to the query as pasted below, but then I get an invalid token error.

var layerUrl_token = layerurl + "?token=" + response.token;

I also tried using turf.js, but I haven’t been successful. I know turf.js uses long/lat, but I haven’t even been able to get the correct syntax to pull the lat and long from the feature layer.


Solution

  • What you're trying to do is not too hard. While there are a handful of tutorials on different parts of what you want to do, let's piece things together. I'm going to use esri-leaflet-geocoder for my search functionality, as its consistent with esri-leaflet, and IMO its one of the best geocoders available for leaflet.

    Setting up the geocoder

    After setting up a basic leaflet map, let's import esri-leaflet and esri-leaflet-geocoder, and create a geocoder:

    import L from "leaflet";
    import * as EL from "esri-leaflet";
    import * as ELG from "esri-leaflet-geocoder";
    
    const search = ELG.geosearch({
      useMapBounds: false,
      expanded: true,
      collapseAfterResult: false
    });
    
    search.addTo(map);
    

    Don't forget to add the geocoder css to your html, as shown in the documentation example.

    Add your layer:

    const layerurl = "YOUR_LAYER_URL";
    const featureLayer = EL.featureLayer({ url: layerurl });
    featureLayer.addTo(map);
    

    If you are using an authenication-required layer, you will need to get a token and use it as one of the options in featurelayer, (featureLayer({ url: layerurl, token: token })). If you're not sure how to get a token, make a comment and I can add some code for that, but there are some nice tutorials already available for that.

    Harness the results of the search

    The ELG.geosearch comes with a results event that you can harness. It is called when the user selects one of the results in the autocomplete dropdown of the geosearch. In that event, we can get the location data of location the user selected. We center the map there (which is a default of the geosearch actually), draw a circle with a given radius, and perform a query (more on that layer):

    let circle;
    
    search.on("results", (results) => {
      if (results && results.latlng) {
        if (circle) {
          circle.remove();
        }
        circle = L.circle(results.latlng, { radius });
        circle.addTo(map);
    
        queryLayer(results.latlng);
      }
    });
    

    Query the layer

    Now we know the latlng of the location the user selected from the search. We can create an esri-leaflet query, which can query your feature layer in various ways. We'll see up a nearby query, which will query the layer for any features within a given radius of a point:

    function queryLayer(point) {
      const query = EL.query({ url: layerurl }).nearby(point, radius);
      query.run(function (error, featureCollection, response) {
        if (error) {
          console.log(error);
          return;
        }
        populateList(featureCollection.features);
      });
    }
    

    If you are querying an authenticated layer, you'll need to add a token to the request. I'm fairly certain the way to do this is like so:

    function queryLayer(point) {
      const query = EL.query({ url: layerurl })
        .token(<your_token_here>)
        .nearby(point, radius);
      // ... same as above
    }
    

    You may also be able to run a query directly off of your layer:

    featureLayer.query().nearby(point, radius)
    

    I'm not as familiar with this second way, but you can read more about it here: Query a feature layer.

    Render to the page

    Once we .run the query, we will have access to the results in the form of a featureCollection. We can loop through the features of that featureCollection and render some HTML:

    function populateList(features) {
      const list = document.getElementById("results-list");
      let listItems = "";
      features.forEach((feature) => {
        listItems =
          listItems +
          `
          <li>
            Place: ${feature.properties?.Location} <br>
            Lat: ${feature.properties?.Latitude} <br>
            Lng: ${feature.properties?.Longitude} <br>
          </li>
        `;
    
        list.innerHTML = listItems;
      });
    }
    

    In this particular example, I am using a point layer I made that is being served through arcgis online. This point layer does not have address data, so feature.properties doesn't contain any address info. For your featurelayer, the attributes of your layer will be available in a feature.properties. So depending on what's there, you might want to use feature.properties?.address or whatever. This last bit is just an example, you will probably customize that a lot differently for your own purposes.

    Working Codesandbox

    Try searching heavily populated areas in this example. Note that in this featurelayer there are many overlapping locations, so there are more results in the list than it looks like there are markers on the map.

    Also note, this example I'm posting using esri-leaflet and esri-leaflet-geocoder versions 2^. These were just updated to versions 3 about 1-2 months ago, and the new versions require use of an API key in the geocoder and in the layer declaration, so if you want to use the latest versions (recommended), you will need to add those in. I used version 2 so as not to expose an API key in a sandbox (and I sort of hate the new API key requirement . The new arcgis developers documentation for esri-leaflet has some examples of that, but the official documentation has not yet been updated to match those examples.