diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md
index 22f5e10096..513021d1a3 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, callbackThis, layerFilterFn, layerFilterThis);
+
+ map.hasFeatureAtPixel(pixel, layerFilterFn, layerFilterThis);
+```
+
+New syntax:
+```
+ map.forEachFeatureAtPixel(pixel, callback, {
+ layerFilter: layerFilterFn
+ });
+
+ map.hasFeatureAtPixel(pixel, {
+ 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
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.
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 a255777607..2ae1aab718 100644
--- a/externs/olx.js
+++ b/externs/olx.js
@@ -302,6 +302,36 @@ olx.MapOptions.prototype.target;
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),
+ * hitTolerance: (number|undefined)}}
+ */
+olx.AtPixelOptions;
+
+
+/**
+ * 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.AtPixelOptions.prototype.layerFilter;
+
+
+/**
+ * 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}
+ * @api
+ */
+olx.AtPixelOptions.prototype.hitTolerance;
+
+
/**
* Object literal with config options for the overlay.
* @typedef {{id: (number|string|undefined),
@@ -2871,7 +2901,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;
@@ -2898,6 +2929,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),
@@ -3173,7 +3214,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;
@@ -3287,6 +3329,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 {{
@@ -7623,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 7817067c3d..c4959c354f 100644
--- a/src/ol/interaction/select.js
+++ b/src/ol/interaction/select.js
@@ -82,6 +82,12 @@ 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({
source: new ol.source.Vector({
useSpatialIndex: false,
@@ -160,6 +166,16 @@ ol.interaction.Select.prototype.getFeatures = function() {
};
+/**
+ * Returns the Hit-detection tolerance.
+ * @returns {number} Hit tolerance in pixels.
+ * @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
@@ -201,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.
@@ -212,7 +228,10 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) {
this.addFeatureLayerAssociation_(feature, layer);
return !this.multi_;
}
- }, this, this.layerFilter_);
+ }).bind(this), {
+ layerFilter: this.layerFilter_,
+ hitTolerance: this.hitTolerance_
+ });
var i;
for (i = features.getLength() - 1; i >= 0; --i) {
var feature = features.item(i);
@@ -231,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.
@@ -249,7 +268,10 @@ ol.interaction.Select.handleEvent = function(mapBrowserEvent) {
}
return !this.multi_;
}
- }, this, this.layerFilter_);
+ }).bind(this), {
+ layerFilter: this.layerFilter_,
+ hitTolerance: this.hitTolerance_
+ });
var j;
for (j = deselected.length - 1; j >= 0; --j) {
features.remove(deselected[j]);
@@ -265,6 +287,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 in pixels.
+ * @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.
diff --git a/src/ol/interaction/translate.js b/src/ol/interaction/translate.js
index 3b61e333dc..9d57e4d37b 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
@@ -197,7 +203,32 @@ ol.interaction.Translate.prototype.featuresAtPixel_ = function(pixel, map) {
ol.array.includes(this.features_.getArray(), feature)) {
return feature;
}
- }, this, this.layerFilter_);
+ }.bind(this), {
+ layerFilter: this.layerFilter_,
+ hitTolerance: this.hitTolerance_
+ });
+};
+
+
+/**
+ * Returns the Hit-detection tolerance.
+ * @returns {number} Hit tolerance in pixels.
+ * @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 in pixels.
+ * @api
+ */
+ol.interaction.Translate.prototype.setHitTolerance = function(hitTolerance) {
+ this.hitTolerance_ = hitTolerance;
};
diff --git a/src/ol/map.js b/src/ol/map.js
index 8acbe9a76a..9703f39d09 100644
--- a/src/ol/map.js
+++ b/src/ol/map.js
@@ -567,31 +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 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 {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,U
+ * @template S,T
* @api stable
*/
-ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this, opt_layerFilter, opt_this2) {
+ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, 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;
+ opt_options = opt_options !== undefined ? opt_options : {};
+ var hitTolerance = opt_options.hitTolerance !== undefined ?
+ opt_options.hitTolerance * this.frameState_.pixelRatio : 0;
+ var layerFilter = opt_options.layerFilter !== undefined ?
+ opt_options.layerFilter : ol.functions.TRUE;
return this.renderer_.forEachFeatureAtCoordinate(
- coordinate, this.frameState_, callback, thisArg,
- layerFilter, thisArg2);
+ coordinate, this.frameState_, hitTolerance, callback, null,
+ layerFilter, null);
};
@@ -637,27 +631,23 @@ 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.AtPixelOptions=} 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) {
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;
+ opt_options = opt_options !== undefined ? opt_options : {};
+ var layerFilter = opt_options.layerFilter !== undefined ?
+ opt_options.layerFilter : ol.functions.TRUE;
+ var hitTolerance = opt_options.hitTolerance !== undefined ?
+ opt_options.hitTolerance * this.frameState_.pixelRatio : 0;
return this.renderer_.hasFeatureAtCoordinate(
- coordinate, this.frameState_, layerFilter, thisArg);
+ coordinate, this.frameState_, hitTolerance, layerFilter, null);
};
diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js
index fae6101bd2..2a7983b14e 100644
--- a/src/ol/render/canvas/replaygroup.js
+++ b/src/ol/render/canvas/replaygroup.js
@@ -77,11 +77,90 @@ ol.render.canvas.ReplayGroup = function(
* @type {ol.Transform}
*/
this.hitDetectionTransform_ = ol.transform.create();
-
};
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
+ */
+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.
+ * A cache is used to increase performance.
+ * @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);
+ }
+
+ var x = radius;
+ var y = 0;
+ var error = 0;
+
+ while (x >= 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;
+ if (2 * (error - x) + 1 > 0) {
+ x -= 1;
+ error += 1 - 2 * x;
+ }
+ }
+
+ ol.render.canvas.ReplayGroup.circleArrayCache_[radius] = arr;
+ return arr;
+};
+
/**
* FIXME empty description for jsdoc
*/
@@ -101,6 +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 in pixels.
* @param {Object.} skippedFeaturesHash Ids of features
* to skip.
* @param {function((ol.Feature|ol.render.Feature)): T} callback Feature
@@ -109,16 +189,23 @@ 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) {
+ hitTolerance = Math.round(hitTolerance);
+ var contextSize = hitTolerance * 2 + 1;
var transform = ol.transform.compose(this.hitDetectionTransform_,
- 0.5, 0.5,
+ 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);
+
+ 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}
@@ -127,9 +214,11 @@ 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);
+
return this.replayHitDetection_(context, transform, rotation,
skippedFeaturesHash,
/**
@@ -137,13 +226,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 = 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]) {
+ 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;
+ }
+ }
+ }
}
- context.clearRect(0, 0, 1, 1);
}
}, hitExtent);
};
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);
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..0999ebbb13 100644
--- a/src/ol/renderer/canvas/vectortilelayer.js
+++ b/src/ol/renderer/canvas/vectortilelayer.js
@@ -176,9 +176,10 @@ 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;
+ hitTolerance = hitTolerance == undefined ? 0 : hitTolerance;
var layer = this.getLayer();
/** @type {Object.} */
var features = {};
@@ -195,7 +196,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi
tile = replayables[i];
tileCoord = tile.tileCoord;
tileExtent = source.getTileGrid().getTileCoordExtent(tileCoord, this.tmpExtent);
- if (!ol.extent.containsCoordinate(tileExtent, coordinate)) {
+ if (!ol.extent.containsCoordinate(ol.extent.buffer(tileExtent, hitTolerance * resolution), coordinate)) {
continue;
}
if (tile.getProjection().getUnits() === ol.proj.Units.TILE_PIXELS) {
@@ -212,7 +213,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..198d91ca37 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 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 e9f0d8bae0..de571dd3ed 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 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`.
@@ -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;
@@ -188,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 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
@@ -196,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, 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/imagelayer.js b/src/ol/renderer/webgl/imagelayer.js
index 8df0c036df..2218f42ede 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.
@@ -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/src/ol/renderer/webgl/map.js b/src/ol/renderer/webgl/map.js
index 5844610adf..7359ce0d16 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;
}
@@ -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()) {
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..596794566c 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 in pixels.
* @param {Object.} skippedFeatureUids Skipped feature uids.
* @param {function((ol.Feature|ol.render.Feature)): T} callback Feature
* callback.
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);
+ }
+ }
+ }
+ });
+ });
+
+});
diff --git a/test/spec/ol/renderer/canvas/map.test.js b/test/spec/ol/renderer/canvas/map.test.js
index 42311d8e79..dea6a791f8 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]
+ })
})
});
});
@@ -97,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() {
- return false;
- });
+ map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]), cb, {
+ layerFilter: function() {
+ return false;
+ }
+ });
expect(cb).to.not.be.called();
});
@@ -113,6 +128,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], 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, {hitTolerance:10});
+ }
+ expect(cb2).not.to.be.called();
+ });
});
describe('#renderFrame()', function() {
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);
});