/* Copyright (c) 2006-2013 by OpenLayers Contributors (see authors.txt for * full list of contributors). Published under the 2-clause BSD license. * See license.txt in the OpenLayers distribution or repository for the * full text of the license. */ /** * @requires OpenLayers/Control.js * @requires OpenLayers/Layer/Vector.js */ /** * Class: OpenLayers.Control.Snapping * Acts as a snapping agent while editing vector features. * * Inherits from: * - */ OpenLayers.Control.Snapping = OpenLayers.Class(OpenLayers.Control, { /** * APIProperty: events * {} Events instance for listeners and triggering * control specific events. * * Register a listener for a particular event with the following syntax: * (code) * control.events.register(type, obj, listener); * (end) * * Supported event types (in addition to those from ): * beforesnap - Triggered before a snap occurs. Listeners receive an * event object with *point*, *x*, *y*, *distance*, *layer*, and * *snapType* properties. The point property will be original point * geometry considered for snapping. The x and y properties represent * coordinates the point will receive. The distance is the distance * of the snap. The layer is the target layer. The snapType property * will be one of "node", "vertex", or "edge". Return false to stop * snapping from occurring. * snap - Triggered when a snap occurs. Listeners receive an event with * *point*, *snapType*, *layer*, and *distance* properties. The point * will be the location snapped to. The snapType will be one of "node", * "vertex", or "edge". The layer will be the target layer. The * distance will be the distance of the snap in map units. * unsnap - Triggered when a vertex is unsnapped. Listeners receive an * event with a *point* property. */ /** * CONSTANT: DEFAULTS * Default target properties. */ DEFAULTS: { tolerance: 10, node: true, edge: true, vertex: true }, /** * Property: greedy * {Boolean} Snap to closest feature in first layer with an eligible * feature. Default is true. */ greedy: true, /** * Property: precedence * {Array} List representing precedence of different snapping types. * Default is "node", "vertex", "edge". */ precedence: ["node", "vertex", "edge"], /** * Property: resolution * {Float} The map resolution for the previously considered snap. */ resolution: null, /** * Property: geoToleranceCache * {Object} A cache of geo-tolerances. Tolerance values (in map units) are * calculated when the map resolution changes. */ geoToleranceCache: null, /** * Property: layer * {} The current editable layer. Set at * construction or after construction with . */ layer: null, /** * Property: feature * {} The current editable feature. */ feature: null, /** * Property: point * {} The currently snapped vertex. */ point: null, /** * Constructor: OpenLayers.Control.Snapping * Creates a new snapping control. A control is constructed with an editable * layer and a set of configuration objects for target layers. While the * control is active, dragging vertices while drawing new features or * modifying existing features on the editable layer will engage * snapping to features on the target layers. Whether a vertex snaps to * a feature on a target layer depends on the target layer configuration. * * Parameters: * options - {Object} An object containing all configuration properties for * the control. * * Valid options: * layer - {} The editable layer. Features from this * layer that are digitized or modified may have vertices snapped to * features from any of the target layers. * targets - {Array(Object | OpenLayers.Layer.Vector)} A list of objects for * configuring target layers. See valid properties of the target * objects below. If the items in the targets list are vector layers * (instead of configuration objects), the defaults from the * property will apply. The editable layer itself may be a target * layer, allowing newly created or edited features to be snapped to * existing features from the same layer. If no targets are provided * the layer given in the constructor (as ) will become the * initial target. * defaults - {Object} An object with default properties to be applied * to all target objects. * greedy - {Boolean} Snap to closest feature in first target layer that * applies. Default is true. If false, all features in all target * layers will be checked and the closest feature in all target layers * will be chosen. The greedy property determines if the order of the * target layers is significant. By default, the order of the target * layers is significant where layers earlier in the target layer list * have precedence over layers later in the list. Within a single * layer, the closest feature is always chosen for snapping. This * property only determines whether the search for a closer feature * continues after an eligible feature is found in a target layer. * * Valid target properties: * layer - {} A target layer. Features from this * layer will be eligible to act as snapping target for the editable * layer. * tolerance - {Float} The distance (in pixels) at which snapping may occur. * Default is 10. * node - {Boolean} Snap to nodes (first or last point in a geometry) in * target layer. Default is true. * nodeTolerance - {Float} Optional distance at which snapping may occur * for nodes specifically. If none is provided, will be * used. * vertex - {Boolean} Snap to vertices in target layer. Default is true. * vertexTolerance - {Float} Optional distance at which snapping may occur * for vertices specifically. If none is provided, will be * used. * edge - {Boolean} Snap to edges in target layer. Default is true. * edgeTolerance - {Float} Optional distance at which snapping may occur * for edges specifically. If none is provided, will be * used. * filter - {} Optional filter to evaluate to determine if * feature is eligible for snapping. If filter evaluates to true for a * target feature a vertex may be snapped to the feature. * minResolution - {Number} If a minResolution is provided, snapping to this * target will only be considered if the map resolution is greater than * or equal to this value (the minResolution is inclusive). Default is * no minimum resolution limit. * maxResolution - {Number} If a maxResolution is provided, snapping to this * target will only be considered if the map resolution is strictly * less than this value (the maxResolution is exclusive). Default is * no maximum resolution limit. */ initialize: function(options) { OpenLayers.Control.prototype.initialize.apply(this, [options]); this.options = options || {}; // TODO: this could be done by the super // set the editable layer if provided if(this.options.layer) { this.setLayer(this.options.layer); } // configure target layers var defaults = OpenLayers.Util.extend({}, this.options.defaults); this.defaults = OpenLayers.Util.applyDefaults(defaults, this.DEFAULTS); this.setTargets(this.options.targets); if(this.targets.length === 0 && this.layer) { this.addTargetLayer(this.layer); } this.geoToleranceCache = {}; }, /** * APIMethod: setLayer * Set the editable layer. Call the setLayer method if the editable layer * changes and the same control should be used on a new editable layer. * If the control is already active, it will be active after the new * layer is set. * * Parameters: * layer - {} The new editable layer. */ setLayer: function(layer) { if(this.active) { this.deactivate(); this.layer = layer; this.activate(); } else { this.layer = layer; } }, /** * Method: setTargets * Set the targets for the snapping agent. * * Parameters: * targets - {Array} An array of target configs or target layers. */ setTargets: function(targets) { this.targets = []; if(targets && targets.length) { var target; for(var i=0, len=targets.length; i} A target layer. */ addTargetLayer: function(layer) { this.addTarget({layer: layer}); }, /** * Method: addTarget * Add a configured target layer. * * Parameters: * target - {Object} A target config. */ addTarget: function(target) { target = OpenLayers.Util.applyDefaults(target, this.defaults); target.nodeTolerance = target.nodeTolerance || target.tolerance; target.vertexTolerance = target.vertexTolerance || target.tolerance; target.edgeTolerance = target.edgeTolerance || target.tolerance; this.targets.push(target); }, /** * Method: removeTargetLayer * Remove a target layer. * * Parameters: * layer - {} The target layer to remove. */ removeTargetLayer: function(layer) { var target; for(var i=this.targets.length-1; i>=0; --i) { target = this.targets[i]; if(target.layer === layer) { this.removeTarget(target); } } }, /** * Method: removeTarget * Remove a target. * * Parameters: * target - {Object} A target config. * * Returns: * {Array} The targets array. */ removeTarget: function(target) { return OpenLayers.Util.removeItem(this.targets, target); }, /** * APIMethod: activate * Activate the control. Activating the control registers listeners for * editing related events so that during feature creation and * modification, moving vertices will trigger snapping. */ activate: function() { var activated = OpenLayers.Control.prototype.activate.call(this); if(activated) { if(this.layer && this.layer.events) { this.layer.events.on({ sketchstarted: this.onSketchModified, sketchmodified: this.onSketchModified, vertexmodified: this.onVertexModified, scope: this }); } } return activated; }, /** * APIMethod: deactivate * Deactivate the control. Deactivating the control unregisters listeners * so feature editing may proceed without engaging the snapping agent. */ deactivate: function() { var deactivated = OpenLayers.Control.prototype.deactivate.call(this); if(deactivated) { if(this.layer && this.layer.events) { this.layer.events.un({ sketchstarted: this.onSketchModified, sketchmodified: this.onSketchModified, vertexmodified: this.onVertexModified, scope: this }); } } this.feature = null; this.point = null; return deactivated; }, /** * Method: onSketchModified * Registered as a listener for the sketchmodified event on the editable * layer. * * Parameters: * event - {Object} The sketch modified event. */ onSketchModified: function(event) { this.feature = event.feature; this.considerSnapping(event.vertex, event.vertex); }, /** * Method: onVertexModified * Registered as a listener for the vertexmodified event on the editable * layer. * * Parameters: * event - {Object} The vertex modified event. */ onVertexModified: function(event) { this.feature = event.feature; var loc = this.layer.map.getLonLatFromViewPortPx(event.pixel); this.considerSnapping( event.vertex, new OpenLayers.Geometry.Point(loc.lon, loc.lat) ); }, /** * Method: considerSnapping * * Parameters: * point - {} The vertex to be snapped (or * unsnapped). * loc - {} The location of the mouse in map * coords. */ considerSnapping: function(point, loc) { var best = { rank: Number.POSITIVE_INFINITY, dist: Number.POSITIVE_INFINITY, x: null, y: null }; var snapped = false; var result, target; for(var i=0, len=this.targets.length; i} The location of the mouse in map * coords. * * Returns: * {Object} A result object with rank, dist, x, and y properties. * Returns null if candidate is not eligible for snapping. */ testTarget: function(target, loc) { var resolution = this.layer.map.getResolution(); if ("minResolution" in target) { if (resolution < target.minResolution) { return null; } } if ("maxResolution" in target) { if (resolution >= target.maxResolution) { return null; } } var tolerance = { node: this.getGeoTolerance(target.nodeTolerance, resolution), vertex: this.getGeoTolerance(target.vertexTolerance, resolution), edge: this.getGeoTolerance(target.edgeTolerance, resolution) }; // this could be cached if we don't support setting tolerance values directly var maxTolerance = Math.max( tolerance.node, tolerance.vertex, tolerance.edge ); var result = { rank: Number.POSITIVE_INFINITY, dist: Number.POSITIVE_INFINITY }; var eligible = false; var features = target.layer.features; var feature, type, vertices, vertex, closest, dist, found; var numTypes = this.precedence.length; var ll = new OpenLayers.LonLat(loc.x, loc.y); for(var i=0, len=features.length; i when the map resolution * has not changed. * * Parameters: * tolerance - {Number} A tolerance value in pixels. * resolution - {Number} Map resolution. * * Returns: * {Number} A tolerance value in map units. */ getGeoTolerance: function(tolerance, resolution) { if(resolution !== this.resolution) { this.resolution = resolution; this.geoToleranceCache = {}; } var geoTolerance = this.geoToleranceCache[tolerance]; if(geoTolerance === undefined) { geoTolerance = tolerance * resolution; this.geoToleranceCache[tolerance] = geoTolerance; } return geoTolerance; }, /** * Method: destroy * Clean up the control. */ destroy: function() { if(this.active) { this.deactivate(); // TODO: this should be handled by the super } delete this.layer; delete this.targets; OpenLayers.Control.prototype.destroy.call(this); }, CLASS_NAME: "OpenLayers.Control.Snapping" });