From 85893646c4197f619e0be258a7da5586a84beb23 Mon Sep 17 00:00:00 2001 From: simonseyock Date: Wed, 28 Sep 2016 12:49:24 +0200 Subject: [PATCH 01/24] Added hitTolerance test --- test/spec/ol/renderer/canvas/map.test.js | 49 +++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/test/spec/ol/renderer/canvas/map.test.js b/test/spec/ol/renderer/canvas/map.test.js index 42311d8e79..a651cb7854 100644 --- a/test/spec/ol/renderer/canvas/map.test.js +++ b/test/spec/ol/renderer/canvas/map.test.js @@ -30,7 +30,7 @@ describe('ol.renderer.canvas.Map', function() { var layer, map, target; - beforeEach(function() { + beforeEach(function(done) { target = document.createElement('div'); target.style.width = '100px'; target.style.height = '100px'; @@ -42,6 +42,14 @@ describe('ol.renderer.canvas.Map', function() { zoom: 0 }) }); + + // 1 x 1 pixel black icon + var img = document.createElement('img'); + img.onload = function() { + done(); + }; + img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=='; + layer = new ol.layer.Vector({ source: new ol.source.Vector({ features: [ @@ -49,6 +57,12 @@ describe('ol.renderer.canvas.Map', function() { geometry: new ol.geom.Point([0, 0]) }) ] + }), + style: new ol.style.Style({ + image: new ol.style.Icon({ + img : img, + imgSize: [1, 1] + }) }) }); }); @@ -113,6 +127,39 @@ describe('ol.renderer.canvas.Map', function() { }).to.not.throwException(); }); + it('calls callback for clicks inside of the hitTolerance', function() { + map.addLayer(layer); + map.renderSync(); + var cb1 = sinon.spy(); + var cb2 = sinon.spy(); + + var pixel = map.getPixelFromCoordinate([0, 0]); + + var pixelsInside = [ + [pixel[0] + 9, pixel[1]], + [pixel[0] - 9, pixel[1]], + [pixel[0], pixel[1] + 9], + [pixel[0], pixel[1] - 9] + ]; + + var 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 (var i = 0; i < 4; i++) { + map.forEachFeatureAtPixel(pixelsInside[i], {hitTolerance:10}, cb1); + } + expect(cb1.callCount).to.be(4); + expect(cb1.firstCall.args[1]).to.be(layer); + + for (var j = 0; j < 4; j++) { + map.forEachFeatureAtPixel(pixelsOutside[j], {hitTolerance:10}, cb2); + } + expect(cb2).not.to.be.called(); + }); }); describe('#renderFrame()', function() { From a6c768ae0779d9cdee97289fc3cb320b4b46b552 Mon Sep 17 00:00:00 2001 From: simonseyock Date: Wed, 28 Sep 2016 17:08:47 +0200 Subject: [PATCH 02/24] Make forEachFeatureAtCoordinate work with variable hitContext size --- src/ol/render/canvas/replaygroup.js | 43 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index fae6101bd2..7f349dac43 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -65,19 +65,6 @@ ol.render.canvas.ReplayGroup = function( * Object.>} */ this.replaysByZIndex_ = {}; - - /** - * @private - * @type {CanvasRenderingContext2D} - */ - this.hitDetectionContext_ = ol.dom.createCanvasContext2D(1, 1); - - /** - * @private - * @type {ol.Transform} - */ - this.hitDetectionTransform_ = ol.transform.create(); - }; ol.inherits(ol.render.canvas.ReplayGroup, ol.render.ReplayGroup); @@ -111,14 +98,17 @@ ol.render.canvas.ReplayGroup.prototype.finish = function() { ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( coordinate, resolution, rotation, skippedFeaturesHash, callback) { - var transform = ol.transform.compose(this.hitDetectionTransform_, - 0.5, 0.5, + var hitTolerance = 10; + var contextSize = hitTolerance * 2 + 1; + + var transform = ol.transform.compose(ol.transform.create(), + hitTolerance + 0.5, hitTolerance + 0.5, 1 / resolution, -1 / resolution, -rotation, -coordinate[0], -coordinate[1]); - var context = this.hitDetectionContext_; - context.clearRect(0, 0, 1, 1); + var context = ol.dom.createCanvasContext2D(contextSize, contextSize); + context.clearRect(0, 0, contextSize, contextSize); /** * @type {ol.Extent} @@ -137,14 +127,21 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( * @return {?} Callback result. */ function(feature) { - var imageData = context.getImageData(0, 0, 1, 1).data; - if (imageData[3] > 0) { - var result = callback(feature); - if (result) { - return result; + var imageData; + for (var i = 0; i < contextSize; i++) { + for (var j = 0; j < contextSize; j++) { + imageData = context.getImageData(i, j, i + 1, j + 1).data; + if (imageData[3] > 0) { + var result = callback(feature); + if (result) { + return result; + } + i = contextSize; + j = contextSize; + } } - context.clearRect(0, 0, 1, 1); } + context.clearRect(0, 0, contextSize, contextSize); }, hitExtent); }; From 80188b204463671775b37fdb50a950e12d152def Mon Sep 17 00:00:00 2001 From: simonseyock Date: Wed, 19 Oct 2016 13:24:15 +0200 Subject: [PATCH 03/24] Change signature of api methods --- src/ol/interaction/select.js | 12 +++++--- src/ol/interaction/translate.js | 4 ++- src/ol/map.js | 38 ++++++++++++++++-------- test/spec/ol/renderer/canvas/map.test.js | 7 +++-- 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/ol/interaction/select.js b/src/ol/interaction/select.js index 7817067c3d..2820df269f 100644 --- a/src/ol/interaction/select.js +++ b/src/ol/interaction/select.js @@ -200,7 +200,9 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { // pixel, or clear the selected feature(s) if there is no feature at // the pixel. ol.obj.clear(this.featureLayerAssociation_); - map.forEachFeatureAtPixel(mapBrowserEvent.pixel, + map.forEachFeatureAtPixel(mapBrowserEvent.pixel, { + layerFilter: this.layerFilter_ + }, /** * @param {ol.Feature|ol.render.Feature} feature Feature. * @param {ol.layer.Layer} layer Layer. @@ -212,7 +214,7 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { this.addFeatureLayerAssociation_(feature, layer); return !this.multi_; } - }, this, this.layerFilter_); + }, this); var i; for (i = features.getLength() - 1; i >= 0; --i) { var feature = features.item(i); @@ -230,7 +232,9 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { } } else { // Modify the currently selected feature(s). - map.forEachFeatureAtPixel(mapBrowserEvent.pixel, + map.forEachFeatureAtPixel(mapBrowserEvent.pixel, { + layerFilter: this.layerFilter_ + }, /** * @param {ol.Feature|ol.render.Feature} feature Feature. * @param {ol.layer.Layer} layer Layer. @@ -249,7 +253,7 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { } return !this.multi_; } - }, this, this.layerFilter_); + }, this); var j; for (j = deselected.length - 1; j >= 0; --j) { features.remove(deselected[j]); diff --git a/src/ol/interaction/translate.js b/src/ol/interaction/translate.js index 3b61e333dc..b1aade1db1 100644 --- a/src/ol/interaction/translate.js +++ b/src/ol/interaction/translate.js @@ -197,7 +197,9 @@ ol.interaction.Translate.prototype.featuresAtPixel_ = function(pixel, map) { ol.array.includes(this.features_.getArray(), feature)) { return feature; } - }, this, this.layerFilter_); + }, this, { + layerFilter: this.layerFilter_ + }); }; diff --git a/src/ol/map.js b/src/ol/map.js index 8acbe9a76a..8cea0b20e3 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -554,11 +554,26 @@ ol.Map.prototype.disposeInternal = function() { }; +/** + * @typedef {object} ol.MapForEachFeatureOptions + * @property {(function(this: U, ol.layer.Layer): boolean)=} layerFilter Layer + * filter function. The filter function will receive one argument, the + * {@link 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. + * @property {U=} layerFilterThis Value to use as `this` when executing `layerFilter`. + * @property {number=} hitTolerance the hitTolerance in pixels in which features + * get hit. + */ + + /** * Detect features that intersect a pixel on the viewport, and execute a * callback with each intersecting feature. Layers included in the detection can * be configured through `opt_layerFilter`. * @param {ol.Pixel} pixel Pixel. + * @param {ol.MapForEachFeatureOptions=} opt_options * @param {function(this: S, (ol.Feature|ol.render.Feature), * ol.layer.Layer): T} callback Feature callback. The callback will be * called with two arguments. The first argument is one @@ -567,30 +582,27 @@ ol.Map.prototype.disposeInternal = function() { * the {@link ol.layer.Layer layer} of the feature and will be null for * unmanaged layers. 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, ol.layer.Layer): boolean)=} opt_layerFilter Layer - * filter function. The filter function will receive one argument, the - * {@link 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 {S=} opt_this * @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 * @api stable */ -ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this, opt_layerFilter, opt_this2) { +ol.Map.prototype.forEachFeatureAtPixel = function(pixel, opt_options, callback, opt_this) { + if (typeof opt_options !== 'object') { + opt_this = callback; + callback = opt_options; + opt_options = {}; + } if (!this.frameState_) { return; } var coordinate = this.getCoordinateFromPixel(pixel); var thisArg = opt_this !== undefined ? opt_this : null; - var layerFilter = opt_layerFilter !== undefined ? - opt_layerFilter : ol.functions.TRUE; - var thisArg2 = opt_this2 !== undefined ? opt_this2 : null; + var layerFilter = opt_options.layerFilter || ol.functions.TRUE; + var thisArg2 = opt_options.layerFilterThis || null; return this.renderer_.forEachFeatureAtCoordinate( - coordinate, this.frameState_, callback, thisArg, + coordinate, this.frameState_, opt_options.hitTolerance || 0, callback, thisArg, layerFilter, thisArg2); }; diff --git a/test/spec/ol/renderer/canvas/map.test.js b/test/spec/ol/renderer/canvas/map.test.js index a651cb7854..a9ae31d819 100644 --- a/test/spec/ol/renderer/canvas/map.test.js +++ b/test/spec/ol/renderer/canvas/map.test.js @@ -111,10 +111,11 @@ describe('ol.renderer.canvas.Map', function() { map.addLayer(layer); map.renderSync(); var cb = sinon.spy(); - map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]), cb, null, - function() { + map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]), { + layerFilter: function() { return false; - }); + } + }, cb); expect(cb).to.not.be.called(); }); From 5ce0d8aa2a8b4d7651b6ea404c92d87ad57b54e4 Mon Sep 17 00:00:00 2001 From: simonseyock Date: Wed, 19 Oct 2016 13:31:37 +0200 Subject: [PATCH 04/24] changed signature of internal methods --- src/ol/render/canvas/replaygroup.js | 4 ++-- src/ol/renderer/canvas/vectorlayer.js | 4 ++-- src/ol/renderer/canvas/vectortilelayer.js | 4 ++-- src/ol/renderer/layer.js | 1 + src/ol/renderer/map.js | 5 +++-- src/ol/renderer/webgl/imagelayer.js | 4 ++-- src/ol/renderer/webgl/map.js | 4 ++-- src/ol/renderer/webgl/vectorlayer.js | 2 +- src/ol/source/imagevector.js | 4 ++-- src/ol/source/source.js | 1 + test/spec/ol/renderer/canvas/vectorlayer.test.js | 4 ++-- test/spec/ol/renderer/canvas/vectortilelayer.test.js | 4 ++-- 12 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index 7f349dac43..26e6dd8291 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -88,6 +88,7 @@ ol.render.canvas.ReplayGroup.prototype.finish = function() { * @param {ol.Coordinate} coordinate Coordinate. * @param {number} resolution Resolution. * @param {number} rotation Rotation. + * @param {number} hitTolerance hit tolerance. * @param {Object.} skippedFeaturesHash Ids of features * to skip. * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature @@ -96,9 +97,8 @@ ol.render.canvas.ReplayGroup.prototype.finish = function() { * @template T */ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( - coordinate, resolution, rotation, skippedFeaturesHash, callback) { + coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback) { - var hitTolerance = 10; var contextSize = hitTolerance * 2 + 1; var transform = ol.transform.compose(ol.transform.create(), diff --git a/src/ol/renderer/canvas/vectorlayer.js b/src/ol/renderer/canvas/vectorlayer.js index cd375e0f94..449df71c7a 100644 --- a/src/ol/renderer/canvas/vectorlayer.js +++ b/src/ol/renderer/canvas/vectorlayer.js @@ -177,7 +177,7 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, lay /** * @inheritDoc */ -ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) { +ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { if (!this.replayGroup_) { return undefined; } else { @@ -187,7 +187,7 @@ ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(c /** @type {Object.} */ var features = {}; return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution, - rotation, {}, + rotation, hitTolerance, {}, /** * @param {ol.Feature|ol.render.Feature} feature Feature. * @return {?} Callback result. diff --git a/src/ol/renderer/canvas/vectortilelayer.js b/src/ol/renderer/canvas/vectortilelayer.js index ee4e14a14c..082d19d62b 100644 --- a/src/ol/renderer/canvas/vectortilelayer.js +++ b/src/ol/renderer/canvas/vectortilelayer.js @@ -176,7 +176,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.drawTileImage = function( /** * @inheritDoc */ -ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) { +ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { var resolution = frameState.viewState.resolution; var rotation = frameState.viewState.rotation; var layer = this.getLayer(); @@ -212,7 +212,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi } replayGroup = tile.getReplayState().replayGroup; found = found || replayGroup.forEachFeatureAtCoordinate( - tileSpaceCoordinate, resolution, rotation, {}, + tileSpaceCoordinate, resolution, rotation, hitTolerance, {}, /** * @param {ol.Feature|ol.render.Feature} feature Feature. * @return {?} Callback result. diff --git a/src/ol/renderer/layer.js b/src/ol/renderer/layer.js index 2e96ea86d3..cab6f36e06 100644 --- a/src/ol/renderer/layer.js +++ b/src/ol/renderer/layer.js @@ -35,6 +35,7 @@ ol.inherits(ol.renderer.Layer, ol.Observable); /** * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState Frame state. + * @param {number} hitTolerance hit tolerance. * @param {function(this: S, (ol.Feature|ol.render.Feature), ol.layer.Layer): T} * callback Feature callback. * @param {S} thisArg Value to use as `this` when executing `callback`. diff --git a/src/ol/renderer/map.js b/src/ol/renderer/map.js index e9f0d8bae0..da80986fd2 100644 --- a/src/ol/renderer/map.js +++ b/src/ol/renderer/map.js @@ -100,6 +100,7 @@ ol.renderer.Map.expireIconCache_ = function(map, frameState) { /** * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState FrameState. + * @param {number} hitTolerance hit tolerance. * @param {function(this: S, (ol.Feature|ol.render.Feature), * ol.layer.Layer): T} callback Feature callback. * @param {S} thisArg Value to use as `this` when executing `callback`. @@ -111,7 +112,7 @@ ol.renderer.Map.expireIconCache_ = function(map, frameState) { * @return {T|undefined} Callback result. * @template S,T,U */ -ol.renderer.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg, +ol.renderer.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) { var result; var viewState = frameState.viewState; @@ -155,7 +156,7 @@ ol.renderer.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, fram if (layer.getSource()) { result = layerRenderer.forEachFeatureAtCoordinate( layer.getSource().getWrapX() ? translatedCoordinate : coordinate, - frameState, forEachFeatureAtCoordinate, thisArg); + frameState, hitTolerance, forEachFeatureAtCoordinate, thisArg); } if (result) { return result; diff --git a/src/ol/renderer/webgl/imagelayer.js b/src/ol/renderer/webgl/imagelayer.js index 8df0c036df..9ef477e9a8 100644 --- a/src/ol/renderer/webgl/imagelayer.js +++ b/src/ol/renderer/webgl/imagelayer.js @@ -68,14 +68,14 @@ ol.renderer.webgl.ImageLayer.prototype.createTexture_ = function(image) { /** * @inheritDoc */ -ol.renderer.webgl.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) { +ol.renderer.webgl.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { var layer = this.getLayer(); var source = layer.getSource(); var resolution = frameState.viewState.resolution; var rotation = frameState.viewState.rotation; var skippedFeatureUids = frameState.skippedFeatureUids; return source.forEachFeatureAtCoordinate( - coordinate, resolution, rotation, skippedFeatureUids, + coordinate, resolution, rotation, hitTolerance, skippedFeatureUids, /** * @param {ol.Feature|ol.render.Feature} feature Feature. diff --git a/src/ol/renderer/webgl/map.js b/src/ol/renderer/webgl/map.js index 5844610adf..4022046ab4 100644 --- a/src/ol/renderer/webgl/map.js +++ b/src/ol/renderer/webgl/map.js @@ -502,7 +502,7 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) { /** * @inheritDoc */ -ol.renderer.webgl.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg, +ol.renderer.webgl.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) { var result; @@ -522,7 +522,7 @@ ol.renderer.webgl.Map.prototype.forEachFeatureAtCoordinate = function(coordinate layerFilter.call(thisArg2, layer)) { var layerRenderer = this.getLayerRenderer(layer); result = layerRenderer.forEachFeatureAtCoordinate( - coordinate, frameState, callback, thisArg); + coordinate, frameState, hitTolerance, callback, thisArg); if (result) { return result; } diff --git a/src/ol/renderer/webgl/vectorlayer.js b/src/ol/renderer/webgl/vectorlayer.js index d94975f8a6..e0a6b701e0 100644 --- a/src/ol/renderer/webgl/vectorlayer.js +++ b/src/ol/renderer/webgl/vectorlayer.js @@ -106,7 +106,7 @@ ol.renderer.webgl.VectorLayer.prototype.disposeInternal = function() { /** * @inheritDoc */ -ol.renderer.webgl.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) { +ol.renderer.webgl.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { if (!this.replayGroup_ || !this.layerState_) { return undefined; } else { diff --git a/src/ol/source/imagevector.js b/src/ol/source/imagevector.js index 2eac2bccb0..42d54d58b1 100644 --- a/src/ol/source/imagevector.js +++ b/src/ol/source/imagevector.js @@ -155,14 +155,14 @@ ol.source.ImageVector.prototype.canvasFunctionInternal_ = function(extent, resol * @inheritDoc */ ol.source.ImageVector.prototype.forEachFeatureAtCoordinate = function( - coordinate, resolution, rotation, skippedFeatureUids, callback) { + coordinate, resolution, rotation, hitTolerance, skippedFeatureUids, callback) { if (!this.replayGroup_) { return undefined; } else { /** @type {Object.} */ var features = {}; return this.replayGroup_.forEachFeatureAtCoordinate( - coordinate, resolution, 0, skippedFeatureUids, + coordinate, resolution, 0, hitTolerance, skippedFeatureUids, /** * @param {ol.Feature|ol.render.Feature} feature Feature. * @return {?} Callback result. diff --git a/src/ol/source/source.js b/src/ol/source/source.js index 6fd4cdf57f..2224022bd8 100644 --- a/src/ol/source/source.js +++ b/src/ol/source/source.js @@ -94,6 +94,7 @@ ol.source.Source.toAttributionsArray_ = function(attributionLike) { * @param {ol.Coordinate} coordinate Coordinate. * @param {number} resolution Resolution. * @param {number} rotation Rotation. + * @param {number} hitTolerance Hit tolerance. * @param {Object.} skippedFeatureUids Skipped feature uids. * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature * callback. diff --git a/test/spec/ol/renderer/canvas/vectorlayer.test.js b/test/spec/ol/renderer/canvas/vectorlayer.test.js index a6bab3856d..6fd6750cf4 100644 --- a/test/spec/ol/renderer/canvas/vectorlayer.test.js +++ b/test/spec/ol/renderer/canvas/vectorlayer.test.js @@ -79,7 +79,7 @@ describe('ol.renderer.canvas.VectorLayer', function() { var replayGroup = {}; renderer.replayGroup_ = replayGroup; replayGroup.forEachFeatureAtCoordinate = function(coordinate, - resolution, rotation, skippedFeaturesUids, callback) { + resolution, rotation, hitTolerance, skippedFeaturesUids, callback) { var feature = new ol.Feature(); callback(feature); callback(feature); @@ -99,7 +99,7 @@ describe('ol.renderer.canvas.VectorLayer', function() { }; frameState.layerStates[ol.getUid(layer)] = {}; renderer.forEachFeatureAtCoordinate( - coordinate, frameState, spy, undefined); + coordinate, frameState, 0, spy, undefined); expect(spy.callCount).to.be(1); expect(spy.getCall(0).args[1]).to.equal(layer); }); diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index 7979fa3d19..0ff44349d2 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -169,7 +169,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { }); renderer = new ol.renderer.canvas.VectorTileLayer(layer); replayGroup.forEachFeatureAtCoordinate = function(coordinate, - resolution, rotation, skippedFeaturesUids, callback) { + resolution, rotation, hitTolerance, skippedFeaturesUids, callback) { var feature = new ol.Feature(); callback(feature); callback(feature); @@ -190,7 +190,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { frameState.layerStates[ol.getUid(layer)] = {}; renderer.renderedTiles = [new TileClass([0, 0, -1])]; renderer.forEachFeatureAtCoordinate( - coordinate, frameState, spy, undefined); + coordinate, frameState, 0, spy, undefined); expect(spy.callCount).to.be(1); expect(spy.getCall(0).args[1]).to.equal(layer); }); From 2493eb2c2018d01d9eedc66796b755f053b75094 Mon Sep 17 00:00:00 2001 From: simonseyock Date: Wed, 19 Oct 2016 15:13:57 +0200 Subject: [PATCH 05/24] Implemented midpoint circle algorithm --- src/ol/render/canvas/replaygroup.js | 73 ++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index 26e6dd8291..089f06df29 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -68,6 +68,59 @@ ol.render.canvas.ReplayGroup = function( }; ol.inherits(ol.render.canvas.ReplayGroup, ol.render.ReplayGroup); +/** + * This methods creates a circle inside an exactly fitting array. Points inside the circle are marked by true, points + * on the outside are marked with false. + * This method uses the midpoint circle algorithm. + * @param {Number} radius + * @returns {Boolean[][]} + * @private + */ +ol.render.canvas.ReplayGroup.createPixelCircle_ = function(radius) { + var arraySize = radius * 2 + 1; + var arr = new Array(arraySize); + for (var i = 0; i < arraySize; i++) { + arr[i] = (new Array(arraySize)).fill(false); + } + + function fillRowToMiddle(x, y) { + var i; + if (x >= radius) { + for (i = radius; i < x; i++) { + arr[i][y] = true + } + } else if (x < radius) { + for (i = x + 1; i < radius; i++) { + arr[i][y] = true + } + } + } + + var x = radius; + var y = 0; + var error = 0; + + while (x >= y) { + fillRowToMiddle(radius + x, radius + y); + fillRowToMiddle(radius + y, radius + x); + fillRowToMiddle(radius - y, radius + x); + fillRowToMiddle(radius - x, radius + y); + fillRowToMiddle(radius - x, radius - y); + fillRowToMiddle(radius - y, radius - x); + fillRowToMiddle(radius + y, radius - x); + fillRowToMiddle(radius + x, radius - y); + + y++; + error += 1 + 2 * y; + if (2 * (error - x) + 1 > 0) + { + x -= 1; + error += 1 - 2 * x; + } + } + + return arr; +}; /** * FIXME empty description for jsdoc @@ -99,6 +152,8 @@ ol.render.canvas.ReplayGroup.prototype.finish = function() { ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback) { + hitTolerance = Math.round(hitTolerance); + var contextSize = hitTolerance * 2 + 1; var transform = ol.transform.compose(ol.transform.create(), @@ -120,6 +175,8 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( ol.extent.buffer(hitExtent, resolution * this.renderBuffer_, hitExtent); } + var mask = ol.render.canvas.ReplayGroup.createPixelCircle_(hitTolerance); + return this.replayHitDetection_(context, transform, rotation, skippedFeaturesHash, /** @@ -130,14 +187,16 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( var imageData; for (var i = 0; i < contextSize; i++) { for (var j = 0; j < contextSize; j++) { - imageData = context.getImageData(i, j, i + 1, j + 1).data; - if (imageData[3] > 0) { - var result = callback(feature); - if (result) { - return result; + if (mask[i][j]) { + imageData = context.getImageData(i, j, i + 1, j + 1).data; + if (imageData[3] > 0) { + var result = callback(feature); + if (result) { + return result; + } + i = contextSize; + j = contextSize; } - i = contextSize; - j = contextSize; } } } From 80e392ea52933cb52b3ea32426c6073e6d9780a1 Mon Sep 17 00:00:00 2001 From: simonseyock Date: Wed, 19 Oct 2016 18:10:21 +0200 Subject: [PATCH 06/24] Satisfying linter, jsdoc & compiler --- externs/olx.js | 43 ++++++++++++++++++++++++ src/ol/interaction/select.js | 12 +++---- src/ol/map.js | 31 ++++------------- src/ol/render/canvas/replaygroup.js | 29 ++++++++++------ src/ol/renderer/map.js | 2 +- src/ol/renderer/webgl/imagelayer.js | 4 +-- test/spec/ol/renderer/canvas/map.test.js | 14 ++++---- 7 files changed, 82 insertions(+), 53 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index a255777607..fede544830 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -301,6 +301,49 @@ olx.MapOptions.prototype.target; */ olx.MapOptions.prototype.view; +/** + * The filter function will receive one argument, the + * {@link ol.layer.Layer layer-candidate} and it should return a boolean + * value. + * @typedef {(function(ol.layer.Layer): boolean)} + */ +olx.LayerFilterFunction; + +/** + * Object literal with options for the forEachFeatureAtCoordinate methods. + * @typedef {{layerFilter: (olx.LayerFilterFunction|undefined), + * layerFilterThis: (Object|undefined), + * hitTolerance: (number|undefined)}} + */ +olx.ForEachFeatureOptions; + + +/** + * 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. + * @type {olx.LayerFilterFunction|undefined} + * @api stable + */ +olx.ForEachFeatureOptions.prototype.layerFilter; + + +/** + * Value to use as `this` when executing `layerFilter`. + * @type {Object} + * @api stable + */ +olx.ForEachFeatureOptions.prototype.layerFilterThis; + + +/** + * Value of a radius in whichs area around the given coordinate features are + * called. + * @type {number} + * @api stable + */ +olx.ForEachFeatureOptions.prototype.hitTolerance; + /** * Object literal with config options for the overlay. diff --git a/src/ol/interaction/select.js b/src/ol/interaction/select.js index 2820df269f..d6b233cab6 100644 --- a/src/ol/interaction/select.js +++ b/src/ol/interaction/select.js @@ -200,9 +200,7 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { // pixel, or clear the selected feature(s) if there is no feature at // the pixel. ol.obj.clear(this.featureLayerAssociation_); - map.forEachFeatureAtPixel(mapBrowserEvent.pixel, { - layerFilter: this.layerFilter_ - }, + map.forEachFeatureAtPixel(mapBrowserEvent.pixel, /** * @param {ol.Feature|ol.render.Feature} feature Feature. * @param {ol.layer.Layer} layer Layer. @@ -214,7 +212,7 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { this.addFeatureLayerAssociation_(feature, layer); return !this.multi_; } - }, this); + }, this, {layerFilter: this.layerFilter_}); var i; for (i = features.getLength() - 1; i >= 0; --i) { var feature = features.item(i); @@ -232,9 +230,7 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { } } else { // Modify the currently selected feature(s). - map.forEachFeatureAtPixel(mapBrowserEvent.pixel, { - layerFilter: this.layerFilter_ - }, + map.forEachFeatureAtPixel(mapBrowserEvent.pixel, /** * @param {ol.Feature|ol.render.Feature} feature Feature. * @param {ol.layer.Layer} layer Layer. @@ -253,7 +249,7 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { } return !this.multi_; } - }, this); + }, this, {layerFilter: this.layerFilter_}); var j; for (j = deselected.length - 1; j >= 0; --j) { features.remove(deselected[j]); diff --git a/src/ol/map.js b/src/ol/map.js index 8cea0b20e3..36d24f0fc6 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -554,26 +554,11 @@ ol.Map.prototype.disposeInternal = function() { }; -/** - * @typedef {object} ol.MapForEachFeatureOptions - * @property {(function(this: U, ol.layer.Layer): boolean)=} layerFilter Layer - * filter function. The filter function will receive one argument, the - * {@link 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. - * @property {U=} layerFilterThis Value to use as `this` when executing `layerFilter`. - * @property {number=} hitTolerance the hitTolerance in pixels in which features - * get hit. - */ - - /** * Detect features that intersect a pixel on the viewport, and execute a * callback with each intersecting feature. Layers included in the detection can * be configured through `opt_layerFilter`. * @param {ol.Pixel} pixel Pixel. - * @param {ol.MapForEachFeatureOptions=} opt_options * @param {function(this: S, (ol.Feature|ol.render.Feature), * ol.layer.Layer): T} callback Feature callback. The callback will be * called with two arguments. The first argument is one @@ -582,27 +567,25 @@ ol.Map.prototype.disposeInternal = function() { * the {@link ol.layer.Layer layer} of the feature and will be null for * unmanaged layers. To stop detection, callback functions can return a * truthy value. - * @param {S=} opt_this + * @param {S=} opt_this Value to use as this when executing callback. + * @param {olx.ForEachFeatureOptions=} opt_options Optional 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 stable */ -ol.Map.prototype.forEachFeatureAtPixel = function(pixel, opt_options, callback, opt_this) { - if (typeof opt_options !== 'object') { - opt_this = callback; - callback = opt_options; - opt_options = {}; - } +ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this, opt_options) { + opt_options = opt_options !== undefined ? opt_options : {}; if (!this.frameState_) { return; } var coordinate = this.getCoordinateFromPixel(pixel); + var hitTolerance = opt_options.hitTolerance || 0; var thisArg = opt_this !== undefined ? opt_this : null; var layerFilter = opt_options.layerFilter || ol.functions.TRUE; var thisArg2 = opt_options.layerFilterThis || null; return this.renderer_.forEachFeatureAtCoordinate( - coordinate, this.frameState_, opt_options.hitTolerance || 0, callback, thisArg, + coordinate, this.frameState_, hitTolerance, callback, thisArg, layerFilter, thisArg2); }; diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index 089f06df29..9250a55887 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -69,29 +69,32 @@ ol.render.canvas.ReplayGroup = function( ol.inherits(ol.render.canvas.ReplayGroup, ol.render.ReplayGroup); /** - * This methods creates a circle inside an exactly fitting array. Points inside the circle are marked by true, points - * on the outside are marked with false. - * This method uses the midpoint circle algorithm. - * @param {Number} radius - * @returns {Boolean[][]} + * This methods creates a circle inside a fitting array. Points inside the + * circle are marked by true, points on the outside are marked with false. + * It uses the midpoint circle algorithm. + * @param {number} radius Radius. + * @returns {Array.>} an array with marked circel points. * @private */ ol.render.canvas.ReplayGroup.createPixelCircle_ = function(radius) { var arraySize = radius * 2 + 1; var arr = new Array(arraySize); for (var i = 0; i < arraySize; i++) { - arr[i] = (new Array(arraySize)).fill(false); + arr[i] = new Array(arraySize); + for (var j = 0; j < arraySize; j++) { + arr[i][j] = false; + } } function fillRowToMiddle(x, y) { var i; if (x >= radius) { for (i = radius; i < x; i++) { - arr[i][y] = true + arr[i][y] = true; } } else if (x < radius) { for (i = x + 1; i < radius; i++) { - arr[i][y] = true + arr[i][y] = true; } } } @@ -112,8 +115,7 @@ ol.render.canvas.ReplayGroup.createPixelCircle_ = function(radius) { y++; error += 1 + 2 * y; - if (2 * (error - x) + 1 > 0) - { + if (2 * (error - x) + 1 > 0) { x -= 1; error += 1 - 2 * x; } @@ -175,7 +177,12 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( ol.extent.buffer(hitExtent, resolution * this.renderBuffer_, hitExtent); } - var mask = ol.render.canvas.ReplayGroup.createPixelCircle_(hitTolerance); + var mask; + if (hitTolerance === 0) { + mask = [[true]]; + } else { + mask = ol.render.canvas.ReplayGroup.createPixelCircle_(hitTolerance); + } return this.replayHitDetection_(context, transform, rotation, skippedFeaturesHash, diff --git a/src/ol/renderer/map.js b/src/ol/renderer/map.js index da80986fd2..401b9b500b 100644 --- a/src/ol/renderer/map.js +++ b/src/ol/renderer/map.js @@ -199,7 +199,7 @@ ol.renderer.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, call */ ol.renderer.Map.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, layerFilter, thisArg) { var hasFeature = this.forEachFeatureAtCoordinate( - coordinate, frameState, ol.functions.TRUE, this, layerFilter, thisArg); + coordinate, frameState, 0, ol.functions.TRUE, this, layerFilter, thisArg); return hasFeature !== undefined; }; diff --git a/src/ol/renderer/webgl/imagelayer.js b/src/ol/renderer/webgl/imagelayer.js index 9ef477e9a8..2218f42ede 100644 --- a/src/ol/renderer/webgl/imagelayer.js +++ b/src/ol/renderer/webgl/imagelayer.js @@ -213,7 +213,7 @@ ol.renderer.webgl.ImageLayer.prototype.updateProjectionMatrix_ = function(canvas */ ol.renderer.webgl.ImageLayer.prototype.hasFeatureAtCoordinate = function(coordinate, frameState) { var hasFeature = this.forEachFeatureAtCoordinate( - coordinate, frameState, ol.functions.TRUE, this); + coordinate, frameState, 0, ol.functions.TRUE, this); return hasFeature !== undefined; }; @@ -232,7 +232,7 @@ ol.renderer.webgl.ImageLayer.prototype.forEachLayerAtPixel = function(pixel, fra var coordinate = ol.transform.apply( frameState.pixelToCoordinateTransform, pixel.slice()); var hasFeature = this.forEachFeatureAtCoordinate( - coordinate, frameState, ol.functions.TRUE, this); + coordinate, frameState, 0, ol.functions.TRUE, this); if (hasFeature) { return callback.call(thisArg, this.getLayer(), null); diff --git a/test/spec/ol/renderer/canvas/map.test.js b/test/spec/ol/renderer/canvas/map.test.js index a9ae31d819..9d82ac5435 100644 --- a/test/spec/ol/renderer/canvas/map.test.js +++ b/test/spec/ol/renderer/canvas/map.test.js @@ -111,11 +111,11 @@ describe('ol.renderer.canvas.Map', function() { map.addLayer(layer); map.renderSync(); var cb = sinon.spy(); - map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]), { - layerFilter: function() { - return false; - } - }, cb); + map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]), cb, null, { + layerFilter: function() { + return false; + } + }); expect(cb).to.not.be.called(); }); @@ -151,13 +151,13 @@ describe('ol.renderer.canvas.Map', function() { ]; for (var i = 0; i < 4; i++) { - map.forEachFeatureAtPixel(pixelsInside[i], {hitTolerance:10}, cb1); + map.forEachFeatureAtPixel(pixelsInside[i], cb1, null, {hitTolerance:10}); } expect(cb1.callCount).to.be(4); expect(cb1.firstCall.args[1]).to.be(layer); for (var j = 0; j < 4; j++) { - map.forEachFeatureAtPixel(pixelsOutside[j], {hitTolerance:10}, cb2); + map.forEachFeatureAtPixel(pixelsOutside[j], cb2, null, {hitTolerance:10}); } expect(cb2).not.to.be.called(); }); From 63f57768a3e73e71af024b9243c3c9836a0daf6e Mon Sep 17 00:00:00 2001 From: simonseyock Date: Thu, 20 Oct 2016 13:46:40 +0200 Subject: [PATCH 07/24] Removed olx.LayerFilter --- externs/olx.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index fede544830..98efe2b854 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -301,17 +301,10 @@ olx.MapOptions.prototype.target; */ olx.MapOptions.prototype.view; -/** - * The filter function will receive one argument, the - * {@link ol.layer.Layer layer-candidate} and it should return a boolean - * value. - * @typedef {(function(ol.layer.Layer): boolean)} - */ -olx.LayerFilterFunction; /** * Object literal with options for the forEachFeatureAtCoordinate methods. - * @typedef {{layerFilter: (olx.LayerFilterFunction|undefined), + * @typedef {{layerFilter: ((function(ol.layer.Layer): boolean)|undefined), * layerFilterThis: (Object|undefined), * hitTolerance: (number|undefined)}} */ @@ -322,7 +315,7 @@ olx.ForEachFeatureOptions; * 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. - * @type {olx.LayerFilterFunction|undefined} + * @type {((function(ol.layer.Layer): boolean)|undefined)} * @api stable */ olx.ForEachFeatureOptions.prototype.layerFilter; From 2ea41afe6ea7d7505fe4ca8a556bf9cf91ca87dd Mon Sep 17 00:00:00 2001 From: simonseyock Date: Tue, 25 Oct 2016 16:39:52 +0200 Subject: [PATCH 08/24] Added hitTolerance to hasFeatureAtPixel. Corrected JsDoc problems. --- externs/olx.js | 29 ++++++++++++++++------------- src/ol/map.js | 21 ++++++++------------- src/ol/renderer/map.js | 5 +++-- src/ol/renderer/webgl/map.js | 2 +- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index 98efe2b854..dd7bac146e 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -303,39 +303,42 @@ olx.MapOptions.prototype.view; /** - * Object literal with options for the forEachFeatureAtCoordinate methods. + * Object literal with options for the forEachFeatureAtPixel and + * hasFeatureAtPixel methods. * @typedef {{layerFilter: ((function(ol.layer.Layer): boolean)|undefined), * layerFilterThis: (Object|undefined), * hitTolerance: (number|undefined)}} */ -olx.ForEachFeatureOptions; +olx.FeatureAtPixelOptions; /** - * 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. + * Layer filter function. The filter function will receive one argument, the + * {@link 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. * @type {((function(ol.layer.Layer): boolean)|undefined)} * @api stable */ -olx.ForEachFeatureOptions.prototype.layerFilter; +olx.FeatureAtPixelOptions.prototype.layerFilter; /** * Value to use as `this` when executing `layerFilter`. - * @type {Object} + * @type {Object|undefined} * @api stable */ -olx.ForEachFeatureOptions.prototype.layerFilterThis; +olx.FeatureAtPixelOptions.prototype.layerFilterThis; /** - * Value of a radius in whichs area around the given coordinate features are - * called. - * @type {number} - * @api stable + * Hit-detection tolerance. Pixels inside the radius around the given position + * will be checked for features. This only works for the canvas renderer and + * not for WebGL. + * @type {number|undefined} + * @api */ -olx.ForEachFeatureOptions.prototype.hitTolerance; +olx.FeatureAtPixelOptions.prototype.hitTolerance; /** diff --git a/src/ol/map.js b/src/ol/map.js index 36d24f0fc6..c915437507 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -568,7 +568,7 @@ ol.Map.prototype.disposeInternal = function() { * unmanaged layers. To stop detection, callback functions can return a * truthy value. * @param {S=} opt_this Value to use as this when executing callback. - * @param {olx.ForEachFeatureOptions=} opt_options Optional options. + * @param {olx.FeatureAtPixelOptions=} opt_options Optional 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 @@ -632,27 +632,22 @@ ol.Map.prototype.forEachLayerAtPixel = function(pixel, callback, opt_this, opt_l * Detect if features intersect a pixel on the viewport. Layers included in the * detection can be configured through `opt_layerFilter`. * @param {ol.Pixel} pixel Pixel. - * @param {(function(this: U, ol.layer.Layer): boolean)=} opt_layerFilter Layer - * filter function. The filter function will receive one argument, the - * {@link 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_this Value to use as `this` when executing `layerFilter`. + * @param {olx.FeatureAtPixelOptions=} opt_options Optional options. * @return {boolean} Is there a feature at the given pixel? * @template U * @api */ -ol.Map.prototype.hasFeatureAtPixel = function(pixel, opt_layerFilter, opt_this) { +ol.Map.prototype.hasFeatureAtPixel = function(pixel, opt_options) { + opt_options = opt_options !== undefined ? opt_options : {}; if (!this.frameState_) { return false; } var coordinate = this.getCoordinateFromPixel(pixel); - var layerFilter = opt_layerFilter !== undefined ? - opt_layerFilter : ol.functions.TRUE; - var thisArg = opt_this !== undefined ? opt_this : null; + var layerFilter = opt_options.layerFilter !== undefined ? + opt_options.layerFilter : ol.functions.TRUE; + var thisArg = opt_options.layerFilterThis !== undefined ? opt_options.layerFilterThis : null; return this.renderer_.hasFeatureAtCoordinate( - coordinate, this.frameState_, layerFilter, thisArg); + coordinate, this.frameState_, opt_options.hitTolerance || 0, layerFilter, thisArg); }; diff --git a/src/ol/renderer/map.js b/src/ol/renderer/map.js index 401b9b500b..8af9c55c26 100644 --- a/src/ol/renderer/map.js +++ b/src/ol/renderer/map.js @@ -189,6 +189,7 @@ ol.renderer.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, call /** * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState FrameState. + * @param {number} hitTolerance HitTolerance. * @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 @@ -197,9 +198,9 @@ ol.renderer.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, call * @return {boolean} Is there a feature at the given coordinate? * @template U */ -ol.renderer.Map.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, layerFilter, thisArg) { +ol.renderer.Map.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, layerFilter, thisArg) { var hasFeature = this.forEachFeatureAtCoordinate( - coordinate, frameState, 0, ol.functions.TRUE, this, layerFilter, thisArg); + coordinate, frameState, hitTolerance, ol.functions.TRUE, this, layerFilter, thisArg); return hasFeature !== undefined; }; diff --git a/src/ol/renderer/webgl/map.js b/src/ol/renderer/webgl/map.js index 4022046ab4..7359ce0d16 100644 --- a/src/ol/renderer/webgl/map.js +++ b/src/ol/renderer/webgl/map.js @@ -535,7 +535,7 @@ ol.renderer.webgl.Map.prototype.forEachFeatureAtCoordinate = function(coordinate /** * @inheritDoc */ -ol.renderer.webgl.Map.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, layerFilter, thisArg) { +ol.renderer.webgl.Map.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, layerFilter, thisArg) { var hasFeature = false; if (this.getGL().isContextLost()) { From e0edefb4d7a8d981cdf4f9960a4167822f317feb Mon Sep 17 00:00:00 2001 From: simonseyock Date: Tue, 25 Oct 2016 17:14:54 +0200 Subject: [PATCH 09/24] Added hitTolerance to select interaction. Added hitTolerance to select interaction example. --- examples/select-features.html | 10 +++++++++- examples/select-features.js | 37 +++++++++++++++++++++++++++++++---- externs/olx.js | 13 +++++++++++- src/ol/interaction/select.js | 34 ++++++++++++++++++++++++++++++-- 4 files changed, 86 insertions(+), 8 deletions(-) diff --git a/examples/select-features.html b/examples/select-features.html index 55481d0372..697b309a39 100644 --- a/examples/select-features.html +++ b/examples/select-features.html @@ -18,5 +18,13 @@ tags: "select, vector" -  0 selected features +  0 selected features +
+ + + diff --git a/examples/select-features.js b/examples/select-features.js index 8d68284e2c..5d0a658f7a 100644 --- a/examples/select-features.js +++ b/examples/select-features.js @@ -31,23 +31,28 @@ var map = new ol.Map({ var select = null; // ref to currently selected interaction // select interaction working on "singleclick" -var selectSingleClick = new ol.interaction.Select(); +var selectSingleClick = new ol.interaction.Select({ + multi: true // multi is used in this example if hitTolerance > 0 +}); // select interaction working on "click" var selectClick = new ol.interaction.Select({ - condition: ol.events.condition.click + condition: ol.events.condition.click, + multi: true }); // select interaction working on "pointermove" var selectPointerMove = new ol.interaction.Select({ - condition: ol.events.condition.pointerMove + condition: ol.events.condition.pointerMove, + multi: true }); var selectAltClick = new ol.interaction.Select({ condition: function(mapBrowserEvent) { return ol.events.condition.click(mapBrowserEvent) && ol.events.condition.altKeyOnly(mapBrowserEvent); - } + }, + multi: true }); var selectElement = document.getElementById('type'); @@ -85,3 +90,27 @@ var changeInteraction = function() { */ selectElement.onchange = changeInteraction; changeInteraction(); + +var selectHitToleranceElement = document.getElementById('hitTolerance'); +var circleCanvas = document.getElementById('circle'); + +var changeHitTolerance = function() { + var value = parseInt(selectHitToleranceElement.value, 10); + selectSingleClick.setHitTolerance(value); + selectClick.setHitTolerance(value); + selectPointerMove.setHitTolerance(value); + selectAltClick.setHitTolerance(value); + + var size = 2 * value + 2; + circleCanvas.width = size; + circleCanvas.height = size; + var ctx = circleCanvas.getContext('2d'); + ctx.clearRect(0, 0, size, size); + ctx.beginPath(); + ctx.arc(value + 1, value + 1, value + 0.5, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); +}; + +selectHitToleranceElement.onchange = changeHitTolerance; +changeHitTolerance(); diff --git a/externs/olx.js b/externs/olx.js index dd7bac146e..d8317a98c0 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -3212,7 +3212,8 @@ olx.interaction.PointerOptions.prototype.handleUpEvent; * multi: (boolean|undefined), * features: (ol.Collection.|undefined), * filter: (ol.SelectFilterFunction|undefined), - * wrapX: (boolean|undefined)}} + * wrapX: (boolean|undefined), + * hitTolerance: (number|undefined)}} */ olx.interaction.SelectOptions; @@ -3326,6 +3327,16 @@ olx.interaction.SelectOptions.prototype.filter; olx.interaction.SelectOptions.prototype.wrapX; +/** + * Hit-detection tolerance. Pixels inside the radius around the given position + * will be checked for features. This only works for the canvas renderer and + * not for WebGL. + * @type {number|undefined} + * @api + */ +olx.interaction.SelectOptions.prototype.hitTolerance; + + /** * Options for snap * @typedef {{ diff --git a/src/ol/interaction/select.js b/src/ol/interaction/select.js index d6b233cab6..dbcd376efc 100644 --- a/src/ol/interaction/select.js +++ b/src/ol/interaction/select.js @@ -82,6 +82,8 @@ ol.interaction.Select = function(opt_options) { this.filter_ = options.filter ? options.filter : ol.functions.TRUE; + this.hitTolerance_ = options.hitTolerance ? options.hitTolerance : 0; + var featureOverlay = new ol.layer.Vector({ source: new ol.source.Vector({ useSpatialIndex: false, @@ -160,6 +162,16 @@ ol.interaction.Select.prototype.getFeatures = function() { }; +/** + * Returns the Hit-detection tolerance. + * @returns {number} Hit tolerance. + * @api + */ +ol.interaction.Select.prototype.getHitTolerance = function() { + return this.hitTolerance_; +}; + + /** * Returns the associated {@link ol.layer.Vector vectorlayer} of * the (last) selected feature. Note that this will not work with any @@ -212,7 +224,10 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { this.addFeatureLayerAssociation_(feature, layer); return !this.multi_; } - }, this, {layerFilter: this.layerFilter_}); + }, this, { + layerFilter: this.layerFilter_, + hitTolerance: this.hitTolerance_ + }); var i; for (i = features.getLength() - 1; i >= 0; --i) { var feature = features.item(i); @@ -249,7 +264,10 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { } return !this.multi_; } - }, this, {layerFilter: this.layerFilter_}); + }, this, { + layerFilter: this.layerFilter_, + hitTolerance: this.hitTolerance_ + }); var j; for (j = deselected.length - 1; j >= 0; --j) { features.remove(deselected[j]); @@ -265,6 +283,18 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { }; +/** + * Hit-detection tolerance. Pixels inside the radius around the given position + * will be checked for features. This only works for the canvas renderer and + * not for WebGL. + * @param {number} hitTolerance Hit tolerance. + * @api + */ +ol.interaction.Select.prototype.setHitTolerance = function(hitTolerance) { + this.hitTolerance_ = hitTolerance; +}; + + /** * Remove the interaction from its current map, if any, and attach it to a new * map, if any. Pass `null` to just remove the interaction from the current map. From da020d77e9d226ae2e6a22efc925ef7207166ffb Mon Sep 17 00:00:00 2001 From: simonseyock Date: Tue, 25 Oct 2016 18:39:59 +0200 Subject: [PATCH 10/24] Added cache for circle arrays. Reusing context. --- src/ol/render/canvas/replaygroup.js | 115 ++++++++++++++++------------ 1 file changed, 68 insertions(+), 47 deletions(-) diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index 9250a55887..3614a54244 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -65,38 +65,65 @@ ol.render.canvas.ReplayGroup = function( * Object.>} */ this.replaysByZIndex_ = {}; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.hitDetectionContext_ = ol.dom.createCanvasContext2D(1, 1); }; ol.inherits(ol.render.canvas.ReplayGroup, ol.render.ReplayGroup); + /** - * This methods creates a circle inside a fitting array. Points inside the - * circle are marked by true, points on the outside are marked with false. - * It uses the midpoint circle algorithm. - * @param {number} radius Radius. - * @returns {Array.>} an array with marked circel points. + * @type {Object.>>} * @private */ -ol.render.canvas.ReplayGroup.createPixelCircle_ = function(radius) { +ol.render.canvas.ReplayGroup.circleArrayCache_ = { + 0: [[true]] +}; + + +/** + * This method fills a row in the array from the given coordinate to the + * middle with `true`. + * @param {Array.>} array The array that will be altered. + * @param {number} x X coordinate. + * @param {number} y Y coordinate. + * @private + */ +ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_ = function(array, x, y) { + var i; + var radius = Math.floor(array.length / 2); + if (x >= radius) { + for (i = radius; i < x; i++) { + array[i][y] = true; + } + } else if (x < radius) { + for (i = x + 1; i < radius; i++) { + array[i][y] = true; + } + } +}; + + +/** + * This methods creates a circle inside a fitting array. Points inside the + * circle are marked by true, points on the outside are undefined. + * It uses the midpoint circle algorithm. + * @param {number} radius Radius. + * @returns {Array.>} An array with marked circle points. + * @private + */ +ol.render.canvas.ReplayGroup.getCircleArray_ = function(radius) { + if (ol.render.canvas.ReplayGroup.circleArrayCache_[radius] !== undefined) { + return ol.render.canvas.ReplayGroup.circleArrayCache_[radius]; + } + var arraySize = radius * 2 + 1; var arr = new Array(arraySize); for (var i = 0; i < arraySize; i++) { arr[i] = new Array(arraySize); - for (var j = 0; j < arraySize; j++) { - arr[i][j] = false; - } - } - - function fillRowToMiddle(x, y) { - var i; - if (x >= radius) { - for (i = radius; i < x; i++) { - arr[i][y] = true; - } - } else if (x < radius) { - for (i = x + 1; i < radius; i++) { - arr[i][y] = true; - } - } } var x = radius; @@ -104,14 +131,14 @@ ol.render.canvas.ReplayGroup.createPixelCircle_ = function(radius) { var error = 0; while (x >= y) { - fillRowToMiddle(radius + x, radius + y); - fillRowToMiddle(radius + y, radius + x); - fillRowToMiddle(radius - y, radius + x); - fillRowToMiddle(radius - x, radius + y); - fillRowToMiddle(radius - x, radius - y); - fillRowToMiddle(radius - y, radius - x); - fillRowToMiddle(radius + y, radius - x); - fillRowToMiddle(radius + x, radius - y); + ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + x, radius + y); + ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + y, radius + x); + ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - y, radius + x); + ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - x, radius + y); + ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - x, radius - y); + ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - y, radius - x); + ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + y, radius - x); + ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + x, radius - y); y++; error += 1 + 2 * y; @@ -121,6 +148,7 @@ ol.render.canvas.ReplayGroup.createPixelCircle_ = function(radius) { } } + ol.render.canvas.ReplayGroup.circleArrayCache_[radius] = arr; return arr; }; @@ -143,7 +171,7 @@ ol.render.canvas.ReplayGroup.prototype.finish = function() { * @param {ol.Coordinate} coordinate Coordinate. * @param {number} resolution Resolution. * @param {number} rotation Rotation. - * @param {number} hitTolerance hit tolerance. + * @param {number} hitTolerance Hit tolerance. * @param {Object.} skippedFeaturesHash Ids of features * to skip. * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature @@ -155,16 +183,15 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback) { hitTolerance = Math.round(hitTolerance); - var contextSize = hitTolerance * 2 + 1; - var transform = ol.transform.compose(ol.transform.create(), hitTolerance + 0.5, hitTolerance + 0.5, 1 / resolution, -1 / resolution, -rotation, -coordinate[0], -coordinate[1]); - - var context = ol.dom.createCanvasContext2D(contextSize, contextSize); + var context = this.hitDetectionContext_; + context.canvas.width = contextSize; + context.canvas.height = contextSize; context.clearRect(0, 0, contextSize, contextSize); /** @@ -177,12 +204,7 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( ol.extent.buffer(hitExtent, resolution * this.renderBuffer_, hitExtent); } - var mask; - if (hitTolerance === 0) { - mask = [[true]]; - } else { - mask = ol.render.canvas.ReplayGroup.createPixelCircle_(hitTolerance); - } + var mask = ol.render.canvas.ReplayGroup.getCircleArray_(hitTolerance); return this.replayHitDetection_(context, transform, rotation, skippedFeaturesHash, @@ -191,23 +213,22 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( * @return {?} Callback result. */ function(feature) { - var imageData; + var imageData = context.getImageData(0, 0, contextSize, contextSize).data; for (var i = 0; i < contextSize; i++) { for (var j = 0; j < contextSize; j++) { if (mask[i][j]) { - imageData = context.getImageData(i, j, i + 1, j + 1).data; - if (imageData[3] > 0) { + if (imageData[(j * contextSize + i) * 4 + 3] > 0) { var result = callback(feature); if (result) { return result; + } else { + context.clearRect(0, 0, contextSize, contextSize); + return undefined; } - i = contextSize; - j = contextSize; } } } } - context.clearRect(0, 0, contextSize, contextSize); }, hitExtent); }; From f0439685adeefce6be90a1a179d534938f800bbd Mon Sep 17 00:00:00 2001 From: simonseyock Date: Tue, 25 Oct 2016 18:59:28 +0200 Subject: [PATCH 11/24] minor typos --- src/ol/renderer/layer.js | 2 +- src/ol/renderer/map.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ol/renderer/layer.js b/src/ol/renderer/layer.js index cab6f36e06..67dcca8f78 100644 --- a/src/ol/renderer/layer.js +++ b/src/ol/renderer/layer.js @@ -35,7 +35,7 @@ ol.inherits(ol.renderer.Layer, ol.Observable); /** * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState Frame state. - * @param {number} hitTolerance hit tolerance. + * @param {number} hitTolerance Hit tolerance. * @param {function(this: S, (ol.Feature|ol.render.Feature), ol.layer.Layer): T} * callback Feature callback. * @param {S} thisArg Value to use as `this` when executing `callback`. diff --git a/src/ol/renderer/map.js b/src/ol/renderer/map.js index 8af9c55c26..9edacb4d66 100644 --- a/src/ol/renderer/map.js +++ b/src/ol/renderer/map.js @@ -100,7 +100,7 @@ ol.renderer.Map.expireIconCache_ = function(map, frameState) { /** * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState FrameState. - * @param {number} hitTolerance hit tolerance. + * @param {number} hitTolerance Hit tolerance. * @param {function(this: S, (ol.Feature|ol.render.Feature), * ol.layer.Layer): T} callback Feature callback. * @param {S} thisArg Value to use as `this` when executing `callback`. @@ -189,7 +189,7 @@ ol.renderer.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, call /** * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState FrameState. - * @param {number} hitTolerance HitTolerance. + * @param {number} hitTolerance Hit tolerance. * @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 From 5608e7284bb054b03ae470e0a84ee504fa09badb Mon Sep 17 00:00:00 2001 From: simonseyock Date: Fri, 28 Oct 2016 13:21:28 +0200 Subject: [PATCH 12/24] Renamed FeatureAtPixelOptions to AtPixelOptions for future use in forEachLayerAtPixel --- externs/olx.js | 8 ++++---- src/ol/map.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index d8317a98c0..72ea000573 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -309,7 +309,7 @@ olx.MapOptions.prototype.view; * layerFilterThis: (Object|undefined), * hitTolerance: (number|undefined)}} */ -olx.FeatureAtPixelOptions; +olx.AtPixelOptions; /** @@ -320,7 +320,7 @@ olx.FeatureAtPixelOptions; * @type {((function(ol.layer.Layer): boolean)|undefined)} * @api stable */ -olx.FeatureAtPixelOptions.prototype.layerFilter; +olx.AtPixelOptions.prototype.layerFilter; /** @@ -328,7 +328,7 @@ olx.FeatureAtPixelOptions.prototype.layerFilter; * @type {Object|undefined} * @api stable */ -olx.FeatureAtPixelOptions.prototype.layerFilterThis; +olx.AtPixelOptions.prototype.layerFilterThis; /** @@ -338,7 +338,7 @@ olx.FeatureAtPixelOptions.prototype.layerFilterThis; * @type {number|undefined} * @api */ -olx.FeatureAtPixelOptions.prototype.hitTolerance; +olx.AtPixelOptions.prototype.hitTolerance; /** diff --git a/src/ol/map.js b/src/ol/map.js index c915437507..abcfa8335a 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -568,7 +568,7 @@ ol.Map.prototype.disposeInternal = function() { * unmanaged layers. To stop detection, callback functions can return a * truthy value. * @param {S=} opt_this Value to use as this when executing callback. - * @param {olx.FeatureAtPixelOptions=} opt_options Optional options. + * @param {olx.AtPixelOptions=} opt_options Optional 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 @@ -632,7 +632,7 @@ ol.Map.prototype.forEachLayerAtPixel = function(pixel, callback, opt_this, opt_l * Detect if features intersect a pixel on the viewport. Layers included in the * detection can be configured through `opt_layerFilter`. * @param {ol.Pixel} pixel Pixel. - * @param {olx.FeatureAtPixelOptions=} opt_options Optional options. + * @param {olx.AtPixelOptions=} opt_options Optional options. * @return {boolean} Is there a feature at the given pixel? * @template U * @api From c93b0ba8592a35af1a43aa18f79e0978718e2e12 Mon Sep 17 00:00:00 2001 From: simonseyock Date: Fri, 28 Oct 2016 13:34:32 +0200 Subject: [PATCH 13/24] Added upgrade note --- changelog/upgrade-notes.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index 22f5e10096..5d3286d269 100644 --- a/changelog/upgrade-notes.md +++ b/changelog/upgrade-notes.md @@ -74,6 +74,20 @@ As last step in the removal of the dependency on Google Closure Library, the `go `ol.format.ogc.filter` was simplified to `ol.format.filter`; to upgrade your code, simply remove the `ogc` string from the name. For example: `ol.format.ogc.filter.and` to `ol.format.filter.and`. +#### `ol.Map#forEachFeatureAtPixel` and `ol.Map#hasFeatureAtPixel` parameters have changed + +If you are using the layer filter of one of these methods, please note that you now have to pass in the layer filter via an `ol.AtPixelOptions` object. If you are not using the layer filter the usage has not changed. + +New syntax: +``` + map.forEachFeatureAtPixel(pixel, callback, callback_this, { + layerFilter: layerFilterFunction, + layerFilterThis: someObject + }) +``` + +This change is due to the introduction of the `hitTolerance` parameter which can be passed in via this `ol.AtPixelOptions` object, too. + #### Changes only relevant to those who compile their applications together with the Closure Compiler A number of internal types have been renamed. This will not affect those who use the API provided by the library, but if you are compiling your application together with OpenLayers and using type names, you'll need to do the following: From 4640c44f2a7e9a6f579de46883b3b177fd177708 Mon Sep 17 00:00:00 2001 From: simonseyock Date: Fri, 28 Oct 2016 14:32:11 +0200 Subject: [PATCH 14/24] Added old syntax and hasFeatureAtPixel to changelog --- changelog/upgrade-notes.md | 40 +++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index 5d3286d269..a534c653af 100644 --- a/changelog/upgrade-notes.md +++ b/changelog/upgrade-notes.md @@ -2,6 +2,32 @@ ### Next release +#### `ol.Map#forEachFeatureAtPixel` and `ol.Map#hasFeatureAtPixel` parameters have changed + +If you are using the layer filter of one of these methods, please note that you now have to pass in the layer filter via an `ol.AtPixelOptions` object. If you are not using the layer filter the usage has not changed. + +Old syntax: +``` + map.forEachFeatureAtPixel(pixel, callback, callback_this, layerFilter_function, layerFilter_this); + + map.hasFeatureAtPixel(pixel, layerFilter_function, layerFilter_this); +``` + +New syntax: +``` + map.forEachFeatureAtPixel(pixel, callback, callback_this, { + layerFilter: layerFilter_function, + layerFilterThis: layerFilter_this + }); + + map.hasFeatureAtPixel(pixel, { + layerFilter: layerFilter_function, + layerFilterThis: layerFilter_this + }); +``` + +This change is due to the introduction of the `hitTolerance` parameter which can be passed in via this `ol.AtPixelOptions` object, too. + #### Use `view.animate()` instead of `map.beforeRender()` and `ol.animation` functions The `map.beforeRender()` and `ol.animation` functions have been deprecated in favor of a new `view.animate()` function. Use of the deprecated functions will result in a warning during development. These functions are subject to removal in an upcoming release. @@ -74,20 +100,6 @@ As last step in the removal of the dependency on Google Closure Library, the `go `ol.format.ogc.filter` was simplified to `ol.format.filter`; to upgrade your code, simply remove the `ogc` string from the name. For example: `ol.format.ogc.filter.and` to `ol.format.filter.and`. -#### `ol.Map#forEachFeatureAtPixel` and `ol.Map#hasFeatureAtPixel` parameters have changed - -If you are using the layer filter of one of these methods, please note that you now have to pass in the layer filter via an `ol.AtPixelOptions` object. If you are not using the layer filter the usage has not changed. - -New syntax: -``` - map.forEachFeatureAtPixel(pixel, callback, callback_this, { - layerFilter: layerFilterFunction, - layerFilterThis: someObject - }) -``` - -This change is due to the introduction of the `hitTolerance` parameter which can be passed in via this `ol.AtPixelOptions` object, too. - #### Changes only relevant to those who compile their applications together with the Closure Compiler A number of internal types have been renamed. This will not affect those who use the API provided by the library, but if you are compiling your application together with OpenLayers and using type names, you'll need to do the following: From e9d2b913eabc286fa37c4e019b55a48ae2ddbb43 Mon Sep 17 00:00:00 2001 From: simonseyock Date: Fri, 28 Oct 2016 15:43:40 +0200 Subject: [PATCH 15/24] removed uses of lazy evaluation of || --- src/ol/map.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/ol/map.js b/src/ol/map.js index abcfa8335a..4bf8ec9dc7 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -574,16 +574,19 @@ ol.Map.prototype.disposeInternal = function() { * @template S,T * @api stable */ -ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this, opt_options) { - opt_options = opt_options !== undefined ? opt_options : {}; +ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this,opt_options) { if (!this.frameState_) { return; } var coordinate = this.getCoordinateFromPixel(pixel); - var hitTolerance = opt_options.hitTolerance || 0; + opt_options = opt_options !== undefined ? opt_options : {}; + var hitTolerance = opt_options.hitTolerance !== undefined ? + opt_options.hitTolerance : 0; var thisArg = opt_this !== undefined ? opt_this : null; - var layerFilter = opt_options.layerFilter || ol.functions.TRUE; - var thisArg2 = opt_options.layerFilterThis || null; + var layerFilter = opt_options.layerFilter !== undefined ? + opt_options.layerFilter : ol.functions.TRUE; + var thisArg2 = opt_options.layerFilterThis !== undefined ? + opt_options.layerFilterThis : null; return this.renderer_.forEachFeatureAtCoordinate( coordinate, this.frameState_, hitTolerance, callback, thisArg, layerFilter, thisArg2); @@ -638,16 +641,19 @@ ol.Map.prototype.forEachLayerAtPixel = function(pixel, callback, opt_this, opt_l * @api */ ol.Map.prototype.hasFeatureAtPixel = function(pixel, opt_options) { - opt_options = opt_options !== undefined ? opt_options : {}; if (!this.frameState_) { return false; } var coordinate = this.getCoordinateFromPixel(pixel); + opt_options = opt_options !== undefined ? opt_options : {}; var layerFilter = opt_options.layerFilter !== undefined ? opt_options.layerFilter : ol.functions.TRUE; - var thisArg = opt_options.layerFilterThis !== undefined ? opt_options.layerFilterThis : null; + var thisArg = opt_options.layerFilterThis !== undefined ? + opt_options.layerFilterThis : null; + var hitTolerance = opt_options.hitTolerance !== undefined ? + opt_options.hitTolerance : 0; return this.renderer_.hasFeatureAtCoordinate( - coordinate, this.frameState_, opt_options.hitTolerance || 0, layerFilter, thisArg); + coordinate, this.frameState_, hitTolerance, layerFilter, thisArg); }; From 3a08cfd7c6b7d71c083b6f27b558c2c1e1ab292b Mon Sep 17 00:00:00 2001 From: simonseyock Date: Wed, 2 Nov 2016 11:36:11 +0100 Subject: [PATCH 16/24] Added test for ol.render.canvas.ReplayGroup#getCircleArray_ --- .../spec/ol/render/canvas/replaygroup.test.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/spec/ol/render/canvas/replaygroup.test.js diff --git a/test/spec/ol/render/canvas/replaygroup.test.js b/test/spec/ol/render/canvas/replaygroup.test.js new file mode 100644 index 0000000000..7de4117e8d --- /dev/null +++ b/test/spec/ol/render/canvas/replaygroup.test.js @@ -0,0 +1,33 @@ +goog.provide('ol.test.render.canvas.ReplayGroup'); + +goog.require('ol.render.canvas.ReplayGroup'); + + +describe('ol.render.canvas.ReplayGroup', function() { + + describe('#getCircleArray_', function() { + it('creates an array with a pixelated circle marked with true', function() { + var radius = 10; + var minRadiusSq = Math.pow(radius - Math.SQRT2, 2); + var maxRadiusSq = Math.pow(radius + Math.SQRT2, 2); + var circleArray = ol.render.canvas.ReplayGroup.getCircleArray_(radius); + var size = radius * 2 + 1; + expect(circleArray.length).to.be(size); + + for (var i = 0; i < size; i++) { + expect(circleArray[i].length).to.be(size); + for (var j = 0; j < size; j++) { + var dx = Math.abs(radius - i); + var dy = Math.abs(radius - j); + var distanceSq = Math.pow(dx, 2) + Math.pow(dy, 2); + if (circleArray[i][j] === true) { + expect(distanceSq).to.be.within(0, maxRadiusSq); + } else { + expect(distanceSq).to.be.within(minRadiusSq, Infinity); + } + } + } + }); + }); + +}); From eede027417ab416929e4cb99e42ad414ffa9802c Mon Sep 17 00:00:00 2001 From: simonseyock Date: Thu, 10 Nov 2016 17:26:48 +0100 Subject: [PATCH 17/24] Added buffer to ol.renderer.canvas.VectorTileLayer#forEachFeatureAtCoordinate buffering by pointResolution times hitTolerance --- src/ol/renderer/canvas/vectortilelayer.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ol/renderer/canvas/vectortilelayer.js b/src/ol/renderer/canvas/vectortilelayer.js index 082d19d62b..01f005590c 100644 --- a/src/ol/renderer/canvas/vectortilelayer.js +++ b/src/ol/renderer/canvas/vectortilelayer.js @@ -179,6 +179,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.drawTileImage = function( ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { var resolution = frameState.viewState.resolution; var rotation = frameState.viewState.rotation; + hitTolerance = hitTolerance == undefined ? 0 : hitTolerance; var layer = this.getLayer(); /** @type {Object.} */ var features = {}; @@ -190,12 +191,13 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi var tileGrid = source.getTileGrid(); var found, tileSpaceCoordinate; var i, ii, origin, replayGroup; - var tile, tileCoord, tileExtent, tilePixelRatio, tileResolution; + var tile, tileCoord, tileExtent, tilePixelRatio, tileResolution, pointResolution; for (i = 0, ii = replayables.length; i < ii; ++i) { tile = replayables[i]; tileCoord = tile.tileCoord; tileExtent = source.getTileGrid().getTileCoordExtent(tileCoord, this.tmpExtent); - if (!ol.extent.containsCoordinate(tileExtent, coordinate)) { + pointResolution = tile.getProjection().getPointResolution(resolution, coordinate); + if (!ol.extent.containsCoordinate(ol.extent.buffer(tileExtent, hitTolerance * pointResolution), coordinate)) { continue; } if (tile.getProjection().getUnits() === ol.proj.Units.TILE_PIXELS) { From f6ee11bb68cafc3219d471f25af406f20debf19b Mon Sep 17 00:00:00 2001 From: simonseyock Date: Fri, 25 Nov 2016 10:07:13 +0100 Subject: [PATCH 18/24] Documentation and linting --- changelog/upgrade-notes.md | 14 +++++++------- externs/olx.js | 4 ++-- src/ol/map.js | 2 +- src/ol/render/canvas/replaygroup.js | 3 +++ 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index a534c653af..52150aea99 100644 --- a/changelog/upgrade-notes.md +++ b/changelog/upgrade-notes.md @@ -8,21 +8,21 @@ If you are using the layer filter of one of these methods, please note that you Old syntax: ``` - map.forEachFeatureAtPixel(pixel, callback, callback_this, layerFilter_function, layerFilter_this); + map.forEachFeatureAtPixel(pixel, callback, callbackThis, layerFilterFn, layerFilterThis); - map.hasFeatureAtPixel(pixel, layerFilter_function, layerFilter_this); + map.hasFeatureAtPixel(pixel, layerFilterFn, layerFilterThis); ``` New syntax: ``` - map.forEachFeatureAtPixel(pixel, callback, callback_this, { - layerFilter: layerFilter_function, - layerFilterThis: layerFilter_this + map.forEachFeatureAtPixel(pixel, callback, callbackThis, { + layerFilter: layerFilterFn, + layerFilterThis: layerFilterThis }); map.hasFeatureAtPixel(pixel, { - layerFilter: layerFilter_function, - layerFilterThis: layerFilter_this + layerFilter: layerFilterFn, + layerFilterThis: layerFilterThis }); ``` diff --git a/externs/olx.js b/externs/olx.js index 72ea000573..8056fa3203 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -303,8 +303,8 @@ olx.MapOptions.prototype.view; /** - * Object literal with options for the forEachFeatureAtPixel and - * hasFeatureAtPixel methods. + * Object literal with options for the {@link ol.Map#forEachFeatureAtPixel} and + * {@link ol.Map#hasFeatureAtPixel} methods. * @typedef {{layerFilter: ((function(ol.layer.Layer): boolean)|undefined), * layerFilterThis: (Object|undefined), * hitTolerance: (number|undefined)}} diff --git a/src/ol/map.js b/src/ol/map.js index 4bf8ec9dc7..d64cb9e12d 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -574,7 +574,7 @@ ol.Map.prototype.disposeInternal = function() { * @template S,T * @api stable */ -ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this,opt_options) { +ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this, opt_options) { if (!this.frameState_) { return; } diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index 3614a54244..b928d8293f 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -76,6 +76,8 @@ ol.inherits(ol.render.canvas.ReplayGroup, ol.render.ReplayGroup); /** + * This cache is used for storing calculated pixel circles for increasing performance. + * It is a static property to allow each Replaygroup to access it. * @type {Object.>>} * @private */ @@ -111,6 +113,7 @@ ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_ = function(array, x, y) * This methods creates a circle inside a fitting array. Points inside the * circle are marked by true, points on the outside are undefined. * It uses the midpoint circle algorithm. + * A cache is used to increase performance. * @param {number} radius Radius. * @returns {Array.>} An array with marked circle points. * @private From 00a4f3b410b19ce6c4fe5fa2fb01273ac3bb5763 Mon Sep 17 00:00:00 2001 From: simonseyock Date: Fri, 25 Nov 2016 10:12:43 +0100 Subject: [PATCH 19/24] buffering extent by resolution*hitTolerance --- src/ol/render/canvas/replaygroup.js | 2 +- src/ol/renderer/canvas/vectortilelayer.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index b928d8293f..a6656fb4a4 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -204,7 +204,7 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( if (this.renderBuffer_ !== undefined) { hitExtent = ol.extent.createEmpty(); ol.extent.extendCoordinate(hitExtent, coordinate); - ol.extent.buffer(hitExtent, resolution * this.renderBuffer_, hitExtent); + ol.extent.buffer(hitExtent, resolution * (this.renderBuffer_ + hitTolerance), hitExtent); } var mask = ol.render.canvas.ReplayGroup.getCircleArray_(hitTolerance); diff --git a/src/ol/renderer/canvas/vectortilelayer.js b/src/ol/renderer/canvas/vectortilelayer.js index 01f005590c..0999ebbb13 100644 --- a/src/ol/renderer/canvas/vectortilelayer.js +++ b/src/ol/renderer/canvas/vectortilelayer.js @@ -191,13 +191,12 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi var tileGrid = source.getTileGrid(); var found, tileSpaceCoordinate; var i, ii, origin, replayGroup; - var tile, tileCoord, tileExtent, tilePixelRatio, tileResolution, pointResolution; + var tile, tileCoord, tileExtent, tilePixelRatio, tileResolution; for (i = 0, ii = replayables.length; i < ii; ++i) { tile = replayables[i]; tileCoord = tile.tileCoord; tileExtent = source.getTileGrid().getTileCoordExtent(tileCoord, this.tmpExtent); - pointResolution = tile.getProjection().getPointResolution(resolution, coordinate); - if (!ol.extent.containsCoordinate(ol.extent.buffer(tileExtent, hitTolerance * pointResolution), coordinate)) { + if (!ol.extent.containsCoordinate(ol.extent.buffer(tileExtent, hitTolerance * resolution), coordinate)) { continue; } if (tile.getProjection().getUnits() === ol.proj.Units.TILE_PIXELS) { From a4698039415143d4b04a66c5dfe219be8d192c2e Mon Sep 17 00:00:00 2001 From: simonseyock Date: Thu, 1 Dec 2016 15:47:34 +0100 Subject: [PATCH 20/24] adjusted hitTolerance by pixelRatio --- src/ol/map.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ol/map.js b/src/ol/map.js index d64cb9e12d..5703503f5e 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -581,7 +581,7 @@ ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this, opt var coordinate = this.getCoordinateFromPixel(pixel); opt_options = opt_options !== undefined ? opt_options : {}; var hitTolerance = opt_options.hitTolerance !== undefined ? - opt_options.hitTolerance : 0; + opt_options.hitTolerance * this.frameState_.pixelRatio : 0; var thisArg = opt_this !== undefined ? opt_this : null; var layerFilter = opt_options.layerFilter !== undefined ? opt_options.layerFilter : ol.functions.TRUE; @@ -651,7 +651,7 @@ ol.Map.prototype.hasFeatureAtPixel = function(pixel, opt_options) { var thisArg = opt_options.layerFilterThis !== undefined ? opt_options.layerFilterThis : null; var hitTolerance = opt_options.hitTolerance !== undefined ? - opt_options.hitTolerance : 0; + opt_options.hitTolerance * this.frameState_.pixelRatio : 0; return this.renderer_.hasFeatureAtCoordinate( coordinate, this.frameState_, hitTolerance, layerFilter, thisArg); }; From 8edc2be103920a7ca31b6627c751c09bd278add9 Mon Sep 17 00:00:00 2001 From: simonseyock Date: Thu, 1 Dec 2016 16:06:13 +0100 Subject: [PATCH 21/24] Added hitTolerance to TranslateInteraction --- externs/olx.js | 13 ++++++++++++- src/ol/interaction/select.js | 4 ++++ src/ol/interaction/translate.js | 31 ++++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index 8056fa3203..30411d8546 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -2910,7 +2910,8 @@ olx.interaction.ExtentOptions.prototype.wrapX; /** * @typedef {{ * features: (ol.Collection.|undefined), - * layers: (undefined|Array.|function(ol.layer.Layer): boolean) + * layers: (undefined|Array.|function(ol.layer.Layer): boolean), + * hitTolerance: (number|undefined) * }} */ olx.interaction.TranslateOptions; @@ -2937,6 +2938,16 @@ olx.interaction.TranslateOptions.prototype.features; olx.interaction.TranslateOptions.prototype.layers; +/** + * Hit-detection tolerance. Pixels inside the radius around the given position + * will be checked for features. This only works for the canvas renderer and + * not for WebGL. + * @type {number|undefined} + * @api + */ +olx.interaction.TranslateOptions.prototype.hitTolerance; + + /** * @typedef {{condition: (ol.EventsConditionType|undefined), * duration: (number|undefined), diff --git a/src/ol/interaction/select.js b/src/ol/interaction/select.js index dbcd376efc..55d6f6c740 100644 --- a/src/ol/interaction/select.js +++ b/src/ol/interaction/select.js @@ -82,6 +82,10 @@ ol.interaction.Select = function(opt_options) { this.filter_ = options.filter ? options.filter : ol.functions.TRUE; + /** + * @private + * @type {number} + */ this.hitTolerance_ = options.hitTolerance ? options.hitTolerance : 0; var featureOverlay = new ol.layer.Vector({ diff --git a/src/ol/interaction/translate.js b/src/ol/interaction/translate.js index b1aade1db1..68f0864b0f 100644 --- a/src/ol/interaction/translate.js +++ b/src/ol/interaction/translate.js @@ -70,6 +70,12 @@ ol.interaction.Translate = function(opt_options) { */ this.layerFilter_ = layerFilter; + /** + * @private + * @type {number} + */ + this.hitTolerance_ = options.hitTolerance ? options.hitTolerance : 0; + /** * @type {ol.Feature} * @private @@ -198,11 +204,34 @@ ol.interaction.Translate.prototype.featuresAtPixel_ = function(pixel, map) { return feature; } }, this, { - layerFilter: this.layerFilter_ + layerFilter: this.layerFilter_, + hitTolerance: this.hitTolerance_ }); }; +/** + * Returns the Hit-detection tolerance. + * @returns {number} Hit tolerance. + * @api + */ +ol.interaction.Translate.prototype.getHitTolerance = function() { + return this.hitTolerance_; +}; + + +/** + * Hit-detection tolerance. Pixels inside the radius around the given position + * will be checked for features. This only works for the canvas renderer and + * not for WebGL. + * @param {number} hitTolerance Hit tolerance. + * @api + */ +ol.interaction.Translate.prototype.setHitTolerance = function(hitTolerance) { + this.hitTolerance_ = hitTolerance; +}; + + /** * @classdesc * Events emitted by {@link ol.interaction.Translate} instances are instances of From 55ec0af0722f720740ca7af18195fae1774e4e9f Mon Sep 17 00:00:00 2001 From: simonseyock Date: Wed, 7 Dec 2016 11:14:59 +0100 Subject: [PATCH 22/24] Simplified api --- changelog/upgrade-notes.md | 10 +++++----- externs/olx.js | 11 +---------- src/ol/interaction/select.js | 8 ++++---- src/ol/interaction/translate.js | 2 +- src/ol/map.js | 14 ++++---------- test/spec/ol/renderer/canvas/map.test.js | 6 +++--- 6 files changed, 18 insertions(+), 33 deletions(-) diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index 52150aea99..513021d1a3 100644 --- a/changelog/upgrade-notes.md +++ b/changelog/upgrade-notes.md @@ -15,17 +15,17 @@ Old syntax: New syntax: ``` - map.forEachFeatureAtPixel(pixel, callback, callbackThis, { - layerFilter: layerFilterFn, - layerFilterThis: layerFilterThis + map.forEachFeatureAtPixel(pixel, callback, { + layerFilter: layerFilterFn }); map.hasFeatureAtPixel(pixel, { - layerFilter: layerFilterFn, - layerFilterThis: layerFilterThis + layerFilter: layerFilterFn }); ``` +To bind a function to a this, please use the bind method of the function (See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind). + This change is due to the introduction of the `hitTolerance` parameter which can be passed in via this `ol.AtPixelOptions` object, too. #### Use `view.animate()` instead of `map.beforeRender()` and `ol.animation` functions diff --git a/externs/olx.js b/externs/olx.js index 30411d8546..3ef5688209 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -306,7 +306,6 @@ olx.MapOptions.prototype.view; * Object literal with options for the {@link ol.Map#forEachFeatureAtPixel} and * {@link ol.Map#hasFeatureAtPixel} methods. * @typedef {{layerFilter: ((function(ol.layer.Layer): boolean)|undefined), - * layerFilterThis: (Object|undefined), * hitTolerance: (number|undefined)}} */ olx.AtPixelOptions; @@ -323,14 +322,6 @@ olx.AtPixelOptions; olx.AtPixelOptions.prototype.layerFilter; -/** - * Value to use as `this` when executing `layerFilter`. - * @type {Object|undefined} - * @api stable - */ -olx.AtPixelOptions.prototype.layerFilterThis; - - /** * Hit-detection tolerance. Pixels inside the radius around the given position * will be checked for features. This only works for the canvas renderer and @@ -7684,7 +7675,7 @@ olx.view.FitOptions.prototype.maxZoom; /** - * The duration of the animation in milliseconds. By default, there is no + * The duration of the animation in milliseconds. By default, there is no * animations. * @type {number|undefined} * @api diff --git a/src/ol/interaction/select.js b/src/ol/interaction/select.js index 55d6f6c740..daf4a0cdd1 100644 --- a/src/ol/interaction/select.js +++ b/src/ol/interaction/select.js @@ -217,7 +217,7 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { // the pixel. ol.obj.clear(this.featureLayerAssociation_); map.forEachFeatureAtPixel(mapBrowserEvent.pixel, - /** + (/** * @param {ol.Feature|ol.render.Feature} feature Feature. * @param {ol.layer.Layer} layer Layer. * @return {boolean|undefined} Continue to iterate over the features. @@ -228,7 +228,7 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { this.addFeatureLayerAssociation_(feature, layer); return !this.multi_; } - }, this, { + }).bind(this), { layerFilter: this.layerFilter_, hitTolerance: this.hitTolerance_ }); @@ -250,7 +250,7 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { } else { // Modify the currently selected feature(s). map.forEachFeatureAtPixel(mapBrowserEvent.pixel, - /** + (/** * @param {ol.Feature|ol.render.Feature} feature Feature. * @param {ol.layer.Layer} layer Layer. * @return {boolean|undefined} Continue to iterate over the features. @@ -268,7 +268,7 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { } return !this.multi_; } - }, this, { + }).bind(this), { layerFilter: this.layerFilter_, hitTolerance: this.hitTolerance_ }); diff --git a/src/ol/interaction/translate.js b/src/ol/interaction/translate.js index 68f0864b0f..2bc23fd46e 100644 --- a/src/ol/interaction/translate.js +++ b/src/ol/interaction/translate.js @@ -203,7 +203,7 @@ ol.interaction.Translate.prototype.featuresAtPixel_ = function(pixel, map) { ol.array.includes(this.features_.getArray(), feature)) { return feature; } - }, this, { + }.bind(this), { layerFilter: this.layerFilter_, hitTolerance: this.hitTolerance_ }); diff --git a/src/ol/map.js b/src/ol/map.js index 5703503f5e..9703f39d09 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -567,14 +567,13 @@ ol.Map.prototype.disposeInternal = function() { * the {@link ol.layer.Layer layer} of the feature and will be null for * unmanaged layers. To stop detection, callback functions can return a * truthy value. - * @param {S=} opt_this Value to use as this when executing callback. * @param {olx.AtPixelOptions=} opt_options Optional 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 * @api stable */ -ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this, opt_options) { +ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_options) { if (!this.frameState_) { return; } @@ -582,14 +581,11 @@ ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this, opt opt_options = opt_options !== undefined ? opt_options : {}; var hitTolerance = opt_options.hitTolerance !== undefined ? opt_options.hitTolerance * this.frameState_.pixelRatio : 0; - var thisArg = opt_this !== undefined ? opt_this : null; var layerFilter = opt_options.layerFilter !== undefined ? opt_options.layerFilter : ol.functions.TRUE; - var thisArg2 = opt_options.layerFilterThis !== undefined ? - opt_options.layerFilterThis : null; return this.renderer_.forEachFeatureAtCoordinate( - coordinate, this.frameState_, hitTolerance, callback, thisArg, - layerFilter, thisArg2); + coordinate, this.frameState_, hitTolerance, callback, null, + layerFilter, null); }; @@ -648,12 +644,10 @@ ol.Map.prototype.hasFeatureAtPixel = function(pixel, opt_options) { opt_options = opt_options !== undefined ? opt_options : {}; var layerFilter = opt_options.layerFilter !== undefined ? opt_options.layerFilter : ol.functions.TRUE; - var thisArg = opt_options.layerFilterThis !== undefined ? - opt_options.layerFilterThis : null; var hitTolerance = opt_options.hitTolerance !== undefined ? opt_options.hitTolerance * this.frameState_.pixelRatio : 0; return this.renderer_.hasFeatureAtCoordinate( - coordinate, this.frameState_, hitTolerance, layerFilter, thisArg); + coordinate, this.frameState_, hitTolerance, layerFilter, null); }; diff --git a/test/spec/ol/renderer/canvas/map.test.js b/test/spec/ol/renderer/canvas/map.test.js index 9d82ac5435..dea6a791f8 100644 --- a/test/spec/ol/renderer/canvas/map.test.js +++ b/test/spec/ol/renderer/canvas/map.test.js @@ -111,7 +111,7 @@ describe('ol.renderer.canvas.Map', function() { map.addLayer(layer); map.renderSync(); var cb = sinon.spy(); - map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]), cb, null, { + map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]), cb, { layerFilter: function() { return false; } @@ -151,13 +151,13 @@ describe('ol.renderer.canvas.Map', function() { ]; for (var i = 0; i < 4; i++) { - map.forEachFeatureAtPixel(pixelsInside[i], cb1, null, {hitTolerance:10}); + map.forEachFeatureAtPixel(pixelsInside[i], cb1, {hitTolerance:10}); } expect(cb1.callCount).to.be(4); expect(cb1.firstCall.args[1]).to.be(layer); for (var j = 0; j < 4; j++) { - map.forEachFeatureAtPixel(pixelsOutside[j], cb2, null, {hitTolerance:10}); + map.forEachFeatureAtPixel(pixelsOutside[j], cb2, {hitTolerance:10}); } expect(cb2).not.to.be.called(); }); From f10ae6c474864a7350a23d0503036e868dc242ff Mon Sep 17 00:00:00 2001 From: simonseyock Date: Thu, 8 Dec 2016 10:11:33 +0100 Subject: [PATCH 23/24] Adressed review. Reusing transform Changing either size of the canvas or clearing it adding unit of hitTolerance to jsdoc --- externs/olx.js | 2 +- src/ol/interaction/select.js | 4 ++-- src/ol/interaction/translate.js | 4 ++-- src/ol/render/canvas/replaygroup.js | 20 +++++++++++++++----- src/ol/renderer/layer.js | 2 +- src/ol/renderer/map.js | 4 ++-- src/ol/source/source.js | 2 +- 7 files changed, 24 insertions(+), 14 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index 3ef5688209..2ae1aab718 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -323,7 +323,7 @@ olx.AtPixelOptions.prototype.layerFilter; /** - * Hit-detection tolerance. Pixels inside the radius around the given position + * Hit-detection tolerance in pixels. Pixels inside the radius around the given position * will be checked for features. This only works for the canvas renderer and * not for WebGL. * @type {number|undefined} diff --git a/src/ol/interaction/select.js b/src/ol/interaction/select.js index daf4a0cdd1..c4959c354f 100644 --- a/src/ol/interaction/select.js +++ b/src/ol/interaction/select.js @@ -168,7 +168,7 @@ ol.interaction.Select.prototype.getFeatures = function() { /** * Returns the Hit-detection tolerance. - * @returns {number} Hit tolerance. + * @returns {number} Hit tolerance in pixels. * @api */ ol.interaction.Select.prototype.getHitTolerance = function() { @@ -291,7 +291,7 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) { * Hit-detection tolerance. Pixels inside the radius around the given position * will be checked for features. This only works for the canvas renderer and * not for WebGL. - * @param {number} hitTolerance Hit tolerance. + * @param {number} hitTolerance Hit tolerance in pixels. * @api */ ol.interaction.Select.prototype.setHitTolerance = function(hitTolerance) { diff --git a/src/ol/interaction/translate.js b/src/ol/interaction/translate.js index 2bc23fd46e..9d57e4d37b 100644 --- a/src/ol/interaction/translate.js +++ b/src/ol/interaction/translate.js @@ -212,7 +212,7 @@ ol.interaction.Translate.prototype.featuresAtPixel_ = function(pixel, map) { /** * Returns the Hit-detection tolerance. - * @returns {number} Hit tolerance. + * @returns {number} Hit tolerance in pixels. * @api */ ol.interaction.Translate.prototype.getHitTolerance = function() { @@ -224,7 +224,7 @@ ol.interaction.Translate.prototype.getHitTolerance = function() { * Hit-detection tolerance. Pixels inside the radius around the given position * will be checked for features. This only works for the canvas renderer and * not for WebGL. - * @param {number} hitTolerance Hit tolerance. + * @param {number} hitTolerance Hit tolerance in pixels. * @api */ ol.interaction.Translate.prototype.setHitTolerance = function(hitTolerance) { diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index a6656fb4a4..2a7983b14e 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -71,6 +71,12 @@ ol.render.canvas.ReplayGroup = function( * @type {CanvasRenderingContext2D} */ this.hitDetectionContext_ = ol.dom.createCanvasContext2D(1, 1); + + /** + * @private + * @type {ol.Transform} + */ + this.hitDetectionTransform_ = ol.transform.create(); }; ol.inherits(ol.render.canvas.ReplayGroup, ol.render.ReplayGroup); @@ -174,7 +180,7 @@ ol.render.canvas.ReplayGroup.prototype.finish = function() { * @param {ol.Coordinate} coordinate Coordinate. * @param {number} resolution Resolution. * @param {number} rotation Rotation. - * @param {number} hitTolerance Hit tolerance. + * @param {number} hitTolerance Hit tolerance in pixels. * @param {Object.} skippedFeaturesHash Ids of features * to skip. * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature @@ -187,15 +193,19 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( hitTolerance = Math.round(hitTolerance); var contextSize = hitTolerance * 2 + 1; - var transform = ol.transform.compose(ol.transform.create(), + var transform = ol.transform.compose(this.hitDetectionTransform_, hitTolerance + 0.5, hitTolerance + 0.5, 1 / resolution, -1 / resolution, -rotation, -coordinate[0], -coordinate[1]); var context = this.hitDetectionContext_; - context.canvas.width = contextSize; - context.canvas.height = contextSize; - context.clearRect(0, 0, contextSize, contextSize); + + if (context.canvas.width !== contextSize || context.canvas.height !== contextSize) { + context.canvas.width = contextSize; + context.canvas.height = contextSize; + } else { + context.clearRect(0, 0, contextSize, contextSize); + } /** * @type {ol.Extent} diff --git a/src/ol/renderer/layer.js b/src/ol/renderer/layer.js index 67dcca8f78..198d91ca37 100644 --- a/src/ol/renderer/layer.js +++ b/src/ol/renderer/layer.js @@ -35,7 +35,7 @@ ol.inherits(ol.renderer.Layer, ol.Observable); /** * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState Frame state. - * @param {number} hitTolerance Hit tolerance. + * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(this: S, (ol.Feature|ol.render.Feature), ol.layer.Layer): T} * callback Feature callback. * @param {S} thisArg Value to use as `this` when executing `callback`. diff --git a/src/ol/renderer/map.js b/src/ol/renderer/map.js index 9edacb4d66..de571dd3ed 100644 --- a/src/ol/renderer/map.js +++ b/src/ol/renderer/map.js @@ -100,7 +100,7 @@ ol.renderer.Map.expireIconCache_ = function(map, frameState) { /** * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState FrameState. - * @param {number} hitTolerance Hit tolerance. + * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(this: S, (ol.Feature|ol.render.Feature), * ol.layer.Layer): T} callback Feature callback. * @param {S} thisArg Value to use as `this` when executing `callback`. @@ -189,7 +189,7 @@ ol.renderer.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, call /** * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState FrameState. - * @param {number} hitTolerance Hit tolerance. + * @param {number} hitTolerance Hit tolerance in pixels. * @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 diff --git a/src/ol/source/source.js b/src/ol/source/source.js index 2224022bd8..596794566c 100644 --- a/src/ol/source/source.js +++ b/src/ol/source/source.js @@ -94,7 +94,7 @@ ol.source.Source.toAttributionsArray_ = function(attributionLike) { * @param {ol.Coordinate} coordinate Coordinate. * @param {number} resolution Resolution. * @param {number} rotation Rotation. - * @param {number} hitTolerance Hit tolerance. + * @param {number} hitTolerance Hit tolerance in pixels. * @param {Object.} skippedFeatureUids Skipped feature uids. * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature * callback. From f28e0ebc1f2ac68682946bcbf5895480cf26f157 Mon Sep 17 00:00:00 2001 From: simonseyock Date: Thu, 8 Dec 2016 10:21:57 +0100 Subject: [PATCH 24/24] Added hitTolerance parameter to reworked files --- src/ol/renderer/canvas/intermediatecanvas.js | 4 ++-- src/ol/renderer/canvas/layer.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ol/renderer/canvas/intermediatecanvas.js b/src/ol/renderer/canvas/intermediatecanvas.js index fd8e8d0447..694697c939 100644 --- a/src/ol/renderer/canvas/intermediatecanvas.js +++ b/src/ol/renderer/canvas/intermediatecanvas.js @@ -98,14 +98,14 @@ ol.renderer.canvas.IntermediateCanvas.prototype.getImageTransform = function() { /** * @inheritDoc */ -ol.renderer.canvas.IntermediateCanvas.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) { +ol.renderer.canvas.IntermediateCanvas.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTolerance, callback, thisArg) { var layer = this.getLayer(); var source = layer.getSource(); var resolution = frameState.viewState.resolution; var rotation = frameState.viewState.rotation; var skippedFeatureUids = frameState.skippedFeatureUids; return source.forEachFeatureAtCoordinate( - coordinate, resolution, rotation, skippedFeatureUids, + coordinate, resolution, rotation, hitTolerance, skippedFeatureUids, /** * @param {ol.Feature|ol.render.Feature} feature Feature. * @return {?} Callback result. diff --git a/src/ol/renderer/canvas/layer.js b/src/ol/renderer/canvas/layer.js index 9d8ecc632c..7601e7403e 100644 --- a/src/ol/renderer/canvas/layer.js +++ b/src/ol/renderer/canvas/layer.js @@ -106,7 +106,7 @@ ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = function(type, contex */ ol.renderer.canvas.Layer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) { var hasFeature = this.forEachFeatureAtCoordinate( - coordinate, frameState, ol.functions.TRUE, this); + coordinate, frameState, 0, ol.functions.TRUE, this); if (hasFeature) { return callback.call(thisArg, this.getLayer(), null);