Merge pull request #11781 from ahocevar/hitdetect-geometry

Pass geometry to forEachFeatureAtPixel callback
This commit is contained in:
Andreas Hocevar
2020-12-01 09:59:43 +01:00
committed by GitHub
12 changed files with 113 additions and 28 deletions

View File

@@ -538,8 +538,7 @@ class PluggableMap extends BaseObject {
* callback with each intersecting feature. Layers included in the detection can
* be configured through the `layerFilter` option in `opt_options`.
* @param {import("./pixel.js").Pixel} pixel Pixel.
* @param {function(this: S, import("./Feature.js").FeatureLike,
* import("./layer/Layer.js").default): T} callback Feature callback. The callback will be
* @param {function(import("./Feature.js").FeatureLike, import("./layer/Layer.js").default, import("./geom/SimpleGeometry.js").default): T} callback Feature callback. The callback will be
* called with two arguments. The first argument is one
* {@link module:ol/Feature feature} or
* {@link module:ol/render/Feature render feature} at the pixel, the second is

View File

@@ -355,19 +355,18 @@ class CanvasBuilder extends VectorContext {
* @param {import("../../Feature.js").FeatureLike} feature Feature.
*/
beginGeometry(geometry, feature) {
const extent = geometry.getExtent();
this.beginGeometryInstruction1_ = [
CanvasInstruction.BEGIN_GEOMETRY,
feature,
0,
extent,
geometry,
];
this.instructions.push(this.beginGeometryInstruction1_);
this.beginGeometryInstruction2_ = [
CanvasInstruction.BEGIN_GEOMETRY,
feature,
0,
extent,
geometry,
];
this.hitDetectionInstructions.push(this.beginGeometryInstruction2_);
}

View File

@@ -53,6 +53,11 @@ import {transform2D} from '../../geom/flat/transform.js';
* @typedef {{0: CanvasRenderingContext2D, 1: number, 2: import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement, 3: ImageOrLabelDimensions, 4: number, 5: Array<*>, 6: Array<*>}} ReplayImageOrLabelArgs
*/
/**
* @template T
* @typedef {function(import("../../Feature.js").FeatureLike, import("../../geom/SimpleGeometry.js").default): T=} FeatureCallback
*/
/**
* @type {import("../../extent.js").Extent}
*/
@@ -597,7 +602,7 @@ class Executor {
* @param {import("../../transform.js").Transform} transform Transform.
* @param {Array<*>} instructions Instructions array.
* @param {boolean} snapToPixel Snap point symbols and text to integer pixels.
* @param {function(import("../../Feature.js").FeatureLike): T|undefined} featureCallback Feature callback.
* @param {FeatureCallback<T>|undefined} featureCallback Feature callback.
* @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this
* extent.
* @param {import("rbush").default=} opt_declutterTree Declutter tree.
@@ -668,18 +673,19 @@ class Executor {
const batchSize =
this.instructions != instructions || this.overlaps ? 0 : 200;
let /** @type {import("../../Feature.js").FeatureLike} */ feature;
let x, y;
let x, y, currentGeometry;
while (i < ii) {
const instruction = instructions[i];
const type = /** @type {import("./Instruction.js").default} */ (instruction[0]);
switch (type) {
case CanvasInstruction.BEGIN_GEOMETRY:
feature = /** @type {import("../../Feature.js").FeatureLike} */ (instruction[1]);
currentGeometry = instruction[3];
if (!feature.getGeometry()) {
i = /** @type {number} */ (instruction[2]);
} else if (
opt_hitExtent !== undefined &&
!intersects(opt_hitExtent, instruction[3])
!intersects(opt_hitExtent, currentGeometry.getExtent())
) {
i = /** @type {number} */ (instruction[2]) + 1;
} else {
@@ -1048,7 +1054,7 @@ class Executor {
case CanvasInstruction.END_GEOMETRY:
if (featureCallback !== undefined) {
feature = /** @type {import("../../Feature.js").FeatureLike} */ (instruction[1]);
const result = featureCallback(feature);
const result = featureCallback(feature, currentGeometry);
if (result) {
return result;
}
@@ -1168,7 +1174,7 @@ class Executor {
* @param {CanvasRenderingContext2D} context Context.
* @param {import("../../transform.js").Transform} transform Transform.
* @param {number} viewRotation View rotation.
* @param {function(import("../../Feature.js").FeatureLike): T=} opt_featureCallback
* @param {FeatureCallback<T>} opt_featureCallback
* Feature callback.
* @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this
* extent.

View File

@@ -161,7 +161,7 @@ class ExecutorGroup {
* @param {number} resolution Resolution.
* @param {number} rotation Rotation.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../../Feature.js").FeatureLike): T} callback Feature callback.
* @param {import("./Executor.js").FeatureCallback<T>} callback Feature callback.
* @param {Array<import("../../Feature.js").FeatureLike>} declutteredFeatures Decluttered features.
* @return {T|undefined} Callback result.
* @template T
@@ -225,9 +225,10 @@ class ExecutorGroup {
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
* @return {?} Callback result.
*/
function featureCallback(feature) {
function featureCallback(feature, geometry) {
const imageData = context.getImageData(0, 0, contextSize, contextSize)
.data;
for (let i = 0; i < contextSize; i++) {
@@ -243,7 +244,7 @@ class ExecutorGroup {
) ||
declutteredFeatures.indexOf(feature) !== -1
) {
result = callback(feature);
result = callback(feature, geometry);
}
if (result) {
return result;

View File

@@ -105,7 +105,7 @@ class LayerRenderer extends Observable {
* @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../Feature.js").FeatureLike, import("../layer/Layer.js").default): T} callback Feature callback.
* @param {import("./vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result.
* @template T
*/

View File

@@ -64,8 +64,7 @@ class MapRenderer extends Disposable {
* @param {import("../PluggableMap.js").FrameState} frameState FrameState.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {boolean} checkWrapped Check for wrapped geometries.
* @param {function(this: S, import("../Feature.js").FeatureLike,
* import("../layer/Layer.js").default): T} callback Feature callback.
* @param {import("./vector.js").FeatureCallback<T>} callback Feature callback.
* @param {S} thisArg Value to use as `this` when executing `callback`.
* @param {function(this: U, import("../layer/Layer.js").default): boolean} layerFilter Layer filter
* function, only layers which are visible and for which this function
@@ -92,10 +91,11 @@ class MapRenderer extends Disposable {
* @param {boolean} managed Managed layer.
* @param {import("../Feature.js").FeatureLike} feature Feature.
* @param {import("../layer/Layer.js").default} layer Layer.
* @param {import("../geom/Geometry.js").default} geometry Geometry.
* @return {?} Callback result.
*/
function forEachFeatureAtCoordinate(managed, feature, layer) {
return callback.call(thisArg, feature, managed ? layer : null);
function forEachFeatureAtCoordinate(managed, feature, layer, geometry) {
return callback.call(thisArg, feature, managed ? layer : null, geometry);
}
const projection = viewState.projection;

View File

@@ -194,7 +194,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
* @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result.
* @template T
*/

View File

@@ -406,7 +406,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
* @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result.
* @template T
*/
@@ -423,13 +423,14 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
* @return {?} Callback result.
*/
const featureCallback = function (feature) {
const featureCallback = function (feature, geometry) {
const key = getUid(feature);
if (!(key in features)) {
features[key] = true;
return callback(feature, layer);
return callback(feature, layer, geometry);
}
};

View File

@@ -380,7 +380,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
* @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result.
* @template T
*/
@@ -432,9 +432,10 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
hitTolerance,
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
* @return {?} Callback result.
*/
function (feature) {
function (feature, geometry) {
if (tileContainsCoordinate) {
let key = feature.getId();
if (key === undefined) {
@@ -442,7 +443,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
}
if (!(key in features)) {
features[key] = true;
return callback(feature, layer);
return callback(feature, layer, geometry);
}
}
},

View File

@@ -6,6 +6,16 @@ import GeometryType from '../geom/GeometryType.js';
import ImageState from '../ImageState.js';
import {getUid} from '../util.js';
/**
* Feature callback. The callback will be called with three arguments. The first
* argument is one {@link module:ol/Feature feature} or {@link module:ol/render/Feature render feature}
* at the pixel, the second is the {@link module:ol/layer/Layer layer} of the feature and will be null for
* unmanaged layers. The third is the {@link module:ol/geom/SimpleGeometry} of the feature. For features
* with a GeometryCollection geometry, it will be the first detected geometry from the collection.
* @template T
* @typedef {function(import("../Feature.js").FeatureLike, import("../layer/Layer.js").default, import("../geom/SimpleGeometry.js").default): T} FeatureCallback
*/
/**
* Tolerance for geometry simplification in device pixels.
* @type {number}

View File

@@ -597,7 +597,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
* @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result.
* @template T
*/
@@ -621,7 +621,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
const source = this.getLayer().getSource();
const feature = source.getFeatureByUid(uid);
if (feature) {
return callback(feature, this.getLayer());
return callback(feature, this.getLayer(), null);
}
}

View File

@@ -6,8 +6,13 @@ import VectorLayer from '../../../../src/ol/layer/Vector.js';
import VectorSource from '../../../../src/ol/source/Vector.js';
import View from '../../../../src/ol/View.js';
import {Circle, Fill, Style} from '../../../../src/ol/style.js';
import {Point} from '../../../../src/ol/geom.js';
import {
GeometryCollection,
MultiPoint,
Point,
} from '../../../../src/ol/geom.js';
import {Projection} from '../../../../src/ol/proj.js';
import {fromExtent} from '../../../../src/ol/geom/Polygon.js';
describe('ol.renderer.Map', function () {
describe('constructor', function () {
@@ -21,6 +26,69 @@ describe('ol.renderer.Map', function () {
});
});
describe('#forEachFeatureAtPixel', function () {
let map;
beforeEach(function () {
const target = document.createElement('div');
target.style.width = '100px';
target.style.height = '100px';
document.body.appendChild(target);
map = new Map({
target: target,
view: new View({
center: [0, 0],
zoom: 2,
}),
});
});
afterEach(function () {
document.body.removeChild(map.getTargetElement());
map.setTarget(null);
});
it('calls callback with feature, layer and geometry', function () {
let hit;
const point = new Point([0, 0]);
const polygon = fromExtent([0, -1e6, 1e6, 1e6]);
const geometryCollection = new Feature(
new GeometryCollection([polygon, point])
);
const multiPoint = new MultiPoint([
[-1e6, -1e6],
[-1e6, 1e6],
]);
const multiGeometry = new Feature(multiPoint);
const layer = new VectorLayer({
source: new VectorSource({
features: [geometryCollection, multiGeometry],
}),
});
map.addLayer(layer);
map.renderSync();
hit = map.forEachFeatureAtPixel([50, 50], (feature, layer, geometry) => ({
feature,
layer,
geometry,
}));
expect(hit.feature).to.be(geometryCollection);
expect(hit.layer).to.be(layer);
expect(hit.geometry).to.be(point);
hit = map.forEachFeatureAtPixel([75, 50], (feature, layer, geometry) => ({
feature,
layer,
geometry,
}));
expect(hit.feature).to.be(geometryCollection);
expect(hit.geometry).to.be(polygon);
hit = map.forEachFeatureAtPixel([25, 25], (feature, layer, geometry) => ({
feature,
layer,
geometry,
}));
expect(hit.feature).to.be(multiGeometry);
expect(hit.geometry).to.be(multiPoint);
});
});
describe('#forEachFeatureAtCoordinate', function () {
let map, source, style;
beforeEach(function () {