--- a/labs/openlayers/lib/OpenLayers/Control/ModifyFeature.js +++ b/labs/openlayers/lib/OpenLayers/Control/ModifyFeature.js @@ -1,1 +1,778 @@ - +/* 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; +