﻿/**
* Class: OpenLayers.Control.DrawFeatureEnhanced
*  - Implements undoable vertices when drawing lines or polygons.
*  - Implements snapping functionality (see also Snapping.js)
*  - Implements measurement functionality similar to OpenLayers.Control.Measure
*
* Inherits from:
*  - <OpenLayers.Control.DrawFeature>
*/
OpenLayers.Control.DrawFeatureEnhanced = OpenLayers.Class(OpenLayers.Control.Measure, OpenLayers.Control.DrawFeature, {
    /**
    * Constant: EVENT_TYPES
    *
    * Supported event types:
    *  - *geometryvalidated* Triggered after geometry validation was performed
    *      Event arguments are:
    *          - valid (contains the result of the validation)
    *          - point (the last point drawn)
    *          - geometry (the geometry validated)
    */
    EVENT_TYPES: ["geometryvalidated"],

    /**
    * APIProperty: useGlobalSnapping
    * {Boolean} Determines if the global snapping options of the assosiated map object is to be used.
    */
    useGlobalSnapping: false,

    /**
    * APIProperty: enableSnapping
    * {Boolean} Enable snapping.
    */
    enableSnapping: false,

    /**
    * APIProperty: snappingOptions
    * {Object} Any optional properties to be set on the snapping object.
    */
    snappingOptions: null,

    /**
    * APIMethod: activateSnapping
    * activateSnapping
    *
    * Parameters:
    * options - {Object} An optional object with properties to be set on
    *     the <OpenLayers.Snapping> object.
    *
    * Returns:
    * {Boolean} False if snapping was already activated, true otherwise.
    */
    activateSnapping: function(options) {
        return this.handler.activateSnapping(options);
    },

    /**
    * APIMethod: deactivateSnapping
    * activateSnapping
    *
    * Returns:
    * {Boolean} False if snapping was already deactivated, true otherwise.
    */
    deactivateSnapping: function() {
        return this.handler.deactivateSnapping();
    },

    /**
    * Method: setMap
    * Set the map property for the control and all handlers.
    *
    * Parameters:
    * map - {<OpenLayers.Map>} The control's map.
    */
    setMap: function(map) {
        OpenLayers.Control.DrawFeature.prototype.setMap.apply(this, arguments);

        if (this.map && this.useGlobalSnapping) {
            this.map.events.on({
                "snappingenabledchanged": this.onGlobalSnappingEnabledChanged,
                "snappingoptionschanged": this.onGlobalSnappingOptionsChanged,
                "scope": this
            });

            //Running the event handler will enable snapping if applicable
            this.onGlobalSnappingEnabledChanged();
        }
    },

    /**
    * Function: Event handler for changing snapping when global configuration (of the assosiated map object) changes
    */
    onGlobalSnappingEnabledChanged: function() {
        if (this.map.getSnappingEnabled()) {
            this.activateSnapping(this.map.getSnappingOptions());
        } else {
            this.deactivateSnapping();
        }
    },

    /**
    * Function: Event handler for changing snapping when global configuration (of the assosiated map object) changes
    */
    onGlobalSnappingOptionsChanged: function() {
        if (this.map.getSnappingEnabled()) {
            this.deactivateSnapping();
            this.activateSnapping(this.map.getSnappingOptions())
        }
    },

    /**
    * APIMethod: destroy
    * Take care of things that are not handled in superclass.
    */
    destroy: function() {
        this.map.events.un({
            "snappingenabledchanged": this.onGlobalSnappingEnabledChanged,
            "snappingoptionschanged": this.onGlobalSnappingOptionsChanged,
            "scope": this
        });

        OpenLayers.Control.DrawFeature.prototype.destroy.apply(this, []);
    },

    /**
    * APIProperty: performGeometryValidation
    * {Boolean} Perform geometry validation. Default is true.
    */
    performGeometryValidation: true,

    /**
    * APIProperty: enforceValidGeometry
    * {Boolean} Prevent point to be drawn if it results in invalid geometry. Default is true.
    *
    * Remarks: Is ignored if performGeometryValidation is false
    */
    enforceValidGeometry: true,

    /**
    * APIProperty: geometryValidationMode
    * {String} Determines when geometries are validated.
    * Valid values are: continuous or final. Default is final
    */
    geometryValidationMode: "final",

    /**
    * APIProperty: geometryIsValid
    * {Boolean} Geometry drawn is valid according to OpenLayers.Geometry.validateGeometry. Null if no validation has been performed.
    */
    geometryIsValid: null,

    /**
    * APIProperty: deleteCodes
    * {Array(Integer)} Keycodes for undoing vertices.  Set to null to disable
    *     vertex undoing by keypress.  If non-null, keypresses with codes
    *     in this array will delete the last vertex drawn. Default
    *     is 46 and 68, the 'delete' and lowercase 'd' keys.
    */
    deleteCodes: null,

    /**
    * APIProperty: cancelCodes
    * {Array(Integer)} Keycodes for cancelling drawing. Set to null to disable
    *     cancellation by keypress.  If non-null, keypresses with codes
    *     in this array will undo drawing. Default is 27, the 'esc' key.
    */
    cancelCodes: null,

    /**
    * Constructor: OpenLayers.Control.DrawFeatureEnhanced
    * 
    * Parameters:
    * layer - {<OpenLayers.Layer.Vector>} 
    * handler - {<OpenLayers.Handler>} 
    * options - {Object} 
    */
    initialize: function(layer, handler, options) {
        this.deleteCodes = [46, 68];
        this.cancelCodes = [27];
        this.keyboardHandler = new OpenLayers.Handler.Keyboard(this, { keydown: this.handleKeypress });

        //Set up callback for point added, this is used for geometry validation and partial measurement
        options = (options && options != null) ? options : {};
        options.callbacks = (options.callbacks && options.callbacks != null) ? options.callbacks : {};
        this._originalPointCallback = (options.callbacks.point) ? options.callbacks.point : function() { };
        options.callbacks.point = function(point, geometry) {
            this._originalPointCallback(point, geometry);
            this.onAddPoint(point, geometry);
            this.measurePartial(point, geometry);
        };

        OpenLayers.Control.DrawFeature.prototype.initialize.apply(this, [layer, handler, options]);

        //Set up callback for done, this is used for measurement
        this._originalDoneCallback = (this.callbacks.done) ? this.callbacks.done : function() { };
        this.callbacks.done = function(geometry) {
            this._originalDoneCallback(geometry);
            this.measureComplete(geometry);
        };

        // need to make sure that all event types are registered properly
        // concatenate all events used by this class and its ancestors
        this.EVENT_TYPES =
            OpenLayers.Control.DrawFeatureEnhanced.prototype.EVENT_TYPES.concat(
                OpenLayers.Control.prototype.EVENT_TYPES,
                (OpenLayers.Control.DrawFeature.prototype.EVENT_TYPES != OpenLayers.Control.prototype.EVENT_TYPES) ? OpenLayers.Control.DrawFeature.prototype.EVENT_TYPES : [],
                OpenLayers.Control.Measure.prototype.EVENT_TYPES
            );

        // add all event types to the events object
        for (var i = 0; i < this.EVENT_TYPES.length; i++) {
            this.events.addEventType(this.EVENT_TYPES[i]);
        }

        // enable snapping on the handler if applicable
        if (this.enableSnapping) {
            this.handler.activateSnapping(this.snappingOptions);
        }

        // enable valid geometry enforcement on dblclick
        this._originalDblClick = OpenLayers.Function.bind(this.handler.dblclick, this.handler);
        this.handler.dblclick = OpenLayers.Function.bind(
            function(evt) {
                if (this.onBeforeDblClick(this.handler.point.geometry, this.handler.getGeometry())) {
                    return this._originalDblClick(evt);
                } else {
                    OpenLayers.Event.stop(evt);
                    return false;
                }
            },
            this
        );
    },

    /**
    * Method: onAddPoint
    * Callback for the addPoint of the handler. If performGeometryValidation is true the method is used to validate the geometry during drawing.
    * If geometry is invalid and enforceValidGeometry is true, the last drawn vertex is removed.
    *
    * Parameters:
    * point - {OpenLayers.Geometry.point} point to send to the geometryvalidated event handlers
    * geometry - {OpenLayer.Geometry} geometry of currently drawn feature
    */
    onAddPoint: function(point, geometry) {
        if (this.geometryValidationMode == "continuous") {
            //Do geometry validation if required
            this.geometryValidation(point, geometry);

            //If valid geometry should be enforced and the geometry of the current feature is not valid, undo the last vertex drawn
            if (this.enforceValidGeometry && this.geometryIsValid == false) {
                this.undoLastVertex();
            }
        }
    },

    /**
    * Function: onBeforeDblClick
    * Called before the original dblclick event handler.
    * This way we are able to validate the geometry of the feature before it is added
    * and if enforcement of valid geometries is enabled we can cancel if the geometry 
    * is invalid by returning false.
    *
    * Parameters:
    * point - {OpenLayers.Geometry.point} point to send to the geometryvalidated event handlers
    * geometry - {OpenLayer.Geometry} geometry of currently drawn feature
    */
    onBeforeDblClick: function(point, geometry) {
        if (this.geometryValidationMode == "final") {
            //Do geometry validation if required
            this.geometryValidation(point, geometry);

            //If valid geometry should be enforced and the geometry of the current feature is not valid, return false
            if (this.enforceValidGeometry && this.geometryIsValid == false) {
                return false;
            }
        }

        //Feature can be added either because validation was not required or the geometry was validated and found valid
        return true;
    },

    /**
    * Method: handleKeypress
    * Event handler for keypress events.  This is used to undo vertices.
    * If the <deleteCodes> property is set, the last vertext drawn will be deleted.
    *
    * Parameters:
    * {Object} With keyCode property corresponding to the keypress event.
    */
    handleKeypress: function(evt) {
        var code = evt.keyCode;

        //Check if the key pressed is a cancelkey
        if (OpenLayers.Util.indexOf(this.cancelCodes, code) != -1) {
            //deactivate and activate does the trick
            this.deactivate();
            this.activate();
            return;
        }

        //Check if the key pressed is a delete key
        if (OpenLayers.Util.indexOf(this.deleteCodes, code) != -1) {
            this.undoLastVertex();
        }
    },

    /**
    * Method: undoLastVertex
    * Removes the last drawn vertex
    */
    undoLastVertex: function() {
        var lastVertex; //The last vertex drawn
        var curveToRemoveFrom; //The OpenLayers.Geometry.Curve to remove the vertex from

        if (this.handler.polygon) {
            curveToRemoveFrom = this.handler.getGeometry().components[0];

            if (curveToRemoveFrom) {
                //Last point clicked is the third last point in the components array.
                //The second last point is the current mouse position and the last one is the same as the first to close the polygon 
                lastVertex = curveToRemoveFrom.components[curveToRemoveFrom.components.length - 3];
            }
        } else if (this.handler.line) {
            curveToRemoveFrom = this.handler.getGeometry();

            if (curveToRemoveFrom) {
                lastVertex = curveToRemoveFrom.components[curveToRemoveFrom.components.length - 1];
            }
        }

        if (lastVertex && curveToRemoveFrom) {
            curveToRemoveFrom.removeComponent(lastVertex);

            //The easiest way to redraw correctly is to call the mousemove event handler,
            //for this we need the current mouse position in pixels,
            var mousepos = this.map.getPixelFromLonLat(
                new OpenLayers.LonLat(this.handler.point.geometry.x,
                    this.handler.point.geometry.y));

            this.handler.mousemove({ xy: mousepos });
        }
    },

    /**
    * APIMethod: activate
    * Activate the control.
    * 
    * Returns:
    * {Boolean} Successfully activated the control.
    */
    activate: function() {
        //When activating the control we also need to activate the keyboard handler
        return (this.keyboardHandler.activate() &&
            OpenLayers.Control.DrawFeature.prototype.activate.apply(this, arguments));
    },

    /**
    * APIMethod: deactivate
    * Deactivate the control.
    * 
    * Returns:
    * {Boolean} Successfully deactivated the control.
    */
    deactivate: function() {
        if (!this.active) {
            return false;
        }

        //OpenLayers 2.7 has a bug in the DrawFeature control,
        //that results in the vertices and virtual vertices of the ModifyFeature control not being shown
        //when the DrawFeature control has been deactivated a few times.
        //The problem is that the div element holding the SVG node is removed from the document when the handler is deactivated.
        //The workaround until we upgrade to 2.8 is to save a reference to the div and append it again after the control has been deactivated.
        //In order not to leave all the old div elements hanging around we will remove the previously used div.
        var handlerLayerDiv = this.handler.layer.div;
        var handlerLayerDivParent = handlerLayerDiv.parentNode;
        if (this.previousHandlerLayerDiv) {
            this.previousHandlerLayerDiv.parentNode.removeChild(this.previousHandlerLayerDiv);
        }
        this.previousHandlerLayerDiv = handlerLayerDiv;

        //When deactivating the control we also need to deactivate the keyboard handler
        var success = (this.keyboardHandler.deactivate() &&
            OpenLayers.Control.DrawFeature.prototype.deactivate.apply(this, arguments));

        handlerLayerDivParent.appendChild(handlerLayerDiv);

        return success;
    },

    /**
    * Method: geometryValidation
    * Performs validation of geometry if required
    *
    * point {OpenLayers.Geometry.Point}
    * geometry {OpenLayers.Geometry.*}
    */
    geometryValidation: function(point, geometry) {
        if (this.performGeometryValidation) {
            this.geometryIsValid = OpenLayers.Geometry.validateGeometry(geometry);
            this.events.triggerEvent("geometryvalidated", { valid: this.geometryIsValid, point: point, geometry: geometry });
        } else {
            this.geometryIsValid = null;
        }
    },

    CLASS_NAME: "OpenLayers.Control.DrawFeatureEnhanced"
});