More trip planner testing with colors
[busui.git] / labs / openlayers / lib / OpenLayers / Control / Split.js
blob:a/labs/openlayers/lib/OpenLayers/Control/Split.js -> blob:b/labs/openlayers/lib/OpenLayers/Control/Split.js
--- a/labs/openlayers/lib/OpenLayers/Control/Split.js
+++ b/labs/openlayers/lib/OpenLayers/Control/Split.js
@@ -1,1 +1,499 @@
-
+/* 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.js
+ * @requires OpenLayers/Handler/Path.js
+ * @requires OpenLayers/Layer/Vector.js
+ */
+
+/**
+ * Class: OpenLayers.Control.Split
+ * Acts as a split feature agent while editing vector features.
+ *
+ * Inherits from:
+ *  - <OpenLayers.Control>
+ */
+OpenLayers.Control.Split = OpenLayers.Class(OpenLayers.Control, {
+
+    /**
+     * Constant: EVENT_TYPES
+     * {Array(String)} Supported application event types.  Register a listener
+     *     for a particular event with the following syntax:
+     * (code)
+     * control.events.register(type, obj, listener);
+     * (end)
+     *
+     * Listeners will be called with a reference to an event object.  The
+     *     properties of this event depends on exactly what happened.
+     *
+     * Supported control event types (in addition to those from <OpenLayers.Control>):
+     * beforesplit - Triggered before a split occurs.  Listeners receive an
+     *     event object with *source* and *target* properties.
+     * split - Triggered when a split occurs.  Listeners receive an event with
+     *     an *original* property and a *features* property.  The original
+     *     is a reference to the target feature that the sketch or modified
+     *     feature intersects.  The features property is a list of all features
+     *     that result from this single split.  This event is triggered before
+     *     the resulting features are added to the layer (while the layer still
+     *     has a reference to the original).
+     * aftersplit - Triggered after all splits resulting from a single sketch
+     *     or feature modification have occurred.  The original features
+     *     have been destroyed and features that result from the split
+     *     have already been added to the layer.  Listeners receive an event
+     *     with a *source* and *features* property.  The source references the
+     *     sketch or modified feature used as a splitter.  The features
+     *     property is a list of all resulting features.
+     */
+    EVENT_TYPES: ["beforesplit", "split", "aftersplit"],
+    
+    /**
+     * APIProperty: layer
+     * {<OpenLayers.Layer.Vector>} The target layer with features to be split.
+     *     Set at construction or after construction with <setLayer>.
+     */
+    layer: null,
+    
+    /**
+     * Property: source
+     * {<OpenLayers.Layer.Vector>} Optional source layer.  Any newly created
+     *     or modified features from this layer will be used to split features
+     *     on the target layer.  If not provided, a temporary sketch layer will
+     *     be created.
+     */
+    source: null,
+    
+    /**
+     * Property: sourceOptions
+     * {Options} If a temporary sketch layer is created, these layer options
+     *     will be applied.
+     */
+    sourceOptions: null,
+
+    /**
+     * APIProperty: tolerance
+     * {Number} Distance between the calculated intersection and a vertex on
+     *     the source geometry below which the existing vertex will be used
+     *     for the split.  Default is null.
+     */
+    tolerance: null,
+    
+    /**
+     * APIProperty: edge
+     * {Boolean} Allow splits given intersection of edges only.  Default is
+     *     true.  If false, a vertex on the source must be within the
+     *     <tolerance> distance of the calculated intersection for a split
+     *     to occur.
+     */
+    edge: true,
+    
+    /**
+     * APIProperty: deferDelete
+     * {Boolean} Instead of removing features from the layer, set feature
+     *     states of split features to DELETE.  This assumes a save strategy
+     *     or other component is in charge of removing features from the
+     *     layer.  Default is false.  If false, split features will be
+     *     immediately deleted from the layer.
+     */
+    deferDelete: false,
+    
+    /**
+     * APIProperty: mutual
+     * {Boolean} If source and target layers are the same, split source
+     *     features and target features where they intersect.  Default is
+     *     true.  If false, only target features will be split.
+     */
+    mutual: true,
+    
+    /**
+     * APIProperty: targetFilter
+     * {OpenLayers.Filter} Optional filter that will be evaluated
+     *     to determine if a feature from the target layer is eligible for
+     *     splitting.
+     */
+    targetFilter: null,
+    
+    /**
+     * APIProperty: sourceFilter
+     * {OpenLayers.Filter} Optional filter that will be evaluated
+     *     to determine if a feature from the target layer is eligible for
+     *     splitting.
+     */
+    sourceFilter: null,
+    
+    /**
+     * Property: handler
+     * {<OpenLayers.Handler.Path>} The temporary sketch handler created if
+     *     no source layer is provided.
+     */
+    handler: null,
+
+    /**
+     * Constructor: OpenLayers.Control.Split
+     * Creates a new split control. A control is constructed with a target
+     *     layer and an optional source layer. While the control is active,
+     *     creating new features or modifying existing features on the source
+     *     layer will result in splitting any eligible features on the target
+     *     layer.  If no source layer is provided, a temporary sketch layer will
+     *     be created to create lines for splitting features on the target.
+     *
+     * Parameters:
+     * options - {Object} An object containing all configuration properties for
+     *     the control.
+     *
+     * Valid options:
+     * layer - {OpenLayers.Layer.Vector} The target layer.  Features from this
+     *     layer will be split by new or modified features on the source layer
+     *     or temporary sketch layer.
+     * source - {OpenLayers.Layer.Vector} Optional source layer.  If provided
+     *     newly created features or modified features will be used to split
+     *     features on the target layer.  If not provided, a temporary sketch
+     *     layer will be created for drawing lines.
+     * tolerance - {Number} Optional value for the distance between a source
+     *     vertex and the calculated intersection below which the split will
+     *     occur at the vertex.
+     * edge - {Boolean} Allow splits given intersection of edges only.  Default
+     *     is true.  If false, a vertex on the source must be within the
+     *     <tolerance> distance of the calculated intersection for a split
+     *     to occur.
+     * mutual - {Boolean} If source and target are the same, split source
+     *     features and target features where they intersect.  Default is
+     *     true.  If false, only target features will be split.
+     * targetFilter - {OpenLayers.Filter} Optional filter that will be evaluated
+     *     to determine if a feature from the target layer is eligible for
+     *     splitting.
+     * sourceFilter - {OpenLayers.Filter} Optional filter that will be evaluated
+     *     to determine if a feature from the target layer is eligible for
+     *     splitting.
+     */
+    initialize: function(options) {
+        // concatenate events specific to measure with those from the base
+        Array.prototype.push.apply(
+            this.EVENT_TYPES, OpenLayers.Control.prototype.EVENT_TYPES
+        );
+        OpenLayers.Control.prototype.initialize.apply(this, [options]);
+        this.options = options || {}; // TODO: this could be done by the super
+        
+        // set the source layer if provided
+        if(this.options.source) {
+            this.setSource(this.options.source);
+        }
+    },
+    
+    /**
+     * APIMethod: setSource
+     * Set the source layer for edits layer.
+     *
+     * Parameters:
+     * layer - {OpenLayers.Layer.Vector}  The new source layer layer.  If
+     *     null, a temporary sketch layer will be created.
+     */
+    setSource: function(layer) {
+        if(this.active) {
+            this.deactivate();
+            if(this.handler) {
+                this.handler.destroy();
+                delete this.handler;
+            }
+            this.source = layer;
+            this.activate();
+        } else {
+            this.source = layer;
+        }
+    },
+    
+    /**
+     * APIMethod: activate
+     * Activate the control.  Activating the control registers listeners for
+     *     editing related events so that during feature creation and
+     *     modification, features in the target will be considered for
+     *     splitting.
+     */
+    activate: function() {
+        var activated = OpenLayers.Control.prototype.activate.call(this);
+        if(activated) {
+            if(!this.source) {
+                if(!this.handler) {
+                    this.handler = new OpenLayers.Handler.Path(this,
+                        {done: function(geometry) {
+                            this.onSketchComplete({
+                                feature: new OpenLayers.Feature.Vector(geometry)
+                            });
+                        }},
+                        {layerOptions: this.sourceOptions}
+                    );
+                }
+                this.handler.activate();
+            } else if(this.source.events) {
+                this.source.events.on({
+                    sketchcomplete: this.onSketchComplete,
+                    afterfeaturemodified: this.afterFeatureModified,
+                    scope: this
+                });
+            }
+        }
+        return activated;
+    },
+    
+    /**
+     * APIMethod: deactivate
+     * Deactivate the control.  Deactivating the control unregisters listeners
+     *     so feature editing may proceed without engaging the split agent.
+     */
+    deactivate: function() {
+        var deactivated = OpenLayers.Control.prototype.deactivate.call(this);
+        if(deactivated) {
+            if(this.source && this.source.events) {
+                this.layer.events.un({
+                    sketchcomplete: this.onSketchComplete,
+                    afterfeaturemodified: this.afterFeatureModified,
+                    scope: this
+                });
+            }
+        }
+        return deactivated;
+    },
+    
+    /**
+     * Method: onSketchComplete
+     * Registered as a listener for the sketchcomplete event on the editable
+     *     layer.
+     *
+     * Parameters:
+     * event - {Object} The sketch complete event.
+     *
+     * Returns:
+     * {Boolean} Stop the sketch from being added to the layer (it has been
+     *     split).
+     */
+    onSketchComplete: function(event) {
+        this.feature = null;
+        return !this.considerSplit(event.feature);
+    },
+    
+    /**
+     * Method: afterFeatureModified
+     * Registered as a listener for the afterfeaturemodified event on the
+     *     editable layer.
+     *
+     * Parameters:
+     * event - {Object} The after feature modified event.
+     */
+    afterFeatureModified: function(event) {
+        if(event.modified) {
+            var feature = event.feature;
+            if(feature.geometry instanceof OpenLayers.Geometry.LineString ||
+               feature.geometry instanceof OpenLayers.Geometry.MultiLineString) {
+                this.feature = event.feature;
+                this.considerSplit(event.feature);
+            }
+        }
+    },
+    
+    /**
+     * Method: removeByGeometry
+     * Remove a feature from a list based on the given geometry.
+     *
+     * Parameters:
+     * features - {Array(<OpenLayers.Feature.Vector>} A list of features.
+     * geometry - {<OpenLayers.Geometry>} A geometry.
+     */
+    removeByGeometry: function(features, geometry) {
+        for(var i=0, len=features.length; i<len; ++i) {
+            if(features[i].geometry === geometry) {
+                features.splice(i, 1);
+                break;
+            }
+        }
+    },
+    
+    /**
+     * Method: isEligible
+     * Test if a target feature is eligible for splitting.
+     *
+     * Parameters:
+     * target - {<OpenLayers.Feature.Vector>} The target feature.
+     *
+     * Returns:
+     * {Boolean} The target is eligible for splitting.
+     */
+    isEligible: function(target) {
+        return (
+            target.state !== OpenLayers.State.DELETE
+        ) && (
+            target.geometry instanceof OpenLayers.Geometry.LineString ||
+            target.geometry instanceof OpenLayers.Geometry.MultiLineString
+        ) && (
+            this.feature !== target
+        ) && (
+            !this.targetFilter ||
+            this.targetFilter.evaluate(target.attributes)
+        );
+    },
+
+    /**
+     * Method: considerSplit
+     * Decide whether or not to split target features with the supplied
+     *     feature.  If <mutual> is true, both the source and target features
+     *     will be split if eligible.
+     *
+     * Parameters:
+     * feature - {<OpenLayers.Feature.Vector}} The newly created or modified
+     *     feature.
+     *
+     * Returns:
+     * {Boolean} The supplied feature was split (and destroyed).
+     */
+    considerSplit: function(feature) {
+        var sourceSplit = false;
+        var targetSplit = false;
+        if(!this.sourceFilter ||
+           this.sourceFilter.evaluate(feature.attributes)) {
+            var features = this.layer && this.layer.features || [];
+            var target, results, proceed;
+            var additions = [], removals = [];
+            var mutual = (this.layer === this.source) && this.mutual;
+            var options = {
+                edge: this.edge,
+                tolerance: this.tolerance,
+                mutual: mutual
+            };
+            var sourceParts = [feature.geometry];
+            var targetFeature, targetParts;
+            var source, parts;
+            for(var i=0, len=features.length; i<len; ++i) {
+                targetFeature = features[i];
+                if(this.isEligible(targetFeature)) {
+                    targetParts = [targetFeature.geometry];
+                    // work through source geoms - this array may change
+                    for(var j=0; j<sourceParts.length; ++j) { 
+                        source = sourceParts[j];
+                        // work through target parts - this array may change
+                        for(var k=0; k<targetParts.length; ++k) {
+                            target = targetParts[k];
+                            if(source.getBounds().intersectsBounds(target.getBounds())) {
+                                results = source.split(target, options);
+                                if(results) {
+                                    proceed = this.events.triggerEvent(
+                                        "beforesplit", {source: feature, target: targetFeature}
+                                    );
+                                    if(proceed !== false) {
+                                        if(mutual) {
+                                            parts = results[0];
+                                            // handle parts that result from source splitting
+                                            if(parts.length > 1) {
+                                                // splice in new source parts
+                                                parts.unshift(j, 1); // add args for splice below
+                                                Array.prototype.splice.apply(sourceParts, parts);
+                                                j += parts.length - 3;
+                                            }
+                                            results = results[1];
+                                        }
+                                        // handle parts that result from target splitting
+                                        if(results.length > 1) {
+                                            // splice in new target parts
+                                            results.unshift(k, 1); // add args for splice below
+                                            Array.prototype.splice.apply(targetParts, results);
+                                            k += results.length - 3;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    if(targetParts && targetParts.length > 1) {
+                        this.geomsToFeatures(targetFeature, targetParts);
+                        this.events.triggerEvent("split", {
+                            original: targetFeature,
+                            features: targetParts
+                        });
+                        Array.prototype.push.apply(additions, targetParts);
+                        removals.push(targetFeature);
+                        targetSplit = true;
+                    }
+                }
+            }
+            if(sourceParts && sourceParts.length > 1) {
+                this.geomsToFeatures(feature, sourceParts);
+                this.events.triggerEvent("split", {
+                    original: feature,
+                    features: sourceParts
+                });
+                Array.prototype.push.apply(additions, sourceParts);
+                removals.push(feature);
+                sourceSplit = true;
+            }
+            if(sourceSplit || targetSplit) {
+                // remove and add feature events are suppressed
+                // listen for split event on this control instead
+                if(this.deferDelete) {
+                    // Set state instead of removing.  Take care to avoid
+                    // setting delete for features that have not yet been
+                    // inserted - those should be destroyed immediately.
+                    var feat, destroys = [];
+                    for(var i=0, len=removals.length; i<len; ++i) {
+                        feat = removals[i];
+                        if(feat.state === OpenLayers.State.INSERT) {
+                            destroys.push(feat);
+                        } else {
+                            feat.state = OpenLayers.State.DELETE;
+                            this.layer.drawFeature(feat);
+                        }
+                    }
+                    this.layer.destroyFeatures(destroys, {silent: true});
+                    for(var i=0, len=additions.length; i<len; ++i) {
+                        additions[i].state = OpenLayers.State.INSERT;
+                    }
+                } else {
+                    this.layer.destroyFeatures(removals, {silent: true});
+                }
+                this.layer.addFeatures(additions, {silent: true});
+                this.events.triggerEvent("aftersplit", {
+                    source: feature,
+                    features: additions
+                });
+            }
+        }
+        return sourceSplit;
+    },
+    
+    /**
+     * Method: geomsToFeatures
+     * Create new features given a template feature and a list of geometries.
+     *     The list of geometries is modified in place.  The result will be
+     *     a list of new features.
+     *
+     * Parameters:
+     * feature - {<OpenLayers.Feature.Vector>} The feature to be cloned.
+     * geoms - {Array(<OpenLayers.Geometry>)} List of goemetries.  This will
+     *     become a list of new features.
+     */
+    geomsToFeatures: function(feature, geoms) {
+        var clone = feature.clone();
+        delete clone.geometry;
+        var newFeature;
+        for(var i=0, len=geoms.length; i<len; ++i) {
+            // turn results list from geoms to features
+            newFeature = clone.clone();
+            newFeature.geometry = geoms[i];
+            newFeature.state = OpenLayers.State.INSERT;
+            geoms[i] = newFeature;
+        }
+    },
+    
+    /**
+     * Method: destroy
+     * Clean up the control.
+     */
+    destroy: function() {
+        if(this.active) {
+            this.deactivate(); // TODO: this should be handled by the super
+        }
+        OpenLayers.Control.prototype.destroy.call(this);
+    },
+
+    CLASS_NAME: "OpenLayers.Control.Split"
+});
+