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.