diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index 4d8128a0ff..48e419254d 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -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 {import("./renderer/vector.js").FeatureCallback} 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 diff --git a/src/ol/render/canvas/Builder.js b/src/ol/render/canvas/Builder.js index fd7e0e478c..1b710a74a9 100644 --- a/src/ol/render/canvas/Builder.js +++ b/src/ol/render/canvas/Builder.js @@ -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_); } diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index e4e2c965c0..e4b7ae775d 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -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|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} opt_featureCallback * Feature callback. * @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this * extent. diff --git a/src/ol/render/canvas/ExecutorGroup.js b/src/ol/render/canvas/ExecutorGroup.js index f714327b45..a625c1ee2d 100644 --- a/src/ol/render/canvas/ExecutorGroup.js +++ b/src/ol/render/canvas/ExecutorGroup.js @@ -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} callback Feature callback. * @param {Array} 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; diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index 1997718683..04344f881d 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -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} callback Feature callback. * @return {T|void} Callback result. * @template T */ diff --git a/src/ol/renderer/Map.js b/src/ol/renderer/Map.js index 28b676b6f4..35368c9293 100644 --- a/src/ol/renderer/Map.js +++ b/src/ol/renderer/Map.js @@ -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} 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; diff --git a/src/ol/renderer/canvas/VectorImageLayer.js b/src/ol/renderer/canvas/VectorImageLayer.js index bcc4ce4d85..0908216610 100644 --- a/src/ol/renderer/canvas/VectorImageLayer.js +++ b/src/ol/renderer/canvas/VectorImageLayer.js @@ -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} callback Feature callback. * @return {T|void} Callback result. * @template T */ diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index 0939d7bd3e..6c46f35611 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -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} 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); } }; diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 71d2a9a6f9..0012c44716 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -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} 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); } } }, diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index 2c626a9eb2..8d7d80129e 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -6,6 +6,11 @@ import GeometryType from '../geom/GeometryType.js'; import ImageState from '../ImageState.js'; import {getUid} from '../util.js'; +/** + * @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} diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index 5305da78d9..349ac05eff 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -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} 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); } } diff --git a/test/spec/ol/renderer/map.test.js b/test/spec/ol/renderer/map.test.js index 86358389bc..52cfcbb697 100644 --- a/test/spec/ol/renderer/map.test.js +++ b/test/spec/ol/renderer/map.test.js @@ -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 () {