Files
openlayers/lib/OpenLayers/Control/ModifyFeature.js
ahocevar d2a32d5421 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.
2013-03-19 23:00:10 +01:00

770 lines
28 KiB
JavaScript

/* Copyright (c) 2006-2013 by OpenLayers Contributors (see authors.txt for
* full list of contributors). Published under the 2-clause BSD license.
* See license.txt in the OpenLayers distribution or repository for the
* full text of the license. */
/**
* @requires OpenLayers/Handler/Drag.js
* @requires OpenLayers/Handler/Keyboard.js
*/
/**
* Class: OpenLayers.Control.ModifyFeature
* Control to modify features. When activated, a click renders the vertices
* of a feature - these vertices can then be dragged. By default, the
* delete key will delete the vertex under the mouse. New features are
* added by dragging "virtual vertices" between vertices. Create a new
* control with the <OpenLayers.Control.ModifyFeature> constructor.
*
* Inherits From:
* - <OpenLayers.Control>
*/
OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
/**
* APIProperty: documentDrag
* {Boolean} If set to true, dragging vertices will continue even if the
* mouse cursor leaves the map viewport. Default is false.
*/
documentDrag: false,
/**
* APIProperty: geometryTypes
* {Array(String)} To restrict modification to a limited set of geometry
* types, send a list of strings corresponding to the geometry class
* names.
*/
geometryTypes: null,
/**
* APIProperty: clickout
* {Boolean} Unselect features when clicking outside any feature.
* Default is true.
*/
clickout: true,
/**
* APIProperty: toggle
* {Boolean} Unselect a selected feature on click.
* Default is true.
*/
toggle: true,
/**
* APIProperty: standalone
* {Boolean} Set to true to create a control without SelectFeature
* capabilities. Default is false. If standalone is true, to modify
* a feature, call the <selectFeature> method with the target feature.
* Note that you must call the <unselectFeature> method to finish
* feature modification in standalone mode (before starting to modify
* another feature).
*/
standalone: false,
/**
* Property: layer
* {<OpenLayers.Layer.Vector>}
*/
layer: null,
/**
* Property: feature
* {<OpenLayers.Feature.Vector>} Feature currently available for modification.
*/
feature: null,
/**
* Property: vertices
* {Array(<OpenLayers.Feature.Vector>)} Verticies currently available
* for dragging.
*/
vertices: null,
/**
* Property: virtualVertices
* {Array(<OpenLayers.Feature.Vector>)} Virtual vertices in the middle
* of each edge.
*/
virtualVertices: null,
/**
* Property: handlers
* {Object}
*/
handlers: null,
/**
* APIProperty: deleteCodes
* {Array(Integer)} Keycodes for deleting verticies. Set to null to disable
* vertex deltion by keypress. If non-null, keypresses with codes
* in this array will delete vertices under the mouse. Default
* is 46 and 68, the 'delete' and lowercase 'd' keys.
*/
deleteCodes: null,
/**
* APIProperty: virtualStyle
* {Object} A symbolizer to be used for virtual vertices.
*/
virtualStyle: null,
/**
* APIProperty: vertexRenderIntent
* {String} The renderIntent to use for vertices. If no <virtualStyle> is
* provided, this renderIntent will also be used for virtual vertices, with
* a fillOpacity and strokeOpacity of 0.3. Default is null, which means
* that the layer's default style will be used for vertices.
*/
vertexRenderIntent: null,
/**
* APIProperty: mode
* {Integer} Bitfields specifying the modification mode. Defaults to
* OpenLayers.Control.ModifyFeature.RESHAPE. To set the mode to a
* combination of options, use the | operator. For example, to allow
* the control to both resize and rotate features, use the following
* syntax
* (code)
* control.mode = OpenLayers.Control.ModifyFeature.RESIZE |
* OpenLayers.Control.ModifyFeature.ROTATE;
* (end)
*/
mode: null,
/**
* APIProperty: createVertices
* {Boolean} Create new vertices by dragging the virtual vertices
* in the middle of each edge. Default is true.
*/
createVertices: true,
/**
* Property: modified
* {Boolean} The currently selected feature has been modified.
*/
modified: false,
/**
* Property: radiusHandle
* {<OpenLayers.Feature.Vector>} A handle for rotating/resizing a feature.
*/
radiusHandle: null,
/**
* Property: dragHandle
* {<OpenLayers.Feature.Vector>} A handle for dragging a feature.
*/
dragHandle: null,
/**
* APIProperty: onModificationStart
* {Function} *Deprecated*. Register for "beforefeaturemodified" instead.
* The "beforefeaturemodified" event is triggered on the layer before
* any modification begins.
*
* Optional function to be called when a feature is selected
* to be modified. The function should expect to be called with a
* feature. This could be used for example to allow to lock the
* feature on server-side.
*/
onModificationStart: function() {},
/**
* APIProperty: onModification
* {Function} *Deprecated*. Register for "featuremodified" instead.
* The "featuremodified" event is triggered on the layer with each
* feature modification.
*
* Optional function to be called when a feature has been
* modified. The function should expect to be called with a feature.
*/
onModification: function() {},
/**
* APIProperty: onModificationEnd
* {Function} *Deprecated*. Register for "afterfeaturemodified" instead.
* The "afterfeaturemodified" event is triggered on the layer after
* a feature has been modified.
*
* Optional function to be called when a feature is finished
* being modified. The function should expect to be called with a
* feature.
*/
onModificationEnd: function() {},
/**
* Constructor: OpenLayers.Control.ModifyFeature
* Create a new modify feature control.
*
* Parameters:
* layer - {<OpenLayers.Layer.Vector>} Layer that contains features that
* will be modified.
* options - {Object} Optional object whose properties will be set on the
* control.
*/
initialize: function(layer, options) {
options = options || {};
this.layer = layer;
this.vertices = [];
this.virtualVertices = [];
this.virtualStyle = OpenLayers.Util.extend({},
this.layer.style ||
this.layer.styleMap.createSymbolizer(null, options.vertexRenderIntent)
);
this.virtualStyle.fillOpacity = 0.3;
this.virtualStyle.strokeOpacity = 0.3;
this.deleteCodes = [46, 68];
this.mode = OpenLayers.Control.ModifyFeature.RESHAPE;
OpenLayers.Control.prototype.initialize.apply(this, [options]);
if(!(OpenLayers.Util.isArray(this.deleteCodes))) {
this.deleteCodes = [this.deleteCodes];
}
// 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);
}
},
move: function(pixel) {
delete this._unselect;
if (this.vertex) {
this.dragVertex(this.vertex, pixel);
}
},
up: function() {
this.handlers.drag.stopDown = false;
if (this._unselect) {
this.unselectFeature(this._unselect);
delete this._unselect;
}
},
done: function(pixel) {
if (this.vertex) {
this.dragComplete(this.vertex);
}
}
};
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),
drag: new OpenLayers.Handler.Drag(this, dragCallbacks, dragOptions)
};
},
/**
* APIMethod: destroy
* Take care of things that are not handled in superclass.
*/
destroy: function() {
this.layer = null;
OpenLayers.Control.prototype.destroy.apply(this, []);
},
/**
* APIMethod: activate
* Activate the control.
*
* Returns:
* {Boolean} Successfully activated the control.
*/
activate: function() {
return (this.handlers.keyboard.activate() &&
this.handlers.drag.activate() &&
OpenLayers.Control.prototype.activate.apply(this, arguments));
},
/**
* APIMethod: deactivate
* Deactivate the control.
*
* Returns:
* {Boolean} Successfully deactivated the control.
*/
deactivate: function() {
var deactivated = false;
// the return from the controls is unimportant in this case
if(OpenLayers.Control.prototype.deactivate.apply(this, arguments)) {
this.layer.removeFeatures(this.vertices, {silent: true});
this.layer.removeFeatures(this.virtualVertices, {silent: true});
this.vertices = [];
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.
*
* Parameters:
* feature - {<OpenLayers.Feature.Vector>} The feature about to be selected.
*/
beforeSelectFeature: function(feature) {
return this.layer.events.triggerEvent(
"beforefeaturemodified", {feature: feature}
);
},
/**
* APIMethod: selectFeature
* Select a feature for modification in standalone mode. In non-standalone
* mode, this method is called when the select feature control selects a
* feature. Register a listener to the beforefeaturemodified event and
* return false to prevent feature modification.
*
* Parameters:
* 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.onModificationStart(this.feature);
}
// keep track of geometry modifications
var modified = feature.modified;
if (feature.geometry && !(modified && modified.geometry)) {
this._originalGeometry = feature.geometry.clone();
}
},
/**
* APIMethod: unselectFeature
* Called when the select feature control unselects a feature.
*
* Parameters:
* feature - {<OpenLayers.Feature.Vector>} The unselected feature.
*/
unselectFeature: function(feature) {
this.layer.removeFeatures(this.vertices, {silent: true});
this.vertices = [];
this.layer.destroyFeatures(this.virtualVertices, {silent: true});
this.virtualVertices = [];
if(this.dragHandle) {
this.layer.destroyFeatures([this.dragHandle], {silent: true});
delete this.dragHandle;
}
if(this.radiusHandle) {
this.layer.destroyFeatures([this.radiusHandle], {silent: true});
delete this.radiusHandle;
}
this.layer.drawFeature(this.feature, 'default');
this.feature = null;
OpenLayers.Util.removeItem(this.layer.selectedFeatures, feature);
this.onModificationEnd(feature);
this.layer.events.triggerEvent("afterfeaturemodified", {
feature: feature,
modified: this.modified
});
this.modified = false;
},
/**
* Method: dragStart
* 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.
*/
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 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:
* 1) dragging a simple point
* 2) dragging a virtual vertex
* 3) dragging a drag handle
* 4) dragging a real vertex
* 5) dragging a radius handle
*/
if(this.feature.geometry.CLASS_NAME == "OpenLayers.Geometry.Point") {
// dragging a simple point
this.layer.events.triggerEvent("vertexmodified", {
vertex: vertex.geometry,
feature: this.feature,
pixel: pixel
});
} else {
if(vertex._index) {
// dragging a virtual vertex
vertex.geometry.parent.addComponent(vertex.geometry,
vertex._index);
// move from virtual to real vertex
delete vertex._index;
OpenLayers.Util.removeItem(this.virtualVertices, vertex);
this.vertices.push(vertex);
} else if(vertex == this.dragHandle) {
// dragging a drag handle
this.layer.removeFeatures(this.vertices, {silent: true});
this.vertices = [];
if(this.radiusHandle) {
this.layer.destroyFeatures([this.radiusHandle], {silent: true});
this.radiusHandle = null;
}
} else if(vertex !== this.radiusHandle) {
// dragging a real vertex
this.layer.events.triggerEvent("vertexmodified", {
vertex: vertex.geometry,
feature: this.feature,
pixel: pixel
});
}
// dragging a radius handle - no special treatment
if(this.virtualVertices.length > 0) {
this.layer.destroyFeatures(this.virtualVertices, {silent: true});
this.virtualVertices = [];
}
this.layer.drawFeature(this.feature, this.standalone ? undefined :
'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
// maintain node z-index
this.layer.drawFeature(vertex);
},
/**
* Method: dragComplete
* Called by the drag handler when the feature dragging is complete.
*
* Parameters:
* vertex - {<OpenLayers.Feature.Vector>} The vertex being dragged.
*/
dragComplete: function(vertex) {
this.resetVertices();
this.setFeatureState();
this.onModification(this.feature);
this.layer.events.triggerEvent("featuremodified",
{feature: this.feature});
},
/**
* Method: setFeatureState
* Called when the feature is modified. If the current state is not
* INSERT or DELETE, the state is set to UPDATE.
*/
setFeatureState: function() {
if(this.feature.state != OpenLayers.State.INSERT &&
this.feature.state != OpenLayers.State.DELETE) {
this.feature.state = OpenLayers.State.UPDATE;
if (this.modified && this._originalGeometry) {
var feature = this.feature;
feature.modified = OpenLayers.Util.extend(feature.modified, {
geometry: this._originalGeometry
});
delete this._originalGeometry;
}
}
},
/**
* Method: resetVertices
*/
resetVertices: function() {
if(this.vertices.length > 0) {
this.layer.removeFeatures(this.vertices, {silent: true});
this.vertices = [];
}
if(this.virtualVertices.length > 0) {
this.layer.removeFeatures(this.virtualVertices, {silent: true});
this.virtualVertices = [];
}
if(this.dragHandle) {
this.layer.destroyFeatures([this.dragHandle], {silent: true});
this.dragHandle = null;
}
if(this.radiusHandle) {
this.layer.destroyFeatures([this.radiusHandle], {silent: true});
this.radiusHandle = null;
}
if(this.feature &&
this.feature.geometry.CLASS_NAME != "OpenLayers.Geometry.Point") {
if((this.mode & OpenLayers.Control.ModifyFeature.DRAG)) {
this.collectDragHandle();
}
if((this.mode & (OpenLayers.Control.ModifyFeature.ROTATE |
OpenLayers.Control.ModifyFeature.RESIZE))) {
this.collectRadiusHandle();
}
if(this.mode & OpenLayers.Control.ModifyFeature.RESHAPE){
// Don't collect vertices when we're resizing
if (!(this.mode & OpenLayers.Control.ModifyFeature.RESIZE)){
this.collectVertices();
}
}
}
},
/**
* Method: handleKeypress
* Called by the feature handler on keypress. This is used to delete
* vertices. If the <deleteCode> property is set, vertices will
* be deleted when a feature is selected for modification and
* the mouse is over a vertex.
*
* Parameters:
* evt - {Event} Keypress event.
*/
handleKeypress: function(evt) {
var code = evt.keyCode;
// check for delete key
if(this.feature &&
OpenLayers.Util.indexOf(this.deleteCodes, code) != -1) {
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", {
vertex: vertex.geometry,
feature: this.feature,
pixel: evt.xy
});
this.layer.drawFeature(this.feature, this.standalone ?
undefined : 'select');
this.modified = true;
this.resetVertices();
this.setFeatureState();
this.onModification(this.feature);
this.layer.events.triggerEvent("featuremodified",
{feature: this.feature});
}
}
},
/**
* Method: collectVertices
* Collect the vertices from the modifiable feature's geometry and push
* them on to the control's vertices array.
*/
collectVertices: function() {
this.vertices = [];
this.virtualVertices = [];
var control = this;
function collectComponentVertices(geometry) {
var i, vertex, component, len;
if(geometry.CLASS_NAME == "OpenLayers.Geometry.Point") {
vertex = new OpenLayers.Feature.Vector(geometry);
vertex._sketch = true;
vertex.renderIntent = control.vertexRenderIntent;
control.vertices.push(vertex);
} else {
var numVert = geometry.components.length;
if(geometry.CLASS_NAME == "OpenLayers.Geometry.LinearRing") {
numVert -= 1;
}
for(i=0; i<numVert; ++i) {
component = geometry.components[i];
if(component.CLASS_NAME == "OpenLayers.Geometry.Point") {
vertex = new OpenLayers.Feature.Vector(component);
vertex._sketch = true;
vertex.renderIntent = control.vertexRenderIntent;
control.vertices.push(vertex);
} else {
collectComponentVertices(component);
}
}
// add virtual vertices in the middle of each edge
if (control.createVertices && geometry.CLASS_NAME != "OpenLayers.Geometry.MultiPoint") {
for(i=0, len=geometry.components.length; i<len-1; ++i) {
var prevVertex = geometry.components[i];
var nextVertex = geometry.components[i + 1];
if(prevVertex.CLASS_NAME == "OpenLayers.Geometry.Point" &&
nextVertex.CLASS_NAME == "OpenLayers.Geometry.Point") {
var x = (prevVertex.x + nextVertex.x) / 2;
var y = (prevVertex.y + nextVertex.y) / 2;
var point = new OpenLayers.Feature.Vector(
new OpenLayers.Geometry.Point(x, y),
null, control.virtualStyle
);
// set the virtual parent and intended index
point.geometry.parent = geometry;
point._index = i + 1;
point._sketch = true;
control.virtualVertices.push(point);
}
}
}
}
}
collectComponentVertices.call(this, this.feature.geometry);
this.layer.addFeatures(this.virtualVertices, {silent: true});
this.layer.addFeatures(this.vertices, {silent: true});
},
/**
* Method: collectDragHandle
* Collect the drag handle for the selected geometry.
*/
collectDragHandle: function() {
var geometry = this.feature.geometry;
var center = geometry.getBounds().getCenterLonLat();
var originGeometry = new OpenLayers.Geometry.Point(
center.lon, center.lat
);
var origin = new OpenLayers.Feature.Vector(originGeometry);
originGeometry.move = function(x, y) {
OpenLayers.Geometry.Point.prototype.move.call(this, x, y);
geometry.move(x, y);
};
origin._sketch = true;
this.dragHandle = origin;
this.dragHandle.renderIntent = this.vertexRenderIntent;
this.layer.addFeatures([this.dragHandle], {silent: true});
},
/**
* Method: collectRadiusHandle
* Collect the radius handle for the selected geometry.
*/
collectRadiusHandle: function() {
var geometry = this.feature.geometry;
var bounds = geometry.getBounds();
var center = bounds.getCenterLonLat();
var originGeometry = new OpenLayers.Geometry.Point(
center.lon, center.lat
);
var radiusGeometry = new OpenLayers.Geometry.Point(
bounds.right, bounds.bottom
);
var radius = new OpenLayers.Feature.Vector(radiusGeometry);
var resize = (this.mode & OpenLayers.Control.ModifyFeature.RESIZE);
var reshape = (this.mode & OpenLayers.Control.ModifyFeature.RESHAPE);
var rotate = (this.mode & OpenLayers.Control.ModifyFeature.ROTATE);
radiusGeometry.move = function(x, y) {
OpenLayers.Geometry.Point.prototype.move.call(this, x, y);
var dx1 = this.x - originGeometry.x;
var dy1 = this.y - originGeometry.y;
var dx0 = dx1 - x;
var dy0 = dy1 - y;
if(rotate) {
var a0 = Math.atan2(dy0, dx0);
var a1 = Math.atan2(dy1, dx1);
var angle = a1 - a0;
angle *= 180 / Math.PI;
geometry.rotate(angle, originGeometry);
}
if(resize) {
var scale, ratio;
// 'resize' together with 'reshape' implies that the aspect
// ratio of the geometry will not be preserved whilst resizing
if (reshape) {
scale = dy1 / dy0;
ratio = (dx1 / dx0) / scale;
} else {
var l0 = Math.sqrt((dx0 * dx0) + (dy0 * dy0));
var l1 = Math.sqrt((dx1 * dx1) + (dy1 * dy1));
scale = l1 / l0;
}
geometry.resize(scale, originGeometry, ratio);
}
};
radius._sketch = true;
this.radiusHandle = radius;
this.radiusHandle.renderIntent = this.vertexRenderIntent;
this.layer.addFeatures([this.radiusHandle], {silent: true});
},
/**
* Method: setMap
* Set the map property for the control and all handlers.
*
* Parameters:
* map - {<OpenLayers.Map>} The control's map.
*/
setMap: function(map) {
this.handlers.drag.setMap(map);
OpenLayers.Control.prototype.setMap.apply(this, arguments);
},
CLASS_NAME: "OpenLayers.Control.ModifyFeature"
});
/**
* Constant: RESHAPE
* {Integer} Constant used to make the control work in reshape mode
*/
OpenLayers.Control.ModifyFeature.RESHAPE = 1;
/**
* Constant: RESIZE
* {Integer} Constant used to make the control work in resize mode
*/
OpenLayers.Control.ModifyFeature.RESIZE = 2;
/**
* Constant: ROTATE
* {Integer} Constant used to make the control work in rotate mode
*/
OpenLayers.Control.ModifyFeature.ROTATE = 4;
/**
* Constant: DRAG
* {Integer} Constant used to make the control work in drag mode
*/
OpenLayers.Control.ModifyFeature.DRAG = 8;