﻿/**
 * Class: OpenLayers.Control.ModifyFeatureEnhanced
 *  - Implements possibility to delete holes in polygons.
 *      Also the validity of polygons modified is checked, if polygon is not valid the last drag is cancelled.
 *  - Implements snapping functionality (see also Snapping.js)
 *
 * Inherits from:
 *  - <OpenLayers.Control.ModifyFeature>
 */
OpenLayers.Control.ModifyFeatureEnhanced = OpenLayers.Class(OpenLayers.Control.ModifyFeature, {
    /**
     * Constant: EVENT_TYPES
     *
     * Supported event types:
     *  - *beforecancel* Triggered before modifications are cancelled. To avoid cancellation, set the eventargs cancel property to true
     *  - *aftercancel* Triggered after modifications are cancelled.
     *  - *geometryvalidated* Triggered after geometry validation was performed. eventarg contains a valid property that contains the result of the validation
     */
    EVENT_TYPES: ["beforecancel", "aftercancel", "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.dragControl.activateSnapping(options);
    },

    /**
     * APIMethod: deactivateSnapping
     * activateSnapping
     *
     * Returns:
     * {Boolean} False if snapping was already deactivated, true otherwise.
     */
    deactivateSnapping: function() {
        return this.dragControl.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.ModifyFeature.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())
        }
    },

    /**
     * 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: geometryIsValid
     * {Boolean} Geometry drawn is valid according to OpenLayers.Geometry.validateGeometry. Null if no validation has been performed.
     */
    geometryIsValid: null,

    /**
     * APIProperty: cancelCodes
     * {Array(Integer)} Keycodes for cancelling modifications. Set to null to disable
     *     cancellation by keypress.  If non-null, keypresses with codes
     *     in this array will undo any modification made. Default
     *     is 27, the 'esc' key.
     */
    cancelCodes: null,
    
    /**
     * Constructor: OpenLayers.Control.ModifyFeatureEnhanced
     * Initializes the cancelCodes
     * Calls base constructor of OpenLayers.Control.ModifyFeature
     *
     * 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) {
        // set the default key codes for cancelling modifications
        this.cancelCodes = [27];            
        
        OpenLayers.Control.ModifyFeature.prototype.initialize.apply(this, arguments);
        
        // 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.ModifyFeatureEnhanced.prototype.EVENT_TYPES.concat(
                OpenLayers.Control.prototype.EVENT_TYPES,
                (OpenLayers.Control.ModifyFeature.prototype.EVENT_TYPES != OpenLayers.Control.prototype.EVENT_TYPES) ? OpenLayers.Control.ModifyFeature.prototype.EVENT_TYPES : []
            );
            
        // add all event types to the events object
        for (var i = 0; i < OpenLayers.Control.ModifyFeatureEnhanced.prototype.EVENT_TYPES.length; i++) {
            this.events.addEventType(OpenLayers.Control.ModifyFeatureEnhanced.prototype.EVENT_TYPES[i]);
        }
        
        // enable snapping on the drag control if applicable
        if (this.enableSnapping) {
            this.dragControl.activateSnapping(this.snappingOptions);
        }
        
        // we need to reset vertices when features are added to the layer (since all features are redrawn)
        this.layer.events.on({
            "featuresadded": this.featuresAdded,
            scope: this
        });
    },
    
    /**
     * Method: featuresAdded
     * Called when features are added to the layer.
     * When features are added to the layer all features are redrawn, that means that we need to reset vertices.
     *
     * Parameters:
     * object - {Object} Object with a feature property referencing the
     *     selected features.
     */
    featuresAdded: function(object) {
        //only reset vertices if the control is active otherwise an error occurs
        if (this.active) {
            try {
            this.resetVertices();
            } catch (ex) {}
        }
    },
    
    /**
     * 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
        });
        
        this.layer.events.un({
            "featuresadded": this.featuresAdded,
            scope: this
        });
        
        this.unmodifiedgeometry = null;
        this.undoinfo = null;
        OpenLayers.Control.ModifyFeature.prototype.destroy.apply(this, []);
    },
    
    /**
     * Method: selectFeature
     * Gets a clone of the unmodified geometry that is used upon cancellation.
     * Calls the base method on OpenLayers.Control.ModifyFeature
     *
     * Parameters:
     * feature - {Object} Object with a feature property referencing the
     *     selected feature.
     */
    selectFeature: function(feature) {
        this.unmodifiedgeometry = feature.geometry.clone();
        OpenLayers.Control.ModifyFeature.prototype.selectFeature.apply(this, arguments);
    },

    /**
     * Method: dragComplete
     * Performs geometry validation if required and cancels the drag
     * if valid geometry should be enforced and the feature modified is not valid.
     * Calls the base method on OpenLayers.Control.ModifyFeature
     *
     * Parameters:
     * vertex - {<OpenLayers.Feature.Vector>} The vertex being dragged. Is not always passed for some reason.
     *  that is why we use this.undoinfo.vertex instead
     */
    dragComplete: function(vertex) {
        //Do geometry validation if required
        this.geometryValidation(this.undoinfo.vertex.geometry, this.feature.geometry);
        
        //If valid geometry should be enforced and the geometry of the current feature is not valid, cancel the drag
        if (this.enforceValidGeometry && this.geometryIsValid == false) {
                //Cancel is done by adding the vertex at the original position and removing it at the new position
                
                //If the vertex is not virtual add it at the original position
                if (!this.undoinfo.virtual) {
                    //Check that undo info was collected before trying to insert point
                    if (this.undoinfo.vertex != null && this.undoinfo.point != null) {
                        //Get original position of the vertex
                        var undoindex = OpenLayers.Util.indexOf(this.undoinfo.vertex.geometry.parent.components, this.undoinfo.vertex.geometry);
                        
                        //Insert point at this position
                        this.undoinfo.vertex.geometry.parent.addComponent(this.undoinfo.point, undoindex);
                    }
                }
                
                //Remove the vertex from the new position
                this.undoinfo.vertex.geometry.parent.removeComponent(this.undoinfo.vertex.geometry);
                
                //Redraw the feature to reflect changes
                this.layer.drawFeature(this.feature, this.selectControl.renderIntent);
        }
        
        OpenLayers.Control.ModifyFeature.prototype.dragComplete.apply(this, arguments);
    },

    /**
     * Method: dragStart
     * Collects information about the state of the vertex before drag.
     * This is used in dragComplete to cancel drag if necesary.
     * Calls the base method on OpenLayers.Control.ModifyFeature
     *
     * 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) {
        this.undoinfo = {vertex: feature, point: feature.geometry.clone(), virtual: typeof(feature._index) != "undefined"};
        
        OpenLayers.Control.ModifyFeature.prototype.dragStart.apply(this, arguments);
    },
    
    /**
     * APIMethod: cancelModification
     * Cancels all modification of the feature, unless the beforecancel event handler
     * sets the cancel property of the eventarg to true
     *
     * Returns:
     * {Boolean} Modification was cancelled (can be false if beforecancel cancelled the event).
     */
    cancelModification: function() {
        //Bail out if no feature is selected
        if (this.feature == null) {
            return true;
        }
    
        // Trigger beforecancel event. Event arguments are:
        //  cancel - determines if the event should be cancelled (can be set in handler)
        //  geometrychanged - determines if any modifications have actually been made
        var e = {cancel: false, geometrychanged: this.feature.geometry.toString() != this.unmodifiedgeometry.toString()};
        this.events.triggerEvent("beforecancel", e);
        
        //If the beforecancel event handler did not cancel the event, cancel any modifications
        if (!e.cancel) {
            //Erase the modified feature
            this.layer.eraseFeatures([this.feature]);
            //Draw the unmodified feature
            this.feature.geometry = this.unmodifiedgeometry;
            this.layer.drawFeature(this.feature);
            //Deactivate and activate to remove vertices
            this.deactivate();
            this.activate();
            
            //Trigger aftercancel event.
            this.events.triggerEvent("aftercancel");
        }
        
        //Return boolean that indicates whether or not the modification was cancelled
        return !e.cancel;
    },

    /**
     * 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.
     *     Also does geometry validation and enforcement if required.
     *
     * Parameters:
     * {Integer} Key code corresponding to the keypress event.
     */
    handleKeypress: function(evt) {
        var code = evt.keyCode;
        
        // check if the key pressed is a cancelkey
        if (this.feature && OpenLayers.Util.indexOf(this.cancelCodes, code) != -1) {
            this.cancelModification();
        }
        
        // 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) {
               
                var undoindex = -1;
                var undogeometry = null;
                var undoparent = null;
                
                //If the vertex resides in a hole and theres is only 3 points left in the hole
                if (vertex.geometry.parent.parent && vertex.geometry.parent.parent.components[0] != vertex.geometry.parent && vertex.geometry.parent.components.length == 4) {
                    //Collect undo information to be used if resulting geometry is not valid
                    undoindex = OpenLayers.Util.indexOf(vertex.geometry.parent.parent.components, vertex.geometry.parent);
                    undogeometry = vertex.geometry.parent.clone();
                    undoparent = vertex.geometry.parent.parent;
                
                    // delete the entire hole
                    vertex.geometry.parent.parent.removeComponent(vertex.geometry.parent);
                } else {
                    //Collect undo information to be used if resulting geometry is not valid
                    undoindex = OpenLayers.Util.indexOf(vertex.geometry.parent.components, vertex.geometry);
                    undogeometry = vertex.geometry.clone()
                    undoparent = vertex.geometry.parent;
                
                    // remove the vertex
                    vertex.geometry.parent.removeComponent(vertex.geometry);
                }
                
                //Do geometry validation is required
                this.geometryValidation(vertex.geometry, this.feature.geometry);
                
                //If valid geometry should be enforced and the geometry of the current feature is not valid, undo deletion
                if (this.enforceValidGeometry && this.geometryIsValid == false) {
                    //Check if undo information was collected
                    if (undoindex > -1 && undogeometry != null && undoparent != null) {
                        undoparent.addComponent(undogeometry, undoindex);
                    }
                }               
                
                this.layer.drawFeature(this.feature,
                                       this.selectControl.renderIntent);
                this.resetVertices();
                this.onModification(this.feature);
                this.layer.events.triggerEvent("featuremodified", 
                                               {feature: this.feature});
            }
        }
    },
    
    /**
     * Method: geometryValidation
     * Performs validation of geometry if required
     *
     * point {OpenLayers.Geometry.Point} - last point dragged
     * 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;
        }
    },

    /**
    * APIMethod: activate
    * Activate the control.
    * 
    * Returns:
    * {Boolean} Successfully activated the control.
    */
    activate: function() {
        var result = OpenLayers.Control.ModifyFeature.prototype.activate.apply(this, arguments);

        if (result && this.layer.features.length == 1) {
            var feature = this.layer.features[0];
            this.selectControl.clickFeature(feature);
            this.selectControl.handlers.feature.feature = feature;
            this.selectControl.handlers.feature.lastFeature = feature;
        }

        return result;
    },

    /**
    * Method: collectVertices
    * Collect the vertices from the modifiable feature's geometry and push
    *     them on to the control's vertices array.
    */
    collectVertices: function() {
        OpenLayers.Control.ModifyFeature.prototype.collectVertices.apply(this, arguments);
        var geometry = this.feature.geometry;
        if (geometry.CLASS_NAME == "OpenLayers.Geometry.LineString") {
            if (geometry.components.length == 2) {
                this.layer.removeFeatures(this.virtualVertices, { silent: true });
            }
        }
    },
    
    CLASS_NAME: "OpenLayers.Control.ModifyFeatureEnhanced"
});
