Files
openlayers/lib/OpenLayers/Control/ModifyFeature.js
Éric Lemoine a68492e003 The deactivate method of the modify feature control calls the unselect method
of its select feature control and passes it the current selected feature (if
any). If that selected feature is destroyed between the time it was selected
and the time the modify feature control is deactivated, the unselect method of
the select feature control will attempt to draw it on the layer while it no
longer has a geometry (it's destroyed!). This patch prevents this from
happening. Thanks to tlpinney for reporting the bug and helping on its
evaluation. r=tschaub (closes #1281) 


git-svn-id: http://svn.openlayers.org/trunk/openlayers@5897 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
2008-01-25 23:18:20 +00:00

642 lines
23 KiB
JavaScript

/* Copyright (c) 2006 MetaCarta, Inc., 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,
/**
* 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.Select>}
*/
selectControl: null,
/**
* Property: dragControl
* {<OpenLayers.Control.DragFeature>}
*/
dragControl: null,
/**
* Property: keyboardHandler
* {<OpenLayers.Handler.Keyboard>}
*/
keyboardHandler: 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 100, the 'delete' and lowercase 'd' keys.
*/
deleteCodes: null,
/**
* APIProperty: virtualStyle
* {<OpenLayers.Feature.Vector.Style>}
*/
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. or 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: 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} 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} 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} 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.styleVirtual = OpenLayers.Util.extend({}, this.layer.style);
this.styleVirtual.fillOpacity = 0.3;
this.styleVirtual.strokeOpacity = 0.3;
this.deleteCodes = [46, 100];
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,
onSelect: function(feature) {
control.selectFeature.apply(control, [feature]);
},
onUnselect: function(feature) {
control.unselectFeature.apply(control, [feature]);
}
};
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) {
control.dragVertex.apply(control, [feature]);
},
onComplete: function(feature) {
control.dragComplete.apply(control, [feature]);
}
};
this.dragControl = new OpenLayers.Control.DragFeature(
layer, dragOptions
);
// configure the keyboard handler
var keyboardOptions = {
keypress: this.handleKeypress
};
this.keyboardHandler = new OpenLayers.Handler.Keyboard(
this, keyboardOptions
);
},
/**
* APIMethod: destroy
* Take care of things that are not handled in superclass.
*/
destroy: function() {
this.layer = null;
this.selectControl.destroy();
this.dragControl.destroy();
this.keyboardHandler.destroy();
OpenLayers.Control.prototype.destroy.apply(this, []);
},
/**
* APIMethod: activate
* Activate the control and the feature handler.
*
* Returns:
* {Boolean} Successfully activated the control and feature handler.
*/
activate: function() {
return (this.selectControl.activate() &&
this.keyboardHandler.activate() &&
OpenLayers.Control.prototype.activate.apply(this, arguments));
},
/**
* APIMethod: deactivate
* Deactivate the controls.
*
* 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);
this.layer.removeFeatures(this.virtualVertices);
this.vertices = [];
this.dragControl.deactivate();
if(this.feature && this.feature.geometry) {
this.selectControl.unselect.apply(this.selectControl,
[this.feature]);
}
this.selectControl.deactivate();
this.keyboardHandler.deactivate();
deactivated = true;
}
return deactivated;
},
/**
* 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.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);
this.vertices = [];
this.layer.destroyFeatures(this.virtualVertices);
this.virtualVertices = [];
if(this.dragHandle) {
this.layer.destroyFeatures([this.dragHandle]);
delete this.dragHandle;
}
if(this.radiusHandle) {
this.layer.destroyFeatures([this.radiusHandle]);
delete this.radiusHandle;
}
this.feature = null;
this.dragControl.deactivate();
this.onModificationEnd(feature);
},
/**
* 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.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.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.dragHandler.started = true;
this.dragControl.dragHandler.start = pixel;
this.dragControl.dragHandler.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.
*/
dragVertex: function(vertex) {
/**
* Five cases:
* 1) dragging a simple point
* 2) dragging a virtual vertex
* 3) dragging a drag handle
* 4) dragging a radius handle
* 5) dragging a real vertex
*/
if(this.feature.geometry.CLASS_NAME == "OpenLayers.Geometry.Point") {
// dragging a simple point
if(this.feature != vertex) {
this.feature = vertex;
}
} 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);
this.vertices = [];
if(this.radiusHandle) {
this.layer.destroyFeatures([this.radiusHandle]);
this.radiusHandle = null;
}
}
// dragging a radius handle - no special treatment
// dragging a real vertex - no special treatment
if(this.virtualVertices.length > 0) {
this.layer.destroyFeatures(this.virtualVertices);
this.virtualVertices = [];
}
this.layer.drawFeature(this.feature, this.selectControl.selectStyle);
}
// 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.onModification(this.feature);
},
/**
* Method: resetVertices
*/
resetVertices: function() {
if(this.vertices.length > 0) {
this.layer.removeFeatures(this.vertices);
this.vertices = [];
}
if(this.virtualVertices.length > 0) {
this.layer.removeFeatures(this.virtualVertices);
this.virtualVertices = [];
}
if(this.dragHandle) {
this.layer.destroyFeatures([this.dragHandle]);
this.dragHandle = null;
}
if(this.radiusHandle) {
this.layer.destroyFeatures([this.radiusHandle]);
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)) {
this.collectVertices();
}
}
},
/**
* Method: handleKeypress
* Called by the feature handler on keypress. This is used to delete
* vertices and point features. If the <deleteCode> property is set,
* vertices and points 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(code) {
// 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.dragHandler.dragging &&
vertex.geometry.parent) {
// remove the vertex
vertex.geometry.parent.removeComponent(vertex.geometry);
this.layer.drawFeature(this.feature,
this.selectControl.selectStyle);
this.resetVertices();
this.onModification(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.virtualVirtices = [];
var control = this;
function collectComponentVertices(geometry) {
var i, vertex, component;
if(geometry.CLASS_NAME == "OpenLayers.Geometry.Point") {
vertex = new OpenLayers.Feature.Vector(geometry);
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);
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; i<geometry.components.length-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.styleVirtual
);
// set the virtual parent and intended index
point.geometry.parent = geometry;
point._index = i + 1;
control.virtualVertices.push(point);
}
}
}
}
}
collectComponentVertices.call(this, this.feature.geometry);
this.layer.addFeatures(this.vertices, {silent: true});
this.layer.addFeatures(this.virtualVertices, {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);
}
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 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 l0 = Math.sqrt((dx0 * dx0) + (dy0 * dy0));
var l1 = Math.sqrt((dx1 * dx1) + (dy1 * dy1));
geometry.resize(l1 / l0, originGeometry);
}
}
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.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;