1 /* Copyright (c) 2006-2008 MetaCarta, Inc., published under the Clear BSD
2 * license. See http://svn.openlayers.org/trunk/openlayers/license.txt for the
3 * full text of the license. */
6 * @requires OpenLayers/Control.js
7 * @requires OpenLayers/Layer/Vector.js
11 * Class: OpenLayers.Control.Snapping
12 * Acts as a snapping agent while editing vector features.
15 * - <OpenLayers.Control>
17 OpenLayers.Control.Snapping = OpenLayers.Class(OpenLayers.Control, {
20 * Constant: EVENT_TYPES
21 * {Array(String)} Supported application event types. Register a listener
22 * for a particular event with the following syntax:
24 * control.events.register(type, obj, listener);
27 * Listeners will be called with a reference to an event object. The
28 * properties of this event depends on exactly what happened.
30 * Supported control event types (in addition to those from <OpenLayers.Control>):
31 * beforesnap - Triggered before a snap occurs. Listeners receive an
32 * event object with *point*, *x*, *y*, *distance*, *layer*, and
33 * *snapType* properties. The point property will be original point
34 * geometry considered for snapping. The x and y properties represent
35 * coordinates the point will receive. The distance is the distance
36 * of the snap. The layer is the target layer. The snapType property
37 * will be one of "node", "vertex", or "edge". Return false to stop
38 * snapping from occurring.
39 * snap - Triggered when a snap occurs. Listeners receive an event with
40 * *point*, *snapType*, *layer*, and *distance* properties. The point
41 * will be the location snapped to. The snapType will be one of "node",
42 * "vertex", or "edge". The layer will be the target layer. The
43 * distance will be the distance of the snap in map units.
44 * unsnap - Triggered when a vertex is unsnapped. Listeners receive an
45 * event with a *point* property.
47 EVENT_TYPES: ["beforesnap", "snap", "unsnap"],
51 * Default target properties.
62 * {Boolean} Snap to closest feature in first layer with an eligible
63 * feature. Default is true.
68 * Property: precedence
69 * {Array} List representing precedence of different snapping types.
70 * Default is "node", "vertex", "edge".
72 precedence: ["node", "vertex", "edge"],
75 * Property: resolution
76 * {Float} The map resolution for the previously considered snap.
81 * Property: geoToleranceCache
82 * {Object} A cache of geo-tolerances. Tolerance values (in map units) are
83 * calculated when the map resolution changes.
85 geoToleranceCache: null,
89 * {<OpenLayers.Layer.Vector>} The current editable layer. Set at
90 * construction or after construction with <setLayer>.
96 * {<OpenLayers.Feature.Vector>} The current editable feature.
102 * {<OpenLayers.Geometry.Point>} The currently snapped vertex.
107 * Constructor: OpenLayers.Control.Snapping
108 * Creates a new snapping control. A control is constructed with an editable
109 * layer and a set of configuration objects for target layers. While the
110 * control is active, dragging vertices while drawing new features or
111 * modifying existing features on the editable layer will engage
112 * snapping to features on the target layers. Whether a vertex snaps to
113 * a feature on a target layer depends on the target layer configuration.
116 * options - {Object} An object containing all configuration properties for
120 * layer - {OpenLayers.Layer.Vector} The editable layer. Features from this
121 * layer that are digitized or modified may have vertices snapped to
122 * features from any of the target layers.
123 * targets - {Array(Object | OpenLayers.Layer.Vector)} A list of objects for
124 * configuring target layers. See valid properties of the target
125 * objects below. If the items in the targets list are vector layers
126 * (instead of configuration objects), the defaults from the <defaults>
127 * property will apply. The editable layer itself may be a target
128 * layer - allowing newly created or edited features to be snapped to
129 * existing features from the same layer. If no targets are provided
130 * the layer given in the constructor (as <layer>) will become the
132 * defaults - {Object} An object with default properties to be applied
133 * to all target objects.
134 * greedy - {Boolean} Snap to closest feature in first target layer that
135 * applies. Default is true. If false, all features in all target
136 * layers will be checked and the closest feature in all target layers
137 * will be chosen. The greedy property determines if the order of the
138 * target layers is significant. By default, the order of the target
139 * layers is significant where layers earlier in the target layer list
140 * have precedence over layers later in the list. Within a single
141 * layer, the closest feature is always chosen for snapping. This
142 * property only determines whether the search for a closer feature
143 * continues after an eligible feature is found in a target layer.
145 * Valid target properties:
146 * layer - {OpenLayers.Layer.Vector} A target layer. Features from this
147 * layer will be eligible to act as snapping target for the editable
149 * tolerance - {Float} The distance (in pixels) at which snapping may occur.
151 * node - {Boolean} Snap to nodes (first or last point in a geometry) in
152 * target layer. Default is true.
153 * nodeTolerance - {Float} Optional distance at which snapping may occur
154 * for nodes specifically. If none is provided, <tolerance> will be
156 * vertex - {Boolean} Snap to vertices in target layer. Default is true.
157 * vertexTolerance - {Float} Optional distance at which snapping may occur
158 * for vertices specifically. If none is provided, <tolerance> will be
160 * edge - {Boolean} Snap to edges in target layer. Default is true.
161 * edgeTolerance - {Float} Optional distance at which snapping may occur
162 * for edges specifically. If none is provided, <tolerance> will be
164 * filter - {OpenLayers.Filter} Optional filter to evaluate to determine if
165 * feature is eligible for snapping. If filter evaluates to true for a
166 * target feature a vertex may be snapped to the feature.
168 initialize: function(options) {
169 // concatenate events specific to measure with those from the base
170 Array.prototype.push.apply(
171 this.EVENT_TYPES, OpenLayers.Control.prototype.EVENT_TYPES
173 OpenLayers.Control.prototype.initialize.apply(this, [options]);
174 this.options = options || {}; // TODO: this could be done by the super
176 // set the editable layer if provided
177 if(this.options.layer) {
178 this.setLayer(this.options.layer);
180 // configure target layers
181 var defaults = OpenLayers.Util.extend({}, this.options.defaults);
182 this.defaults = OpenLayers.Util.applyDefaults(defaults, this.DEFAULTS);
183 this.setTargets(this.options.targets);
184 if(this.targets.length === 0 && this.layer) {
185 this.addTargetLayer(this.layer);
188 this.geoToleranceCache = {};
192 * APIMethod: setLayer
193 * Set the editable layer. Call the setLayer method if the editable layer
194 * changes and the same control should be used on a new editable layer.
195 * If the control is already active, it will be active after the new
199 * layer - {OpenLayers.Layer.Vector} The new editable layer.
201 setLayer: function(layer) {
213 * Set the targets for the snapping agent.
216 * targets - {Array} An array of target configs or target layers.
218 setTargets: function(targets) {
220 if(targets && targets.length) {
222 for(var i=0, len=targets.length; i<len; ++i) {
224 if(target instanceof OpenLayers.Layer.Vector) {
225 this.addTargetLayer(target);
227 this.addTarget(target);
234 * Method: addTargetLayer
235 * Add a target layer with the default target config.
238 * layer - {<OpenLayers.Layer.Vector>} A target layer.
240 addTargetLayer: function(layer) {
241 this.addTarget({layer: layer});
246 * Add a configured target layer.
249 * target - {Object} A target config.
251 addTarget: function(target) {
252 target = OpenLayers.Util.applyDefaults(target, this.defaults);
253 target.nodeTolerance = target.nodeTolerance || target.tolerance;
254 target.vertexTolerance = target.vertexTolerance || target.tolerance;
255 target.edgeTolerance = target.edgeTolerance || target.tolerance;
256 this.targets.push(target);
260 * Method: removeTargetLayer
261 * Remove a target layer.
264 * layer - {<OpenLayers.Layer.Vector>} The target layer to remove.
266 removeTargetLayer: function(layer) {
268 for(var i=this.targets.length-1; i>=0; --i) {
269 target = this.targets[i];
270 if(target.layer === layer) {
271 this.removeTarget(target);
277 * Method: removeTarget
281 * target - {Object} A target config.
284 * {Array} The targets array.
286 removeTarget: function(target) {
287 return OpenLayers.Util.removeItem(this.targets, target);
291 * APIMethod: activate
292 * Activate the control. Activating the control registers listeners for
293 * editing related events so that during feature creation and
294 * modification, moving vertices will trigger snapping.
296 activate: function() {
297 var activated = OpenLayers.Control.prototype.activate.call(this);
299 if(this.layer && this.layer.events) {
300 this.layer.events.on({
301 sketchstarted: this.onSketchModified,
302 sketchmodified: this.onSketchModified,
303 vertexmodified: this.onVertexModified,
312 * APIMethod: deactivate
313 * Deactivate the control. Deactivating the control unregisters listeners
314 * so feature editing may proceed without engaging the snapping agent.
316 deactivate: function() {
317 var deactivated = OpenLayers.Control.prototype.deactivate.call(this);
319 if(this.layer && this.layer.events) {
320 this.layer.events.un({
321 sketchstarted: this.onSketchModified,
322 sketchmodified: this.onSketchModified,
323 vertexmodified: this.onVertexModified,
334 * Method: onSketchModified
335 * Registered as a listener for the sketchmodified event on the editable
339 * event - {Object} The sketch modified event.
341 onSketchModified: function(event) {
342 this.feature = event.feature;
343 this.considerSnapping(event.vertex, event.vertex);
347 * Method: onVertexModified
348 * Registered as a listener for the vertexmodified event on the editable
352 * event - {Object} The vertex modified event.
354 onVertexModified: function(event) {
355 this.feature = event.feature;
356 var loc = this.layer.map.getLonLatFromViewPortPx(event.pixel);
357 this.considerSnapping(
358 event.vertex, new OpenLayers.Geometry.Point(loc.lon, loc.lat)
363 * Method: considerSnapping
366 * point - {<OpenLayers.Geometry.Point}} The vertex to be snapped (or
368 * loc - {<OpenLayers.Geometry.Point>} The location of the mouse in map
371 considerSnapping: function(point, loc) {
373 rank: Number.POSITIVE_INFINITY,
374 dist: Number.POSITIVE_INFINITY,
379 for(var i=0, len=this.targets.length; i<len; ++i) {
380 target = this.targets[i];
381 result = this.testTarget(target, loc);
385 best.target = target;
389 if((result.rank < best.rank) ||
390 (result.rank === best.rank && result.dist < best.dist)) {
392 best.target = target;
399 var proceed = this.events.triggerEvent("beforesnap", {
400 point: point, x: best.x, y: best.y, distance: best.dist,
401 layer: best.target.layer, snapType: this.precedence[best.rank]
403 if(proceed !== false) {
407 this.events.triggerEvent("snap", {
409 snapType: this.precedence[best.rank],
410 layer: best.target.layer,
417 if(this.point && !snapped) {
421 this.events.triggerEvent("unsnap", {point: point});
429 * target - {Object} Object with target layer configuration.
430 * loc - {<OpenLayers.Geometry.Point>} The location of the mouse in map
434 * {Object} A result object with rank, dist, x, and y properties.
435 * Returns null if candidate is not eligible for snapping.
437 testTarget: function(target, loc) {
439 node: this.getGeoTolerance(target.nodeTolerance),
440 vertex: this.getGeoTolerance(target.vertexTolerance),
441 edge: this.getGeoTolerance(target.edgeTolerance)
443 // this could be cached if we don't support setting tolerance values directly
444 var maxTolerance = Math.max(
445 tolerance.node, tolerance.vertex, tolerance.edge
448 rank: Number.POSITIVE_INFINITY, dist: Number.POSITIVE_INFINITY
450 var eligible = false;
451 var features = target.layer.features;
452 var feature, type, vertices, vertex, closest, dist, found;
453 var numTypes = this.precedence.length;
454 var ll = new OpenLayers.LonLat(loc.x, loc.y);
455 for(var i=0, len=features.length; i<len; ++i) {
456 feature = features[i];
457 if(feature !== this.feature && !feature._sketch &&
458 feature.state !== OpenLayers.State.DELETE &&
459 (!target.filter || target.filter.evaluate(feature.attributes))) {
460 if(feature.atPoint(ll, maxTolerance, maxTolerance)) {
461 for(var j=0, stop=Math.min(result.rank+1, numTypes); j<stop; ++j) {
462 type = this.precedence[j];
464 if(type === "edge") {
465 closest = feature.geometry.distanceTo(loc, {details: true});
466 dist = closest.distance;
467 if(dist <= tolerance[type] && dist < result.dist) {
470 x: closest.x0, y: closest.y0 // closest coords on feature
473 // don't look for lower precedence types for this feature
477 // look for nodes or vertices
478 vertices = feature.geometry.getVertices(type === "node");
480 for(var k=0, klen=vertices.length; k<klen; ++k) {
481 vertex = vertices[k];
482 dist = vertex.distanceTo(loc);
483 if(dist <= tolerance[type] &&
484 (j < result.rank || (j === result.rank && dist < result.dist))) {
487 x: vertex.x, y: vertex.y
494 // don't look for lower precedence types for this feature
503 return eligible ? result : null;
507 * Method: getGeoTolerance
508 * Calculate a tolerance in map units given a tolerance in pixels. This
509 * takes advantage of the <geoToleranceCache> when the map resolution
513 * tolerance - {Number} A tolerance value in pixels.
516 * {Number} A tolerance value in map units.
518 getGeoTolerance: function(tolerance) {
519 var resolution = this.layer.map.getResolution();
520 if(resolution !== this.resolution) {
521 this.resolution = resolution;
522 this.geoToleranceCache = {};
524 var geoTolerance = this.geoToleranceCache[tolerance];
525 if(geoTolerance === undefined) {
526 geoTolerance = tolerance * resolution;
527 this.geoToleranceCache[tolerance] = geoTolerance;
534 * Clean up the control.
536 destroy: function() {
538 this.deactivate(); // TODO: this should be handled by the super
542 OpenLayers.Control.prototype.destroy.call(this);
545 CLASS_NAME: "OpenLayers.Control.Snapping"