diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index c48e02aca3..2fb008dbb1 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -612,29 +612,22 @@ PluggableMap.prototype.getFeaturesAtPixel = function(pixel, opt_options) { * [R, G, B, A] pixel values (0 - 255) and will be `null` for layer types * that do not currently support this argument. To stop detection, callback * functions can return a truthy value. - * @param {S=} opt_this Value to use as `this` when executing `callback`. - * @param {(function(this: U, module:ol/layer/Layer): boolean)=} opt_layerFilter Layer - * filter function. The filter function will receive one argument, the - * {@link module:ol/layer/Layer layer-candidate} and it should return a boolean - * value. 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=} opt_this2 Value to use as `this` when executing `layerFilter`. + * @param {module:ol/PluggableMap~AtPixelOptions=} opt_options Configuration options. * @return {T|undefined} Callback result, i.e. the return value of last * callback execution, or the first truthy callback return value. - * @template S,T,U + * @template S,T * @api */ -PluggableMap.prototype.forEachLayerAtPixel = function(pixel, callback, opt_this, opt_layerFilter, opt_this2) { +PluggableMap.prototype.forEachLayerAtPixel = function(pixel, callback, opt_options) { if (!this.frameState_) { return; } - const thisArg = opt_this !== undefined ? opt_this : null; - const layerFilter = opt_layerFilter !== undefined ? opt_layerFilter : TRUE; - const thisArg2 = opt_this2 !== undefined ? opt_this2 : null; + const options = opt_options || {}; + const hitTolerance = options.hitTolerance !== undefined ? + opt_options.hitTolerance * this.frameState_.pixelRatio : 0; + const layerFilter = options.layerFilter || TRUE; return this.renderer_.forEachLayerAtPixel( - pixel, this.frameState_, callback, thisArg, - layerFilter, thisArg2); + pixel, this.frameState_, hitTolerance, callback, null, layerFilter, null); }; diff --git a/src/ol/renderer/Map.js b/src/ol/renderer/Map.js index 530c910476..e1e64a40c1 100644 --- a/src/ol/renderer/Map.js +++ b/src/ol/renderer/Map.js @@ -181,6 +181,7 @@ MapRenderer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameSta * @abstract * @param {module:ol~Pixel} pixel Pixel. * @param {module:ol/PluggableMap~FrameState} frameState FrameState. + * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(this: S, module:ol/layer/Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer * callback. * @param {S} thisArg Value to use as `this` when executing `callback`. @@ -192,7 +193,7 @@ MapRenderer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameSta * @return {T|undefined} Callback result. * @template S,T,U */ -MapRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg, +MapRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) {}; diff --git a/src/ol/renderer/canvas/IntermediateCanvas.js b/src/ol/renderer/canvas/IntermediateCanvas.js index adf64c6bf3..a86897399d 100644 --- a/src/ol/renderer/canvas/IntermediateCanvas.js +++ b/src/ol/renderer/canvas/IntermediateCanvas.js @@ -119,7 +119,7 @@ IntermediateCanvasRenderer.prototype.forEachFeatureAtCoordinate = function(coord /** * @inheritDoc */ -IntermediateCanvasRenderer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) { +IntermediateCanvasRenderer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { if (!this.getImage()) { return undefined; } @@ -147,4 +147,5 @@ IntermediateCanvasRenderer.prototype.forEachLayerAtCoordinate = function(coordin } } }; + export default IntermediateCanvasRenderer; diff --git a/src/ol/renderer/canvas/Layer.js b/src/ol/renderer/canvas/Layer.js index 5decbcd76e..f07024db51 100644 --- a/src/ol/renderer/canvas/Layer.js +++ b/src/ol/renderer/canvas/Layer.js @@ -101,14 +101,15 @@ CanvasLayerRenderer.prototype.dispatchComposeEvent_ = function(type, context, fr /** * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. * @param {module:ol/PluggableMap~FrameState} frameState FrameState. + * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(this: S, module:ol/layer/Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer * callback. * @param {S} thisArg Value to use as `this` when executing `callback`. * @return {T|undefined} Callback result. * @template S,T,U */ -CanvasLayerRenderer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) { - const hasFeature = this.forEachFeatureAtCoordinate(coordinate, frameState, 0, TRUE, this); +CanvasLayerRenderer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { + const hasFeature = this.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, TRUE, this); if (hasFeature) { return callback.call(thisArg, this.getLayer(), null); diff --git a/src/ol/renderer/canvas/Map.js b/src/ol/renderer/canvas/Map.js index 83c947288b..e544b2e513 100644 --- a/src/ol/renderer/canvas/Map.js +++ b/src/ol/renderer/canvas/Map.js @@ -182,7 +182,7 @@ CanvasMapRenderer.prototype.renderFrame = function(frameState) { /** * @inheritDoc */ -CanvasMapRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg, +CanvasMapRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) { let result; const viewState = frameState.viewState; @@ -201,7 +201,7 @@ CanvasMapRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, ca if (visibleAtResolution(layerState, viewResolution) && layerFilter.call(thisArg2, layer)) { const layerRenderer = /** @type {module:ol/renderer/canvas/Layer} */ (this.getLayerRenderer(layer)); result = layerRenderer.forEachLayerAtCoordinate( - coordinate, frameState, callback, thisArg); + coordinate, frameState, hitTolerance, callback, thisArg); if (result) { return result; } diff --git a/src/ol/renderer/webgl/Map.js b/src/ol/renderer/webgl/Map.js index 1e3e668d37..960e1a093d 100644 --- a/src/ol/renderer/webgl/Map.js +++ b/src/ol/renderer/webgl/Map.js @@ -557,7 +557,7 @@ WebGLMapRenderer.prototype.hasFeatureAtCoordinate = function(coordinate, frameSt /** * @inheritDoc */ -WebGLMapRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg, +WebGLMapRenderer.prototype.forEachLayerAtPixel = function(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) { if (this.getGL().isContextLost()) { return false; diff --git a/test/spec/ol/renderer/canvas/map.test.js b/test/spec/ol/renderer/canvas/map.test.js index 3ebbbeea1f..692bcefb3a 100644 --- a/test/spec/ol/renderer/canvas/map.test.js +++ b/test/spec/ol/renderer/canvas/map.test.js @@ -163,6 +163,88 @@ describe('ol.renderer.canvas.Map', function() { }); }); + describe('#forEachLayerAtCoordinate', function() { + + let layer, map, target; + + beforeEach(function(done) { + target = document.createElement('div'); + target.style.width = '100px'; + target.style.height = '100px'; + document.body.appendChild(target); + map = new Map({ + pixelRatio: 1, + target: target, + view: new View({ + center: [0, 0], + zoom: 0 + }) + }); + + // 1 x 1 pixel black icon + const img = document.createElement('img'); + img.onload = function() { + done(); + }; + img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=='; + + layer = new VectorLayer({ + source: new VectorSource({ + features: [ + new Feature({ + geometry: new Point([0, 0]) + }) + ] + }), + style: new Style({ + image: new Icon({ + img: img, + imgSize: [1, 1] + }) + }) + }); + }); + + afterEach(function() { + map.setTarget(null); + document.body.removeChild(target); + }); + + it('calls callback for clicks inside of the hitTolerance', function() { + map.addLayer(layer); + map.renderSync(); + const cb1 = sinon.spy(); + const cb2 = sinon.spy(); + + const pixel = map.getPixelFromCoordinate([0, 0]); + + const pixelsInside = [ + [pixel[0] + 9, pixel[1]], + [pixel[0] - 9, pixel[1]], + [pixel[0], pixel[1] + 9], + [pixel[0], pixel[1] - 9] + ]; + + const pixelsOutside = [ + [pixel[0] + 9, pixel[1] + 9], + [pixel[0] - 9, pixel[1] + 9], + [pixel[0] + 9, pixel[1] - 9], + [pixel[0] - 9, pixel[1] - 9] + ]; + + for (let i = 0; i < 4; i++) { + map.forEachLayerAtPixel(pixelsInside[i], cb1, {hitTolerance: 10}); + } + expect(cb1.callCount).to.be(4); + expect(cb1.firstCall.args[0]).to.be(layer); + + for (let j = 0; j < 4; j++) { + map.forEachLayerAtPixel(pixelsOutside[j], cb2, {hitTolerance: 10}); + } + expect(cb2).not.to.be.called(); + }); + }); + describe('#renderFrame()', function() { let layer, map, renderer;