1 /* Copyright (c) 2006 MetaCarta, Inc., published under the Clear BSD license.
2 * See http://svn.openlayers.org/trunk/openlayers/license.txt
3 * for the full text of the license. */
7 * @requires OpenLayers/Control/DragFeature.js
8 * @requires OpenLayers/Control/SelectFeature.js
9 * @requires OpenLayers/Handler/Keyboard.js
13 * Class: OpenLayers.Control.ModifyFeature
14 * Control to modify features. When activated, a click renders the vertices
15 * of a feature - these vertices can then be dragged. By default, the
16 * delete key will delete the vertex under the mouse. New features are
17 * added by dragging "virtual vertices" between vertices. Create a new
18 * control with the <OpenLayers.Control.ModifyFeature> constructor.
21 * - <OpenLayers.Control>
23 OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, {
26 * APIProperty: geometryTypes
27 * {Array(String)} To restrict modification to a limited set of geometry
28 * types, send a list of strings corresponding to the geometry class
34 * APIProperty: clickout
35 * {Boolean} Unselect features when clicking outside any feature.
42 * {Boolean} Unselect a selected feature on click.
49 * {<OpenLayers.Layer.Vector>}
55 * {<OpenLayers.Feature.Vector>} Feature currently available for modification.
61 * {Array(<OpenLayers.Feature.Vector>)} Verticies currently available
67 * Property: virtualVertices
68 * {Array(<OpenLayers.Feature.Vector>)} Virtual vertices in the middle
71 virtualVertices: null,
74 * Property: selectControl
75 * {<OpenLayers.Control.SelectFeature>}
80 * Property: dragControl
81 * {<OpenLayers.Control.DragFeature>}
92 * APIProperty: deleteCodes
93 * {Array(Integer)} Keycodes for deleting verticies. Set to null to disable
94 * vertex deltion by keypress. If non-null, keypresses with codes
95 * in this array will delete vertices under the mouse. Default
96 * is 46 and 68, the 'delete' and lowercase 'd' keys.
101 * APIProperty: virtualStyle
102 * {Object} A symbolizer to be used for virtual vertices.
108 * {Integer} Bitfields specifying the modification mode. Defaults to
109 * OpenLayers.Control.ModifyFeature.RESHAPE. To set the mode to a
110 * combination of options, use the | operator. or example, to allow
111 * the control to both resize and rotate features, use the following
114 * control.mode = OpenLayers.Control.ModifyFeature.RESIZE |
115 * OpenLayers.Control.ModifyFeature.ROTATE;
122 * {Boolean} The currently selected feature has been modified.
127 * Property: radiusHandle
128 * {<OpenLayers.Feature.Vector>} A handle for rotating/resizing a feature.
133 * Property: dragHandle
134 * {<OpenLayers.Feature.Vector>} A handle for dragging a feature.
139 * APIProperty: onModificationStart
140 * {Function} *Deprecated*. Register for "beforefeaturemodified" instead.
141 * The "beforefeaturemodified" event is triggered on the layer before
142 * any modification begins.
144 * Optional function to be called when a feature is selected
145 * to be modified. The function should expect to be called with a
146 * feature. This could be used for example to allow to lock the
147 * feature on server-side.
149 onModificationStart: function() {},
152 * APIProperty: onModification
153 * {Function} *Deprecated*. Register for "featuremodified" instead.
154 * The "featuremodified" event is triggered on the layer with each
155 * feature modification.
157 * Optional function to be called when a feature has been
158 * modified. The function should expect to be called with a feature.
160 onModification: function() {},
163 * APIProperty: onModificationEnd
164 * {Function} *Deprecated*. Register for "afterfeaturemodified" instead.
165 * The "afterfeaturemodified" event is triggered on the layer after
166 * a feature has been modified.
168 * Optional function to be called when a feature is finished
169 * being modified. The function should expect to be called with a
172 onModificationEnd: function() {},
175 * Constructor: OpenLayers.Control.ModifyFeature
176 * Create a new modify feature control.
179 * layer - {<OpenLayers.Layer.Vector>} Layer that contains features that
181 * options - {Object} Optional object whose properties will be set on the
184 initialize: function(layer, options) {
187 this.virtualVertices = [];
188 this.virtualStyle = OpenLayers.Util.extend({},
189 this.layer.style || this.layer.styleMap.createSymbolizer());
190 this.virtualStyle.fillOpacity = 0.3;
191 this.virtualStyle.strokeOpacity = 0.3;
192 this.deleteCodes = [46, 68];
193 this.mode = OpenLayers.Control.ModifyFeature.RESHAPE;
194 OpenLayers.Control.prototype.initialize.apply(this, [options]);
195 if(!(this.deleteCodes instanceof Array)) {
196 this.deleteCodes = [this.deleteCodes];
200 // configure the select control
201 var selectOptions = {
202 geometryTypes: this.geometryTypes,
203 clickout: this.clickout,
205 onBeforeSelect: this.beforeSelectFeature,
206 onSelect: this.selectFeature,
207 onUnselect: this.unselectFeature,
210 this.selectControl = new OpenLayers.Control.SelectFeature(
214 // configure the drag control
216 geometryTypes: ["OpenLayers.Geometry.Point"],
217 snappingOptions: this.snappingOptions,
218 onStart: function(feature, pixel) {
219 control.dragStart.apply(control, [feature, pixel]);
221 onDrag: function(feature, pixel) {
222 control.dragVertex.apply(control, [feature, pixel]);
224 onComplete: function(feature) {
225 control.dragComplete.apply(control, [feature]);
228 this.dragControl = new OpenLayers.Control.DragFeature(
232 // configure the keyboard handler
233 var keyboardOptions = {
234 keydown: this.handleKeypress
237 keyboard: new OpenLayers.Handler.Keyboard(this, keyboardOptions)
243 * Take care of things that are not handled in superclass.
245 destroy: function() {
247 this.selectControl.destroy();
248 this.dragControl.destroy();
249 OpenLayers.Control.prototype.destroy.apply(this, []);
253 * APIMethod: activate
254 * Activate the control.
257 * {Boolean} Successfully activated the control.
259 activate: function() {
260 return (this.selectControl.activate() &&
261 this.handlers.keyboard.activate() &&
262 OpenLayers.Control.prototype.activate.apply(this, arguments));
266 * APIMethod: deactivate
267 * Deactivate the control.
270 * {Boolean} Successfully deactivated the control.
272 deactivate: function() {
273 var deactivated = false;
274 // the return from the controls is unimportant in this case
275 if(OpenLayers.Control.prototype.deactivate.apply(this, arguments)) {
276 this.layer.removeFeatures(this.vertices, {silent: true});
277 this.layer.removeFeatures(this.virtualVertices, {silent: true});
279 this.dragControl.deactivate();
280 if(this.feature && this.feature.geometry && this.feature.layer) {
281 this.selectControl.unselect.apply(this.selectControl,
284 this.selectControl.deactivate();
285 this.handlers.keyboard.deactivate();
292 * Method: beforeSelectFeature
293 * Called before a feature is selected.
296 * feature - {<OpenLayers.Feature.Vector>} The feature about to be selected.
298 beforeSelectFeature: function(feature) {
299 return this.layer.events.triggerEvent(
300 "beforefeaturemodified", {feature: feature}
305 * Method: selectFeature
306 * Called when the select feature control selects a feature.
309 * feature - {<OpenLayers.Feature.Vector>} the selected feature.
311 selectFeature: function(feature) {
312 this.feature = feature;
313 this.modified = false;
314 this.resetVertices();
315 this.dragControl.activate();
316 this.onModificationStart(this.feature);
320 * Method: unselectFeature
321 * Called when the select feature control unselects a feature.
324 * feature - {<OpenLayers.Feature.Vector>} The unselected feature.
326 unselectFeature: function(feature) {
327 this.layer.removeFeatures(this.vertices, {silent: true});
329 this.layer.destroyFeatures(this.virtualVertices, {silent: true});
330 this.virtualVertices = [];
331 if(this.dragHandle) {
332 this.layer.destroyFeatures([this.dragHandle], {silent: true});
333 delete this.dragHandle;
335 if(this.radiusHandle) {
336 this.layer.destroyFeatures([this.radiusHandle], {silent: true});
337 delete this.radiusHandle;
340 this.dragControl.deactivate();
341 this.onModificationEnd(feature);
342 this.layer.events.triggerEvent("afterfeaturemodified", {
344 modified: this.modified
346 this.modified = false;
351 * Called by the drag feature control with before a feature is dragged.
352 * This method is used to differentiate between points and vertices
353 * of higher order geometries. This respects the <geometryTypes>
354 * property and forces a select of points when the drag control is
355 * already active (and stops events from propagating to the select
359 * feature - {<OpenLayers.Feature.Vector>} The point or vertex about to be
361 * pixel - {<OpenLayers.Pixel>} Pixel location of the mouse event.
363 dragStart: function(feature, pixel) {
364 // only change behavior if the feature is not in the vertices array
365 if(feature != this.feature && !feature.geometry.parent &&
366 feature != this.dragHandle && feature != this.radiusHandle) {
368 // unselect the currently selected feature
369 this.selectControl.clickFeature.apply(this.selectControl,
372 // check any constraints on the geometry type
373 if(this.geometryTypes == null ||
374 OpenLayers.Util.indexOf(this.geometryTypes,
375 feature.geometry.CLASS_NAME) != -1) {
377 this.selectControl.clickFeature.apply(this.selectControl,
380 * TBD: These lines improve workflow by letting the user
381 * immediately start dragging after the mouse down.
382 * However, it is very ugly to be messing with controls
383 * and their handlers in this way. I'd like a better
384 * solution if the workflow change is necessary.
386 // prepare the point for dragging
387 this.dragControl.overFeature.apply(this.dragControl,
389 this.dragControl.lastPixel = pixel;
390 this.dragControl.handlers.drag.started = true;
391 this.dragControl.handlers.drag.start = pixel;
392 this.dragControl.handlers.drag.last = pixel;
399 * Called by the drag feature control with each drag move of a vertex.
402 * vertex - {<OpenLayers.Feature.Vector>} The vertex being dragged.
403 * pixel - {<OpenLayers.Pixel>} Pixel location of the mouse event.
405 dragVertex: function(vertex, pixel) {
406 this.modified = true;
409 * 1) dragging a simple point
410 * 2) dragging a virtual vertex
411 * 3) dragging a drag handle
412 * 4) dragging a real vertex
413 * 5) dragging a radius handle
415 if(this.feature.geometry.CLASS_NAME == "OpenLayers.Geometry.Point") {
416 // dragging a simple point
417 if(this.feature != vertex) {
418 this.feature = vertex;
420 this.layer.events.triggerEvent("vertexmodified", {
421 vertex: vertex.geometry,
422 feature: this.feature,
427 // dragging a virtual vertex
428 vertex.geometry.parent.addComponent(vertex.geometry,
430 // move from virtual to real vertex
431 delete vertex._index;
432 OpenLayers.Util.removeItem(this.virtualVertices, vertex);
433 this.vertices.push(vertex);
434 } else if(vertex == this.dragHandle) {
435 // dragging a drag handle
436 this.layer.removeFeatures(this.vertices, {silent: true});
438 if(this.radiusHandle) {
439 this.layer.destroyFeatures([this.radiusHandle], {silent: true});
440 this.radiusHandle = null;
442 } else if(vertex !== this.radiusHandle) {
443 // dragging a real vertex
444 this.layer.events.triggerEvent("vertexmodified", {
445 vertex: vertex.geometry,
446 feature: this.feature,
450 // dragging a radius handle - no special treatment
451 if(this.virtualVertices.length > 0) {
452 this.layer.destroyFeatures(this.virtualVertices, {silent: true});
453 this.virtualVertices = [];
455 this.layer.drawFeature(this.feature, this.selectControl.renderIntent);
457 // keep the vertex on top so it gets the mouseout after dragging
458 // this should be removed in favor of an option to draw under or
459 // maintain node z-index
460 this.layer.drawFeature(vertex);
464 * Method: dragComplete
465 * Called by the drag feature control when the feature dragging is complete.
468 * vertex - {<OpenLayers.Feature.Vector>} The vertex being dragged.
470 dragComplete: function(vertex) {
471 this.resetVertices();
472 this.setFeatureState();
473 this.onModification(this.feature);
474 this.layer.events.triggerEvent("featuremodified",
475 {feature: this.feature});
479 * Method: setFeatureState
480 * Called when the feature is modified. If the current state is not
481 * INSERT or DELETE, the state is set to UPDATE.
483 setFeatureState: function() {
484 if(this.feature.state != OpenLayers.State.INSERT &&
485 this.feature.state != OpenLayers.State.DELETE) {
486 this.feature.state = OpenLayers.State.UPDATE;
491 * Method: resetVertices
493 resetVertices: function() {
494 // if coming from a drag complete we're about to destroy the vertex
495 // that was just dragged. For that reason, the drag feature control
496 // will never detect a mouse-out on that vertex, meaning that the drag
497 // handler won't be deactivated. This can cause errors because the drag
498 // feature control still has a feature to drag but that feature is
499 // destroyed. To prevent this, we call outFeature on the drag feature
500 // control if the control actually has a feature to drag.
501 if(this.dragControl.feature) {
502 this.dragControl.outFeature(this.dragControl.feature);
504 if(this.vertices.length > 0) {
505 this.layer.removeFeatures(this.vertices, {silent: true});
508 if(this.virtualVertices.length > 0) {
509 this.layer.removeFeatures(this.virtualVertices, {silent: true});
510 this.virtualVertices = [];
512 if(this.dragHandle) {
513 this.layer.destroyFeatures([this.dragHandle], {silent: true});
514 this.dragHandle = null;
516 if(this.radiusHandle) {
517 this.layer.destroyFeatures([this.radiusHandle], {silent: true});
518 this.radiusHandle = null;
521 this.feature.geometry.CLASS_NAME != "OpenLayers.Geometry.Point") {
522 if((this.mode & OpenLayers.Control.ModifyFeature.DRAG)) {
523 this.collectDragHandle();
525 if((this.mode & (OpenLayers.Control.ModifyFeature.ROTATE |
526 OpenLayers.Control.ModifyFeature.RESIZE))) {
527 this.collectRadiusHandle();
529 if(this.mode & OpenLayers.Control.ModifyFeature.RESHAPE){
530 // Don't collect vertices when we're resizing
531 if (!(this.mode & OpenLayers.Control.ModifyFeature.RESIZE)){
532 this.collectVertices();
539 * Method: handleKeypress
540 * Called by the feature handler on keypress. This is used to delete
541 * vertices. If the <deleteCode> property is set, vertices will
542 * be deleted when a feature is selected for modification and
543 * the mouse is over a vertex.
546 * {Integer} Key code corresponding to the keypress event.
548 handleKeypress: function(evt) {
549 var code = evt.keyCode;
551 // check for delete key
553 OpenLayers.Util.indexOf(this.deleteCodes, code) != -1) {
554 var vertex = this.dragControl.feature;
556 OpenLayers.Util.indexOf(this.vertices, vertex) != -1 &&
557 !this.dragControl.handlers.drag.dragging &&
558 vertex.geometry.parent) {
560 vertex.geometry.parent.removeComponent(vertex.geometry);
561 this.layer.drawFeature(this.feature,
562 this.selectControl.renderIntent);
563 this.resetVertices();
564 this.setFeatureState();
565 this.onModification(this.feature);
566 this.layer.events.triggerEvent("featuremodified",
567 {feature: this.feature});
573 * Method: collectVertices
574 * Collect the vertices from the modifiable feature's geometry and push
575 * them on to the control's vertices array.
577 collectVertices: function() {
579 this.virtualVertices = [];
581 function collectComponentVertices(geometry) {
582 var i, vertex, component, len;
583 if(geometry.CLASS_NAME == "OpenLayers.Geometry.Point") {
584 vertex = new OpenLayers.Feature.Vector(geometry);
585 vertex._sketch = true;
586 control.vertices.push(vertex);
588 var numVert = geometry.components.length;
589 if(geometry.CLASS_NAME == "OpenLayers.Geometry.LinearRing") {
592 for(i=0; i<numVert; ++i) {
593 component = geometry.components[i];
594 if(component.CLASS_NAME == "OpenLayers.Geometry.Point") {
595 vertex = new OpenLayers.Feature.Vector(component);
596 vertex._sketch = true;
597 control.vertices.push(vertex);
599 collectComponentVertices(component);
603 // add virtual vertices in the middle of each edge
604 if(geometry.CLASS_NAME != "OpenLayers.Geometry.MultiPoint") {
605 for(i=0, len=geometry.components.length; i<len-1; ++i) {
606 var prevVertex = geometry.components[i];
607 var nextVertex = geometry.components[i + 1];
608 if(prevVertex.CLASS_NAME == "OpenLayers.Geometry.Point" &&
609 nextVertex.CLASS_NAME == "OpenLayers.Geometry.Point") {
610 var x = (prevVertex.x + nextVertex.x) / 2;
611 var y = (prevVertex.y + nextVertex.y) / 2;
612 var point = new OpenLayers.Feature.Vector(
613 new OpenLayers.Geometry.Point(x, y),
614 null, control.virtualStyle
616 // set the virtual parent and intended index
617 point.geometry.parent = geometry;
618 point._index = i + 1;
619 point._sketch = true;
620 control.virtualVertices.push(point);
626 collectComponentVertices.call(this, this.feature.geometry);
627 this.layer.addFeatures(this.virtualVertices, {silent: true});
628 this.layer.addFeatures(this.vertices, {silent: true});
632 * Method: collectDragHandle
633 * Collect the drag handle for the selected geometry.
635 collectDragHandle: function() {
636 var geometry = this.feature.geometry;
637 var center = geometry.getBounds().getCenterLonLat();
638 var originGeometry = new OpenLayers.Geometry.Point(
639 center.lon, center.lat
641 var origin = new OpenLayers.Feature.Vector(originGeometry);
642 originGeometry.move = function(x, y) {
643 OpenLayers.Geometry.Point.prototype.move.call(this, x, y);
646 origin._sketch = true;
647 this.dragHandle = origin;
648 this.layer.addFeatures([this.dragHandle], {silent: true});
652 * Method: collectRadiusHandle
653 * Collect the radius handle for the selected geometry.
655 collectRadiusHandle: function() {
656 var geometry = this.feature.geometry;
657 var bounds = geometry.getBounds();
658 var center = bounds.getCenterLonLat();
659 var originGeometry = new OpenLayers.Geometry.Point(
660 center.lon, center.lat
662 var radiusGeometry = new OpenLayers.Geometry.Point(
663 bounds.right, bounds.bottom
665 var radius = new OpenLayers.Feature.Vector(radiusGeometry);
666 var resize = (this.mode & OpenLayers.Control.ModifyFeature.RESIZE);
667 var reshape = (this.mode & OpenLayers.Control.ModifyFeature.RESHAPE);
668 var rotate = (this.mode & OpenLayers.Control.ModifyFeature.ROTATE);
670 radiusGeometry.move = function(x, y) {
671 OpenLayers.Geometry.Point.prototype.move.call(this, x, y);
672 var dx1 = this.x - originGeometry.x;
673 var dy1 = this.y - originGeometry.y;
677 var a0 = Math.atan2(dy0, dx0);
678 var a1 = Math.atan2(dy1, dx1);
680 angle *= 180 / Math.PI;
681 geometry.rotate(angle, originGeometry);
685 // 'resize' together with 'reshape' implies that the aspect
686 // ratio of the geometry will not be preserved whilst resizing
689 ratio = (dx1 / dx0) / scale;
691 var l0 = Math.sqrt((dx0 * dx0) + (dy0 * dy0));
692 var l1 = Math.sqrt((dx1 * dx1) + (dy1 * dy1));
695 geometry.resize(scale, originGeometry, ratio);
698 radius._sketch = true;
699 this.radiusHandle = radius;
700 this.layer.addFeatures([this.radiusHandle], {silent: true});
705 * Set the map property for the control and all handlers.
708 * map - {<OpenLayers.Map>} The control's map.
710 setMap: function(map) {
711 this.selectControl.setMap(map);
712 this.dragControl.setMap(map);
713 OpenLayers.Control.prototype.setMap.apply(this, arguments);
716 CLASS_NAME: "OpenLayers.Control.ModifyFeature"
721 * {Integer} Constant used to make the control work in reshape mode
723 OpenLayers.Control.ModifyFeature.RESHAPE = 1;
726 * {Integer} Constant used to make the control work in resize mode
728 OpenLayers.Control.ModifyFeature.RESIZE = 2;
731 * {Integer} Constant used to make the control work in rotate mode
733 OpenLayers.Control.ModifyFeature.ROTATE = 4;
736 * {Integer} Constant used to make the control work in drag mode
738 OpenLayers.Control.ModifyFeature.DRAG = 8;