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>
Solved! Go to Solution.
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>
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>
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?
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.
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.
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.
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
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
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