Merge pull request #5995 from KlausBenndorf/hitTolerance

Add hit tolerance parameter to ol.Map#forEachFeatureAtPixel
This commit is contained in:
Andreas Hocevar
2016-12-08 11:06:46 +01:00
committed by GitHub
23 changed files with 437 additions and 84 deletions

View File

@@ -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.

View File

@@ -18,5 +18,13 @@ tags: "select, vector"
<option value="altclick">Alt+Click</option>
<option value="none">None</option>
</select>
<span id="status">&nbsp;0 selected features</span>
<span id="status">&nbsp;0 selected features</span>
<br />
<label>Hit tolerance for selecting features </label>
<select id="hitTolerance" class="form-control">
<option value="0" selected>0 Pixels</option>
<option value="5">5 Pixels</option>
<option value="10">10 Pixels</option>
</select>
<canvas id="circle" style="vertical-align: middle"/>
</form>

View File

@@ -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();

View File

@@ -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.<ol.Feature>|undefined),
* layers: (undefined|Array.<ol.layer.Layer>|function(ol.layer.Layer): boolean)
* layers: (undefined|Array.<ol.layer.Layer>|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.<ol.Feature>|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

View File

@@ -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.

View File

@@ -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;
};

View File

@@ -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);
};

View File

@@ -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.<number, Array.<Array.<(boolean|undefined)>>>}
* @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.<(boolean|undefined)>>} 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.<Array.<(boolean|undefined)>>} 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.<string, boolean>} 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);
};

View File

@@ -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.

View File

@@ -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);

View File

@@ -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.<string, boolean>} */
var features = {};
return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution,
rotation, {},
rotation, hitTolerance, {},
/**
* @param {ol.Feature|ol.render.Feature} feature Feature.
* @return {?} Callback result.

View File

@@ -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.<string, boolean>} */
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.

View File

@@ -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`.

View File

@@ -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;
};

View File

@@ -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);

View File

@@ -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()) {

View File

@@ -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 {

View File

@@ -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.<string, boolean>} */
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.

View File

@@ -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.<string, boolean>} skippedFeatureUids Skipped feature uids.
* @param {function((ol.Feature|ol.render.Feature)): T} callback Feature
* callback.

View File

@@ -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);
}
}
}
});
});
});

View File

@@ -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() {

View File

@@ -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);
});

View File

@@ -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);
});