Merge pull request #11783 from MoonE/hitdetect-indexes
Cache hit detect indexes and check closest pixels first.
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: example.html
|
||||||
|
title: Hit tolerance priority
|
||||||
|
shortdesc: Shows bad behavior of hit detection with hit tolerance.
|
||||||
|
docs: >
|
||||||
|
Hover over the map and observe how the small circles get a black outline as you hover over them. Is the expected feature getting highlighted?
|
||||||
|
tags: "simple, openstreetmap"
|
||||||
|
---
|
||||||
|
<div id="map" class="map"></div>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import CircleStyle from '../src/ol/style/Circle.js';
|
||||||
|
import Feature from '../src/ol/Feature.js';
|
||||||
|
import Map from '../src/ol/Map.js';
|
||||||
|
import VectorLayer from '../src/ol/layer/Vector.js';
|
||||||
|
import VectorSource from '../src/ol/source/Vector.js';
|
||||||
|
import View from '../src/ol/View.js';
|
||||||
|
import {Fill, Stroke, Style} from '../src/ol/style.js';
|
||||||
|
import {Point} from '../src/ol/geom.js';
|
||||||
|
|
||||||
|
const map = new Map({
|
||||||
|
target: 'map',
|
||||||
|
view: new View({
|
||||||
|
center: [0, 0],
|
||||||
|
resolution: 1,
|
||||||
|
resolutions: [1],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const vectorLayer = new VectorLayer({
|
||||||
|
source: new VectorSource({
|
||||||
|
features: [
|
||||||
|
new Feature({
|
||||||
|
geometry: new Point([0, 0]),
|
||||||
|
color: 'white',
|
||||||
|
}),
|
||||||
|
new Feature({
|
||||||
|
geometry: new Point([-10, 0]),
|
||||||
|
color: 'fuchsia',
|
||||||
|
}),
|
||||||
|
new Feature({
|
||||||
|
geometry: new Point([-10, -10]),
|
||||||
|
color: 'orange',
|
||||||
|
}),
|
||||||
|
new Feature({
|
||||||
|
geometry: new Point([-10, 10]),
|
||||||
|
color: 'cyan',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
style: (feature) => {
|
||||||
|
return new Style({
|
||||||
|
image: new CircleStyle({
|
||||||
|
radius: 5,
|
||||||
|
fill: new Fill({
|
||||||
|
color: feature.get('color'),
|
||||||
|
}),
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: 'gray',
|
||||||
|
width: 1,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
map.addLayer(vectorLayer);
|
||||||
|
|
||||||
|
const highlightFeature = new Feature(new Point([NaN, NaN]));
|
||||||
|
highlightFeature.setStyle(
|
||||||
|
new Style({
|
||||||
|
image: new CircleStyle({
|
||||||
|
radius: 5,
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: 'black',
|
||||||
|
width: 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
vectorLayer.getSource().addFeature(highlightFeature);
|
||||||
|
map.on('pointermove', (e) => {
|
||||||
|
const hit = map.forEachFeatureAtPixel(
|
||||||
|
e.pixel,
|
||||||
|
(feature) => {
|
||||||
|
highlightFeature.setGeometry(feature.getGeometry().clone());
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hitTolerance: 10,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!hit) {
|
||||||
|
highlightFeature.setGeometry(new Point([NaN, NaN]));
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,43 +220,39 @@ class ExecutorGroup {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mask = getCircleArray(hitTolerance);
|
const indexes = getPixelIndexArray(hitTolerance);
|
||||||
|
|
||||||
let builderType;
|
let builderType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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)
|
||||||
.data;
|
.data;
|
||||||
for (let i = 0; i < contextSize; i++) {
|
for (let i = 0, ii = indexes.length; i < ii; i++) {
|
||||||
for (let j = 0; j < contextSize; j++) {
|
if (imageData[indexes[i]] > 0) {
|
||||||
if (mask[i][j]) {
|
if (
|
||||||
if (imageData[(j * contextSize + i) * 4 + 3] > 0) {
|
!declutteredFeatures ||
|
||||||
let result;
|
(builderType !== BuilderType.IMAGE &&
|
||||||
if (
|
builderType !== BuilderType.TEXT) ||
|
||||||
!(
|
declutteredFeatures.indexOf(feature) !== -1
|
||||||
declutteredFeatures &&
|
) {
|
||||||
(builderType == BuilderType.IMAGE ||
|
const idx = (indexes[i] - 3) / 4;
|
||||||
builderType == BuilderType.TEXT)
|
const x = hitTolerance - (idx % contextSize);
|
||||||
) ||
|
const y = hitTolerance - ((idx / contextSize) | 0);
|
||||||
declutteredFeatures.indexOf(feature) !== -1
|
const result = callback(feature, geometry, x * x + y * y);
|
||||||
) {
|
if (result) {
|
||||||
result = callback(feature, geometry);
|
return result;
|
||||||
}
|
|
||||||
if (result) {
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
context.clearRect(0, 0, contextSize, contextSize);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
context.clearRect(0, 0, contextSize, contextSize);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {Array<number>} */
|
/** @type {Array<number>} */
|
||||||
@@ -372,78 +369,61 @@ class ExecutorGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This cache is used for storing calculated pixel circles for increasing performance.
|
* This cache is used to store arrays of indexes for calculated pixel circles
|
||||||
|
* to increase performance.
|
||||||
* It is a static property to allow each Replaygroup to access it.
|
* It is a static property to allow each Replaygroup to access it.
|
||||||
* @type {Object<number, Array<Array<(boolean|undefined)>>>}
|
* @type {Object<number, Array<number>>}
|
||||||
*/
|
*/
|
||||||
const circleArrayCache = {
|
const circlePixelIndexArrayCache = {};
|
||||||
0: [[true]],
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method fills a row in the array from the given coordinate to the
|
* This methods creates an array with indexes of all pixels within a circle,
|
||||||
* middle with `true`.
|
* ordered by how close they are to the center.
|
||||||
* @param {Array<Array<(boolean|undefined)>>} array The array that will be altered.
|
|
||||||
* @param {number} x X coordinate.
|
|
||||||
* @param {number} y Y coordinate.
|
|
||||||
*/
|
|
||||||
function fillCircleArrayRowToMiddle(array, x, y) {
|
|
||||||
let i;
|
|
||||||
const 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.
|
* A cache is used to increase performance.
|
||||||
* @param {number} radius Radius.
|
* @param {number} radius Radius.
|
||||||
* @returns {Array<Array<(boolean|undefined)>>} An array with marked circle points.
|
* @returns {Array<number>} An array with indexes within a circle.
|
||||||
*/
|
*/
|
||||||
export function getCircleArray(radius) {
|
export function getPixelIndexArray(radius) {
|
||||||
if (circleArrayCache[radius] !== undefined) {
|
if (circlePixelIndexArrayCache[radius] !== undefined) {
|
||||||
return circleArrayCache[radius];
|
return circlePixelIndexArrayCache[radius];
|
||||||
}
|
}
|
||||||
|
|
||||||
const arraySize = radius * 2 + 1;
|
const size = radius * 2 + 1;
|
||||||
const arr = new Array(arraySize);
|
const maxDistanceSq = radius * radius;
|
||||||
for (let i = 0; i < arraySize; i++) {
|
const distances = new Array(maxDistanceSq + 1);
|
||||||
arr[i] = new Array(arraySize);
|
for (let i = 0; i <= radius; ++i) {
|
||||||
}
|
for (let j = 0; j <= radius; ++j) {
|
||||||
|
const distanceSq = i * i + j * j;
|
||||||
let x = radius;
|
if (distanceSq > maxDistanceSq) {
|
||||||
let y = 0;
|
break;
|
||||||
let error = 0;
|
}
|
||||||
|
let distance = distances[distanceSq];
|
||||||
while (x >= y) {
|
if (!distance) {
|
||||||
fillCircleArrayRowToMiddle(arr, radius + x, radius + y);
|
distance = [];
|
||||||
fillCircleArrayRowToMiddle(arr, radius + y, radius + x);
|
distances[distanceSq] = distance;
|
||||||
fillCircleArrayRowToMiddle(arr, radius - y, radius + x);
|
}
|
||||||
fillCircleArrayRowToMiddle(arr, radius - x, radius + y);
|
distance.push(((radius + i) * size + (radius + j)) * 4 + 3);
|
||||||
fillCircleArrayRowToMiddle(arr, radius - x, radius - y);
|
if (i > 0) {
|
||||||
fillCircleArrayRowToMiddle(arr, radius - y, radius - x);
|
distance.push(((radius - i) * size + (radius + j)) * 4 + 3);
|
||||||
fillCircleArrayRowToMiddle(arr, radius + y, radius - x);
|
}
|
||||||
fillCircleArrayRowToMiddle(arr, radius + x, radius - y);
|
if (j > 0) {
|
||||||
|
distance.push(((radius + i) * size + (radius - j)) * 4 + 3);
|
||||||
y++;
|
if (i > 0) {
|
||||||
error += 1 + 2 * y;
|
distance.push(((radius - i) * size + (radius - j)) * 4 + 3);
|
||||||
if (2 * (error - x) + 1 > 0) {
|
}
|
||||||
x -= 1;
|
}
|
||||||
error += 1 - 2 * x;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
circleArrayCache[radius] = arr;
|
const pixelIndex = [];
|
||||||
return arr;
|
for (let i = 0, ii = distances.length; i < ii; ++i) {
|
||||||
|
if (distances[i]) {
|
||||||
|
pixelIndex.push(...distances[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
circlePixelIndexArrayCache[radius] = pixelIndex;
|
||||||
|
return pixelIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExecutorGroup;
|
export default ExecutorGroup;
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import {
|
|||||||
translate as translateTransform,
|
translate as translateTransform,
|
||||||
} from '../../transform.js';
|
} from '../../transform.js';
|
||||||
import {
|
import {
|
||||||
|
boundingExtent,
|
||||||
buffer,
|
buffer,
|
||||||
containsCoordinate,
|
|
||||||
containsExtent,
|
containsExtent,
|
||||||
equals,
|
equals,
|
||||||
getIntersection,
|
getIntersection,
|
||||||
@@ -381,76 +381,105 @@ 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;
|
||||||
const layer = this.getLayer();
|
const layer = this.getLayer();
|
||||||
const declutter = layer.getDeclutter();
|
|
||||||
const source = layer.getSource();
|
const source = layer.getSource();
|
||||||
const tileGrid = source.getTileGridForProjection(
|
const tileGrid = source.getTileGridForProjection(
|
||||||
frameState.viewState.projection
|
frameState.viewState.projection
|
||||||
);
|
);
|
||||||
/** @type {!Object<string, boolean>} */
|
|
||||||
|
const hitExtent = boundingExtent([coordinate]);
|
||||||
|
buffer(hitExtent, resolution * hitTolerance, hitExtent);
|
||||||
|
|
||||||
|
/** @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);
|
||||||
const tileContainsCoordinate = containsCoordinate(tileExtent, coordinate);
|
if (!intersects(tileExtent, hitExtent)) {
|
||||||
|
continue;
|
||||||
if (!declutter) {
|
|
||||||
// When not decluttering, we only need to consider the tile that contains the given
|
|
||||||
// coordinate, because each feature will be rendered for each tile that contains it.
|
|
||||||
if (!tileContainsCoordinate) {
|
|
||||||
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) {
|
|
||||||
if (tileContainsCoordinate) {
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
import {getCircleArray} from '../../../../../src/ol/render/canvas/ExecutorGroup.js';
|
import {getPixelIndexArray} from '../../../../../src/ol/render/canvas/ExecutorGroup.js';
|
||||||
|
|
||||||
describe('ol.render.canvas.ExecutorGroup', function () {
|
describe('ol.render.canvas.ExecutorGroup', function () {
|
||||||
describe('#getCircleArray_', function () {
|
describe('#getPixelIndexArray', function () {
|
||||||
it('creates an array with a pixelated circle marked with true', function () {
|
it('creates an array with every index within distance', function () {
|
||||||
const radius = 10;
|
const radius = 10;
|
||||||
|
const size = radius * 2 + 1;
|
||||||
|
const hitIndexes = getPixelIndexArray(radius);
|
||||||
|
|
||||||
|
const circleArray = new Array(size);
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
circleArray[i] = new Array(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
hitIndexes.forEach(function (d) {
|
||||||
|
const x = ((d - 3) / 4) % size;
|
||||||
|
const y = ((d - 3) / 4 / size) | 0;
|
||||||
|
circleArray[x][y] = true;
|
||||||
|
});
|
||||||
|
|
||||||
const minRadiusSq = Math.pow(radius - Math.SQRT2, 2);
|
const minRadiusSq = Math.pow(radius - Math.SQRT2, 2);
|
||||||
const maxRadiusSq = Math.pow(radius + Math.SQRT2, 2);
|
const maxRadiusSq = Math.pow(radius + Math.SQRT2, 2);
|
||||||
const circleArray = getCircleArray(radius);
|
|
||||||
const size = radius * 2 + 1;
|
|
||||||
expect(circleArray.length).to.be(size);
|
expect(circleArray.length).to.be(size);
|
||||||
|
|
||||||
for (let i = 0; i < size; i++) {
|
for (let i = 0; i < size; i++) {
|
||||||
@@ -24,5 +36,33 @@ describe('ol.render.canvas.ExecutorGroup', function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
it('orders the indexes correctly from closest to farthest away', function () {
|
||||||
|
const radius = 10;
|
||||||
|
const size = radius * 2 + 1;
|
||||||
|
const hitIndexes = getPixelIndexArray(radius);
|
||||||
|
|
||||||
|
// Center first
|
||||||
|
expect(hitIndexes[0]).to.be((size * radius + radius) * 4 + 3);
|
||||||
|
|
||||||
|
// 4 Pixels above/below/left/right of center next
|
||||||
|
const begin = hitIndexes.slice(1, 5);
|
||||||
|
expect(begin).to.contain((radius * size + radius + 1) * 4 + 3);
|
||||||
|
expect(begin).to.contain(((radius + 1) * size + radius) * 4 + 3);
|
||||||
|
expect(begin).to.contain(((radius - 1) * size + radius) * 4 + 3);
|
||||||
|
expect(begin).to.contain((radius * size + radius - 1) * 4 + 3);
|
||||||
|
|
||||||
|
// 4 Pixels in the middle of each side in the last 12 elements (at radius 10)
|
||||||
|
const last = hitIndexes.slice(hitIndexes.length - 12);
|
||||||
|
expect(last).to.contain((0 * size + radius) * 4 + 3);
|
||||||
|
expect(last).to.contain((radius * size + 0) * 4 + 3);
|
||||||
|
expect(last).to.contain((radius * size + size - 1) * 4 + 3);
|
||||||
|
expect(last).to.contain(((size - 1) * size + radius) * 4 + 3);
|
||||||
|
});
|
||||||
|
it('has no duplicate indexes', function () {
|
||||||
|
const radius = 10;
|
||||||
|
const hitIndexes = getPixelIndexArray(radius);
|
||||||
|
|
||||||
|
expect(new Set(hitIndexes).size).to.be(hitIndexes.length);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user