From 612e95793c7579fd7e209aee755c926f8f5acd60 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Tue, 10 Mar 2009 23:31:53 +0000 Subject: [PATCH] Added GetFeature control to get features based on spatial filters created by clicking or dragging boxes on the map. Thanks tschaub for the review and the final patch with valuable improvements. r=tschaub (closes #1936) git-svn-id: http://svn.openlayers.org/trunk/openlayers@9003 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf --- examples/getfeature-wfs.html | 80 ++++ lib/OpenLayers.js | 1 + lib/OpenLayers/Control/GetFeature.js | 557 +++++++++++++++++++++++++++ tests/Control/GetFeature.html | 104 +++++ tests/list-tests.html | 1 + theme/default/style.css | 7 + 6 files changed, 750 insertions(+) create mode 100644 examples/getfeature-wfs.html create mode 100644 lib/OpenLayers/Control/GetFeature.js create mode 100644 tests/Control/GetFeature.html diff --git a/examples/getfeature-wfs.html b/examples/getfeature-wfs.html new file mode 100644 index 0000000000..31ef5cdc34 --- /dev/null +++ b/examples/getfeature-wfs.html @@ -0,0 +1,80 @@ + + + + + WFS: GetFeature Example (GeoServer) + + + + +

WFS GetFeature Example (GeoServer)

+ +
+
+ +

+ Shows how to use the GetFeature control to select features from a + WMS layer. Click or drag a box to select features, use the Shift key to + add features to the selection, use the Ctrl key to toggle a feature's + selected status. Note that this control also has a hover option, which is + enabled in this example. This gives you a visual feedback by loading the + feature underneath the mouse pointer from the WFS, but causes a lot of + GetFeature requests to be issued. +

+ +
+ +
+ + + + + + diff --git a/lib/OpenLayers.js b/lib/OpenLayers.js index 9d16029336..e755788845 100644 --- a/lib/OpenLayers.js +++ b/lib/OpenLayers.js @@ -245,6 +245,7 @@ "OpenLayers/Format/WMC/v1_1_0.js", "OpenLayers/Format/WMSGetFeatureInfo.js", "OpenLayers/Layer/WFS.js", + "OpenLayers/Control/GetFeature.js", "OpenLayers/Control/MouseToolbar.js", "OpenLayers/Control/NavToolbar.js", "OpenLayers/Control/PanPanel.js", diff --git a/lib/OpenLayers/Control/GetFeature.js b/lib/OpenLayers/Control/GetFeature.js new file mode 100644 index 0000000000..ad846cbb5b --- /dev/null +++ b/lib/OpenLayers/Control/GetFeature.js @@ -0,0 +1,557 @@ +/* Copyright (c) 2006-2008 MetaCarta, Inc., 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/Click.js + * @requires OpenLayers/Handler/Box.js + * @requires OpenLayers/Handler/Hover.js + * @requires OpenLayers/Filter/Spatial.js + */ + +/** + * Class: OpenLayers.Control.GetFeature + * Gets vector features for locations underneath the mouse cursor. Can be + * configured to act on click, hover or dragged boxes. Uses an + * that supports spatial filters (BBOX) to retrieve + * features from a server and fires events that notify applications of the + * selected features. + * + * Inherits from: + * - + */ +OpenLayers.Control.GetFeature = OpenLayers.Class(OpenLayers.Control, { + + /** + * APIProperty: protocol + * {} Required. The protocol used for fetching + * features. + */ + protocol: null, + + /** + * APIProperty: multipleKey + * {String} An event modifier ('altKey' or 'shiftKey') that temporarily sets + * the property to true. Default is null. + */ + multipleKey: null, + + /** + * APIProperty: toggleKey + * {String} An event modifier ('altKey' or 'shiftKey') that temporarily sets + * the property to true. Default is null. + */ + toggleKey: null, + + /** + * Property: modifiers + * {Object} The event modifiers to use, according to the current event + * being handled by this control's handlers + */ + modifiers: null, + + /** + * APIProperty: multiple + * {Boolean} Allow selection of multiple geometries. Default is false. + */ + multiple: false, + + /** + * APIProperty: click + * {Boolean} Use a click handler for selecting/unselecting features. + * Default is true. + */ + click: true, + + /** + * APIProperty: clickout + * {Boolean} Unselect features when clicking outside any feature. + * Applies only if is true. Default is true. + */ + clickout: true, + + /** + * APIProperty: toggle + * {Boolean} Unselect a selected feature on click. Applies only if + * is true. Default is false. + */ + toggle: false, + + /** + * APIProperty: clickTolerance + * {Integer} Tolerance for the BBOX query in pixels. This has the + * same effect as the tolerance parameter on WMS GetFeatureInfo + * requests. Will be ignored for box selections. Applies only if + * is true. Default is 5. + */ + clickTolerance: 5, + + /** + * APIProperty: hover + * {Boolean} Send feature requests on mouse moves. Default is false. + */ + hover: false, + + /** + * APIProperty: box + * {Boolean} Allow feature selection by drawing a box. + */ + box: false, + + /** + * APIProperty: maxFeatures + * {Integer} Maximum number of features to return from a query, if + * supported by the . Default is 10. + */ + maxFeatures: 10, + + /** + * Property: features + * {Object} Hash of {}, keyed by fid, holding + * the currently selected features + */ + features: null, + + /** + * Proeprty: hoverFeature + * {} The feature currently selected by the + * hover handler + */ + hoverFeature: null, + + /** + * APIProperty: handlerOptions + * {Object} Additional options for the handlers used by this control. This + * is a hash with the keys "click", "box" and "hover". + */ + handlerOptions: null, + + /** + * Property: handlers + * {Object} Object with references to multiple + * instances. + */ + handlers: null, + + /** + * Property: hoverRequest + * {} contains the currently running hover request + * (if any). + */ + hoverRequest: null, + + /** + * Constant: EVENT_TYPES + * + * Supported event types: + * beforefeatureselected - Triggered when is true before a + * feature is selected. The event object has a feature property with + * the feature about to select + * featureselected - Triggered when is true and a feature is + * selected. The event object has a feature property with the + * selected feature + * featureunselected - Triggered when is true and a feature is + * unselected. The event object has a feature property with the + * unselected feature + * clickout - Triggered when when is true and no feature was + * selected. + * hoverfeature - Triggered when is true and the mouse has + * stopped over a feature + * outfeature - Triggered when is true and the mouse moves + * moved away from a hover-selected feature + */ + EVENT_TYPES: ["featureselected", "featureunselected", "clickout", + "beforefeatureselected", "hoverfeature", "outfeature"], + + /** + * Constructor: + * + * Parameters: + * options - {Object} A configuration object which at least has to contain + * a property + */ + initialize: function(options) { + // concatenate events specific to vector with those from the base + this.EVENT_TYPES = + OpenLayers.Control.GetFeature.prototype.EVENT_TYPES.concat( + OpenLayers.Control.prototype.EVENT_TYPES + ); + + options.handlerOptions = options.handlerOptions || {}; + + OpenLayers.Control.prototype.initialize.apply(this, [options]); + + this.features = {}; + + this.handlers = {}; + + if(this.click) { + this.handlers.click = new OpenLayers.Handler.Click(this, + {click: this.selectSingle}, this.handlerOptions.click || {}) + }; + + if(this.box) { + this.handlers.box = new OpenLayers.Handler.Box( + this, {done: this.selectBox}, + OpenLayers.Util.extend(this.handlerOptions.box, { + boxDivClassName: "olHandlerBoxSelectFeature" + }) + ); + } + + if(this.hover) { + this.handlers.hover = new OpenLayers.Handler.Hover( + this, {'move': this.cancelHover, 'pause': this.selectHover}, + OpenLayers.Util.extend(this.handlerOptions.hover, { + 'delay': 250 + }) + ); + } + }, + + /** + * Method: activate + * Activates the control. + * + * Returns: + * {Boolean} The control was effectively activated. + */ + activate: function () { + if (!this.active) { + for(var i in this.handlers) { + this.handlers[i].activate(); + } + } + return OpenLayers.Control.prototype.activate.apply( + this, arguments + ); + }, + + /** + * Method: deactivate + * Deactivates the control. + * + * Returns: + * {Boolean} The control was effectively deactivated. + */ + deactivate: function () { + if (this.active) { + for(var i in this.handlers) { + this.handlers[i].deactivate(); + } + } + return OpenLayers.Control.prototype.deactivate.apply( + this, arguments + ); + }, + + /** + * Method: unselectAll + * Unselect all selected features. To unselect all except for a single + * feature, set the options.except property to the feature. + * + * Parameters: + * options - {Object} Optional configuration object. + */ + unselectAll: function(options) { + // we'll want an option to supress notification here + var feature; + for(var i=this.features.length-1; i>=0; --i) { + feature = this.features[i]; + if(!options || options.except != feature) { + this.unselect(feature); + } + } + }, + + /** + * Method: selectSingle + * Called on click + * + * Parameters: + * evt - {} + */ + selectSingle: function(evt) { + // Set the cursor to "wait" to tell the user we're working on their click. + OpenLayers.Element.addClass(this.map.div, "olCursorWait"); + + var bounds = this.pixelToBounds(evt.xy); + + this.setModifiers(evt); + this.request(bounds, {single: true}); + }, + + /** + * Method: selectBox + * Callback from the handlers.box set up when selection is on + * + * Parameters: + * position - {} + */ + selectBox: function(position) { + if (position instanceof OpenLayers.Bounds) { + var minXY = this.map.getLonLatFromPixel( + new OpenLayers.Pixel(position.left, position.bottom) + ); + var maxXY = this.map.getLonLatFromPixel( + new OpenLayers.Pixel(position.right, position.top) + ); + var bounds = new OpenLayers.Bounds( + minXY.lon, minXY.lat, maxXY.lon, maxXY.lat + ); + + this.setModifiers(this.handlers.box.dragHandler.evt); + this.request(bounds); + } + }, + + /** + * Method selectHover + * Callback from the handlers.hover set up when selection is on + * + * Parameters: + * evt {Object} - event object with an xy property + */ + selectHover: function(evt) { + var bounds = this.pixelToBounds(evt.xy); + this.request(bounds, {single: true, hover: true}); + }, + + /** + * Method: cancelHover + * Callback from the handlers.hover set up when selection is on + */ + cancelHover: function() { + if (this.hoverRequest) { + this.hoverRequest.abort(); + this.hoverRequest = null; + } + }, + + /** + * Method: request + * Sends a GetFeature request to the WFS + * + * Parameters: + * bounds - {} bounds for the request's BBOX filter + * options - {Object} additional options for this method. + * + * Supported options include: + * single - {Boolean} A single feature should be returned. + * Note that this will be ignored if the protocol does not + * return the geometries of the features. + * hover - {Boolean} Do the request for the hover handler. + */ + request: function(bounds, options) { + options = options || {}; + var filter = new OpenLayers.Filter.Spatial({ + type: OpenLayers.Filter.Spatial.BBOX, + value: bounds + }); + + var response = this.protocol.read({ + maxFeatures: options.single == true ? this.maxFeatures : undefined, + filter: filter, + callback: function(result) { + if(result.code == 1) { + if(result.features.length) { + if(options.single == true) { + this.selectBestFeature(result.features, + bounds.getCenterLonLat(), options); + } else { + this.select(result.features); + } + } else if(options.hover) { + this.hoverSelect(); + } else { + this.events.triggerEvent("clickout"); + if(this.clickout) { + this.unselectAll(); + } + } + } + // Reset the cursor. + OpenLayers.Element.removeClass(this.map.div, "olCursorWait"); + }, + scope: this + }); + if(options.hover == true) { + this.hoverRequest = response.priv; + } + }, + + /** + * Method: selectBestFeature + * Selects the feature from an array of features that is the best match + * for the click position. + * + * Parameters: + * features - {Array()} + * clickPosition - {} + * options - {Object} additional options for this method + * + * Supported options include: + * hover - {Boolean} Do the selection for the hover handler. + */ + selectBestFeature: function(features, clickPosition, options) { + options = options || {}; + if(features.length) { + var point = new OpenLayers.Geometry.Point(clickPosition.lon, + clickPosition.lat); + var feature, resultFeature, dist; + var minDist = Number.MAX_VALUE; + for(var i=0; i} + */ + setModifiers: function(evt) { + this.modifiers = { + multiple: this.multiple || (this.multipleKey && evt[this.multipleKey]), + toggle: this.toggle || (this.toggleKey && evt[this.toggleKey]) + } + }, + + /** + * Method: select + * Add feature to the hash of selected features and trigger the + * featureselected event. + * + * Parameters: + * features - {} or an array of features + */ + select: function(features) { + if(!this.modifiers.multiple && !this.modifiers.toggle) { + this.unselectAll(); + } + if(!(features instanceof Array)) { + features = [features]; + } + + var feature; + for(var i=0, len=features.length; i + * + * Parameters: + * feature - {} the feature to hover-select. + * If none is provided, the current will be nulled and + * the outfeature event will be triggered. + */ + hoverSelect: function(feature) { + var fid = feature ? feature.fid || feature.id : null; + var hfid = this.hoverFeature ? + this.hoverFeature.fid || this.hoverFeature.id : null; + + if(hfid && hfid != fid) { + this.events.triggerEvent("outfeature", + {feature: this.hoverFeature}); + this.hoverFeature = null; + } + if(fid && fid != hfid) { + this.events.triggerEvent("hoverfeature", {feature: feature}); + this.hoverFeature = feature; + } + }, + + /** + * Method: unselect + * Remove feature from the hash of selected features and trigger the + * featureunselected event. + * + * Parameters: + * feature - {} + */ + unselect: function(feature) { + delete this.features[feature.fid || feature.id]; + this.events.triggerEvent("featureunselected", {feature: feature}); + }, + + /** + * Method: unselectAll + * Unselect all selected features. + */ + unselectAll: function() { + // we'll want an option to supress notification here + for(var fid in this.features) { + this.unselect(this.features[fid]); + } + }, + + /** + * Method: setMap + * Set the map property for the control. + * + * Parameters: + * map - {} + */ + setMap: function(map) { + for(var i in this.handlers) { + this.handlers[i].setMap(map); + } + OpenLayers.Control.prototype.setMap.apply(this, arguments); + }, + + /** + * Method: pixelToBounds + * Takes a pixel as argument and creates bounds after adding the + * . + * + * Parameters: + * pixel - {} + */ + pixelToBounds: function(pixel) { + var llPx = pixel.add(-this.clickTolerance/2, this.clickTolerance/2); + var urPx = pixel.add(this.clickTolerance/2, -this.clickTolerance/2); + var ll = this.map.getLonLatFromPixel(llPx); + var ur = this.map.getLonLatFromPixel(urPx); + return new OpenLayers.Bounds(ll.lon, ll.lat, ur.lon, ur.lat); + }, + + CLASS_NAME: "OpenLayers.Control.GetFeature" +}); diff --git a/tests/Control/GetFeature.html b/tests/Control/GetFeature.html new file mode 100644 index 0000000000..75803bccaf --- /dev/null +++ b/tests/Control/GetFeature.html @@ -0,0 +1,104 @@ + + + + + + +
+ + diff --git a/tests/list-tests.html b/tests/list-tests.html index 52338708f8..11b8a53456 100644 --- a/tests/list-tests.html +++ b/tests/list-tests.html @@ -14,6 +14,7 @@
  • Control/DragFeature.html
  • Control/DragPan.html
  • Control/DrawFeature.html
  • +
  • Control/GetFeature.html
  • Control/KeyboardDefaults.html
  • Control/LayerSwitcher.html
  • Control/ModifyFeature.html
  • diff --git a/theme/default/style.css b/theme/default/style.css index 7db68ec5e6..ca1d66602f 100644 --- a/theme/default/style.css +++ b/theme/default/style.css @@ -321,3 +321,10 @@ div.olControlMousePosition { -moz-user-select: none; } +/** + * Cursor styles + */ + +.olCursorWait { + cursor: wait; +} \ No newline at end of file