Select to view content in your preferred language

Looping through WebMap feature layers for spatial filtering in a SketchViewModel

788
4
Jump to solution
11-20-2023 05:00 AM
EmilyGoldsmith
New Contributor III

Hello,

I am very new to using the ArcGIS JavaScript API plugin and JavaScript in general so, to learn, I have been playing around with the sample code, specifically that for the SketchViewModel, here: https://developers.arcgis.com/javascript/latest/sample-code/layers-scenelayer-feature-masking/

My issue:

Currently, I am experimenting with converting the SketchViewModel example above, which works with 3D SceneView data, to one which is able to filter through feature layers in a 2D WebMap. However, am having difficulty looping it over each feature layer (of which there are 2) in my webmap:

 

 

 

​<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <title>Filter webmap with FeatureFilter | Test | ArcGIS Maps SDK for JavaScript 4.28</title>
    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }

      #infoDiv {
        background-color: white;
        padding: 10px;
        width: 260px;
        margin: 5px;
        position: absolute;
        bottom: 15px;
        right: 10px;
        font-size: 14px;
        display: none;
      }

      .geometry-options {
        display: flex;
        flex-direction: row;
      }

      .geometry-button {
        flex: 1;
        border-style: solid;
        border-width: 1px;
        border-image: none;
      }

      .geometry-button-selected {
        background: #4c4c4c;
        color: #fff;
      }

      .options {
        max-width: 260px;
        width: 100%;
        height: 25px;
      }

    </style>

    <link rel="stylesheet" href="https://js.arcgis.com/4.28/esri/themes/light/main.css" />
    <script src="https://js.arcgis.com/4.28/"></script>

    <script>
      require([
        "esri/WebMap",
        "esri/views/MapView",
        "esri/layers/GraphicsLayer",
        "esri/widgets/Sketch/SketchViewModel",
        "esri/widgets/Slider",
        "esri/layers/support/FeatureFilter",
        "esri/geometry/geometryEngine",
        "esri/Graphic"
      ], (WebMap, MapView, GraphicsLayer, SketchViewModel, Slider, FeatureFilter, geometryEngine, Graphic) => {
        // Load webmap and display it in a webmap
        const map = new WebMap({
          portalItem: {
            id: "1a3366d6e59945289a141e3ae62698b6"
          }
        });

        // create the mapview
        const view = new MapView({
          container: "viewDiv",
          map: map,
          popupEnabled: false
        });

        // add a GraphicsLayer for the sketches and the buffer
        const sketchLayer = new GraphicsLayer();
        const bufferLayer = new GraphicsLayer();
        view.map.addMany([bufferLayer, sketchLayer]);

        // create the layerView's to add the filter
        let featureLayerView = null;
        map.load().then(() => {
          // loop through webmap's operational layers
          map.layers.forEach((layer) => {
            view
              .whenLayerView(layer)
              .then((layerView) => {
                if (layer.type === "feature") {
                  featureLayerView = layerView;
                }
              })
              .catch(console.error);
          });
        });

        const bufferNumSlider = new Slider({
          container: "bufferNum",
          min: 0,
          max: 1000,
          steps: 1,
          visibleElements: {
            labels: true,
          },
          precision: 0,
          labelFormatFunction: (value, type) => {
            return `${value.toString()}m`;
          },
          values: [0]
        });

        let bufferSize = 0;
        bufferNumSlider.on(["thumb-change", "thumb-drag"], bufferVariablesChanged);
        function bufferVariablesChanged(event) {
          bufferSize = event.value;
          updateFilter();
        }

        // use SketchViewModel to draw polygons that are used as a filter
        let sketchGeometry = null;
        const sketchViewModel = new SketchViewModel({
          layer: sketchLayer,
          view: view,
          pointSymbol: {
            type: "simple-marker",
            style: "circle",
            size: 10,
            color: [255, 255, 255, 0.8],
            outline: {
              color: [211, 132, 80, 0.7],
              size: 10
            }
          },
          polylineSymbol: {
            type: "simple-line",
            color: [211, 132, 80, 0.7],
            width: 6
          },
          polygonSymbol: {
            type: "simple-fill",
            symbolLayers: [
              {
                type: "fill",
                material: {
                  color: [255, 255, 255, 0.8]
                },
                outline: {
                  color: [211, 132, 80, 0.7],
                  size: "10px"
                }
              }
            ]
          },
          defaultCreateOptions: { hasZ: false }
        });

        sketchViewModel.on(["create"], (event) => {
          // update the filter every time the user finishes drawing the filtergeometry
          if (event.state == "complete") {
            sketchGeometry = event.graphic.geometry;
            updateFilter();
          }
        });

        sketchViewModel.on(["update"], (event) => {
          const eventInfo = event.toolEventInfo;
          // update the filter every time the user moves the filtergeometry
          if (event.toolEventInfo && event.toolEventInfo.type.includes("stop")) {
              sketchGeometry = event.graphics[0].geometry;
              updateFilter();
          }
        });

        // select the layer to filter on
        let featureLayerViewFilterSelected = true;
        document.getElementById("featureLayerViewFilter").addEventListener("change", (event) => {
          featureLayerViewFilterSelected = !!event.target.checked;
          updateFilter();
        });

        // draw geometry buttons - use the selected geometry to sktech
        document.getElementById("point-geometry-button").onclick = geometryButtonsClickHandler;
        document.getElementById("line-geometry-button").onclick = geometryButtonsClickHandler;
        document.getElementById("polygon-geometry-button").onclick = geometryButtonsClickHandler;
        function geometryButtonsClickHandler(event) {
          const geometryType = event.target.value;
          clearFilter();
          sketchViewModel.create(geometryType);
        }

        // get the selected spatialRelationship
        let selectedFilter = "disjoint";
        document.getElementById("relationship-select").addEventListener("change", (event) => {
          const select = event.target;
          selectedFilter = select.options[select.selectedIndex].value;
          updateFilter();
        });

        // remove the filter
        document.getElementById("clearFilter").addEventListener("click", clearFilter);

        function clearFilter() {
          sketchGeometry = null;
          filterGeometry = null;
          sketchLayer.removeAll();
          bufferLayer.removeAll();
          featureLayerView.filter = null;
        }

        // set the geometry filter on the visible FeatureLayerView
        function updateFilter() {
          updateFilterGeometry();
          const featureFilter = {
            // autocasts to FeatureFilter
            geometry: filterGeometry,
            spatialRelationship: selectedFilter
          };

          if (featureLayerView) {
            if (featureLayerViewFilterSelected) {
              featureLayerView.filter = featureFilter;
            } else {
              featureLayerView.filter = null;
            }
          }

        }

        // update the filter geometry depending on bufferSize
        let filterGeometry = null;
        function updateFilterGeometry() {
          // add a polygon graphic for the bufferSize
          if (sketchGeometry) {
            if (bufferSize > 0) {
              const bufferGeometry = geometryEngine.geodesicBuffer(sketchGeometry, bufferSize, "meters");
              if (bufferLayer.graphics.length === 0) {
                bufferLayer.add(
                  new Graphic({
                    geometry: bufferGeometry,
                    symbol: sketchViewModel.polygonSymbol
                  })
                );
              } else {
                bufferLayer.graphics.getItemAt(0).geometry = bufferGeometry;
              }
              filterGeometry = bufferGeometry;
            } else {
              bufferLayer.removeAll();
              filterGeometry = sketchGeometry;
            }
          }
        }

        document.getElementById("infoDiv").style.display = "block";
      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
    <div id="infoDiv" class="esri-widget">
      <br id="featureLayerViewFilter" />Draw a geometry to filter by:
      <div class="geometry-options">
        <button
          class="esri-widget--button esri-icon-map-pin geometry-button"
          id="point-geometry-button"
          value="point"
          title="Filter by point"
        ></button>
        <button
          class="esri-widget--button esri-icon-polyline geometry-button"
          id="line-geometry-button"
          value="polyline"
          title="Filter by line"
        ></button>
        <button
          class="esri-widget--button esri-icon-polygon geometry-button"
          id="polygon-geometry-button"
          value="polygon"
          title="Filter by polygon"
        ></button>
      </div>
      <br />
      <label for="relationship-select">Spatial relationship:</label>
      <select id="relationship-select" class="options">
        <option value="intersects">intersects</option>
        <option value="contains">contains</option>
        <option value="disjoint" selected>disjoint</option> </select
      ><br /><br />
      <button class="esri-button" id="clearFilter" type="button">
        Clear filter
      </button>
    </div>
  </body>
</html>

 

 

 

I would be very appreciative if anybody could give me some guidance on where i've gone wrong. For example, if i've configured the forEach function incorrectly.

Thank you.

0 Kudos
1 Solution

Accepted Solutions
rbrowning
New Contributor

Hi Emily,

As you found you can't append to a featureLayerView - it just contains a reference to one layer. You probably realised that the original code you were working from only assumed one feature layer and one scene layer, it didn't deal with multiple feature layers.

In keeping with the original code, I would create a simple array:

const featureLayerViews = [];
.... featureLayerViews.push(layerView);
...

 Then in the updateFilter function loop through the array applying the filter. Something similar to:
featureLayerViews.forEach((featureLayerView) => {do work here});

In the original code there was layer check box in the html which you removed so I am assuming you just wanted to apply the filter to all the feature layers, which means you can remove the code related to whether the feature layer has been selected.

View solution in original post

0 Kudos
4 Replies
rbrowning
New Contributor

Hi Emily,

It looks like your webmap contains your two feature layers within a group layer. The layers property on the map will only loop through the top level layers, then if you encounter a group layer you can use its layers property to loop through the sub layers.

You can check to see if the layer is a group using: 

if (layer.type === "group") {}
 
Its also worth noting, as it stands, the filter will only be applied to the last feature layer it finds. I am not sure if you are looking for this functionality., but you might want to consider holding an array of 
featureLayerView's to apply the filter to.
 
I hope that helps.
0 Kudos
EmilyGoldsmith
New Contributor III

Hi,

Ah, I didn't know you could check layers within groups. This is useful, thank you! For simplicitys sake, I removed the group to only deal with features.

I can see what you mean with how the filter will only be applied to the last feature layer., so i've tried to adjust the code to loop through each layer's layerview and then add it to the featureLayerView for later filtering. It didn't work sadly, so i'm assuming the .add() function isn't appropriate in this case. Would you recommend a different function? Or should I just start from scratch and go down the array route?

        // create the layerView's to add the filter
        let featureLayerView = null;
        map.load().then(() => {
          // loop through webmap's operational layers
          view.map.layers.forEach((layer) => { //For each layer in the webmap
            view.whenLayerView(layer) //Get its layerview
            .then((layerView) => { //Then, for each view,
                if (layer.type === "feature") { //So long as it is a feature
                  featureLayerView.add(layerView); //Add it to the featureLayerView to be filtered
                }
              })
              .catch(console.error); //Otherwise, give an error
          });
        });

 

I haven't quite figured out the array route yet, but is it a bit similar to how you can iterate through a set of LayerViews in a WebMap via the fetchFeatures() function? In this example: https://developers.arcgis.com/javascript/latest/sample-code/widgets-feature-multiplelayers/

Many thanks for your help.

0 Kudos
rbrowning
New Contributor

Hi Emily,

As you found you can't append to a featureLayerView - it just contains a reference to one layer. You probably realised that the original code you were working from only assumed one feature layer and one scene layer, it didn't deal with multiple feature layers.

In keeping with the original code, I would create a simple array:

const featureLayerViews = [];
.... featureLayerViews.push(layerView);
...

 Then in the updateFilter function loop through the array applying the filter. Something similar to:
featureLayerViews.forEach((featureLayerView) => {do work here});

In the original code there was layer check box in the html which you removed so I am assuming you just wanted to apply the filter to all the feature layers, which means you can remove the code related to whether the feature layer has been selected.

0 Kudos
EmilyGoldsmith
New Contributor III

Hi, thank you so much for all your help! Adding in the push function and looping the featureLayerViews through the updateFilter function did the trick. One extra thing I had to add in, however, was a loop to remove all filters from each featurelayerview in the clearFilter() function section, but this makes sense as a filter has to be added to each featurelayerview to make sure the app does its job. Here's the working code now:

 

 

​<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <title>Filter webmap with FeatureFilter | Test | ArcGIS Maps SDK for JavaScript 4.28</title>
    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }

      #infoDiv {
        background-color: white;
        padding: 10px;
        width: 260px;
        margin: 5px;
        position: absolute;
        bottom: 15px;
        right: 10px;
        font-size: 14px;
        display: none;
      }

      .geometry-options {
        display: flex;
        flex-direction: row;
      }

      .geometry-button {
        flex: 1;
        border-style: solid;
        border-width: 1px;
        border-image: none;
      }

      .geometry-button-selected {
        background: #4c4c4c;
        color: #fff;
      }

      .options {
        max-width: 260px;
        width: 100%;
        height: 25px;
      }

    </style>

    <link rel="stylesheet" href="https://js.arcgis.com/4.28/esri/themes/light/main.css" />
    <script src="https://js.arcgis.com/4.28/"></script>

    <script>
      require([
        "esri/WebMap",
        "esri/views/MapView",
        "esri/layers/GraphicsLayer",
        "esri/widgets/Sketch/SketchViewModel",
        "esri/widgets/Slider",
        "esri/layers/support/FeatureFilter",
        "esri/geometry/geometryEngine",
        "esri/Graphic",
        "esri/widgets/Legend"
      ], (WebMap, MapView, GraphicsLayer, SketchViewModel, Slider, FeatureFilter, geometryEngine, Graphic, Legend) => {
        // Load webmap and display it in a webmap
        const map = new WebMap({
          portalItem: {
            id: "1a3366d6e59945289a141e3ae62698b6"
          }
        });

        // create the mapview
        const view = new MapView({
          container: "viewDiv",
          map: map,
          popupEnabled: false
        });

        // add a GraphicsLayer for the sketches and the buffer
        const sketchLayer = new GraphicsLayer();
        const bufferLayer = new GraphicsLayer(); //BUFFER FROM PLANNING AREA - QUERY SITES OF INTEREST! GPs, FLOODING AREAS, etc
        view.map.addMany([bufferLayer, sketchLayer]);

        // create the layerView's to add the filter
        const featureLayerView = [];
        map.load().then(() => {
          // loop through webmap's operational layers
          view.map.layers.forEach((layer) => { //For each layer in the webmap //Select the layerview
            view.whenLayerView(layer) //Get its view
            .then((layerView) => { //Then for each view
                 featureLayerView.push(layerView);//Add it to the featureLayerView to be filtered
              })
              .catch(console.error); //Otherwise, give an error
          });
        });

        const bufferNumSlider = new Slider({
          container: "bufferNum",
          min: 0,
          max: 1000,
          steps: 1,
          visibleElements: {
            labels: true,
          },
          precision: 0,
          labelFormatFunction: (value, type) => {
            return `${value.toString()}m`;
          },
          values: [0]
        });

        let bufferSize = 0;
        bufferNumSlider.on(["thumb-change", "thumb-drag"], bufferVariablesChanged);
        function bufferVariablesChanged(event) {
          bufferSize = event.value;
          updateFilter();
        }

        // use SketchViewModel to draw polygons that are used as a filter
        let sketchGeometry = null;
        const sketchViewModel = new SketchViewModel({
          layer: sketchLayer,
          view: view,
          pointSymbol: {
            type: "simple-marker",
            style: "circle",
            size: 10,
            color: [255, 255, 255, 0.8],
            outline: {
              color: [211, 132, 80, 0.7],
              size: 10
            }
          },
          polylineSymbol: {
            type: "simple-line",
            color: [211, 132, 80, 0.7],
            width: 6
          },
          polygonSymbol: {
            type: "simple-fill",
            symbolLayers: [
              {
                type: "fill",
                material: {
                  color: [255, 255, 255, 0.8]
                },
                outline: {
                  color: [211, 132, 80, 0.7],
                  size: "10px"
                }
              }
            ]
          },
          defaultCreateOptions: { hasZ: false }
        });

        sketchViewModel.on(["create"], (event) => {
          // update the filter every time the user finishes drawing the filtergeometry
          if (event.state == "complete") {
            sketchGeometry = event.graphic.geometry;
            updateFilter();
          }
        });

        sketchViewModel.on(["update"], (event) => {
          const eventInfo = event.toolEventInfo;
          // update the filter every time the user moves the filtergeometry
          if (event.toolEventInfo && event.toolEventInfo.type.includes("stop")) {
              sketchGeometry = event.graphics[0].geometry;
              updateFilter();
          }
        });

        // select the layer to filter on
        let featureLayerViewFilterSelected = true;
        document.getElementById("featureLayerViewFilter").addEventListener("change", (event) => {
          featureLayerViewFilterSelected = !!event.target.checked;
          updateFilter();
        });

        // draw geometry buttons - use the selected geometry to sktech
        document.getElementById("point-geometry-button").onclick = geometryButtonsClickHandler;
        document.getElementById("line-geometry-button").onclick = geometryButtonsClickHandler;
        document.getElementById("polygon-geometry-button").onclick = geometryButtonsClickHandler;
        function geometryButtonsClickHandler(event) {
          const geometryType = event.target.value;
          clearFilter();
          sketchViewModel.create(geometryType);
        }

        // get the selected spatialRelationship
        let selectedFilter = "disjoint";
        document.getElementById("relationship-select").addEventListener("change", (event) => {
          const select = event.target;
          selectedFilter = select.options[select.selectedIndex].value;
          updateFilter();
        });

        // remove the filter
        document.getElementById("clearFilter").addEventListener("click", clearFilter);

        function clearFilter() {
          sketchGeometry = null;
          filterGeometry = null;
          sketchLayer.removeAll();
          bufferLayer.removeAll();
          featureLayerView.forEach((featureLayerView) => { //For each featurelayerview
          featureLayerView.filter = null;}) //Remove its filter
        }

        // set the geometry filter on the visible FeatureLayerView
        function updateFilter() {
          updateFilterGeometry();
          const featureFilter = {
            // autocasts to FeatureFilter
            geometry: filterGeometry,
            spatialRelationship: selectedFilter
          };

          featureLayerView.forEach((featureLayerView) => {
            if (featureLayerView) {
            if (featureLayerViewFilterSelected) {
              featureLayerView.filter = featureFilter;
            } else {
              featureLayerView.filter = null;
            }
          }

        })};

        // update the filter geometry depending on bufferSize
        let filterGeometry = null;
        function updateFilterGeometry() {
          // add a polygon graphic for the bufferSize
          if (sketchGeometry) {
            if (bufferSize > 0) {
              const bufferGeometry = geometryEngine.geodesicBuffer(sketchGeometry, bufferSize, "meters");
              if (bufferLayer.graphics.length === 0) {
                bufferLayer.add(
                  new Graphic({
                    geometry: bufferGeometry,
                    symbol: sketchViewModel.polygonSymbol
                  })
                );
              } else {
                bufferLayer.graphics.getItemAt(0).geometry = bufferGeometry;
              }
              filterGeometry = bufferGeometry;
            } else {
              bufferLayer.removeAll();
              filterGeometry = sketchGeometry;
            }
          }
        }

        document.getElementById("infoDiv").style.display = "block";

        let legend = new Legend({
          view: view
        });
        
        view.ui.add(legend, "bottom-left");

      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
    <div id="infoDiv" class="esri-widget">
      <br id="featureLayerViewFilter" />Draw a geometry to filter by:
      <div class="geometry-options">
        <button
          class="esri-widget--button esri-icon-map-pin geometry-button"
          id="point-geometry-button"
          value="point"
          title="Filter by point"
        ></button>
        <button
          class="esri-widget--button esri-icon-polyline geometry-button"
          id="line-geometry-button"
          value="polyline"
          title="Filter by line"
        ></button>
        <button
          class="esri-widget--button esri-icon-polygon geometry-button"
          id="polygon-geometry-button"
          value="polygon"
          title="Filter by polygon"
        ></button>
      </div>
      <br />
      <label for="relationship-select">Spatial relationship:</label>
      <select id="relationship-select" class="options">
        <option value="intersects">intersects</option>
        <option value="contains">contains</option>
        <option value="disjoint" selected>disjoint</option> </select
      ><br /><br />
      <button class="esri-button" id="clearFilter" type="button">
        Clear filter
      </button>
    </div>
  </body>
</html>

Otherwise, i'm actually going to try and add back in the layer checkbox feature, as well as the buffer filter, as I quite like the functionality - its a simplistic and interesting way to toggle through the layers, a bit different to the LayerList widget.

Thanks again!

0 Kudos