diff --git a/examples/getfeatureinfo.html b/examples/getfeatureinfo.html new file mode 100644 index 0000000000..d7a0095198 --- /dev/null +++ b/examples/getfeatureinfo.html @@ -0,0 +1,61 @@ + + + + + + + + + + + Get feature info example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

GetFeatureInfo example

+

Example of a WMS layer and a vector layer, both configured to provide feature information on click.

+
+

See the getfeatureinfo.js source to see how this is done.

+
+
getfeatureinfo
+
+
+
+   +
+
+ +
+ +
+ + + + + + diff --git a/examples/getfeatureinfo.js b/examples/getfeatureinfo.js new file mode 100644 index 0000000000..02f3bc06ee --- /dev/null +++ b/examples/getfeatureinfo.js @@ -0,0 +1,58 @@ +goog.require('ol.Map'); +goog.require('ol.RendererHint'); +goog.require('ol.View2D'); +goog.require('ol.layer.TileLayer'); +goog.require('ol.layer.Vector'); +goog.require('ol.parser.GeoJSON'); +goog.require('ol.source.TiledWMS'); +goog.require('ol.source.Vector'); +goog.require('ol.style.Polygon'); +goog.require('ol.style.Rule'); +goog.require('ol.style.Style'); + + +var wms = new ol.layer.TileLayer({ + source: new ol.source.TiledWMS({ + url: 'http://demo.opengeo.org/geoserver/wms', + params: {'LAYERS': 'ne:ne'} + }) +}); + +var vector = new ol.layer.Vector({ + source: new ol.source.Vector({ + parser: new ol.parser.GeoJSON(), + url: 'data/countries.geojson' + }), + style: new ol.style.Style({rules: [ + new ol.style.Rule({ + symbolizers: [ + new ol.style.Polygon({ + strokeColor: '#bada55' + }) + ] + }) + ]}), + transformFeatureInfo: function(features) { + return features.length > 0 ? + features[0].getFeatureId() + ': ' + features[0].get('name') : ' '; + } +}); + +var map = new ol.Map({ + layers: [wms, vector], + renderer: ol.RendererHint.CANVAS, + target: 'map', + view: new ol.View2D({ + center: [0, 0], + zoom: 1 + }) +}); + +map.on('click', function(evt) { + map.getFeatureInfo({ + pixel: evt.getPixel(), + success: function(featureInfoByLayer) { + document.getElementById('info').innerHTML = featureInfoByLayer.join(''); + } + }); +}); diff --git a/examples/gpx.js b/examples/gpx.js index 44e307effb..28e3c05c43 100644 --- a/examples/gpx.js +++ b/examples/gpx.js @@ -15,14 +15,7 @@ var vector = new ol.layer.Vector({ source: new ol.source.Vector({ parser: new ol.parser.GPX(), url: 'data/gpx/yahoo.xml' - }), - transformFeatureInfo: function(features) { - var info = []; - for (var i = 0, ii = features.length; i < ii; ++i) { - info.push(features[i].get('name') + ': ' + features[i].get('type')); - } - return info.join(', '); - } + }) }); var map = new ol.Map({ @@ -36,11 +29,16 @@ var map = new ol.Map({ }); map.on(['click', 'mousemove'], function(evt) { - map.getFeatureInfo({ + map.getFeatures({ pixel: evt.getPixel(), layers: [vector], - success: function(featureInfo) { - document.getElementById('info').innerHTML = featureInfo[0] || ' '; + success: function(featuresByLayer) { + var features = featuresByLayer[0]; + var info = []; + for (var i = 0, ii = features.length; i < ii; ++i) { + info.push(features[i].get('name') + ': ' + features[i].get('type')); + } + document.getElementById('info').innerHTML = info.join(', ') || ' '; } }); }); diff --git a/examples/kml.js b/examples/kml.js index 772eb74928..148b73a1f4 100644 --- a/examples/kml.js +++ b/examples/kml.js @@ -25,14 +25,7 @@ var vector = new ol.layer.Vector({ maxDepth: 1, dimension: 2, extractStyles: true, extractAttributes: true }), url: 'data/kml/lines.kml' - }), - transformFeatureInfo: function(features) { - var info = []; - for (var i = 0, ii = features.length; i < ii; ++i) { - info.push(features[i].get('name')); - } - return info.join(', '); - } + }) }); var map = new ol.Map({ @@ -47,11 +40,16 @@ var map = new ol.Map({ }); map.on(['click', 'mousemove'], function(evt) { - map.getFeatureInfo({ + map.getFeatures({ pixel: evt.getPixel(), layers: [vector], - success: function(featureInfo) { - document.getElementById('info').innerHTML = featureInfo[0] || ' '; + success: function(featuresByLayer) { + var features = featuresByLayer[0]; + var info = []; + for (var i = 0, ii = features.length; i < ii; ++i) { + info.push(features[i].get('name')); + } + document.getElementById('info').innerHTML = info.join(', ') || ' '; } }); }); diff --git a/examples/vector-layer.js b/examples/vector-layer.js index c799c64789..6d0ba24ba4 100644 --- a/examples/vector-layer.js +++ b/examples/vector-layer.js @@ -49,11 +49,7 @@ var vector = new ol.layer.Vector({ }) ] }) - ]}), - transformFeatureInfo: function(features) { - return features.length > 0 ? - features[0].getFeatureId() + ': ' + features[0].get('name') : ' '; - } + ]}) }); var map = new ol.Map({ @@ -67,11 +63,14 @@ var map = new ol.Map({ }); map.on(['click', 'mousemove'], function(evt) { - map.getFeatureInfo({ + map.getFeatures({ pixel: evt.getPixel(), layers: [vector], - success: function(featureInfo) { - document.getElementById('info').innerHTML = featureInfo[0]; + success: function(featuresByLayer) { + var features = featuresByLayer[0]; + document.getElementById('info').innerHTML = features.length > 0 ? + features[0].getFeatureId() + ': ' + features[0].get('name') : + ' '; } }); }); diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index d2962adc37..5231e4d45d 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -20,9 +20,9 @@ * information for each layer, with array indices being the same as in the * passed `layers` array or in the layer collection as returned from * `ol.Map#getLayers()` if no `layers` were provided. - * @property {function(Object)|undefined} error Callback for unsuccessful - * queries. Note that queries with no matching features trigger the success - * callback, not the error callback. + * @property {function()|undefined} error Callback for unsuccessful queries. + * Note that queries with no matching features trigger the success callback, + * not the error callback. */ /** @@ -35,9 +35,9 @@ * each layer, with array indices being the same as in the passed `layers` * array or in the layer collection as returned from `ol.Map#getLayers()` if * no layers were provided. - * @property {function(Object)|undefined} error Callback for unsuccessful - * queries. Note that queries with no matching features trigger the success - * callback, not the error callback. + * @property {function()|undefined} error Callback for unsuccessful queries. + * Note that queries with no matching features trigger the success callback, + * not the error callback. */ /** @@ -450,6 +450,8 @@ * @property {null|string|undefined} crossOrigin crossOrigin setting for image * requests. * @property {ol.Extent|undefined} extent Extent. + * @property {ol.source.WMSGetFeatureInfoOptions|undefined} + * getFeatureInfoOptions Options for GetFeatureInfo. * @property {Object.} params WMS request parameters. At least a * `LAYERS` param is required. `STYLES` is '' by default. `VERSION` is * '1.3.0' by default. `WIDTH`, `HEIGHT`, `BBOX` and `CRS` (`SRS` for WMS @@ -509,6 +511,8 @@ * @property {null|string|undefined} crossOrigin crossOrigin setting for image * requests. * @property {ol.Extent|undefined} extent Extent. + * @property {ol.source.WMSGetFeatureInfoOptions|undefined} + * getFeatureInfoOptions Options for GetFeatureInfo. * @property {ol.tilegrid.TileGrid|undefined} tileGrid Tile grid. * @property {number|undefined} maxZoom Maximum zoom. * @property {ol.ProjectionLike} projection Projection. @@ -543,6 +547,16 @@ * @property {ol.ProjectionLike} projection Projection. */ + +/** + * @typedef {Object} ol.source.WMSGetFeatureInfoOptions + * @property {ol.source.WMSGetFeatureInfoMethod} method Method for requesting + * GetFeatureInfo. Default is `ol.source.WMSGetFeatureInfoMethod.IFRAME`. + * @property {Object} params Params for the GetFeatureInfo request. Default is + * `{'INFO_FORMAT': 'text/html'}`. + */ + + /** * @typedef {Object} ol.source.WMTSOptions * @property {Array.|undefined} attributions Attributions. diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index 3c87277977..0eff0ddd05 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -73,6 +73,26 @@ ol.renderer.Layer = function(mapRenderer, layer) { goog.inherits(ol.renderer.Layer, goog.Disposable); +/** + * @param {ol.Pixel} pixel Pixel coordinate relative to the map viewport. + * @param {function(string, ol.layer.Layer)} success Callback for + * successful queries. The passed arguments are the resulting feature + * information and the layer. + * @param {function()=} opt_error Callback for unsuccessful queries. + */ +ol.renderer.Layer.prototype.getFeatureInfoForPixel = + function(pixel, success, opt_error) { + var layer = this.getLayer(); + var source = layer.getSource(); + if (goog.isFunction(source.getFeatureInfoForPixel)) { + var callback = function(layerFeatureInfo) { + success(layerFeatureInfo, layer); + }; + source.getFeatureInfoForPixel(pixel, this.getMap(), callback, opt_error); + } +}; + + /** * @protected * @return {ol.layer.Layer} Layer. diff --git a/src/ol/renderer/maprenderer.js b/src/ol/renderer/maprenderer.js index 4fedb0fff4..100246ffe2 100644 --- a/src/ol/renderer/maprenderer.js +++ b/src/ol/renderer/maprenderer.js @@ -106,7 +106,7 @@ ol.renderer.Map.prototype.getCanvas = goog.functions.NULL; * information. Layers that are able to provide attribute data will put * ol.Feature instances, other layers will put a string which can either * be plain text or markup. - * @param {function(Object)=} opt_error Callback for unsuccessful + * @param {function()=} opt_error Callback for unsuccessful * queries. */ ol.renderer.Map.prototype.getFeatureInfoForPixel = @@ -142,7 +142,7 @@ ol.renderer.Map.prototype.getFeatureInfoForPixel = * information. Layers that are able to provide attribute data will put * ol.Feature instances, other layers will put a string which can either * be plain text or markup. - * @param {function(Object)=} opt_error Callback for unsuccessful + * @param {function()=} opt_error Callback for unsuccessful * queries. */ ol.renderer.Map.prototype.getFeaturesForPixel = diff --git a/src/ol/source/featureinfosource.js b/src/ol/source/featureinfosource.js new file mode 100644 index 0000000000..6d061866c0 --- /dev/null +++ b/src/ol/source/featureinfosource.js @@ -0,0 +1,18 @@ +goog.provide('ol.source.FeatureInfoSource'); + + + +/** + * @interface + */ +ol.source.FeatureInfoSource = function() {}; + + +/** + * @param {ol.Pixel} pixel Pixel. + * @param {ol.Map} map The map that the pixel belongs to. + * @param {function(string)} success Callback with feature info. + * @param {function()=} opt_error Optional error callback. + */ +ol.source.FeatureInfoSource.prototype.getFeatureInfoForPixel = + goog.abstractMethod; diff --git a/src/ol/source/singleimagewmssource.js b/src/ol/source/singleimagewmssource.js index c1b9f1dd0d..459b37e0b6 100644 --- a/src/ol/source/singleimagewmssource.js +++ b/src/ol/source/singleimagewmssource.js @@ -1,8 +1,10 @@ goog.provide('ol.source.SingleImageWMS'); +goog.require('goog.asserts'); goog.require('ol.Image'); goog.require('ol.ImageUrlFunction'); goog.require('ol.extent'); +goog.require('ol.source.FeatureInfoSource'); goog.require('ol.source.ImageSource'); goog.require('ol.source.wms'); @@ -11,6 +13,7 @@ goog.require('ol.source.wms'); /** * @constructor * @extends {ol.source.ImageSource} + * @implements {ol.source.FeatureInfoSource} * @param {ol.source.SingleImageWMSOptions} options Options. */ ol.source.SingleImageWMS = function(options) { @@ -28,6 +31,13 @@ ol.source.SingleImageWMS = function(options) { imageUrlFunction: imageUrlFunction }); + /** + * @private + * @type {ol.source.WMSGetFeatureInfoOptions} + */ + this.getFeatureInfoOptions_ = goog.isDef(options.getFeatureInfoOptions) ? + options.getFeatureInfoOptions : {}; + /** * @private * @type {ol.Image} @@ -68,3 +78,22 @@ ol.source.SingleImageWMS.prototype.getImage = this.image_ = this.createImage(extent, resolution, size, projection); return this.image_; }; + + +/** + * @inheritDoc + */ +ol.source.SingleImageWMS.prototype.getFeatureInfoForPixel = + function(pixel, map, success, opt_error) { + var view2D = map.getView().getView2D(), + projection = view2D.getProjection(), + size = map.getSize(), + bottomLeft = map.getCoordinateFromPixel([0, size[1]]), + topRight = map.getCoordinateFromPixel([size[0], 0]), + extent = [bottomLeft[0], topRight[0], bottomLeft[1], topRight[1]], + url = this.imageUrlFunction(extent, size, projection); + goog.asserts.assert(goog.isDef(url), + 'ol.source.SingleImageWMS#imageUrlFunction does not return a url'); + ol.source.wms.getFeatureInfo(url, pixel, this.getFeatureInfoOptions_, success, + opt_error); +}; diff --git a/src/ol/source/tiledwmssource.js b/src/ol/source/tiledwmssource.js index b2ee6562f7..6f3b1817cd 100644 --- a/src/ol/source/tiledwmssource.js +++ b/src/ol/source/tiledwmssource.js @@ -3,10 +3,12 @@ goog.provide('ol.source.TiledWMS'); goog.require('goog.array'); +goog.require('goog.asserts'); goog.require('goog.math'); goog.require('ol.TileCoord'); goog.require('ol.TileUrlFunction'); goog.require('ol.extent'); +goog.require('ol.source.FeatureInfoSource'); goog.require('ol.source.ImageTileSource'); goog.require('ol.source.wms'); @@ -15,9 +17,11 @@ goog.require('ol.source.wms'); /** * @constructor * @extends {ol.source.ImageTileSource} + * @implements {ol.source.FeatureInfoSource} * @param {ol.source.TiledWMSOptions} options Tiled WMS options. */ ol.source.TiledWMS = function(options) { + var tileGrid; if (goog.isDef(options.tileGrid)) { tileGrid = options.tileGrid; @@ -28,6 +32,7 @@ ol.source.TiledWMS = function(options) { if (!goog.isDef(urls) && goog.isDef(options.url)) { urls = ol.TileUrlFunction.expandUrl(options.url); } + if (goog.isDef(urls)) { var tileUrlFunctions = goog.array.map( urls, function(url) { @@ -81,5 +86,35 @@ ol.source.TiledWMS = function(options) { tileCoordTransform, tileUrlFunction) }); + /** + * @private + * @type {ol.source.WMSGetFeatureInfoOptions} + */ + this.getFeatureInfoOptions_ = goog.isDef(options.getFeatureInfoOptions) ? + options.getFeatureInfoOptions : {}; + }; goog.inherits(ol.source.TiledWMS, ol.source.ImageTileSource); + + +/** + * @inheritDoc + */ +ol.source.TiledWMS.prototype.getFeatureInfoForPixel = + function(pixel, map, success, opt_error) { + var coord = map.getCoordinateFromPixel(pixel), + view2D = map.getView().getView2D(), + projection = view2D.getProjection(), + tileGrid = goog.isNull(this.tileGrid) ? + ol.tilegrid.getForProjection(projection) : this.tileGrid, + tileCoord = tileGrid.getTileCoordForCoordAndResolution(coord, + view2D.getResolution()), + tileExtent = tileGrid.getTileCoordExtent(tileCoord), + offset = map.getPixelFromCoordinate(ol.extent.getTopLeft(tileExtent)), + url = this.tileUrlFunction(tileCoord, projection); + goog.asserts.assert(goog.isDef(url), + 'ol.source.TiledWMS#tileUrlFunction does not return a url'); + ol.source.wms.getFeatureInfo(url, + [pixel[0] - offset[0], pixel[1] - offset[1]], this.getFeatureInfoOptions_, + success, opt_error); +}; diff --git a/src/ol/source/wmssource.exports b/src/ol/source/wmssource.exports new file mode 100644 index 0000000000..7e9a24f714 --- /dev/null +++ b/src/ol/source/wmssource.exports @@ -0,0 +1 @@ +@exportSymbol ol.source.WMSGetFeatureInfoMethod diff --git a/src/ol/source/wmssource.js b/src/ol/source/wmssource.js index 9eb6e22be7..6bf7aee552 100644 --- a/src/ol/source/wmssource.js +++ b/src/ol/source/wmssource.js @@ -1,9 +1,29 @@ +goog.provide('ol.source.WMSGetFeatureInfoMethod'); goog.provide('ol.source.wms'); +goog.require('goog.net.XhrIo'); goog.require('goog.object'); goog.require('goog.uri.utils'); +/** + * Method to use to get WMS feature info. + * @enum {string} + */ +ol.source.WMSGetFeatureInfoMethod = { + /** + * Load the info in an IFRAME. Only works with 'text/html and 'text/plain' as + * `INFO_FORMAT`. + */ + IFRAME: 'iframe', + /** + * Use an asynchronous GET request. Requires CORS headers or a server at the + * same origin as the application script. + */ + XHR_GET: 'xhr_get' +}; + + /** * @param {string} baseUrl WMS base url. * @param {Object.} params Request parameters. @@ -40,3 +60,60 @@ ol.source.wms.getUrl = return goog.uri.utils.appendParamsFromMap(baseUrl, baseParams); }; + + +/** + * @param {string} url URL as provided by the url function. + * @param {ol.Pixel} pixel Pixel. + * @param {Object} options Options as defined in the source. + * @param {function(string)} success Callback function for successful queries. + * @param {function()=} opt_error Optional callback function for unsuccessful + * queries. + */ +ol.source.wms.getFeatureInfo = + function(url, pixel, options, success, opt_error) { + // TODO: This could be done in a smarter way if the url function was not a + // closure + url = url.replace('REQUEST=GetMap', 'REQUEST=GetFeatureInfo') + .replace(ol.source.wms.regExes.layers, 'LAYERS=$1&QUERY_LAYERS=$1'); + options = goog.isDef(options) ? goog.object.clone(options) : {}; + var localOptions = { + method: ol.source.WMSGetFeatureInfoMethod.IFRAME, + params: {} + }; + goog.object.extend(localOptions, options); + var params = {'INFO_FORMAT': 'text/html'}, + version = parseFloat(url.match(ol.source.wms.regExes.version)[1]), + x = Math.round(pixel[0]), + y = Math.round(pixel[1]); + if (version >= 1.3) { + goog.object.extend(params, {'I': x, 'J': y}); + } else { + goog.object.extend(params, {'X': x, 'Y': y}); + } + goog.object.extend(params, localOptions.params); + url = goog.uri.utils.appendParamsFromMap(url, params); + if (localOptions.method == ol.source.WMSGetFeatureInfoMethod.IFRAME) { + goog.global.setTimeout(function() { + success(''); + }, 0); + } else if (localOptions.method == ol.source.WMSGetFeatureInfoMethod.XHR_GET) { + goog.net.XhrIo.send(url, function(event) { + var xhr = event.target; + if (xhr.isSuccess()) { + success(xhr.getResponseText()); + } else if (goog.isDef(opt_error)) { + opt_error(); + } + }); + } +}; + + +/** + * @enum {RegExp} + */ +ol.source.wms.regExes = { + layers: (/LAYERS=([^&]+)/), + version: (/VERSION=([^&]+)/) +}; diff --git a/test/spec/ol/source/wmssource.test.js b/test/spec/ol/source/wmssource.test.js index cb4a6248ec..af93e5c7e8 100644 --- a/test/spec/ol/source/wmssource.test.js +++ b/test/spec/ol/source/wmssource.test.js @@ -26,8 +26,30 @@ describe('ol.source.wms', function() { }); }); + describe('ol.source.wms.getFeatureInfo', function() { + it('calls a callback with a feature info IFRAME as result', function(done) { + ol.source.wms.getFeatureInfo('?REQUEST=GetMap&VERSION=1.3&LAYERS=foo', + [5, 10], {params: {'INFO_FORMAT': 'text/plain'}}, + function(info) { + expect(info).to.eql(''); + done(); + }); + }); + it('can do xhr to retrieve feature info', function(done) { + ol.source.wms.getFeatureInfo('?REQUEST=GetMap&VERSION=1.1.1&LAYERS=foo', + [5, 10], {method: ol.source.WMSGetFeatureInfoMethod.XHR_GET}, + function(info) { + expect(info).to.contain(''); + done(); + }); + }); + }); + }); goog.require('ol.proj'); +goog.require('ol.source.WMSGetFeatureInfoMethod'); goog.require('ol.source.wms');