New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PluggableMap: avoid crash when multiple interactions are removed #11305
PluggableMap: avoid crash when multiple interactions are removed #11305
Conversation
I think it has to be done using slice. Working through a collection's array as it is being updated may mean an active interaction is moved up the array and you miss it. e.g. looping through the getLayers() array calling removeLayer only removes half the layers. |
Sure, I'm happy to swap that. I was leaning towards that approach anyway. |
5c3ae24
to
225edaa
Compare
@@ -1045,7 +1045,7 @@ class PluggableMap extends BaseObject { | |||
} | |||
} | |||
mapBrowserEvent.frameState = this.frameState_; | |||
const interactionsArray = this.getInteractions().getArray(); | |||
const interactionsArray = this.getInteractions().getArray().slice(); | |||
if (this.dispatchEvent(mapBrowserEvent) !== false) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just checking is there a reason interactionArray
isnt built in this if
condition?
Doing That said, the issue could also be easily solved on the application level, by removing the interactions in the next tick. I've done something similar before: draw.on("drawend", () => {
setTimeout(() => {
map.getInteractions().getArray().slice().forEach(i => map.removeInteraction(i));
}, 0);
}); |
Yes, I agree it is more an applicable issue, it's not a good idea for an interaction to remove itself directly within its own event. (Also to make sure all interactions were removed would you need |
Yeah, that's why the first implementation was changing the if condition as below to avoid creating any performance differences - if (!interaction.getActive()) {
+ if (!interaction || !interaction.getActive()) {
continue;
}
That wouldn't be necessary as the
draw.on("drawend", () => {
map.getInteractions().getArray().slice().forEach(i => map.removeInteraction(i)); // or whatever
return false; // Avoid continue propogating the event in PluggableMap
}); |
@megawac In your application, it would be better to use The first version of your pull request had the problem that it might result in an interaction's |
An acceptable change I could think of would be to change diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js
index b5818bd71..d2c5f0e06 100644
--- a/src/ol/PluggableMap.js
+++ b/src/ol/PluggableMap.js
@@ -156,6 +156,12 @@ class PluggableMap extends BaseObject {
/** @private */
this.boundHandleBrowserEvent_ = this.handleBrowserEvent.bind(this);
+ /**
+ * @type {boolean}
+ * @private
+ */
+ this.handlingMapBrowserEvent_ = false;
+
/**
* @type {number}
* @private
@@ -1022,6 +1028,7 @@ class PluggableMap extends BaseObject {
mapBrowserEvent.frameState = this.frameState_;
const interactionsArray = this.getInteractions().getArray();
if (this.dispatchEvent(mapBrowserEvent) !== false) {
+ this.handlingMapBrowserEvent_ = true;
for (let i = interactionsArray.length - 1; i >= 0; i--) {
const interaction = interactionsArray[i];
if (!interaction.getActive()) {
@@ -1032,6 +1039,7 @@ class PluggableMap extends BaseObject {
break;
}
}
+ this.handlingMapBrowserEvent_ = false;
}
}
@@ -1329,7 +1337,18 @@ class PluggableMap extends BaseObject {
* @api
*/
removeInteraction(interaction) {
- return this.getInteractions().remove(interaction);
+ const interactions = this.getInteractions();
+ const foundInteraction = interactions.getArray().indexOf(interaction) !== -1;
+ if (this.handlingMapBrowserEvent_) {
+ setTimeout(
+ function () {
+ interactions.remove(interaction);
+ }.bind(this)
+ );
+ } else {
+ interactions.remove(interaction);
+ }
+ return foundInteraction ? interaction : undefined;
}
/** |
There is a slight chance that if the same isn't done for
|
You're right @megawac, |
However, it might be better to refactor all interactions to not fire events during MapBrowserEvent handling. This would make the behavior much more predictable and avoid unnecessary setTimeout() calls elsewhere. |
225edaa
to
d7a8e45
Compare
I still think what I suggested in #11305 (comment) would be a safer solution. |
I agree, I would need to look into it further to take a crack at such an implementation. Regarding unit testing for this, in which suite would it be best to implement a regression test for either of these changes? |
@megawac I would say in the suite for the respective interaction. Thanks for willing to address this! |
@megawac I just took a closer look at this, and I think firing interaction events in the next tick when they occur in a map browser event handler will be hard to achieve because at least for Modify and Draw, all the geometry modifications would then be in a different sequence with the events. That said, I'm inclined to accept your solution here. |
As shown in this codepen it is possible to cause a crash when handling a map browser event. The way to achieve this error is to remove multiple interactions are part of one of the map browser event handlers. Due to the way the for loop is structure, we changes in the
interactionsArray
can cause inconsistencies.In my case I ran into this issue when using a draw event -- when the draw was completed the interaction would remove itself and a snap interaction from the map. As the draw was the last interaction in the
interactionsArray
itsdrawend
handler was called first by PluggableMap, then my event handler removed 2 interactions, thehandleMapBrowserEvent
continue to the index that had the snap provider which causedinteraction = undefined
and the following error to occur.Can you provide pointers of where the unit test for this should be added?
An alternative fix which may be cleaner may be to
slice
theinteractionsArray
prior to looping over it in order to guarantee the collection doesn't change during the event handler.