Order callback calls by distance to click position

All callback calls for hits with a tolerance > 0 are queued and
called ordered by distance after all hits are detected.
This commit is contained in:
Maximilian Krög
2020-11-29 02:32:04 +01:00
parent 4546eff66e
commit 23dc768c2e
10 changed files with 239 additions and 119 deletions
+10 -11
View File
@@ -55,7 +55,7 @@ import {transform2D} from '../../geom/flat/transform.js';
/** /**
* @template T * @template T
* @typedef {function(import("../../Feature.js").FeatureLike, import("../../geom/SimpleGeometry.js").default): T=} FeatureCallback * @typedef {function(import("../../Feature.js").FeatureLike, import("../../geom/SimpleGeometry.js").default): T} FeatureCallback
*/ */
/** /**
@@ -602,9 +602,9 @@ class Executor {
* @param {import("../../transform.js").Transform} transform Transform. * @param {import("../../transform.js").Transform} transform Transform.
* @param {Array<*>} instructions Instructions array. * @param {Array<*>} instructions Instructions array.
* @param {boolean} snapToPixel Snap point symbols and text to integer pixels. * @param {boolean} snapToPixel Snap point symbols and text to integer pixels.
* @param {FeatureCallback<T>|undefined} featureCallback Feature callback. * @param {FeatureCallback<T>=} opt_featureCallback Feature callback.
* @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this * @param {import("../../extent.js").Extent=} opt_hitExtent Only check
* extent. * features that intersect this extent.
* @param {import("rbush").default=} opt_declutterTree Declutter tree. * @param {import("rbush").default=} opt_declutterTree Declutter tree.
* @return {T|undefined} Callback result. * @return {T|undefined} Callback result.
* @template T * @template T
@@ -615,7 +615,7 @@ class Executor {
transform, transform,
instructions, instructions,
snapToPixel, snapToPixel,
featureCallback, opt_featureCallback,
opt_hitExtent, opt_hitExtent,
opt_declutterTree opt_declutterTree
) { ) {
@@ -1052,9 +1052,9 @@ class Executor {
++i; ++i;
break; break;
case CanvasInstruction.END_GEOMETRY: case CanvasInstruction.END_GEOMETRY:
if (featureCallback !== undefined) { if (opt_featureCallback !== undefined) {
feature = /** @type {import("../../Feature.js").FeatureLike} */ (instruction[1]); feature = /** @type {import("../../Feature.js").FeatureLike} */ (instruction[1]);
const result = featureCallback(feature, currentGeometry); const result = opt_featureCallback(feature, currentGeometry);
if (result) { if (result) {
return result; return result;
} }
@@ -1174,10 +1174,9 @@ class Executor {
* @param {CanvasRenderingContext2D} context Context. * @param {CanvasRenderingContext2D} context Context.
* @param {import("../../transform.js").Transform} transform Transform. * @param {import("../../transform.js").Transform} transform Transform.
* @param {number} viewRotation View rotation. * @param {number} viewRotation View rotation.
* @param {FeatureCallback<T>} opt_featureCallback * @param {FeatureCallback<T>=} opt_featureCallback Feature callback.
* Feature callback. * @param {import("../../extent.js").Extent=} opt_hitExtent Only check
* @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this * features that intersect this extent.
* extent.
* @return {T|undefined} Callback result. * @return {T|undefined} Callback result.
* @template T * @template T
*/ */
+9 -5
View File
@@ -161,7 +161,7 @@ class ExecutorGroup {
* @param {number} resolution Resolution. * @param {number} resolution Resolution.
* @param {number} rotation Rotation. * @param {number} rotation Rotation.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("./Executor.js").FeatureCallback<T>} callback Feature callback. * @param {function(import("../../Feature.js").FeatureLike, import("../../geom/SimpleGeometry.js").default, number): T} callback Feature callback.
* @param {Array<import("../../Feature.js").FeatureLike>} declutteredFeatures Decluttered features. * @param {Array<import("../../Feature.js").FeatureLike>} declutteredFeatures Decluttered features.
* @return {T|undefined} Callback result. * @return {T|undefined} Callback result.
* @template T * @template T
@@ -187,7 +187,8 @@ class ExecutorGroup {
-coordinate[1] -coordinate[1]
); );
if (!this.hitDetectionContext_) { const newContext = !this.hitDetectionContext_;
if (newContext) {
this.hitDetectionContext_ = createCanvasContext2D( this.hitDetectionContext_ = createCanvasContext2D(
contextSize, contextSize,
contextSize contextSize
@@ -201,7 +202,7 @@ class ExecutorGroup {
) { ) {
context.canvas.width = contextSize; context.canvas.width = contextSize;
context.canvas.height = contextSize; context.canvas.height = contextSize;
} else { } else if (!newContext) {
context.clearRect(0, 0, contextSize, contextSize); context.clearRect(0, 0, contextSize, contextSize);
} }
@@ -226,7 +227,7 @@ class ExecutorGroup {
/** /**
* @param {import("../../Feature.js").FeatureLike} feature Feature. * @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry. * @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
* @return {?} Callback result. * @return {T|undefined} Callback result.
*/ */
function featureCallback(feature, geometry) { function featureCallback(feature, geometry) {
const imageData = context.getImageData(0, 0, contextSize, contextSize) const imageData = context.getImageData(0, 0, contextSize, contextSize)
@@ -239,7 +240,10 @@ class ExecutorGroup {
builderType !== BuilderType.TEXT) || builderType !== BuilderType.TEXT) ||
declutteredFeatures.indexOf(feature) !== -1 declutteredFeatures.indexOf(feature) !== -1
) { ) {
const result = callback(feature, geometry); const idx = (indexes[i] - 3) / 4;
const x = hitTolerance - (idx % contextSize);
const y = hitTolerance - ((idx / contextSize) | 0);
const result = callback(feature, geometry, x * x + y * y);
if (result) { if (result) {
return result; return result;
} }
+11 -2
View File
@@ -106,10 +106,19 @@ class LayerRenderer extends Observable {
* @param {import("../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("./vector.js").FeatureCallback<T>} callback Feature callback. * @param {import("./vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result. * @param {Array<import("./Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T * @template T
*/ */
forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {} forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
return undefined;
}
/** /**
* @abstract * @abstract
+25 -4
View File
@@ -10,6 +10,16 @@ import {shared as iconImageCache} from '../style/IconImageCache.js';
import {inView} from '../layer/Layer.js'; import {inView} from '../layer/Layer.js';
import {wrapX} from '../coordinate.js'; import {wrapX} from '../coordinate.js';
/**
* @typedef HitMatch
* @property {import("../Feature.js").FeatureLike} feature
* @property {import("../layer/Layer.js").default} layer
* @property {import("../geom/SimpleGeometry.js").default} geometry
* @property {number} distanceSq
* @property {import("./vector.js").FeatureCallback<T>} callback
* @template T
*/
/** /**
* @abstract * @abstract
*/ */
@@ -92,7 +102,7 @@ class MapRenderer extends Disposable {
* @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../Feature.js").FeatureLike} feature Feature.
* @param {import("../layer/Layer.js").default} layer Layer. * @param {import("../layer/Layer.js").default} layer Layer.
* @param {import("../geom/Geometry.js").default} geometry Geometry. * @param {import("../geom/Geometry.js").default} geometry Geometry.
* @return {?} Callback result. * @return {T|undefined} Callback result.
*/ */
function forEachFeatureAtCoordinate(managed, feature, layer, geometry) { function forEachFeatureAtCoordinate(managed, feature, layer, geometry) {
return callback.call(thisArg, feature, managed ? layer : null, geometry); return callback.call(thisArg, feature, managed ? layer : null, geometry);
@@ -111,11 +121,12 @@ class MapRenderer extends Disposable {
const layerStates = frameState.layerStatesArray; const layerStates = frameState.layerStatesArray;
const numLayers = layerStates.length; const numLayers = layerStates.length;
const matches = /** @type {Array<HitMatch<T>>} */ ([]);
const tmpCoord = []; const tmpCoord = [];
for (let i = 0; i < offsets.length; i++) { for (let i = 0; i < offsets.length; i++) {
for (let j = numLayers - 1; j >= 0; --j) { for (let j = numLayers - 1; j >= 0; --j) {
const layerState = layerStates[j]; const layerState = layerStates[j];
const layer = /** @type {import("../layer/Layer.js").default} */ (layerState.layer); const layer = layerState.layer;
if ( if (
layer.hasRenderer() && layer.hasRenderer() &&
inView(layerState, viewState) && inView(layerState, viewState) &&
@@ -137,7 +148,8 @@ class MapRenderer extends Disposable {
tmpCoord, tmpCoord,
frameState, frameState,
hitTolerance, hitTolerance,
callback callback,
matches
); );
} }
if (result) { if (result) {
@@ -146,7 +158,16 @@ class MapRenderer extends Disposable {
} }
} }
} }
return undefined; if (matches.length === 0) {
return undefined;
}
const order = 1 / matches.length;
matches.forEach((m, i) => (m.distanceSq += i * order));
matches.sort((a, b) => a.distanceSq - b.distanceSq);
matches.some((m) => {
return (result = m.callback(m.feature, m.layer, m.geometry));
});
return result;
} }
/** /**
+13 -4
View File
@@ -195,23 +195,32 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback. * @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result. * @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T * @template T
*/ */
forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) { forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
if (this.vectorRenderer_) { if (this.vectorRenderer_) {
return this.vectorRenderer_.forEachFeatureAtCoordinate( return this.vectorRenderer_.forEachFeatureAtCoordinate(
coordinate, coordinate,
frameState, frameState,
hitTolerance, hitTolerance,
callback callback,
matches
); );
} else { } else {
return super.forEachFeatureAtCoordinate( return super.forEachFeatureAtCoordinate(
coordinate, coordinate,
frameState, frameState,
hitTolerance, hitTolerance,
callback callback,
matches
); );
} }
} }
+63 -37
View File
@@ -407,55 +407,81 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback. * @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result. * @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T * @template T
*/ */
forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) { forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
if (!this.replayGroup_) { if (!this.replayGroup_) {
return undefined; return undefined;
} else { }
const resolution = frameState.viewState.resolution; const resolution = frameState.viewState.resolution;
const rotation = frameState.viewState.rotation; const rotation = frameState.viewState.rotation;
const layer = this.getLayer(); const layer = this.getLayer();
/** @type {!Object<string, boolean>} */ /** @type {!Object<string, import("../Map.js").HitMatch<T>|true>} */
const features = {}; const features = {};
/** /**
* @param {import("../../Feature.js").FeatureLike} feature Feature. * @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry. * @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
* @return {?} Callback result. * @param {number} distanceSq The squared distance to the click position
*/ * @return {T|undefined} Callback result.
const featureCallback = function (feature, geometry) { */
const key = getUid(feature); const featureCallback = function (feature, geometry, distanceSq) {
if (!(key in features)) { const key = getUid(feature);
const match = features[key];
if (!match) {
if (distanceSq === 0) {
features[key] = true; features[key] = true;
return callback(feature, layer, geometry); return callback(feature, layer, geometry);
} }
}; matches.push(
(features[key] = {
let result; feature: feature,
const executorGroups = [this.replayGroup_]; layer: layer,
if (this.declutterExecutorGroup) { geometry: geometry,
executorGroups.push(this.declutterExecutorGroup); distanceSq: distanceSq,
callback: callback,
})
);
} else if (match !== true && distanceSq < match.distanceSq) {
if (distanceSq === 0) {
features[key] = true;
matches.splice(matches.lastIndexOf(match), 1);
return callback(feature, layer, geometry);
}
match.geometry = geometry;
match.distanceSq = distanceSq;
} }
executorGroups.forEach((executorGroup) => { return undefined;
result = };
result ||
executorGroup.forEachFeatureAtCoordinate(
coordinate,
resolution,
rotation,
hitTolerance,
featureCallback,
executorGroup === this.declutterExecutorGroup
? frameState.declutterTree.all().map((item) => item.value)
: null
);
});
return result; let result;
const executorGroups = [this.replayGroup_];
if (this.declutterExecutorGroup) {
executorGroups.push(this.declutterExecutorGroup);
} }
executorGroups.some((executorGroup) => {
return (result = executorGroup.forEachFeatureAtCoordinate(
coordinate,
resolution,
rotation,
hitTolerance,
featureCallback,
executorGroup === this.declutterExecutorGroup
? frameState.declutterTree.all().map((item) => item.value)
: null
));
});
return result;
} }
/** /**
+66 -32
View File
@@ -381,10 +381,17 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback. * @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result. * @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T * @template T
*/ */
forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) { forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
const resolution = frameState.viewState.resolution; const resolution = frameState.viewState.resolution;
const rotation = frameState.viewState.rotation; const rotation = frameState.viewState.rotation;
hitTolerance = hitTolerance == undefined ? 0 : hitTolerance; hitTolerance = hitTolerance == undefined ? 0 : hitTolerance;
@@ -397,55 +404,82 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const hitExtent = boundingExtent([coordinate]); const hitExtent = boundingExtent([coordinate]);
buffer(hitExtent, resolution * hitTolerance, hitExtent); buffer(hitExtent, resolution * hitTolerance, hitExtent);
/** @type {!Object<string, boolean>} */ /** @type {!Object<string, import("../Map.js").HitMatch<T>|true>} */
const features = {}; const features = {};
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
* @param {number} distanceSq The squared distance to the click position.
* @return {T|undefined} Callback result.
*/
const featureCallback = function (feature, geometry, distanceSq) {
let key = feature.getId();
if (key === undefined) {
key = getUid(feature);
}
const match = features[key];
if (!match) {
if (distanceSq === 0) {
features[key] = true;
return callback(feature, layer, geometry);
}
matches.push(
(features[key] = {
feature: feature,
layer: layer,
geometry: geometry,
distanceSq: distanceSq,
callback: callback,
})
);
} else if (match !== true && distanceSq < match.distanceSq) {
if (distanceSq === 0) {
features[key] = true;
matches.splice(matches.lastIndexOf(match), 1);
return callback(feature, layer, geometry);
}
match.geometry = geometry;
match.distanceSq = distanceSq;
}
return undefined;
};
const renderedTiles = /** @type {Array<import("../../VectorRenderTile.js").default>} */ (this const renderedTiles = /** @type {Array<import("../../VectorRenderTile.js").default>} */ (this
.renderedTiles); .renderedTiles);
let found; let found;
let i, ii; for (let i = 0, ii = renderedTiles.length; !found && i < ii; ++i) {
for (i = 0, ii = renderedTiles.length; i < ii; ++i) {
const tile = renderedTiles[i]; const tile = renderedTiles[i];
const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord); const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
if (!intersects(tileExtent, hitExtent)) { if (!intersects(tileExtent, hitExtent)) {
continue; continue;
} }
const layerUid = getUid(layer); const layerUid = getUid(layer);
const executorGroups = [tile.executorGroups[layerUid]]; const executorGroups = [tile.executorGroups[layerUid]];
const declutterExecutorGroups = tile.declutterExecutorGroups[layerUid]; const declutterExecutorGroups = tile.declutterExecutorGroups[layerUid];
if (declutterExecutorGroups) { if (declutterExecutorGroups) {
executorGroups.push(declutterExecutorGroups); executorGroups.push(declutterExecutorGroups);
} }
executorGroups.forEach((executorGroups) => { executorGroups.some((executorGroups) => {
const declutteredFeatures =
executorGroups === declutterExecutorGroups
? frameState.declutterTree.all().map((item) => item.value)
: null;
for (let t = 0, tt = executorGroups.length; t < tt; ++t) { for (let t = 0, tt = executorGroups.length; t < tt; ++t) {
const executorGroup = executorGroups[t]; const executorGroup = executorGroups[t];
found = found = executorGroup.forEachFeatureAtCoordinate(
found || coordinate,
executorGroup.forEachFeatureAtCoordinate( resolution,
coordinate, rotation,
resolution, hitTolerance,
rotation, featureCallback,
hitTolerance, declutteredFeatures
/** );
* @param {import("../../Feature.js").FeatureLike} feature Feature. if (found) {
* @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry. return true;
* @return {?} Callback result. }
*/
function (feature, geometry) {
let key = feature.getId();
if (key === undefined) {
key = getUid(feature);
}
if (!(key in features)) {
features[key] = true;
return callback(feature, layer, geometry);
}
},
executorGroups === declutterExecutorGroups
? frameState.declutterTree.all().map((item) => item.value)
: null
);
} }
}); });
} }
+11 -3
View File
@@ -598,13 +598,20 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback. * @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result. * @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T * @template T
*/ */
forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) { forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
assert(this.hitDetectionEnabled_, 66); assert(this.hitDetectionEnabled_, 66);
if (!this.hitRenderInstructions_) { if (!this.hitRenderInstructions_) {
return; return undefined;
} }
const pixel = applyTransform( const pixel = applyTransform(
@@ -623,6 +630,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
if (feature) { if (feature) {
return callback(feature, this.getLayer(), null); return callback(feature, this.getLayer(), null);
} }
return undefined;
} }
/** /**
@@ -189,7 +189,8 @@ describe('ol.renderer.canvas.VectorLayer', function () {
}); });
describe('#forEachFeatureAtCoordinate', function () { describe('#forEachFeatureAtCoordinate', function () {
let layer, renderer; /** @type {VectorLayer} */ let layer;
/** @type {CanvasVectorLayerRenderer} */ let renderer;
beforeEach(function () { beforeEach(function () {
layer = new VectorLayer({ layer = new VectorLayer({
@@ -205,15 +206,17 @@ describe('ol.renderer.canvas.VectorLayer', function () {
hitTolerance, hitTolerance,
callback callback
) { ) {
const feature = new Feature(); const feature = new Feature(new Point([0, 0]));
callback(feature); const distanceSq = 0;
callback(feature); callback(feature, feature.getGeometry(), distanceSq);
callback(feature, feature.getGeometry(), distanceSq);
}; };
}); });
it('calls callback once per feature with a layer as 2nd arg', function () { it('calls callback once per feature with a layer as 2nd arg', function () {
const spy = sinon.spy(); const spy = sinon.spy();
const coordinate = [0, 0]; const coordinate = [0, 0];
const matches = [];
const frameState = { const frameState = {
layerStatesArray: [{}], layerStatesArray: [{}],
viewState: { viewState: {
@@ -227,10 +230,11 @@ describe('ol.renderer.canvas.VectorLayer', function () {
frameState, frameState,
0, 0,
spy, spy,
undefined matches
); );
expect(spy.callCount).to.be(1); expect(spy.callCount).to.be(1);
expect(spy.getCall(0).args[1]).to.equal(layer); expect(spy.getCall(0).args[1]).to.be(layer);
expect(matches).to.be.empty();
}); });
}); });
@@ -114,7 +114,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function () {
it('creates a new instance', function () { it('creates a new instance', function () {
const renderer = new CanvasVectorTileLayerRenderer(layer); const renderer = new CanvasVectorTileLayerRenderer(layer);
expect(renderer).to.be.a(CanvasVectorTileLayerRenderer); expect(renderer).to.be.a(CanvasVectorTileLayerRenderer);
expect(renderer.getLayer()).to.equal(layer); expect(renderer.getLayer()).to.be(layer);
}); });
it('does not render replays for pure image rendering', function () { it('does not render replays for pure image rendering', function () {
@@ -321,7 +321,10 @@ describe('ol.renderer.canvas.VectorTileLayer', function () {
}); });
describe('#forEachFeatureAtCoordinate', function () { describe('#forEachFeatureAtCoordinate', function () {
let layer, renderer, executorGroup, source; /** @type {VectorTileLayer] */ let layer;
/** @type {CanvasVectorTileLayerRenderer} */ let renderer;
/** @type {VectorTileSource} */ let source;
let executorGroup;
class TileClass extends VectorRenderTile { class TileClass extends VectorRenderTile {
constructor() { constructor() {
super(...arguments); super(...arguments);
@@ -339,6 +342,18 @@ describe('ol.renderer.canvas.VectorTileLayer', function () {
}); });
source.sourceTileCache.set('0/0/0.mvt', sourceTile); source.sourceTileCache.set('0/0/0.mvt', sourceTile);
executorGroup = {}; executorGroup = {};
executorGroup.forEachFeatureAtCoordinate = function (
coordinate,
resolution,
rotation,
hitTolerance,
callback
) {
const feature = new Feature(new Point([0, 0]));
const distanceSq = 0;
callback(feature, feature.getGeometry(), distanceSq);
callback(feature, feature.getGeometry(), distanceSq);
};
source.getTile = function () { source.getTile = function () {
const tile = VectorTileSource.prototype.getTile.apply( const tile = VectorTileSource.prototype.getTile.apply(
source, source,
@@ -352,22 +367,12 @@ describe('ol.renderer.canvas.VectorTileLayer', function () {
source: source, source: source,
}); });
renderer = new CanvasVectorTileLayerRenderer(layer); renderer = new CanvasVectorTileLayerRenderer(layer);
executorGroup.forEachFeatureAtCoordinate = function (
coordinate,
resolution,
rotation,
hitTolerance,
callback
) {
const feature = new Feature();
callback(feature);
callback(feature);
};
}); });
it('calls callback once per feature with a layer as 2nd arg', function () { it('calls callback once per feature with a layer as 2nd arg', function () {
const spy = sinon.spy(); const spy = sinon.spy();
const coordinate = [0, 0]; const coordinate = [0, 0];
const matches = [];
const frameState = { const frameState = {
layerStatesArray: [{}], layerStatesArray: [{}],
viewState: { viewState: {
@@ -384,10 +389,11 @@ describe('ol.renderer.canvas.VectorTileLayer', function () {
frameState, frameState,
0, 0,
spy, spy,
undefined matches
); );
expect(spy.callCount).to.be(1); expect(spy.callCount).to.be(1);
expect(spy.getCall(0).args[1]).to.equal(layer); expect(spy.getCall(0).args[1]).to.be(layer);
expect(matches).to.be.empty();
}); });
it('does not give false positives when overzoomed', function (done) { it('does not give false positives when overzoomed', function (done) {