Select to view content in your preferred language

Alternative for applyEdits for client side features that is performant?

963
8
Jump to solution
10-05-2023 05:00 AM
AlexJohnK
New Contributor

I'm creating my own "Editor" widget and stumbled upon some very strange behavior of the `applyEdits` method on `FeatureLayer`. When using `applyEdits`, if you're trying to use `addFeatures` and then shortly after call `applyEdits` again but with `deleteFeatures` of the newly added features, it won't delete them.

I'm assuming the above happens due to `applyEdits` being an asynchronous function and thus it hasn't actually finished `addFeatures` before we're calling `applyEdits` with `deleteFeatures`.

In the code attached below I've implemented three versions of "draw a point to the screen and delete what was previously drawn".

1. `drawPointGraphicsIntended` - this draws Graphic points using the "intended" way according to documentation, but it slow and buggy (NOTE: this isn't bugged if you move the cursor very slowly)

2. `drawPointGraphicsIntendedFixed` - this draws Graphic points using the "intended" way, but it "fixed" the buggy part, by basically deleting a graphic twice.

3. `drawPointGraphicsFastAndResponsive` - this draws Graphic points onto the `view`'s graphics. This is very fast and responsive and works as the API intended.

-----------------

I'm curious if there is an actual way to use the `FeatureLayer` API to not suck. The reason why I'd prefer to use the `FeatureLayer` API is so that I can access `Graphic.layer` to get the parent Layer, if I use the `view`, it'd get the global View layer instead of the actual parent layer.

Code:

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="initial-scale=1,maximum-scale=1,user-scalable=no"
    />
    <title>
      Update FeatureLayer using applyEdits() | Sample | ArcGIS Maps SDK for
      JavaScript 4.27
    </title>

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

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
    </style>

    <script>
      require([
        "esri/Map",
        "esri/views/MapView",
        "esri/layers/FeatureLayer",
        "esri/Graphic",
      ], (Map, MapView, FeatureLayer, Graphic) => {
        const pointLayer = new FeatureLayer({
          source: [],
          geometryType: "point",
          fields: [
            {
              name: "ObjectID",
              alias: "ObjectID",
              type: "oid",
            },
          ],
          objectIdField: "ObjectID",
        });

        const map = new Map({
          basemap: "dark-gray-vector",
          layers: [pointLayer],
        });

        const view = new MapView({
          container: "viewDiv",
          map: map,
          center: [-117.18, 34.06],
          zoom: 14,
        });

        // cache for drawn graphics
        let pointGraphicCache = [];

        // implementation of what _should_ be used according to documentation
        const drawPointGraphicsIntended = (paths) => {
          pointLayer.applyEdits({
            deleteFeatures: pointGraphicCache,
          });

          pointGraphicCache = paths[0].map(
            (point) =>
              new Graphic({
                geometry: {
                  type: "point",
                  longitude: point[0],
                  latitude: point[1],
                },
              })
          );

          pointLayer.applyEdits({
            addFeatures: pointGraphicCache,
          });
        };

        // implementation of how it should be used according to documentation, but fixed
        const drawPointGraphicsIntendedFixed = (paths) => {
          pointLayer.applyEdits({
            deleteFeatures: pointGraphicCache.map((data) => {
              data.deleted = true;
              return data.graphic;
            }),
          });

          const graphics = paths[0].map((point) => ({
            graphic: new Graphic({
              geometry: {
                type: "point",
                longitude: point[0],
                latitude: point[1],
              },
            }),
            deleted: false,
          }));
          pointGraphicCache = graphics;

          pointLayer
            .applyEdits({
              addFeatures: graphics.map((g) => g.graphic),
            })
            // after applyEdits is finished, check if it got deleted during execution
            .then(() => {
              const graphicsToDelete = graphics
                .filter((g) => g.deleted)
                .map((g) => g.graphic);
              if (graphicsToDelete.length) {
                pointLayer.applyEdits({
                  deleteFeatures: graphicsToDelete,
                });
              }
            });
        };

        // implementation of what is _expected_ of how the behavior of the map should work/look
        const drawPointGraphicsFastAndResponsive = (paths) => {
          for (const graphic of pointGraphicCache) {
            view.graphics.remove(graphic);
          }

          pointGraphicCache = paths[0].map(
            (point) =>
              new Graphic({
                geometry: {
                  type: "point",
                  longitude: point[0],
                  latitude: point[1],
                },
              })
          );

          for (const graphic of pointGraphicCache) {
            view.graphics.add(graphic);
          }
        };

        // draw point on pointer move
        view.on("pointer-move", (event) => {
          const mapPoint = view.toMap(event);

          const paths = [[[mapPoint.longitude, mapPoint.latitude]]];

          // NOTE: change execution method here
          drawPointGraphicsIntended(paths);
          // drawPointGraphicsIntendedFixed(paths);
          // drawPointGraphicsFastAndResponsive(paths);
        });
      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
  </body>
</html>

 

0 Kudos
1 Solution

Accepted Solutions
JoelBennett
MVP Regular Contributor

If you want the performance seen when using the view's graphics collection, but want a separate layer for the graphic, then it seems using a GraphicsLayer would be suitable:

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="initial-scale=1,maximum-scale=1,user-scalable=no"
    />
    <title>
      Update FeatureLayer using applyEdits() | Sample | ArcGIS Maps SDK for
      JavaScript 4.27
    </title>

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

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
    </style>

    <script>
      require([
        "esri/Map",
        "esri/views/MapView",
        "esri/layers/GraphicsLayer",
        "esri/Graphic"
      ], (Map, MapView, GraphicsLayer, Graphic) => {
        const graphic = new Graphic({ geometry: null });

        const pointLayer = new GraphicsLayer({});
        pointLayer.add(graphic);

        const map = new Map({
          basemap: "dark-gray-vector",
          layers: [pointLayer]
        });

        const view = new MapView({
          container: "viewDiv",
          map: map,
          center: [-117.18, 34.06],
          zoom: 14
        });

        // draw point on pointer move
        view.on("pointer-move", (event) => {
          graphic.geometry = view.toMap(event);
        });
      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
  </body>
</html>

 

View solution in original post

0 Kudos
8 Replies
JoelBennett
MVP Regular Contributor

If you want the performance seen when using the view's graphics collection, but want a separate layer for the graphic, then it seems using a GraphicsLayer would be suitable:

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="initial-scale=1,maximum-scale=1,user-scalable=no"
    />
    <title>
      Update FeatureLayer using applyEdits() | Sample | ArcGIS Maps SDK for
      JavaScript 4.27
    </title>

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

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
    </style>

    <script>
      require([
        "esri/Map",
        "esri/views/MapView",
        "esri/layers/GraphicsLayer",
        "esri/Graphic"
      ], (Map, MapView, GraphicsLayer, Graphic) => {
        const graphic = new Graphic({ geometry: null });

        const pointLayer = new GraphicsLayer({});
        pointLayer.add(graphic);

        const map = new Map({
          basemap: "dark-gray-vector",
          layers: [pointLayer]
        });

        const view = new MapView({
          container: "viewDiv",
          map: map,
          center: [-117.18, 34.06],
          zoom: 14
        });

        // draw point on pointer move
        view.on("pointer-move", (event) => {
          graphic.geometry = view.toMap(event);
        });
      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
  </body>
</html>

 

0 Kudos
AlexJohnK
New Contributor

I'd lose the ability to use the Symbol from the FeatureLayer, but I suppose I could just change the API I'm making a bit, so not a deal breaker. Suppose this is a decent alternative, are there really no other ways of making FeatureLayer not take second(s) to update?

0 Kudos
JoelBennett
MVP Regular Contributor

Somebody else can chime in if they know differently, but I don't think you'll find any other way.  The FeatureLayer module just doesn't appear to have been designed for this kind of thing.

0 Kudos
mleahy_cl
New Contributor III

You are correct that `applyEdits()` is synchronous.  With that in mind, I wouldn't expect to do anything that depends on the applyEdits method being completed without waiting for its result.  Even if the result is effectively instantaneous, the next synchronous line of code immediately after the line that calls applyEdits is likely (maybe guaranteed?) to execute before the applyEdits function has completed.  The applyEdits method returns a promise - that enables you you wait for it to be completed before doing anything, for example:

layer.applyEdits(...).then(result => doSomething(result)).catch(error => console.log(error));


If you're making a bunch of apply edits, you could wait for all of them - I believe this would work:

Promise.all(
  layer1.applyEdits(...),
  layer2.applyEdits(...),
  ...,
).then(results => doSomething(results)).catch(error => console.log(error));

 

As long as you wait for the result returned by the promises before doing anything else, you should be fine.

0 Kudos
mleahy_cl
New Contributor III

PS: re-reading your code...since it's an event repeatedly raised by the point-move, you might consider debouncing calls to the function that applies the edits, maybe using requestAnimationFrame()...at the completion of the apply edits, store a reference to the added graphics.  On subsequent calls, you could then update features that you have added instead of delete/remove, or just make sure any previous features are deleted simultaneously...and perhaps do a similar cleanup of previous features at the completion of each applyEdits.

0 Kudos
AlexJohnK
New Contributor

like mentioned in your reply, it isn't really viable due to needing to await the previous async function to complete, as I want real time update to track cursor movement

0 Kudos
ReneRubalcava
Frequent Contributor II

FeatureLayer isn't really the right tool for fast updates like this, using mouse move events. GraphicsLayer would be the first option, but you said you want to use the renderer of a FeatureLayer.

In that case, another option might be a client-side StreamLayer. There's a sample showing how to use this client-side.

https://developers.arcgis.com/javascript/latest/sample-code/layers-streamlayer-client/

Here's a modified version using points.

https://codepen.io/odoe/pen/MWZzLbY?editors=1000

 

AlexJohnK
New Contributor

Looks like a decent option, but seems very specificly designed for certain use cases, in particular the way cleanup works, so not really applicable to my application. Guess I'll go for GraphicsLayer as mentioned here and earlier

0 Kudos