]> dev.renevier.net Git - syp.git/blob - openlayers/lib/OpenLayers/Control/Snapping.js
fixes notices
[syp.git] / openlayers / lib / OpenLayers / Control / Snapping.js
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. */
4
5 /**
6  * @requires OpenLayers/Control.js
7  * @requires OpenLayers/Layer/Vector.js
8  */
9
10 /**
11  * Class: OpenLayers.Control.Snapping
12  * Acts as a snapping agent while editing vector features.
13  *
14  * Inherits from:
15  *  - <OpenLayers.Control>
16  */
17 OpenLayers.Control.Snapping = OpenLayers.Class(OpenLayers.Control, {
18
19     /**
20      * Constant: EVENT_TYPES
21      * {Array(String)} Supported application event types.  Register a listener
22      *     for a particular event with the following syntax:
23      * (code)
24      * control.events.register(type, obj, listener);
25      * (end)
26      *
27      * Listeners will be called with a reference to an event object.  The
28      *     properties of this event depends on exactly what happened.
29      *
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.
46      */
47     EVENT_TYPES: ["beforesnap", "snap", "unsnap"],
48     
49     /**
50      * CONSTANT: DEFAULTS
51      * Default target properties.
52      */
53     DEFAULTS: {
54         tolerance: 10,
55         node: true,
56         edge: true,
57         vertex: true
58     },
59     
60     /**
61      * Property: greedy
62      * {Boolean} Snap to closest feature in first layer with an eligible
63      *     feature.  Default is true.
64      */
65     greedy: true,
66     
67     /**
68      * Property: precedence
69      * {Array} List representing precedence of different snapping types.
70      *     Default is "node", "vertex", "edge".
71      */
72     precedence: ["node", "vertex", "edge"],
73     
74     /**
75      * Property: resolution
76      * {Float} The map resolution for the previously considered snap.
77      */
78     resolution: null,
79     
80     /**
81      * Property: geoToleranceCache
82      * {Object} A cache of geo-tolerances.  Tolerance values (in map units) are
83      *     calculated when the map resolution changes.
84      */
85     geoToleranceCache: null,
86     
87     /**
88      * Property: layer
89      * {<OpenLayers.Layer.Vector>} The current editable layer.  Set at
90      *     construction or after construction with <setLayer>.
91      */
92     layer: null,
93     
94     /**
95      * Property: feature
96      * {<OpenLayers.Feature.Vector>} The current editable feature.
97      */
98     feature: null,
99     
100     /**
101      * Property: point
102      * {<OpenLayers.Geometry.Point>} The currently snapped vertex.
103      */
104     point: null,
105
106     /**
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.
114      *
115      * Parameters:
116      * options - {Object} An object containing all configuration properties for
117      *     the control.
118      *
119      * Valid options:
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
131      *     initial target.
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.
144      *
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
148      *     layer.
149      * tolerance - {Float} The distance (in pixels) at which snapping may occur.
150      *     Default is 10.
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
155      *     used.
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
159      *     used.
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
163      *     used.
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. 
167      */
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
172         );
173         OpenLayers.Control.prototype.initialize.apply(this, [options]);
174         this.options = options || {}; // TODO: this could be done by the super
175         
176         // set the editable layer if provided
177         if(this.options.layer) {
178             this.setLayer(this.options.layer);
179         }
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);
186         }
187
188         this.geoToleranceCache = {};
189     },
190     
191     /**
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
196      *     layer is set.
197      *
198      * Parameters:
199      * layer - {OpenLayers.Layer.Vector}  The new editable layer.
200      */
201     setLayer: function(layer) {
202         if(this.active) {
203             this.deactivate();
204             this.layer = layer;
205             this.activate();
206         } else {
207             this.layer = layer;
208         }
209     },
210     
211     /**
212      * Method: setTargets
213      * Set the targets for the snapping agent.
214      *
215      * Parameters:
216      * targets - {Array} An array of target configs or target layers.
217      */
218     setTargets: function(targets) {
219         this.targets = [];
220         if(targets && targets.length) {
221             var target;
222             for(var i=0, len=targets.length; i<len; ++i) {
223                 target = targets[i];
224                 if(target instanceof OpenLayers.Layer.Vector) {
225                     this.addTargetLayer(target);
226                 } else {
227                     this.addTarget(target);
228                 }
229             }
230         }
231     },
232     
233     /**
234      * Method: addTargetLayer
235      * Add a target layer with the default target config.
236      *
237      * Parameters:
238      * layer - {<OpenLayers.Layer.Vector>} A target layer.
239      */
240     addTargetLayer: function(layer) {
241         this.addTarget({layer: layer});
242     },
243     
244     /**
245      * Method: addTarget
246      * Add a configured target layer.
247      *
248      * Parameters:
249      * target - {Object} A target config.
250      */
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);
257     },
258     
259     /**
260      * Method: removeTargetLayer
261      * Remove a target layer.
262      *
263      * Parameters:
264      * layer - {<OpenLayers.Layer.Vector>} The target layer to remove.
265      */
266     removeTargetLayer: function(layer) {
267         var target;
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);
272             }
273         }
274     },
275     
276     /**
277      * Method: removeTarget
278      * Remove a target.
279      *
280      * Parameters:
281      * target - {Object} A target config.
282      *
283      * Returns:
284      * {Array} The targets array.
285      */
286     removeTarget: function(target) {
287         return OpenLayers.Util.removeItem(this.targets, target);
288     },
289     
290     /**
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.
295      */
296     activate: function() {
297         var activated = OpenLayers.Control.prototype.activate.call(this);
298         if(activated) {
299             if(this.layer && this.layer.events) {
300                 this.layer.events.on({
301                     sketchstarted: this.onSketchModified,
302                     sketchmodified: this.onSketchModified,
303                     vertexmodified: this.onVertexModified,
304                     scope: this
305                 });
306             }
307         }
308         return activated;
309     },
310     
311     /**
312      * APIMethod: deactivate
313      * Deactivate the control.  Deactivating the control unregisters listeners
314      *     so feature editing may proceed without engaging the snapping agent.
315      */
316     deactivate: function() {
317         var deactivated = OpenLayers.Control.prototype.deactivate.call(this);
318         if(deactivated) {
319             if(this.layer && this.layer.events) {
320                 this.layer.events.un({
321                     sketchstarted: this.onSketchModified,
322                     sketchmodified: this.onSketchModified,
323                     vertexmodified: this.onVertexModified,
324                     scope: this
325                 });
326             }
327         }
328         this.feature = null;
329         this.point = null;
330         return deactivated;
331     },
332     
333     /**
334      * Method: onSketchModified
335      * Registered as a listener for the sketchmodified event on the editable
336      *     layer.
337      *
338      * Parameters:
339      * event - {Object} The sketch modified event.
340      */
341     onSketchModified: function(event) {
342         this.feature = event.feature;
343         this.considerSnapping(event.vertex, event.vertex);
344     },
345     
346     /**
347      * Method: onVertexModified
348      * Registered as a listener for the vertexmodified event on the editable
349      *     layer.
350      *
351      * Parameters:
352      * event - {Object} The vertex modified event.
353      */
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)
359         );
360     },
361
362     /**
363      * Method: considerSnapping
364      *
365      * Parameters:
366      * point - {<OpenLayers.Geometry.Point}} The vertex to be snapped (or
367      *     unsnapped).
368      * loc - {<OpenLayers.Geometry.Point>} The location of the mouse in map
369      *     coords.
370      */
371     considerSnapping: function(point, loc) {
372         var best = {
373             rank: Number.POSITIVE_INFINITY,
374             dist: Number.POSITIVE_INFINITY,
375             x: null, y: null
376         };
377         var snapped = false;
378         var result, target;
379         for(var i=0, len=this.targets.length; i<len; ++i) {
380             target = this.targets[i];
381             result = this.testTarget(target, loc);
382             if(result) {
383                 if(this.greedy) {
384                     best = result;
385                     best.target = target; 
386                     snapped = true;
387                     break;
388                 } else {
389                     if((result.rank < best.rank) ||
390                        (result.rank === best.rank && result.dist < best.dist)) {
391                         best = result;
392                         best.target = target;
393                         snapped = true;
394                     }
395                 }
396             }
397         }
398         if(snapped) {
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]
402             });
403             if(proceed !== false) {
404                 point.x = best.x;
405                 point.y = best.y;
406                 this.point = point;
407                 this.events.triggerEvent("snap", {
408                     point: point,
409                     snapType: this.precedence[best.rank],
410                     layer: best.target.layer,
411                     distance: best.dist
412                 });
413             } else {
414                 snapped = false;
415             }
416         }
417         if(this.point && !snapped) {
418             point.x = loc.x;
419             point.y = loc.y;
420             this.point = null;
421             this.events.triggerEvent("unsnap", {point: point});
422         }
423     },
424     
425     /**
426      * Method: testTarget
427      *
428      * Parameters:
429      * target - {Object} Object with target layer configuration.
430      * loc - {<OpenLayers.Geometry.Point>} The location of the mouse in map
431      *     coords.
432      *
433      * Returns:
434      * {Object} A result object with rank, dist, x, and y properties.
435      *     Returns null if candidate is not eligible for snapping.
436      */
437     testTarget: function(target, loc) {
438         var tolerance = {
439             node: this.getGeoTolerance(target.nodeTolerance),
440             vertex: this.getGeoTolerance(target.vertexTolerance),
441             edge: this.getGeoTolerance(target.edgeTolerance)
442         };
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
446         );
447         var result = {
448             rank: Number.POSITIVE_INFINITY, dist: Number.POSITIVE_INFINITY
449         };
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];
463                         if(target[type]) {
464                             if(type === "edge") {
465                                 closest = feature.geometry.distanceTo(loc, {details: true});
466                                 dist = closest.distance;
467                                 if(dist <= tolerance[type] && dist < result.dist) {
468                                     result = {
469                                         rank: j, dist: dist,
470                                         x: closest.x0, y: closest.y0 // closest coords on feature
471                                     };
472                                     eligible = true;
473                                     // don't look for lower precedence types for this feature
474                                     break;
475                                 }
476                             } else {
477                                 // look for nodes or vertices
478                                 vertices = feature.geometry.getVertices(type === "node");
479                                 found = false;
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))) {
485                                         result = {
486                                             rank: j, dist: dist,
487                                             x: vertex.x, y: vertex.y
488                                         };
489                                         eligible = true;
490                                         found = true;
491                                     }
492                                 }
493                                 if(found) {
494                                     // don't look for lower precedence types for this feature
495                                     break;
496                                 }
497                             }
498                         }
499                     }
500                 }
501             }
502         }
503         return eligible ? result : null;
504     },
505     
506     /**
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
510      *     has not changed.
511      *     
512      * Parameters:
513      * tolerance - {Number} A tolerance value in pixels.
514      *
515      * Returns:
516      * {Number} A tolerance value in map units.
517      */
518     getGeoTolerance: function(tolerance) {
519         var resolution = this.layer.map.getResolution();
520         if(resolution !== this.resolution) {
521             this.resolution = resolution;
522             this.geoToleranceCache = {};
523         }
524         var geoTolerance = this.geoToleranceCache[tolerance];
525         if(geoTolerance === undefined) {
526             geoTolerance = tolerance * resolution;
527             this.geoToleranceCache[tolerance] = geoTolerance;
528         }
529         return geoTolerance;
530     },
531     
532     /**
533      * Method: destroy
534      * Clean up the control.
535      */
536     destroy: function() {
537         if(this.active) {
538             this.deactivate(); // TODO: this should be handled by the super
539         }
540         delete this.layer;
541         delete this.targets;
542         OpenLayers.Control.prototype.destroy.call(this);
543     },
544
545     CLASS_NAME: "OpenLayers.Control.Snapping"
546 });