/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for
* full list of contributors). Published under the Clear BSD license.
* See http://svn.openlayers.org/trunk/openlayers/license.txt for the
* full text of the license. */
/**
* @requires OpenLayers/Control/DragFeature.js
* @requires OpenLayers/Control/SelectFeature.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: 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: selectControl
* {<OpenLayers.Control.SelectFeature>}
*/
selectControl: null,
/**
* Property: dragControl
* {<OpenLayers.Control.DragFeature>}
*/
dragControl: 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: 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,
/**
* 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) {
this.layer = layer;
this.vertices = [];
this.virtualVertices = [];
this.virtualStyle = OpenLayers.Util.extend({},
this.layer.style || this.layer.styleMap.createSymbolizer());
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(!(this.deleteCodes instanceof Array)) {
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 = {
geometryTypes: ["OpenLayers.Geometry.Point"],
snappingOptions: this.snappingOptions,
onStart: function(feature, pixel) {
control.dragStart.apply(control, [feature, pixel]);
},
onDrag: function(feature, pixel) {
control.dragVertex.apply(control, [feature, pixel]);
},
onComplete: function(feature) {
control.dragComplete.apply(control, [feature]);
},
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]);
}
}
}
};
this.dragControl = new OpenLayers.Control.DragFeature(
layer, dragOptions
);
// configure the keyboard handler
var keyboardOptions = {
keydown: this.handleKeypress
};
this.handlers = {
keyboard: new OpenLayers.Handler.Keyboard(this, keyboardOptions)
};
},
/**
* APIMethod: destroy
* Take care of things that are not handled in superclass.
*/
destroy: function() {
this.layer = null;
this.standalone || this.selectControl.destroy();
this.dragControl.destroy();
OpenLayers.Control.prototype.destroy.apply(this, []);
},
/**
* APIMethod: activate
* Activate the control.
*
* Returns:
* {Boolean} Successfully activated the control.
*/
activate: function() {
return ((this.standalone || this.selectControl.activate()) &&
this.handlers.keyboard.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.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.keyboard.deactivate();
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}
);
},
/**
* Method: selectFeature
* Called when the select feature control selects a feature.
*
* Parameters:
* feature - {<OpenLayers.Feature.Vector>} the selected feature.
*/
selectFeature: function(feature) {
this.feature = feature;
this.modified = false;
this.resetVertices();
this.dragControl.activate();
this.onModificationStart(this.feature);
},
/**
* Method: 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.feature = null;
this.dragControl.deactivate();
this.onModificationEnd(feature);
this.layer.events.triggerEvent("afterfeaturemodified", {
feature: feature,
modified: this.modified
});
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).
*
* 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;
}
}
},
/**
* Method: dragVertex
* Called by the drag feature control 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) {
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
if(this.feature != vertex) {
this.feature = vertex;
}
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 :
this.selectControl.renderIntent);
}
// 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 feature control 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;
}
},
/**
* 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 = [];
}
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:
* {Integer} Key code corresponding to the 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.dragControl.feature;
if(vertex &&
OpenLayers.Util.indexOf(this.vertices, vertex) != -1 &&
!this.dragControl.handlers.drag.dragging &&
vertex.geometry.parent) {
// remove the vertex
vertex.geometry.parent.removeComponent(vertex.geometry);
this.layer.drawFeature(this.feature, this.standalone ?
undefined :
this.selectControl.renderIntent);
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;
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;
control.vertices.push(vertex);
} else {
collectComponentVertices(component);
}
}
// add virtual vertices in the middle of each edge
if(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.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.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.standalone || this.selectControl.setMap(map);
this.dragControl.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;