Simplifying and unhacking the ModifyFeature control

With two nested controls (DragFeature, SelectFeature), which - among
others - activated two OpenLayers.Handler.Feature instances, the
ModifyFeature control was fragile and hard to debug.

The issue that @iwillig, @bartvde and myself tried to track down was
that the two Feature handlers interfered with each other, making it hard
to select features on mobile devices, and causing points to jump during
dragging because of not updated last pixel positions.

With this refactoring, there are no more nested controls. All that's left
is a Drag handler. All tests pass. I had to remove one test that checked
for dragging of an unselected point while another was selected, because
now (and partially even before this change, thanks to the ugly drag
handler hack that is now removed) dragging a point will also select it,
saving the user an extra click.
This commit is contained in:
ahocevar
2013-03-19 23:00:10 +01:00
parent 7002ec49e7
commit d2a32d5421
2 changed files with 125 additions and 210 deletions

View File

@@ -4,8 +4,7 @@
* full text of the license. */
/**
* @requires OpenLayers/Control/DragFeature.js
* @requires OpenLayers/Control/SelectFeature.js
* @requires OpenLayers/Handler/Drag.js
* @requires OpenLayers/Handler/Keyboard.js
*/
@@ -88,18 +87,6 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
*/
virtualVertices: null,
/**
* Property: selectControl
* {<OpenLayers.Control.SelectFeature>}
*/
selectControl: null,
/**
* Property: dragControl
* {<OpenLayers.Control.DragFeature>}
*/
dragControl: null,
/**
* Property: handlers
* {Object}
@@ -232,64 +219,50 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
if(!(OpenLayers.Util.isArray(this.deleteCodes))) {
this.deleteCodes = [this.deleteCodes];
}
var control = this;
// configure the select control
var selectOptions = {
geometryTypes: this.geometryTypes,
clickout: this.clickout,
toggle: this.toggle,
onBeforeSelect: this.beforeSelectFeature,
onSelect: this.selectFeature,
onUnselect: this.unselectFeature,
scope: this
};
if(this.standalone === false) {
this.selectControl = new OpenLayers.Control.SelectFeature(
layer, selectOptions
);
}
// configure the drag control
var dragOptions = {
documentDrag: this.documentDrag,
geometryTypes: ["OpenLayers.Geometry.Point"],
onStart: function(feature, pixel) {
control.dragStart.apply(control, [feature, pixel]);
// configure the drag handler
var dragCallbacks = {
down: function(pixel) {
this.vertex = null;
var feature = this.layer.getFeatureFromEvent(
this.handlers.drag.evt);
if (feature) {
this.dragStart(feature);
} else if (this.feature && this.clickout) {
this.unselectFeature(this.feature);
}
},
onDrag: function(feature, pixel) {
control.dragVertex.apply(control, [feature, pixel]);
move: function(pixel) {
delete this._unselect;
if (this.vertex) {
this.dragVertex(this.vertex, pixel);
}
},
onComplete: function(feature) {
control.dragComplete.apply(control, [feature]);
up: function() {
this.handlers.drag.stopDown = false;
if (this._unselect) {
this.unselectFeature(this._unselect);
delete this._unselect;
}
},
featureCallbacks: {
over: function(feature) {
/**
* In normal mode, the feature handler is set up to allow
* dragging of all points. In standalone mode, we only
* want to allow dragging of sketch vertices and virtual
* vertices - or, in the case of a modifiable point, the
* point itself.
*/
if(control.standalone !== true || feature._sketch ||
control.feature === feature) {
control.dragControl.overFeature.apply(
control.dragControl, [feature]);
}
done: function(pixel) {
if (this.vertex) {
this.dragComplete(this.vertex);
}
}
};
this.dragControl = new OpenLayers.Control.DragFeature(
layer, dragOptions
);
var dragOptions = {
documentDrag: this.documentDrag,
stopDown: false
};
// configure the keyboard handler
var keyboardOptions = {
keydown: this.handleKeypress
};
this.handlers = {
keyboard: new OpenLayers.Handler.Keyboard(this, keyboardOptions)
keyboard: new OpenLayers.Handler.Keyboard(this, keyboardOptions),
drag: new OpenLayers.Handler.Drag(this, dragCallbacks, dragOptions)
};
},
@@ -299,8 +272,6 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
*/
destroy: function() {
this.layer = null;
this.standalone || this.selectControl.destroy();
this.dragControl.destroy();
OpenLayers.Control.prototype.destroy.apply(this, []);
},
@@ -312,8 +283,8 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
* {Boolean} Successfully activated the control.
*/
activate: function() {
return ((this.standalone || this.selectControl.activate()) &&
this.handlers.keyboard.activate() &&
return (this.handlers.keyboard.activate() &&
this.handlers.drag.activate() &&
OpenLayers.Control.prototype.activate.apply(this, arguments));
},
@@ -331,26 +302,17 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
this.layer.removeFeatures(this.vertices, {silent: true});
this.layer.removeFeatures(this.virtualVertices, {silent: true});
this.vertices = [];
this.dragControl.deactivate();
var feature = this.feature;
var valid = feature && feature.geometry && feature.layer;
if(this.standalone === false) {
if(valid) {
this.selectControl.unselect.apply(this.selectControl,
[feature]);
}
this.selectControl.deactivate();
} else {
if(valid) {
this.unselectFeature(feature);
}
}
this.handlers.drag.deactivate();
this.handlers.keyboard.deactivate();
var feature = this.feature;
if (feature && feature.geometry && feature.layer) {
this.unselectFeature(feature);
}
deactivated = true;
}
return deactivated;
},
/**
* Method: beforeSelectFeature
* Called before a feature is selected.
@@ -375,11 +337,19 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
* feature - {<OpenLayers.Feature.Vector>} the selected feature.
*/
selectFeature: function(feature) {
if (this.geometryTypes && OpenLayers.Util.indexOf(this.geometryTypes,
feature.geometry.CLASS_NAME) == -1) {
return;
}
if (!this.standalone || this.beforeSelectFeature(feature) !== false) {
if (this.feature) {
this.unselectFeature(this.feature);
}
this.feature = feature;
this.layer.selectedFeatures.push(feature);
this.layer.drawFeature(feature, 'select');
this.modified = false;
this.resetVertices();
this.dragControl.activate();
this.onModificationStart(this.feature);
}
// keep track of geometry modifications
@@ -409,8 +379,9 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
this.layer.destroyFeatures([this.radiusHandle], {silent: true});
delete this.radiusHandle;
}
this.layer.drawFeature(this.feature, 'default');
this.feature = null;
this.dragControl.deactivate();
OpenLayers.Util.removeItem(this.layer.selectedFeatures, feature);
this.onModificationEnd(feature);
this.layer.events.triggerEvent("afterfeaturemodified", {
feature: feature,
@@ -418,64 +389,48 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
});
this.modified = false;
},
/**
* Method: dragStart
* Called by the drag feature control with before a feature is dragged.
* This method is used to differentiate between points and vertices
* of higher order geometries. This respects the <geometryTypes>
* property and forces a select of points when the drag control is
* already active (and stops events from propagating to the select
* control).
* Called by the drag handler before a feature is dragged. This method is
* used to differentiate between points and vertices
* of higher order geometries.
*
* Parameters:
* feature - {<OpenLayers.Feature.Vector>} The point or vertex about to be
* dragged.
* pixel - {<OpenLayers.Pixel>} Pixel location of the mouse event.
*/
dragStart: function(feature, pixel) {
// only change behavior if the feature is not in the vertices array
if(feature != this.feature && !feature.geometry.parent &&
feature != this.dragHandle && feature != this.radiusHandle) {
if(this.standalone === false && this.feature) {
// unselect the currently selected feature
this.selectControl.clickFeature.apply(this.selectControl,
[this.feature]);
}
// check any constraints on the geometry type
if(this.geometryTypes == null ||
OpenLayers.Util.indexOf(this.geometryTypes,
feature.geometry.CLASS_NAME) != -1) {
// select the point
this.standalone || this.selectControl.clickFeature.apply(
this.selectControl, [feature]);
/**
* TBD: These lines improve workflow by letting the user
* immediately start dragging after the mouse down.
* However, it is very ugly to be messing with controls
* and their handlers in this way. I'd like a better
* solution if the workflow change is necessary.
*/
// prepare the point for dragging
this.dragControl.overFeature.apply(this.dragControl,
[feature]);
this.dragControl.lastPixel = pixel;
this.dragControl.handlers.drag.started = true;
this.dragControl.handlers.drag.start = pixel;
this.dragControl.handlers.drag.last = pixel;
dragStart: function(feature) {
var isPoint = feature.geometry.CLASS_NAME ==
'OpenLayers.Geometry.Point';
if (!this.standalone &&
((!feature._sketch && isPoint) || !feature._sketch)) {
if (this.toggle && this.feature === feature) {
// mark feature for unselection
this._unselect = feature;
}
this.selectFeature(feature);
}
if (feature._sketch || isPoint) {
// feature is a drag or virtual handle or point
this.vertex = feature;
this.handlers.drag.stopDown = true;
}
},
/**
* Method: dragVertex
* Called by the drag feature control with each drag move of a vertex.
* Called by the drag handler with each drag move of a vertex.
*
* Parameters:
* vertex - {<OpenLayers.Feature.Vector>} The vertex being dragged.
* pixel - {<OpenLayers.Pixel>} Pixel location of the mouse event.
*/
dragVertex: function(vertex, pixel) {
var pos = this.map.getLonLatFromViewPortPx(pixel);
var geom = vertex.geometry;
geom.move(pos.lon - geom.x, pos.lat - geom.y);
this.modified = true;
/**
* Five cases:
@@ -487,9 +442,6 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
*/
if(this.feature.geometry.CLASS_NAME == "OpenLayers.Geometry.Point") {
// dragging a simple point
if(this.feature != vertex) {
this.feature = vertex;
}
this.layer.events.triggerEvent("vertexmodified", {
vertex: vertex.geometry,
feature: this.feature,
@@ -526,7 +478,7 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
this.virtualVertices = [];
}
this.layer.drawFeature(this.feature, this.standalone ? undefined :
this.selectControl.renderIntent);
'select');
}
// keep the vertex on top so it gets the mouseout after dragging
// this should be removed in favor of an option to draw under or
@@ -536,7 +488,7 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
/**
* Method: dragComplete
* Called by the drag feature control when the feature dragging is complete.
* Called by the drag handler when the feature dragging is complete.
*
* Parameters:
* vertex - {<OpenLayers.Feature.Vector>} The vertex being dragged.
@@ -572,16 +524,6 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
* Method: resetVertices
*/
resetVertices: function() {
// if coming from a drag complete we're about to destroy the vertex
// that was just dragged. For that reason, the drag feature control
// will never detect a mouse-out on that vertex, meaning that the drag
// handler won't be deactivated. This can cause errors because the drag
// feature control still has a feature to drag but that feature is
// destroyed. To prevent this, we call outFeature on the drag feature
// control if the control actually has a feature to drag.
if(this.dragControl.feature) {
this.dragControl.outFeature(this.dragControl.feature);
}
if(this.vertices.length > 0) {
this.layer.removeFeatures(this.vertices, {silent: true});
this.vertices = [];
@@ -632,11 +574,10 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
// check for delete key
if(this.feature &&
OpenLayers.Util.indexOf(this.deleteCodes, code) != -1) {
var vertex = this.dragControl.feature;
if(vertex &&
OpenLayers.Util.indexOf(this.vertices, vertex) != -1 &&
!this.dragControl.handlers.drag.dragging &&
vertex.geometry.parent) {
var vertex = this.layer.getFeatureFromEvent(this.handlers.drag.evt);
if (vertex &&
OpenLayers.Util.indexOf(this.vertices, vertex) != -1 &&
!this.handlers.drag.dragging && vertex.geometry.parent) {
// remove the vertex
vertex.geometry.parent.removeComponent(vertex.geometry);
this.layer.events.triggerEvent("vertexremoved", {
@@ -645,8 +586,7 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
pixel: evt.xy
});
this.layer.drawFeature(this.feature, this.standalone ?
undefined :
this.selectControl.renderIntent);
undefined : 'select');
this.modified = true;
this.resetVertices();
this.setFeatureState();
@@ -800,8 +740,7 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
* map - {<OpenLayers.Map>} The control's map.
*/
setMap: function(map) {
this.standalone || this.selectControl.setMap(map);
this.dragControl.setMap(map);
this.handlers.drag.setMap(map);
OpenLayers.Control.prototype.setMap.apply(this, arguments);
},