]> dev.renevier.net Git - syp.git/blob - openlayers/lib/OpenLayers/Control/GetFeature.js
initial commit
[syp.git] / openlayers / lib / OpenLayers / Control / GetFeature.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/Handler/Click.js
8  * @requires OpenLayers/Handler/Box.js
9  * @requires OpenLayers/Handler/Hover.js
10  * @requires OpenLayers/Filter/Spatial.js
11  */
12
13 /**
14  * Class: OpenLayers.Control.GetFeature
15  * Gets vector features for locations underneath the mouse cursor. Can be
16  *     configured to act on click, hover or dragged boxes. Uses an
17  *     <OpenLayers.Protocol> that supports spatial filters (BBOX) to retrieve
18  *     features from a server and fires events that notify applications of the
19  *     selected features. 
20  *
21  * Inherits from:
22  *  - <OpenLayers.Control>
23  */
24 OpenLayers.Control.GetFeature = OpenLayers.Class(OpenLayers.Control, {
25     
26     /**
27      * APIProperty: protocol
28      * {<OpenLayers.Protocol>} Required. The protocol used for fetching
29      *     features.
30      */
31     protocol: null,
32     
33     /**
34      * APIProperty: multipleKey
35      * {String} An event modifier ('altKey' or 'shiftKey') that temporarily sets
36      *     the <multiple> property to true.  Default is null.
37      */
38     multipleKey: null,
39     
40     /**
41      * APIProperty: toggleKey
42      * {String} An event modifier ('altKey' or 'shiftKey') that temporarily sets
43      *     the <toggle> property to true.  Default is null.
44      */
45     toggleKey: null,
46     
47     /**
48      * Property: modifiers
49      * {Object} The event modifiers to use, according to the current event
50      *     being handled by this control's handlers
51      */
52     modifiers: null,
53     
54     /**
55      * APIProperty: multiple
56      * {Boolean} Allow selection of multiple geometries.  Default is false.
57      */
58     multiple: false, 
59
60     /**
61      * APIProperty: click
62      * {Boolean} Use a click handler for selecting/unselecting features.
63      *     Default is true.
64      */
65     click: true,
66     
67     /**
68      * APIProperty: clickout
69      * {Boolean} Unselect features when clicking outside any feature.
70      *     Applies only if <click> is true.  Default is true.
71      */
72     clickout: true,
73     
74     /**
75      * APIProperty: toggle
76      * {Boolean} Unselect a selected feature on click.  Applies only if
77      *     <click> is true.  Default is false.
78      */
79     toggle: false,
80
81     /**
82      * APIProperty: clickTolerance
83      * {Integer} Tolerance for the BBOX query in pixels. This has the
84      *     same effect as the tolerance parameter on WMS GetFeatureInfo
85      *     requests.  Will be ignored for box selections.  Applies only if
86      *     <click> is true.  Default is 5.
87      */
88     clickTolerance: 5,
89     
90     /**
91      * APIProperty: hover
92      * {Boolean} Send feature requests on mouse moves.  Default is false.
93      */
94     hover: false,
95
96     /**
97      * APIProperty: box
98      * {Boolean} Allow feature selection by drawing a box.
99      */
100     box: false,
101     
102     /**
103      * APIProperty: maxFeatures
104      * {Integer} Maximum number of features to return from a query, if
105      *     supported by the <protocol>.  Default is 10.
106      */
107     maxFeatures: 10,
108     
109     /**
110      * Property: features
111      * {Object} Hash of {<OpenLayers.Feature.Vector>}, keyed by fid, holding
112      *     the currently selected features
113      */
114     features: null,
115     
116     /**
117      * Proeprty: hoverFeature
118      * {<OpenLayers.Feature.Vector>} The feature currently selected by the
119      *     hover handler
120      */
121     hoverFeature: null,
122     
123     /**
124      * APIProperty: handlerOptions
125      * {Object} Additional options for the handlers used by this control. This
126      *     is a hash with the keys "click", "box" and "hover".
127      */
128     handlerOptions: null,
129     
130     /**
131      * Property: handlers
132      * {Object} Object with references to multiple <OpenLayers.Handler>
133      *     instances.
134      */
135     handlers: null,
136
137     /**
138      * Property: hoverResponse
139      * {<OpenLayers.Protocol.Response>} The response object associated with
140      *     the currently running hover request (if any).
141      */
142     hoverResponse: null,
143     
144     /**
145      * Constant: EVENT_TYPES
146      *
147      * Supported event types:
148      * beforefeatureselected - Triggered when <click> is true before a
149      *      feature is selected. The event object has a feature property with
150      *      the feature about to select
151      * featureselected - Triggered when <click> is true and a feature is
152      *      selected. The event object has a feature property with the
153      *      selected feature
154      * featureunselected - Triggered when <click> is true and a feature is
155      *      unselected. The event object has a feature property with the
156      *      unselected feature
157      * clickout - Triggered when when <click> is true and no feature was
158      *      selected.
159      * hoverfeature - Triggered when <hover> is true and the mouse has
160      *      stopped over a feature
161      * outfeature - Triggered when <hover> is true and the mouse moves
162      *      moved away from a hover-selected feature
163      */
164     EVENT_TYPES: ["featureselected", "featureunselected", "clickout",
165         "beforefeatureselected", "hoverfeature", "outfeature"],
166
167     /**
168      * Constructor: OpenLayers.Control.GetFeature
169      * Create a new control for fetching remote features.
170      *
171      * Parameters:
172      * options - {Object} A configuration object which at least has to contain
173      *     a <protocol> property
174      */
175     initialize: function(options) {
176         // concatenate events specific to vector with those from the base
177         this.EVENT_TYPES =
178             OpenLayers.Control.GetFeature.prototype.EVENT_TYPES.concat(
179             OpenLayers.Control.prototype.EVENT_TYPES
180         );
181
182         options.handlerOptions = options.handlerOptions || {};
183
184         OpenLayers.Control.prototype.initialize.apply(this, [options]);
185         
186         this.features = {};
187
188         this.handlers = {};
189         
190         if(this.click) {
191             this.handlers.click = new OpenLayers.Handler.Click(this,
192                 {click: this.selectSingle}, this.handlerOptions.click || {})
193         };
194
195         if(this.box) {
196             this.handlers.box = new OpenLayers.Handler.Box(
197                 this, {done: this.selectBox},
198                 OpenLayers.Util.extend(this.handlerOptions.box, {
199                     boxDivClassName: "olHandlerBoxSelectFeature"
200                 })
201             ); 
202         }
203         
204         if(this.hover) {
205             this.handlers.hover = new OpenLayers.Handler.Hover(
206                 this, {'move': this.cancelHover, 'pause': this.selectHover},
207                 OpenLayers.Util.extend(this.handlerOptions.hover, {
208                     'delay': 250
209                 })
210             );
211         }
212     },
213     
214     /**
215      * Method: activate
216      * Activates the control.
217      * 
218      * Returns:
219      * {Boolean} The control was effectively activated.
220      */
221     activate: function () {
222         if (!this.active) {
223             for(var i in this.handlers) {
224                 this.handlers[i].activate();
225             }
226         }
227         return OpenLayers.Control.prototype.activate.apply(
228             this, arguments
229         );
230     },
231
232     /**
233      * Method: deactivate
234      * Deactivates the control.
235      * 
236      * Returns:
237      * {Boolean} The control was effectively deactivated.
238      */
239     deactivate: function () {
240         if (this.active) {
241             for(var i in this.handlers) {
242                 this.handlers[i].deactivate();
243             }
244         }
245         return OpenLayers.Control.prototype.deactivate.apply(
246             this, arguments
247         );
248     },
249     
250     /**
251      * Method: unselectAll
252      * Unselect all selected features.  To unselect all except for a single
253      *     feature, set the options.except property to the feature.
254      *
255      * Parameters:
256      * options - {Object} Optional configuration object.
257      */
258     unselectAll: function(options) {
259         // we'll want an option to supress notification here
260         var feature;
261         for(var i=this.features.length-1; i>=0; --i) {
262             feature = this.features[i];
263             if(!options || options.except != feature) {
264                 this.unselect(feature);
265             }
266         }
267     },
268
269     /**
270      * Method: selectSingle
271      * Called on click
272      *
273      * Parameters:
274      * evt - {<OpenLayers.Event>} 
275      */
276     selectSingle: function(evt) {
277         // Set the cursor to "wait" to tell the user we're working on their click.
278         OpenLayers.Element.addClass(this.map.viewPortDiv, "olCursorWait");
279         
280         var bounds = this.pixelToBounds(evt.xy);
281         
282         this.setModifiers(evt);
283         this.request(bounds, {single: true});
284     },
285
286     /**
287      * Method: selectBox
288      * Callback from the handlers.box set up when <box> selection is on
289      *
290      * Parameters:
291      * position - {<OpenLayers.Bounds>}  
292      */
293     selectBox: function(position) {
294         if (position instanceof OpenLayers.Bounds) {
295             var minXY = this.map.getLonLatFromPixel(
296                 new OpenLayers.Pixel(position.left, position.bottom)
297             );
298             var maxXY = this.map.getLonLatFromPixel(
299                 new OpenLayers.Pixel(position.right, position.top)
300             );
301             var bounds = new OpenLayers.Bounds(
302                 minXY.lon, minXY.lat, maxXY.lon, maxXY.lat
303             );
304             
305             this.setModifiers(this.handlers.box.dragHandler.evt);
306             this.request(bounds);
307         }
308     },
309     
310     /**
311      * Method selectHover
312      * Callback from the handlers.hover set up when <hover> selection is on
313      *
314      * Parameters:
315      * evt {Object} - event object with an xy property
316      */
317     selectHover: function(evt) {
318         var bounds = this.pixelToBounds(evt.xy);
319         this.request(bounds, {single: true, hover: true});
320     },
321
322     /**
323      * Method: cancelHover
324      * Callback from the handlers.hover set up when <hover> selection is on
325      */
326     cancelHover: function() {
327         if (this.hoverResponse) {
328             this.protocol.abort(this.hoverResponse);
329             this.hoverResponse = null;
330         }
331     },
332
333     /**
334      * Method: request
335      * Sends a GetFeature request to the WFS
336      * 
337      * Parameters:
338      * bounds - {<OpenLayers.Bounds>} bounds for the request's BBOX filter
339      * options - {Object} additional options for this method.
340      * 
341      * Supported options include:
342      * single - {Boolean} A single feature should be returned.
343      *     Note that this will be ignored if the protocol does not
344      *     return the geometries of the features.
345      * hover - {Boolean} Do the request for the hover handler.
346      */
347     request: function(bounds, options) {
348         options = options || {};
349         var filter = new OpenLayers.Filter.Spatial({
350             type: OpenLayers.Filter.Spatial.BBOX,
351             value: bounds
352         });
353         
354         var response = this.protocol.read({
355             maxFeatures: options.single == true ? this.maxFeatures : undefined,
356             filter: filter,
357             callback: function(result) {
358                 if(result.code == 1) {
359                     if(result.features.length) {
360                         if(options.single == true) {
361                             this.selectBestFeature(result.features,
362                                 bounds.getCenterLonLat(), options);
363                         } else {
364                             this.select(result.features);
365                         }
366                     } else if(options.hover) {
367                         this.hoverSelect();
368                     } else {
369                         this.events.triggerEvent("clickout");
370                         if(this.clickout) {
371                             this.unselectAll();
372                         }
373                     }
374                 }
375                 // Reset the cursor.
376                 OpenLayers.Element.removeClass(this.map.viewPortDiv, "olCursorWait");
377             },
378             scope: this
379         });
380         if(options.hover == true) {
381             this.hoverResponse = response;
382         }
383     },
384
385     /**
386      * Method: selectBestFeature
387      * Selects the feature from an array of features that is the best match
388      *     for the click position.
389      * 
390      * Parameters:
391      * features - {Array(<OpenLayers.Feature.Vector>)}
392      * clickPosition - {<OpenLayers.LonLat>}
393      * options - {Object} additional options for this method
394      * 
395      * Supported options include:
396      * hover - {Boolean} Do the selection for the hover handler.
397      */
398     selectBestFeature: function(features, clickPosition, options) {
399         options = options || {};
400         if(features.length) {
401             var point = new OpenLayers.Geometry.Point(clickPosition.lon,
402                 clickPosition.lat);
403             var feature, resultFeature, dist;
404             var minDist = Number.MAX_VALUE;
405             for(var i=0; i<features.length; ++i) {
406                 feature = features[i];
407                 if(feature.geometry) {
408                     dist = point.distanceTo(feature.geometry, {edge: false});
409                     if(dist < minDist) {
410                         minDist = dist;
411                         resultFeature = feature;
412                         if(minDist == 0) {
413                             break;
414                         }
415                     }
416                 }
417             }
418             
419             if(options.hover == true) {
420                 this.hoverSelect(resultFeature);
421             } else {
422                 this.select(resultFeature || features);
423             } 
424         };
425     },
426     
427     /**
428      * Method: setModifiers
429      * Sets the multiple and toggle modifiers according to the current event
430      * 
431      * Parameters:
432      * evt {<OpenLayers.Event>}
433      */
434     setModifiers: function(evt) {
435         this.modifiers = {
436             multiple: this.multiple || (this.multipleKey && evt[this.multipleKey]),
437             toggle: this.toggle || (this.toggleKey && evt[this.toggleKey])
438         }        
439     },
440
441     /**
442      * Method: select
443      * Add feature to the hash of selected features and trigger the
444      * featureselected event.
445      * 
446      * Parameters:
447      * features - {<OpenLayers.Feature.Vector>} or an array of features
448      */
449     select: function(features) {
450         if(!this.modifiers.multiple && !this.modifiers.toggle) {
451             this.unselectAll();
452         }
453         if(!(features instanceof Array)) {
454             features = [features];
455         }
456         
457         var feature;
458         for(var i=0, len=features.length; i<len; ++i) {
459             feature = features[i];
460             if(this.features[feature.fid || feature.id]) {
461                 if(this.modifiers.toggle) {
462                     this.unselect(this.features[feature.fid || feature.id]);
463                 }
464             } else {
465                 cont = this.events.triggerEvent("beforefeatureselected", {
466                     feature: feature
467                 });
468                 if(cont !== false) {
469                     this.features[feature.fid || feature.id] = feature;
470             
471                     this.events.triggerEvent("featureselected",
472                         {feature: feature});
473                 }
474             }
475         }
476     },
477     
478     /**
479      * Method: hoverSelect
480      * Sets/unsets the <hoverFeature>
481      * 
482      * Parameters:
483      * feature - {<OpenLayers.Feature.Vector>} the feature to hover-select.
484      *     If none is provided, the current <hoverFeature> will be nulled and
485      *     the outfeature event will be triggered.
486      */
487     hoverSelect: function(feature) {
488         var fid = feature ? feature.fid || feature.id : null;
489         var hfid = this.hoverFeature ?
490             this.hoverFeature.fid || this.hoverFeature.id : null;
491             
492         if(hfid && hfid != fid) {
493             this.events.triggerEvent("outfeature",
494                 {feature: this.hoverFeature});
495             this.hoverFeature = null;
496         }
497         if(fid && fid != hfid) {
498             this.events.triggerEvent("hoverfeature", {feature: feature});
499             this.hoverFeature = feature;
500         }
501     },
502
503     /**
504      * Method: unselect
505      * Remove feature from the hash of selected features and trigger the
506      * featureunselected event.
507      *
508      * Parameters:
509      * feature - {<OpenLayers.Feature.Vector>}
510      */
511     unselect: function(feature) {
512         delete this.features[feature.fid || feature.id];
513         this.events.triggerEvent("featureunselected", {feature: feature});
514     },
515     
516     /**
517      * Method: unselectAll
518      * Unselect all selected features.
519      */
520     unselectAll: function() {
521         // we'll want an option to supress notification here
522         for(var fid in this.features) {
523             this.unselect(this.features[fid]);
524         }
525     },
526     
527     /** 
528      * Method: setMap
529      * Set the map property for the control. 
530      * 
531      * Parameters:
532      * map - {<OpenLayers.Map>} 
533      */
534     setMap: function(map) {
535         for(var i in this.handlers) {
536             this.handlers[i].setMap(map);
537         }
538         OpenLayers.Control.prototype.setMap.apply(this, arguments);
539     },
540     
541     /**
542      * Method: pixelToBounds
543      * Takes a pixel as argument and creates bounds after adding the
544      * <clickTolerance>.
545      * 
546      * Parameters:
547      * pixel - {<OpenLayers.Pixel>}
548      */
549     pixelToBounds: function(pixel) {
550         var llPx = pixel.add(-this.clickTolerance/2, this.clickTolerance/2);
551         var urPx = pixel.add(this.clickTolerance/2, -this.clickTolerance/2);
552         var ll = this.map.getLonLatFromPixel(llPx);
553         var ur = this.map.getLonLatFromPixel(urPx);
554         return new OpenLayers.Bounds(ll.lon, ll.lat, ur.lon, ur.lat);
555     },
556
557     CLASS_NAME: "OpenLayers.Control.GetFeature"
558 });