Select to view content in your preferred language

Looping with GeometryService Distance (deferred problem?)

2776
2
Jump to solution
08-22-2014 03:59 PM
ZoeZaloudek
Occasional Contributor

OK, there has GOT to be a way to do this that I'm just not seeing on my own.

I have a feature layer of points (stations) in my map.  The task is to find the three points in the feature layer that are closest to where the user clicks on the map (and then display data from those stations, etc...).

So far, I have been able to select all points within a 50-mile radius circle of the clicked-point, and I have saved the points' names/IDs and locations (as point geometry) to an array.

Next, I want to loop through the objects in the array I've just made, and for each, find its distance from the clicked-point (using the geometryService.distance method).  Then I add the station's name and distance to a new array/list.  My plan from there is to figure out which three stations are the closest, and get the data I need to display, etc.

My problem is that the code is looping through the array faster than it can figure out each distance.  In my second array/list, every distance has the same station name, which happens to be the name of the last station in the first array.  I know that the result of the geometryService.distance method is a deferred object.  I've been trying to educate myself on these.  I think I get the jist of deferreds, but I can't figure out how to make them work for me here.  I have seen suggestions by some to use a deferred list, but it appears that you have to know how many items you want in that list beforehand.  In my case, I don't know exactly how many points will be within that 50-mile radius of the user's clicked-point.

Here is my javascript code as it stands.  At this point, my feature layer (named fLayer) already has the points within the 50-mile radius selected.

var selGraphics = fLayer.getSelectedFeatures();

var feature;

list = [];

for (i = 0; i < selGraphics.length; i++) {

    var stnname;

    feature = selGraphics;

    stnname = feature.attributes["Name"];

    uoid = feature.attributes["uid"];

    ptloc = feature.geometry;

    list.push({name:stnname, uid:uoid, loc:ptloc});                           

}

console.log('outside first loop now');

console.log(list);

distlist = [];

for (i in list)  {

    station = list

    console.log(station);

    ptloc = station.loc;

    distParams.geometry2 = ptloc;

    thisdist = geometryService.distance(distParams, function(distance) {

        console.log(station.name, distance);

        distlist.push({name:station.name, dist:distance});

    });

}

console.log('outside second loop now');

console.log(distlist);

BTW, I am much more comfortable with python than I am with javascript/html, hence my personal need to use lists/arrays.  If there is a better way to deal with this I'm open to suggestion!

Thanks!

0 Kudos
1 Solution

Accepted Solutions
RobertScheitlin__GISP
MVP Emeritus

Zoe,

  So here is a sample that uses promises and deferred and does exactly what you are asking about:

<!DOCTYPE html>

<html>

  <head>

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

    <!--The viewport meta tag is used to improve the presentation and behavior of the samples

      on iOS devices-->

    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">

    <title>Select with feature layer</title>

    <link rel="stylesheet" href="http://js.arcgis.com/3.10/js/dojo/dijit/themes/tundra/tundra.css">

    <link rel="stylesheet" href="http://js.arcgis.com/3.10/js/esri/css/esri.css">

    <style>

      html, body, #mapDiv {

        padding: 0;

        margin: 0;

        height: 100%;

      }

      #messages{

        background-color: #fff;

        box-shadow: 0 0 5px #888;

        font-size: 1.1em;

        max-width: 15em;

        padding: 0.5em;

        position: absolute;

        right: 20px;

        top: 20px;

        z-index: 40;

      }

    </style>

    <script src="http://js.arcgis.com/3.10/"></script>

    <script>

      var map, userMP;

      require([

        "esri/map", "esri/layers/FeatureLayer", "dojo/_base/lang",

        "esri/tasks/query", "esri/geometry/Circle", "esri/layers/GraphicsLayer",

        "esri/graphic", "esri/InfoTemplate", "esri/symbols/SimpleMarkerSymbol",

        "esri/symbols/SimpleLineSymbol", "esri/symbols/SimpleFillSymbol", "esri/renderers/SimpleRenderer",

        "esri/config", "esri/Color", "dojo/dom", "esri/tasks/DistanceParameters", "esri/tasks/GeometryService",

        "dojo/promise/all", "dojox/gfx/fx", "dojo/domReady!"

      ], function(

        Map, FeatureLayer, lang,

        Query, Circle, GraphicsLayer,

        Graphic, InfoTemplate, SimpleMarkerSymbol,

        SimpleLineSymbol, SimpleFillSymbol, SimpleRenderer,

        esriConfig, Color, dom, DistanceParameters, GeometryService, all, fx

      ) {

        // use a proxy page if a URL generated by this page is greater than 2000 characters

        //

        // this should not be needed as nearly all query & select functions are performed on the client

        esriConfig.defaults.io.proxyUrl = "/proxy";

        map = new Map("mapDiv", {

          basemap: "streets",

          center: [-95.249, 38.954],

          zoom: 14,

          slider: false

        });

        

        //add the census block points in on demand mode. Note that an info template has been defined so when

        //selected features are clicked a popup window will appear displaying the content defined in the info template.

        var featureLayer = new FeatureLayer("http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/0",{

          infoTemplate: new InfoTemplate("Block: ${BLOCK}", "${*}"),

          outFields: ["POP2000","HOUSEHOLDS","HSE_UNITS", "TRACT", "BLOCK"]

        });

        // selection symbol used to draw the selected census block points within the buffer polygon

        var symbol = new SimpleMarkerSymbol(

          SimpleMarkerSymbol.STYLE_CIRCLE,

          12,

          new SimpleLineSymbol(

            SimpleLineSymbol.STYLE_NULL,

            new Color([247, 34, 101, 0.9]),

            1

          ),

          new Color([207, 34, 171, 0.5])

        );

        var symbol2 = new SimpleMarkerSymbol(

          SimpleMarkerSymbol.STYLE_DIAMOND,

          15,

          new SimpleLineSymbol(

            SimpleLineSymbol.STYLE_NULL,

            new Color([0, 0, 0, 0.9]),

            1

          ),

          new Color([255, 0, 0, 1])

        );

      

        var graphic;

        

        featureLayer.setSelectionSymbol(symbol);

      

        //make unselected features invisible

        var nullSymbol = new SimpleMarkerSymbol().setSize(0);

        featureLayer.setRenderer(new SimpleRenderer(nullSymbol));

      

        map.addLayer(featureLayer);

      

        var circleSymb = new SimpleFillSymbol(

          SimpleFillSymbol.STYLE_NULL,

          new SimpleLineSymbol(

            SimpleLineSymbol.STYLE_SHORTDASHDOTDOT,

            new Color([105, 105, 105]),

            2

          ), new Color([255, 255, 0, 0.25])

        );

        var circle;

        //when the map is clicked create a buffer around the click point of the specified distance.

        map.on("click", function(evt){

          userMP = evt.mapPoint;

          circle = new Circle({

            center: evt.mapPoint,

            geodesic: true,

            radius: 0.25,

            radiusUnit: "esriMiles"

          });

          map.graphics.clear();

          map.infoWindow.hide();

          var graphic = new Graphic(circle, circleSymb);

          map.graphics.add(graphic);

          var query = new Query();

          query.geometry = circle.getExtent();

          //use a fast bounding box query. will only go to the server if bounding box is outside of the visible map

          featureLayer.queryFeatures(query, selectInBuffer);

        });

        function selectInBuffer(response){

          var feature;

          var features = response.features;

          var inBuffer = [];

          //filter out features that are not actually in buffer, since we got all points in the buffer's bounding box

          for (var i = 0; i < features.length; i++) {

            feature = features;

            if(circle.contains(feature.geometry)){

              inBuffer.push(feature.attributes[featureLayer.objectIdField]);

            }

          }

          var query = new Query();

          query.objectIds = inBuffer;

          //use a fast objectIds selection query (should not need to go to the server)

          featureLayer.selectFeatures(query, FeatureLayer.SELECTION_NEW, function(results){

            findClosest(results);

          });

        }

        

        function findClosest(features) {

          var geometryService = new GeometryService("http://utility.arcgisonline.com/arcgis/rest/services/Geometry/GeometryServer");

          var promises;

          var dlist = [];

          var list = [];

          var rstr = "";

          var graphicFlash;

          var BLOCK, ptloc, distp;

          for (var x = 0; x < features.length; x++) {

            var distParams = new DistanceParameters();

            distParams.distanceUnit = GeometryService.UNIT_FOOT;

            distParams.geometry1 = userMP;

            distParams.geodesic = true;

            BLOCK = features.attributes["BLOCK"];

            ptloc = features.geometry;

            distParams.geometry2 = features.geometry;

            dlist.push({name:BLOCK, loc:ptloc});

          

            distp = geometryService.distance(distParams);

            list.push(distp)

          }

          all(list).then(lang.hitch(this,function(results){

            var shortestDist = Number.POSITIVE_INFINITY;

            var closestBlock = "";

            for (var dv = 0; dv < dlist.length; dv++){

              rstr += "Block: " + dlist[dv].name + " is " + results[dv].toFixed(2) + "ft<br>";

              if(results[dv] < shortestDist){

                shortestDist = results[dv];

                closestBlock = dlist[dv].name;

                graphicFlash = new esri.Graphic(dlist[dv].loc, symbol2)

              }

            }

            rstr += "<br><strong>The closest Block is " + closestBlock + " at " + shortestDist.toFixed(2) + "ft</strong>"

            dom.byId("messages").innerHTML = rstr;

            map.graphics.add(graphicFlash);

          

            var shape = graphicFlash.getDojoShape();

            var animStroke = fx.animateStroke({

                shape: shape,

                duration: 500,

                color: { end: new dojo.Color([0, 0, 0, 0]) }

            });

            var animFill = fx.animateFill({

                shape: shape,

                duration: 500,

                color: { end: new dojo.Color([0, 0, 0, 0]) }

            });

            var anim = dojo.fx.combine([animStroke, animFill]).play();

            var animConnect = dojo.connect(anim, "onEnd", function () {

                map.graphics.remove(graphicFlash);

            });

          

          }));

        }

      });

    </script>

  </head>

  <body>

    <span id="messages">Click on the map to select census block points within 1/4 mile and calculate the distance from map click to each block.</span>

    <div id="mapDiv"></div>

  </body>

</html>

View solution in original post

0 Kudos
2 Replies
RobertScheitlin__GISP
MVP Emeritus

Zoe,

  So here is a sample that uses promises and deferred and does exactly what you are asking about:

<!DOCTYPE html>

<html>

  <head>

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

    <!--The viewport meta tag is used to improve the presentation and behavior of the samples

      on iOS devices-->

    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">

    <title>Select with feature layer</title>

    <link rel="stylesheet" href="http://js.arcgis.com/3.10/js/dojo/dijit/themes/tundra/tundra.css">

    <link rel="stylesheet" href="http://js.arcgis.com/3.10/js/esri/css/esri.css">

    <style>

      html, body, #mapDiv {

        padding: 0;

        margin: 0;

        height: 100%;

      }

      #messages{

        background-color: #fff;

        box-shadow: 0 0 5px #888;

        font-size: 1.1em;

        max-width: 15em;

        padding: 0.5em;

        position: absolute;

        right: 20px;

        top: 20px;

        z-index: 40;

      }

    </style>

    <script src="http://js.arcgis.com/3.10/"></script>

    <script>

      var map, userMP;

      require([

        "esri/map", "esri/layers/FeatureLayer", "dojo/_base/lang",

        "esri/tasks/query", "esri/geometry/Circle", "esri/layers/GraphicsLayer",

        "esri/graphic", "esri/InfoTemplate", "esri/symbols/SimpleMarkerSymbol",

        "esri/symbols/SimpleLineSymbol", "esri/symbols/SimpleFillSymbol", "esri/renderers/SimpleRenderer",

        "esri/config", "esri/Color", "dojo/dom", "esri/tasks/DistanceParameters", "esri/tasks/GeometryService",

        "dojo/promise/all", "dojox/gfx/fx", "dojo/domReady!"

      ], function(

        Map, FeatureLayer, lang,

        Query, Circle, GraphicsLayer,

        Graphic, InfoTemplate, SimpleMarkerSymbol,

        SimpleLineSymbol, SimpleFillSymbol, SimpleRenderer,

        esriConfig, Color, dom, DistanceParameters, GeometryService, all, fx

      ) {

        // use a proxy page if a URL generated by this page is greater than 2000 characters

        //

        // this should not be needed as nearly all query & select functions are performed on the client

        esriConfig.defaults.io.proxyUrl = "/proxy";

        map = new Map("mapDiv", {

          basemap: "streets",

          center: [-95.249, 38.954],

          zoom: 14,

          slider: false

        });

        

        //add the census block points in on demand mode. Note that an info template has been defined so when

        //selected features are clicked a popup window will appear displaying the content defined in the info template.

        var featureLayer = new FeatureLayer("http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/0",{

          infoTemplate: new InfoTemplate("Block: ${BLOCK}", "${*}"),

          outFields: ["POP2000","HOUSEHOLDS","HSE_UNITS", "TRACT", "BLOCK"]

        });

        // selection symbol used to draw the selected census block points within the buffer polygon

        var symbol = new SimpleMarkerSymbol(

          SimpleMarkerSymbol.STYLE_CIRCLE,

          12,

          new SimpleLineSymbol(

            SimpleLineSymbol.STYLE_NULL,

            new Color([247, 34, 101, 0.9]),

            1

          ),

          new Color([207, 34, 171, 0.5])

        );

        var symbol2 = new SimpleMarkerSymbol(

          SimpleMarkerSymbol.STYLE_DIAMOND,

          15,

          new SimpleLineSymbol(

            SimpleLineSymbol.STYLE_NULL,

            new Color([0, 0, 0, 0.9]),

            1

          ),

          new Color([255, 0, 0, 1])

        );

      

        var graphic;

        

        featureLayer.setSelectionSymbol(symbol);

      

        //make unselected features invisible

        var nullSymbol = new SimpleMarkerSymbol().setSize(0);

        featureLayer.setRenderer(new SimpleRenderer(nullSymbol));

      

        map.addLayer(featureLayer);

      

        var circleSymb = new SimpleFillSymbol(

          SimpleFillSymbol.STYLE_NULL,

          new SimpleLineSymbol(

            SimpleLineSymbol.STYLE_SHORTDASHDOTDOT,

            new Color([105, 105, 105]),

            2

          ), new Color([255, 255, 0, 0.25])

        );

        var circle;

        //when the map is clicked create a buffer around the click point of the specified distance.

        map.on("click", function(evt){

          userMP = evt.mapPoint;

          circle = new Circle({

            center: evt.mapPoint,

            geodesic: true,

            radius: 0.25,

            radiusUnit: "esriMiles"

          });

          map.graphics.clear();

          map.infoWindow.hide();

          var graphic = new Graphic(circle, circleSymb);

          map.graphics.add(graphic);

          var query = new Query();

          query.geometry = circle.getExtent();

          //use a fast bounding box query. will only go to the server if bounding box is outside of the visible map

          featureLayer.queryFeatures(query, selectInBuffer);

        });

        function selectInBuffer(response){

          var feature;

          var features = response.features;

          var inBuffer = [];

          //filter out features that are not actually in buffer, since we got all points in the buffer's bounding box

          for (var i = 0; i < features.length; i++) {

            feature = features;

            if(circle.contains(feature.geometry)){

              inBuffer.push(feature.attributes[featureLayer.objectIdField]);

            }

          }

          var query = new Query();

          query.objectIds = inBuffer;

          //use a fast objectIds selection query (should not need to go to the server)

          featureLayer.selectFeatures(query, FeatureLayer.SELECTION_NEW, function(results){

            findClosest(results);

          });

        }

        

        function findClosest(features) {

          var geometryService = new GeometryService("http://utility.arcgisonline.com/arcgis/rest/services/Geometry/GeometryServer");

          var promises;

          var dlist = [];

          var list = [];

          var rstr = "";

          var graphicFlash;

          var BLOCK, ptloc, distp;

          for (var x = 0; x < features.length; x++) {

            var distParams = new DistanceParameters();

            distParams.distanceUnit = GeometryService.UNIT_FOOT;

            distParams.geometry1 = userMP;

            distParams.geodesic = true;

            BLOCK = features.attributes["BLOCK"];

            ptloc = features.geometry;

            distParams.geometry2 = features.geometry;

            dlist.push({name:BLOCK, loc:ptloc});

          

            distp = geometryService.distance(distParams);

            list.push(distp)

          }

          all(list).then(lang.hitch(this,function(results){

            var shortestDist = Number.POSITIVE_INFINITY;

            var closestBlock = "";

            for (var dv = 0; dv < dlist.length; dv++){

              rstr += "Block: " + dlist[dv].name + " is " + results[dv].toFixed(2) + "ft<br>";

              if(results[dv] < shortestDist){

                shortestDist = results[dv];

                closestBlock = dlist[dv].name;

                graphicFlash = new esri.Graphic(dlist[dv].loc, symbol2)

              }

            }

            rstr += "<br><strong>The closest Block is " + closestBlock + " at " + shortestDist.toFixed(2) + "ft</strong>"

            dom.byId("messages").innerHTML = rstr;

            map.graphics.add(graphicFlash);

          

            var shape = graphicFlash.getDojoShape();

            var animStroke = fx.animateStroke({

                shape: shape,

                duration: 500,

                color: { end: new dojo.Color([0, 0, 0, 0]) }

            });

            var animFill = fx.animateFill({

                shape: shape,

                duration: 500,

                color: { end: new dojo.Color([0, 0, 0, 0]) }

            });

            var anim = dojo.fx.combine([animStroke, animFill]).play();

            var animConnect = dojo.connect(anim, "onEnd", function () {

                map.graphics.remove(graphicFlash);

            });

          

          }));

        }

      });

    </script>

  </head>

  <body>

    <span id="messages">Click on the map to select census block points within 1/4 mile and calculate the distance from map click to each block.</span>

    <div id="mapDiv"></div>

  </body>

</html>

0 Kudos
ZoeZaloudek
Occasional Contributor

Thank you!  I was finally able to get back around to my project today.  This is exactly what I needed, a functioning example.  I think part of my problem may have been that I was not defining a function when selecting the stations within the buffer circle.  I was just doing this:

fLayer.selectFeatures(query, FeatureLayer.SELECTION_NEW);

var selGraphics = fLayer.getSelectedFeatures();

and then looping through the objects in selGraphics.

I have also never used "lang.hitch()" before.  Something new to read up on!

Side note, the code posted is a little different from the "Select with Feature Layer" sample code that I see on the Samples page now... interesting.

Thanks for your help!  Now I can move forward with this web map!

0 Kudos