diff --git a/examples/icon-sprite-webgl.html b/examples/icon-sprite-webgl.html index 7f07409746..1d50a55616 100644 --- a/examples/icon-sprite-webgl.html +++ b/examples/icon-sprite-webgl.html @@ -30,7 +30,7 @@
-
+

Icon sprite with WebGL example

Icon sprite with WebGL.

@@ -39,6 +39,11 @@
webgl, icon, sprite, vector, point
+
+
+   +
+
diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js index 26933f30f1..79993c97bf 100644 --- a/examples/icon-sprite-webgl.js +++ b/examples/icon-sprite-webgl.js @@ -109,3 +109,31 @@ var featureOverlay = new ol.FeatureOverlay({ }), features: overlayFeatures }); + +map.on('click', function(evt) { + var info = document.getElementById('info'); + info.innerHTML = + 'Hold on a second, while I catch those butterflies for you ...'; + + window.setTimeout(function() { + var features = []; + map.forEachFeatureAtPixel(evt.pixel, function(feature, layer) { + features.push(features); + return false; + }); + + if (features.length === 1) { + info.innerHTML = 'Got one butterfly'; + } else if (features.length > 1) { + info.innerHTML = 'Got ' + features.length + ' butterflies'; + } else { + info.innerHTML = 'Couldn\'t catch a single butterfly'; + } + }, 1); +}); + +$(map.getViewport()).on('mousemove', function(evt) { + var pixel = map.getEventPixel(evt.originalEvent); + var hit = map.hasFeatureAtPixel(pixel); + map.getTarget().style.cursor = hit ? 'pointer' : ''; +}); diff --git a/examples/icon.js b/examples/icon.js index 5ffb2e1ad7..1316d68799 100644 --- a/examples/icon.js +++ b/examples/icon.js @@ -5,7 +5,7 @@ goog.require('ol.View'); goog.require('ol.geom.Point'); goog.require('ol.layer.Tile'); goog.require('ol.layer.Vector'); -goog.require('ol.source.OSM'); +goog.require('ol.source.TileJSON'); goog.require('ol.source.Vector'); goog.require('ol.style.Icon'); goog.require('ol.style.Style'); @@ -39,7 +39,10 @@ var vectorLayer = new ol.layer.Vector({ }); var rasterLayer = new ol.layer.Tile({ - source: new ol.source.OSM() + source: new ol.source.TileJSON({ + url: 'http://api.tiles.mapbox.com/v3/mapbox.geography-class.jsonp', + crossOrigin: '' + }) }); var map = new ol.Map({ @@ -85,12 +88,6 @@ map.on('click', function(evt) { // change mouse cursor when over marker $(map.getViewport()).on('mousemove', function(e) { var pixel = map.getEventPixel(e.originalEvent); - var hit = map.forEachFeatureAtPixel(pixel, function(feature, layer) { - return true; - }); - if (hit) { - map.getTarget().style.cursor = 'pointer'; - } else { - map.getTarget().style.cursor = ''; - } + var hit = map.hasFeatureAtPixel(pixel); + map.getTarget().style.cursor = hit ? 'pointer' : ''; }); diff --git a/src/ol/map.js b/src/ol/map.js index 38bf57732d..6b1c51fb33 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -589,6 +589,34 @@ ol.Map.prototype.forEachFeatureAtPixel = }; +/** + * Detect if features intersect a pixel on the viewport. Layers included in the + * detection can be configured through `opt_layerFilter`. Feature overlays will + * always be included in the detection. + * @param {ol.Pixel} pixel Pixel. + * @param {(function(this: U, ol.layer.Layer): boolean)=} opt_layerFilter Layer + * filter function, only layers which are visible and for which this + * function returns `true` will be tested for features. By default, all + * visible layers will be tested. Feature overlays will always be tested. + * @param {U=} opt_this2 Value to use as `this` when executing `layerFilter`. + * @return {boolean} Is there a feature at the given pixel? + * @template U + * @api + */ +ol.Map.prototype.hasFeatureAtPixel = + function(pixel, opt_layerFilter, opt_this2) { + if (goog.isNull(this.frameState_)) { + return false; + } + var coordinate = this.getCoordinateFromPixel(pixel); + var layerFilter = goog.isDef(opt_layerFilter) ? + opt_layerFilter : goog.functions.TRUE; + var thisArg2 = goog.isDef(opt_this2) ? opt_this2 : null; + return this.renderer_.hasFeatureAtPixel( + coordinate, this.frameState_, layerFilter, thisArg2); +}; + + /** * Returns the geographical coordinate for a browser event. * @param {Event} event Event. diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js index 36ff9183f5..4f309d0fd7 100644 --- a/src/ol/render/webgl/webglreplay.js +++ b/src/ol/render/webgl/webglreplay.js @@ -557,6 +557,7 @@ ol.render.webgl.ImageReplay.prototype.createTextures_ = * @param {number} saturation Global saturation. * @param {Object} skippedFeaturesHash Ids of features to skip. * @param {function(ol.Feature): T|undefined} featureCallback Feature callback. + * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion. * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting * this extent are checked. * @return {T|undefined} Callback result. @@ -565,7 +566,7 @@ ol.render.webgl.ImageReplay.prototype.createTextures_ = ol.render.webgl.ImageReplay.prototype.replay = function(context, center, resolution, rotation, size, pixelRatio, opacity, brightness, contrast, hue, saturation, skippedFeaturesHash, - featureCallback, opt_hitExtent) { + featureCallback, oneByOne, opt_hitExtent) { var gl = context.getGL(); // bind the vertices buffer @@ -672,7 +673,7 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, } else { // draw feature by feature for the hit-detection result = this.drawHitDetectionReplay_(gl, context, featureCallback, - opt_hitExtent); + oneByOne, opt_hitExtent); } // disable the vertex attrib arrays @@ -715,21 +716,82 @@ ol.render.webgl.ImageReplay.prototype.drawReplay_ = * @param {WebGLRenderingContext} gl gl. * @param {ol.webgl.Context} context Context. * @param {function(ol.Feature): T|undefined} featureCallback Feature callback. + * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion. * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting * this extent are checked. * @return {T|undefined} Callback result. * @template T */ ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplay_ = - function(gl, context, featureCallback, opt_hitExtent) { + function(gl, context, featureCallback, oneByOne, opt_hitExtent) { goog.asserts.assert(this.hitDetectionTextures_.length === this.hitDetectionGroupIndices_.length); var elementType = context.hasOESElementIndexUint ? goog.webgl.UNSIGNED_INT : goog.webgl.UNSIGNED_SHORT; var elementSize = context.hasOESElementIndexUint ? 4 : 2; - var i, groupStart, groupEnd, numItems, start, end, feature; + if (!oneByOne) { + // draw all hit-detection features in "once" (by texture group) + return this.drawHitDetectionReplayAll_(gl, context, featureCallback, + elementType, elementSize); + } else { + // draw hit-detection features one by one + return this.drawHitDetectionReplayOneByOne_(gl, context, featureCallback, + elementType, elementSize, opt_hitExtent); + } +}; + + +/** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @param {function(ol.Feature): T|undefined} featureCallback Feature callback. + * @param {number} elementType Element type. + * @param {number} elementSize Element size. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayAll_ = + function(gl, context, featureCallback, elementType, elementSize) { + var i, ii, start; + for (i = 0, ii = this.hitDetectionTextures_.length, start = 0; i < ii; ++i) { + gl.bindTexture(goog.webgl.TEXTURE_2D, this.hitDetectionTextures_[i]); + var end = this.hitDetectionGroupIndices_[i]; + var numItems = end - start; + var offsetInBytes = start * elementSize; + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + gl.drawElements(goog.webgl.TRIANGLES, numItems, elementType, offsetInBytes); + start = end; + } + + var result = featureCallback(null); + if (result) { + return result; + } else { + return undefined; + } +}; + + +/** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @param {function(ol.Feature): T|undefined} featureCallback Feature callback. + * @param {number} elementType Element type. + * @param {number} elementSize Element size. + * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting + * this extent are checked. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayOneByOne_ = + function(gl, context, featureCallback, elementType, elementSize, + opt_hitExtent) { + var i, groupStart, numItems, start, end, feature; var featureIndex = this.startIndices_.length - 1; + for (i = this.hitDetectionTextures_.length - 1; i >= 0; --i) { gl.bindTexture(goog.webgl.TEXTURE_2D, this.hitDetectionTextures_[i]); groupStart = (i > 0) ? this.hitDetectionGroupIndices_[i - 1] : 0; @@ -957,7 +1019,7 @@ ol.render.webgl.ReplayGroup.prototype.replay = function(context, replay.replay(context, center, resolution, rotation, size, pixelRatio, opacity, brightness, contrast, hue, saturation, skippedFeaturesHash, - undefined); + undefined, false); } } }; @@ -978,6 +1040,7 @@ ol.render.webgl.ReplayGroup.prototype.replay = function(context, * @param {number} saturation Global saturation. * @param {Object} skippedFeaturesHash Ids of features to skip. * @param {function(ol.Feature): T|undefined} featureCallback Feature callback. + * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion. * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting * this extent are checked. * @return {T|undefined} Callback result. @@ -986,7 +1049,7 @@ ol.render.webgl.ReplayGroup.prototype.replay = function(context, ol.render.webgl.ReplayGroup.prototype.replayHitDetection_ = function(context, center, resolution, rotation, size, pixelRatio, opacity, brightness, contrast, hue, saturation, skippedFeaturesHash, - featureCallback, opt_hitExtent) { + featureCallback, oneByOne, opt_hitExtent) { var i, replay, result; for (i = ol.render.REPLAY_ORDER.length - 1; i >= 0; --i) { replay = this.replays_[ol.render.REPLAY_ORDER[i]]; @@ -994,7 +1057,7 @@ ol.render.webgl.ReplayGroup.prototype.replayHitDetection_ = function(context, result = replay.replay(context, center, resolution, rotation, size, pixelRatio, opacity, brightness, contrast, hue, saturation, - skippedFeaturesHash, featureCallback, opt_hitExtent); + skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent); if (result) { return result; } @@ -1061,7 +1124,49 @@ ol.render.webgl.ReplayGroup.prototype.forEachFeatureAtPixel = function( return result; } } - }, hitExtent); + }, true, hitExtent); +}; + + +/** + * @param {ol.webgl.Context} context Context. + * @param {ol.Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {ol.Size} size Size. + * @param {number} pixelRatio Pixel ratio. + * @param {number} opacity Global opacity. + * @param {number} brightness Global brightness. + * @param {number} contrast Global contrast. + * @param {number} hue Global hue. + * @param {number} saturation Global saturation. + * @param {Object} skippedFeaturesHash Ids of features to skip. + * @param {ol.Coordinate} coordinate Coordinate. + * @return {boolean} Is there a feature at the given pixel? + */ +ol.render.webgl.ReplayGroup.prototype.hasFeatureAtPixel = function( + context, center, resolution, rotation, size, pixelRatio, + opacity, brightness, contrast, hue, saturation, skippedFeaturesHash, + coordinate) { + var gl = context.getGL(); + gl.bindFramebuffer( + gl.FRAMEBUFFER, context.getHitDetectionFramebuffer()); + + var hasFeature = this.replayHitDetection_(context, + coordinate, resolution, rotation, ol.render.webgl.HIT_DETECTION_SIZE_, + pixelRatio, opacity, brightness, contrast, hue, saturation, + skippedFeaturesHash, + /** + * @param {ol.Feature} feature Feature. + * @return {boolean} Is there a feature? + */ + function(feature) { + var imageData = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData); + return imageData[3] > 0; + }, false); + + return goog.isDef(hasFeature); }; diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index 235ccff4c7..77b76f17f9 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -56,6 +56,14 @@ goog.inherits(ol.renderer.Layer, goog.Disposable); ol.renderer.Layer.prototype.forEachFeatureAtPixel = goog.nullFunction; +/** + * @param {ol.Coordinate} coordinate Coordinate. + * @param {olx.FrameState} frameState Frame state. + * @return {boolean} Is there a feature at the given pixel? + */ +ol.renderer.Layer.prototype.hasFeatureAtPixel = goog.functions.FALSE; + + /** * @protected * @return {ol.layer.Layer} Layer. diff --git a/src/ol/renderer/maprenderer.js b/src/ol/renderer/maprenderer.js index 7dd9bc0025..1b147e71a3 100644 --- a/src/ol/renderer/maprenderer.js +++ b/src/ol/renderer/maprenderer.js @@ -169,6 +169,26 @@ ol.renderer.Map.prototype.forEachFeatureAtPixel = }; +/** + * @param {ol.Coordinate} coordinate Coordinate. + * @param {olx.FrameState} frameState FrameState. + * @param {function(this: U, ol.layer.Layer): boolean} layerFilter Layer filter + * function, only layers which are visible and for which this function + * returns `true` will be tested for features. By default, all visible + * layers will be tested. + * @param {U} thisArg Value to use as `this` when executing `layerFilter`. + * @return {boolean} Is there a feature at the given pixel? + * @template U + */ +ol.renderer.Map.prototype.hasFeatureAtPixel = + function(coordinate, frameState, layerFilter, thisArg) { + var hasFeature = this.forEachFeatureAtPixel( + coordinate, frameState, goog.functions.TRUE, this, layerFilter, thisArg); + + return goog.isDef(hasFeature); +}; + + /** * @param {ol.layer.Layer} layer Layer. * @protected diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index 1d6efe5372..fa9425b647 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -601,8 +601,56 @@ ol.renderer.webgl.Map.prototype.forEachFeatureAtPixel = }; +/** + * @inheritDoc + */ +ol.renderer.webgl.Map.prototype.hasFeatureAtPixel = + function(coordinate, frameState, layerFilter, thisArg) { + var hasFeature = false; + + if (this.getGL().isContextLost()) { + return false; + } + + var context = this.getContext(); + var viewState = frameState.viewState; + + // do the hit-detection for the overlays first + if (!goog.isNull(this.replayGroup)) { + // use default color values + var d = ol.renderer.webgl.Map.DEFAULT_COLOR_VALUES_; + + hasFeature = this.replayGroup.hasFeatureAtPixel(context, + viewState.center, viewState.resolution, viewState.rotation, + frameState.size, frameState.pixelRatio, + d.opacity, d.brightness, d.contrast, d.hue, d.saturation, {}, + coordinate); + if (hasFeature) { + return true; + } + } + var layerStates = this.getMap().getLayerGroup().getLayerStatesArray(); + var numLayers = layerStates.length; + var i; + for (i = numLayers - 1; i >= 0; --i) { + var layerState = layerStates[i]; + var layer = layerState.layer; + if (ol.layer.Layer.visibleAtResolution(layerState, viewState.resolution) && + layerFilter.call(thisArg, layer)) { + var layerRenderer = this.getLayerRenderer(layer); + hasFeature = layerRenderer.hasFeatureAtPixel(coordinate, frameState); + if (hasFeature) { + return true; + } + } + } + return hasFeature; +}; + + /** * @private + * @const */ ol.renderer.webgl.Map.DEFAULT_COLOR_VALUES_ = { opacity: 1, diff --git a/src/ol/renderer/webgl/webglvectorlayerrenderer.js b/src/ol/renderer/webgl/webglvectorlayerrenderer.js index fac9f58d50..f2a00be9e0 100644 --- a/src/ol/renderer/webgl/webglvectorlayerrenderer.js +++ b/src/ol/renderer/webgl/webglvectorlayerrenderer.js @@ -140,6 +140,28 @@ ol.renderer.webgl.VectorLayer.prototype.forEachFeatureAtPixel = }; +/** + * @inheritDoc + */ +ol.renderer.webgl.VectorLayer.prototype.hasFeatureAtPixel = + function(coordinate, frameState) { + if (goog.isNull(this.replayGroup_) || goog.isNull(this.layerState_)) { + return false; + } else { + var mapRenderer = this.getWebGLMapRenderer(); + var context = mapRenderer.getContext(); + var viewState = frameState.viewState; + var layerState = this.layerState_; + return this.replayGroup_.hasFeatureAtPixel(context, + viewState.center, viewState.resolution, viewState.rotation, + frameState.size, frameState.pixelRatio, + layerState.opacity, layerState.brightness, layerState.contrast, + layerState.hue, layerState.saturation, frameState.skippedFeatureUids, + coordinate); + } +}; + + /** * Handle changes in image style state. * @param {goog.events.Event} event Image style change event.