Querying features around a buffer point with geoprocessing

3915
2
Jump to solution
08-08-2016 02:30 PM
Labels (1)
DerekYadlowski
New Contributor

I'm trying to create a map with ArcGIS layers that can be queried using a buffer point. As an end result, I would like all the points in the queried area to have their properties output into an array. To this end I'm using leafletJS for the map itself, esri-leaflet for the ArcGIS integration, and esri-leaflet-gp for the geoprocessing.  Unfortunately I've not been able to find much in the way of documentation or examples for useage regarding esri-leaflet-gp.

 

So far I've come up with this:

http://codepen.io/Metalyph/pen/rLQGEV?editors=1111

 

I think my first problem is the example data / task that I'm using.  They might not be conducive to the result I'm trying to achieve.  Unfortunately since I'm not sure what to look for it's hard to find example data to show what I'm trying to do.

 

My next problem pertains to the parameters.  One of the buffer parameter requires a GPLinearUnit variable. Using this page​ I determined that the parameter should look like this:

var gpBuffer = {
  "distance" : 700,
  "units" : "meters"
};

However, when I try to use that in the setParam() function I recieve this warning:

"invalid geometry passed as GP input. Should be an L.LatLng, L.LatLngBounds, L.Marker or GeoJSON Point Line or Polygon object"

Another parameter is a GPFeatureRecordSetLayer data type, which I'm having trouble understanding in and of itself.  The parameter seems to be asking for an address, and has a number of fields, like OBJECTID, CUSTOMER_NAME, CITY, etc.  Do I need to provide a JSON variable with all these fields filled out?  Or is it asking for something else entirely?  The buffer task can be found here.

 

And lastly, how do I associate a map layer to this task? (in this case, layer_03)

 

Also, a little bit off-topic, but is there potentially a better way to go about doing this?

 

I hope my issue comes across clearly, I'm still fairly new to the GIS side of web development.  Please feel free to ask me for clarification on any of these points.

0 Kudos
1 Solution

Accepted Solutions
JohnGravois
Frequent Contributor

Your GP task will execute sucessfully if you set the parameters below.

gpTask.setParam("Input_Features", gpPoint );
gpTask.setParam("Distance__value_or_field_", JSON.stringify(gpBuffer) );
gpTask.setOutputParam("Addresses_Buffer");

1. the esri-leaflet-gp plugin is able to translate L.LatLng, L.LatLngBounds, L.Marker or GeoJSON Point Line and Polygon geometry objects into a GPFeatureRecordSetLayer for ArcGIS Server.  The default value you see in the service itself defines an attribute schema and an empty array of features, but since it appears that the service just buffers input geometries, it seems that providing specific attributes that correspond with the geometries isn't technically required.

2. for better or worse, esril-leaflet-gp assumes that all objects passed to .setParam() are geometries.  because in your case you just want to pass a generic JSON object literal to your service, it can be passed as a string instead.

3. its also currently necessary to tell esri-leaflet-gp the same of our output parameter so that it knows where to fetch the results of async requests.

i see a couple generic usability issues and opportunities to improve the documentation of the library as a result of your questions, so if you'd be gracious enough to open an issue and act as a sounding board, i'd be happy to chat further.

And now for something completely different.

based on what you've written (and what we discussed previously in github) it  doesn't sound like you are actually attempting to buffer a known collection of features at all, but rather to filter an existing service to find features that are inside a known circular polygon on the fly.  if this is the case you were on the right track originally when attempting to use QueryTask.nearby(). 

the good news is, that even if the specific service you want to query doesn't (and can't be made to) support the nearby operation (which only requires a center and radius), ALL esri feature layers allow you to pass an actual circular geometry that you've already constructed in order to quickly determine which features intersect it.

since libraries like turf.js provide clientside JavaScript buffering directly in the browser, you'd be a lot better off using something like that to generate GeoJSON representing your circle and then pass that to QueryTask.within() or intersects().  another option would be to rip off some code i ripped of from stack overflow (below) for creating your own geodesic circles without a new external dependency.

https://github.com/Esri/geotrigger-editor/pull/225/files

View solution in original post

2 Replies
JohnGravois
Frequent Contributor

Your GP task will execute sucessfully if you set the parameters below.

gpTask.setParam("Input_Features", gpPoint );
gpTask.setParam("Distance__value_or_field_", JSON.stringify(gpBuffer) );
gpTask.setOutputParam("Addresses_Buffer");

1. the esri-leaflet-gp plugin is able to translate L.LatLng, L.LatLngBounds, L.Marker or GeoJSON Point Line and Polygon geometry objects into a GPFeatureRecordSetLayer for ArcGIS Server.  The default value you see in the service itself defines an attribute schema and an empty array of features, but since it appears that the service just buffers input geometries, it seems that providing specific attributes that correspond with the geometries isn't technically required.

2. for better or worse, esril-leaflet-gp assumes that all objects passed to .setParam() are geometries.  because in your case you just want to pass a generic JSON object literal to your service, it can be passed as a string instead.

3. its also currently necessary to tell esri-leaflet-gp the same of our output parameter so that it knows where to fetch the results of async requests.

i see a couple generic usability issues and opportunities to improve the documentation of the library as a result of your questions, so if you'd be gracious enough to open an issue and act as a sounding board, i'd be happy to chat further.

And now for something completely different.

based on what you've written (and what we discussed previously in github) it  doesn't sound like you are actually attempting to buffer a known collection of features at all, but rather to filter an existing service to find features that are inside a known circular polygon on the fly.  if this is the case you were on the right track originally when attempting to use QueryTask.nearby(). 

the good news is, that even if the specific service you want to query doesn't (and can't be made to) support the nearby operation (which only requires a center and radius), ALL esri feature layers allow you to pass an actual circular geometry that you've already constructed in order to quickly determine which features intersect it.

since libraries like turf.js provide clientside JavaScript buffering directly in the browser, you'd be a lot better off using something like that to generate GeoJSON representing your circle and then pass that to QueryTask.within() or intersects().  another option would be to rip off some code i ripped of from stack overflow (below) for creating your own geodesic circles without a new external dependency.

https://github.com/Esri/geotrigger-editor/pull/225/files

BillChappell
Occasional Contributor II

Here is some rough code I used to prototypef. The idea was to find addresses (points) within a distance of ___. Since my Address source had over a million points, I created a circle and used it's bounds to select the points, then used a function to select points inside the circle. Finally drawing them based on the quad they were in and creating a CSV of the points.

<html>
<head>
  <meta charset=utf-8 />
  <title>test App</title>
  <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />

  <!-- Load Leaflet from CDN-->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/leaflet/1.0.0-rc.1/leaflet.css" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/leaflet.esri.geocoder/2.0.3/esri-leaflet-geocoder.css">

    <script src="https://cdn.jsdelivr.net/leaflet/1.0.0-rc.1/leaflet-src.js"></script>
    <script src="https://cdn.jsdelivr.net/leaflet.esri/2.0.0/esri-leaflet.js"></script>
    <script src="https://cdn.jsdelivr.net/leaflet.esri.geocoder/2.0.3/esri-leaflet-geocoder.js"></script>
      
  <style>
body { margin:0; padding:0; }
#map { position: absolute; top:40; bottom:0; right:0; left:0; max-height:640px; max-width:950px}
    

  </style>
</head>
<body>
<div id="header">
         <div id="Point" class="leaflet-bar">
            <label>
                Search Coordinates:
                <input id="Ypt" class="leaflet-bar" placeholder="Latitude" />
                <input id="Xpt" class="leaflet-bar" placeholder="Longitude" />
            </label>
            </label>
            Search Radius:
            <input id="Rdistance" class="leaflet-bar" placeholder="Radius (Yards)"size="6" />
            <button id="goBtn" onClick="goBtn()" class="leaflet-bar">Go</button>
        </div>    
</div>
<div id="map"></div>

<script>
document.getElementById("Rdistance").value = 200;
var map = L.map('map').setView([43, -75], 7);
var theRadius;

//start Basemaps
var layer1 = L.esri.basemapLayer('Topographic');
var layer2 = L.esri.basemapLayer('Streets');
var layer3 = L.esri.basemapLayer('NationalGeographic');
// maps with labels
var layer4 = L.layerGroup([
    L.esri.basemapLayer('Imagery'),
    L.esri.basemapLayer('ImageryLabels')
]);
var layer6 = L.layerGroup([
    L.esri.basemapLayer('Oceans'),
    L.esri.basemapLayer('OceansLabels')
]);
var layer7 = L.layerGroup([
    L.esri.basemapLayer('Gray'),
    L.esri.basemapLayer('GrayLabels')
]);
var layer8 = L.layerGroup([
    L.esri.basemapLayer('DarkGray'),
    L.esri.basemapLayer('DarkGrayLabels')
]);
var layer9 = L.layerGroup([
    L.esri.basemapLayer('Terrain'),
    L.esri.basemapLayer('TerrainLabels')
]);

var osm = new L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// end basemaps

function goBtn() {
    var theLatValue = document.getElementById("Ypt").value;
    var theLonValue = document.getElementById("Xpt").value;
    var theRadiusValue = document.getElementById("Rdistance").value;

    theRadius = parseFloat(theRadiusValue) * 0.9144;
    if (theRadius !== 0) {
        console.log("Button Pressed, coords are: " + theLatValue + ", " + theLonValue);
        console.log("Radius is: " + theRadius + " Meters");

        // Call function to pass it an X,Y, and have it run.
        Mapit(theLonValue, theLatValue, theRadius); //    LAT: 42.76346 and LONG: -73.78285

    } else {
        alert("Radius is Zero Length, Please change value");
    }
}

alert("To test coord entry use LAT: 42.76346 and LONG: -73.78285 Distance is Yards");

var thelatlng;
var clickCircle;
var bounds;
var geojsonLayer;
var PointX = 0;
var PointY = 0;


function Mapit(x, y, theRadius) {
    console.log(x + ", " + y + " Values");

    PointX = parseFloat(x);
    PointY = parseFloat(y);
    var latY = parseFloat(y);
    var longX = parseFloat(x);

    if (latY === 0 || longX === 0) {
        alert("Invalid Coordinate value , Please check value");
    }

    var thelatlng = L.latLng(latY, longX);

    theCircle = L.circle(thelatlng, theRadius, {
        color: 'blue',
        fillOpacity: 0,
        opacity: 0.5
    }).addTo(map);

    //Zoom to Circle  //I use the bounds of the circle to query against to avoid a 1 million point query.
    map.fitBounds(theCircle.getBounds());
    var bounds = theCircle.getBounds();  

    var query = L.esri.query({
        url: 'http://gisservices.dhses.ny.gov/arcgis/rest/services/SAM_Address_Points/MapServer/1'
    });

    query.within(bounds);
    
    query.run(function(error, featureCollection, response) {
        console.log('Found ' + featureCollection.features.length + ' features');
        if (featureCollection.features.length < 1) {
            alert("No Addresses in the selected zone");
        }

        var myGeoJSON = JSON.stringify(featureCollection);

        //Symbolize the Points
        var geojsonMarkerOptions = {
            radius: 8,
            fillColor: "green",
            color: "#green",
            weight: 1,
            opacity: 1,
            fillOpacity: 0.8
        };

        geojsonLayer = L.geoJson(featureCollection, {
            pointToLayer: function(feature, latlng) {
                return L.circleMarker(latlng, geojsonMarkerOptions);
            },
            onEachFeature: function(feature, layer) {

            }
        }); //.addTo(map);  //no need to add to map, these are the points from the bounds query

        var test = [];

        var theCenterPt = thelatlng; // bounds.getCenter();  // layer.getLatLng();
        //var theRadius = theRadius;  
        var counter_points_in_circle = 0;
        var marker = L.marker(thelatlng).bindPopup("CTR").addTo(map);
        // Loop through each point in GeoJSON file  //var allPoints = L.geoJson(data);
        geojsonLayer.eachLayer(function(layer) {

            // Lat, long of current point
            layer_lat_long = layer.getLatLng();
            // Distance from our circle marker To current point in meters
            distance_from_centerPoint = layer_lat_long.distanceTo(theCenterPt);
            // See if meters is within radius
            if (distance_from_centerPoint <= theRadius) {
                counter_points_in_circle += 1;

                test.push(layer.feature);
            }
        });
        console.log(test.length + " Points in Circle");

        var myGeoJSON2 = JSON.stringify(test);  //data from the  buffer query

        dataExport(test, theCenterPt);
        //Symbolize the Points
        var geojsonMarkerOptions2 = {
            radius: 8,
            fillColor: "red",
            color: "red",
            weight: 1,
            opacity: 1,
            fillOpacity: 0.8
        };

        geojsonLayer2 = L.geoJson(test, {
            pointToLayer: function(feature, latlng) {
                return L.circleMarker(latlng, geojsonMarkerOptions2);
            },
            onEachFeature: function(feature, layer) {}
        });

    });
    // creating a CSV file from selection
    function dataExport(response, theCenter) {
        var homes = new L.LayerGroup();
        var thebounds = new L.LatLngBounds();

        var i;
        var str = "Record, Longitude, Latitude, Street, City, Quad \r\n ";
        for (i = 0; i < response.length; i++) {

            var city = response.properties.CityTownName;
            var theQuad = quad(response.geometry.coordinates);

            var add = '';
            
            if (response.properties.AP_Flag !== null) {
                add = response.properties.AddressLabel.replace(/(\r\n|\n|\r)/gm, " ").replace(response.properties.AP_Flag, '');
            } else {
                add = response.properties.AddressLabel.replace(/(\r\n|\n|\r)/gm, " ");
            }
            response.properties.Record = (i + 1);  //Adding in Record to GeoJSON
            response.properties.Quad = theQuad;    //Adding in Quad to GeoJSON
            response.properties.FullAdd = add.trim();
            var line = '';

            line += (i + 1) + ", " +

                response.geometry.coordinates + ", " +
                add.trim() + ", " +
                city + ", " +
                theQuad;

            str += line + '\r\n';
        }

        sites(response);

        function sites(response5) {
            
            function getColor(d) {
                return d == 'NE' ? '#0000CD' :
                    d == 'SE' ? '#008000' :
                    d == 'SW' ? '#FFFF00' :
                    d == 'NW' ? '#FF0000' :
                    '#A9A9A9';
            }

            geojsonLayer3 = L.geoJson(response5, {
                pointToLayer: function(feature, latlng) {
                    var markerStyle = {
                        fillColor: getColor(feature.properties.Quad),
                        color: "#696969",
                        fillOpacity: 0.8,
                        opacity: 0.9,
                        weight: 1,
                        radius: 8
                    };
                    return L.circleMarker(latlng, markerStyle);
                 },
                onEachFeature: function(feature, layer) {
                    layer.bindPopup("<p>Record: " + feature.properties.Record + "</br>Address: " + feature.properties.FullAdd + "</br> Quad: " + feature.properties.Quad + "</p>").addTo(map),
                        
                        layer.addTo(map);
                        
                }
            });

            map.addLayer(geojsonLayer3);
            map.fitBounds(geojsonLayer3.getBounds()); //Zoom to selected set
        }

        var exportFilename = "MapData.csv";
        var csvData = new Blob([str], {
            type: 'text/csv;charset=utf-8;'
        });
        //IE11 & Edge
        if (navigator.msSaveBlob) {
            navigator.msSaveBlob(csvData, exportFilename);
        } else {
            //In FF link must be added to DOM to be clicked
            var link = document.createElement('a');
            link.href = window.URL.createObjectURL(csvData);
            link.setAttribute('download', exportFilename);
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }

        // Takes the point coords and determines if they are NE,SE,SW, or NW of the center Pt
        function quad(value) {
            var theValue = "";
            var theAddress = value.toString().split(',');
            var addresslatC = theAddress[0];
            var addresslongC = theAddress[1];
            var theY = parseFloat(addresslongC);
            var theX = parseFloat(addresslatC);

            var theCentY = PointY;
            var theCentX = PointX;

            if (theX > theCentX) {
                if (theY > theCentY) {
                    theValue = "NE";
                } else {
                    theValue = "SE";
                }

            } else {
                if (theY > theCentY) {
                    theValue = "NW";
                } else {
                    theValue = "SW";
                }
            }
            return theValue;
                // End of Quad function
        }
    }
}

// create the geocoding control and add it to the map
var arcgisOnline = L.esri.Geocoding.arcgisOnlineProvider();

var searchControl = L.esri.Geocoding.geosearch({
    providers: [arcgisOnline]
}).addTo(map);

var results = L.layerGroup().addTo(map);

// listen for the results event and add every result to the map
searchControl.on("results", function(data) {
    results.clearLayers();
    for (var i = data.results.length - 1; i >= 0; i--) {
        // results.addLayer(L.marker(data.results[0].latlng));

        var home = data.results[0].latlng;
            //    alert(home);

        var coord = home.toString().split(',');
        var lat = coord[0].split('(');
        var long1 = coord[1].split(')');

        if (i < 1) {
            var theRadiusValue = document.getElementById("Rdistance").value;
            theRadius = parseFloat(theRadiusValue) * 0.9144;
            Mapit(long1[0], lat[1], theRadius);
        }

    }
});
//layer control
var baseMaps = {
    'Topographic': layer1,
    'Streets': layer2,
    'NationalGeographic': layer3,
    'Imagery': layer4,
    'Oceans': layer6,
    'Gray': layer7,
    'DarkGray': layer8,
    'Terrain': layer9
};

var overlayMaps = { };

L.control.layers(baseMaps, overlayMaps, {
    autoZIndex: "true"
}).addTo(map);
////////////// end layer control
 
</script>
</body>
</html>

0 Kudos