From c076d273e76b20da009845048f98763f1620cb93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Sat, 28 Nov 2020 00:17:58 +0100 Subject: [PATCH] Cache hit detect indexes and check closest pixels first. --- src/ol/render/canvas/ExecutorGroup.js | 140 ++++++++---------- .../ol/render/canvas/executorgroup.test.js | 50 ++++++- 2 files changed, 103 insertions(+), 87 deletions(-) diff --git a/src/ol/render/canvas/ExecutorGroup.js b/src/ol/render/canvas/ExecutorGroup.js index a625c1ee2d..6d59379791 100644 --- a/src/ol/render/canvas/ExecutorGroup.js +++ b/src/ol/render/canvas/ExecutorGroup.js @@ -219,7 +219,7 @@ class ExecutorGroup { ); } - const mask = getCircleArray(hitTolerance); + const indexes = getPixelIndexArray(hitTolerance); let builderType; @@ -231,31 +231,24 @@ class ExecutorGroup { function featureCallback(feature, geometry) { const imageData = context.getImageData(0, 0, contextSize, contextSize) .data; - for (let i = 0; i < contextSize; i++) { - for (let j = 0; j < contextSize; j++) { - if (mask[i][j]) { - if (imageData[(j * contextSize + i) * 4 + 3] > 0) { - let result; - if ( - !( - declutteredFeatures && - (builderType == BuilderType.IMAGE || - builderType == BuilderType.TEXT) - ) || - declutteredFeatures.indexOf(feature) !== -1 - ) { - result = callback(feature, geometry); - } - if (result) { - return result; - } else { - context.clearRect(0, 0, contextSize, contextSize); - return undefined; - } + for (let i = 0, ii = indexes.length; i < ii; i++) { + if (imageData[indexes[i]] > 0) { + if ( + !declutteredFeatures || + (builderType !== BuilderType.IMAGE && + builderType !== BuilderType.TEXT) || + declutteredFeatures.indexOf(feature) !== -1 + ) { + const result = callback(feature, geometry); + if (result) { + return result; } } + context.clearRect(0, 0, contextSize, contextSize); + break; } } + return undefined; } /** @type {Array} */ @@ -372,78 +365,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. - * @type {Object>>} + * @type {Object>} */ -const circleArrayCache = { - 0: [[true]], -}; +const circlePixelIndexArrayCache = {}; /** - * This method fills a row in the array from the given coordinate to the - * middle with `true`. - * @param {Array>} 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. + * This methods creates an array with indexes of all pixels within a circle, + * ordered by how close they are to the center. * A cache is used to increase performance. * @param {number} radius Radius. - * @returns {Array>} An array with marked circle points. + * @returns {Array} An array with indexes within a circle. */ -export function getCircleArray(radius) { - if (circleArrayCache[radius] !== undefined) { - return circleArrayCache[radius]; +export function getPixelIndexArray(radius) { + if (circlePixelIndexArrayCache[radius] !== undefined) { + return circlePixelIndexArrayCache[radius]; } - const arraySize = radius * 2 + 1; - const arr = new Array(arraySize); - for (let i = 0; i < arraySize; i++) { - arr[i] = new Array(arraySize); - } - - let x = radius; - let y = 0; - let error = 0; - - while (x >= y) { - fillCircleArrayRowToMiddle(arr, radius + x, radius + y); - fillCircleArrayRowToMiddle(arr, radius + y, radius + x); - fillCircleArrayRowToMiddle(arr, radius - y, radius + x); - fillCircleArrayRowToMiddle(arr, radius - x, radius + y); - fillCircleArrayRowToMiddle(arr, radius - x, radius - y); - fillCircleArrayRowToMiddle(arr, radius - y, radius - x); - fillCircleArrayRowToMiddle(arr, radius + y, radius - x); - fillCircleArrayRowToMiddle(arr, radius + x, radius - y); - - y++; - error += 1 + 2 * y; - if (2 * (error - x) + 1 > 0) { - x -= 1; - error += 1 - 2 * x; + const size = radius * 2 + 1; + const maxDistanceSq = radius * radius; + const distances = new Array(maxDistanceSq + 1); + for (let i = 0; i <= radius; ++i) { + for (let j = 0; j <= radius; ++j) { + const distanceSq = i * i + j * j; + if (distanceSq > maxDistanceSq) { + break; + } + let distance = distances[distanceSq]; + if (!distance) { + distance = []; + distances[distanceSq] = distance; + } + distance.push(((radius + i) * size + (radius + j)) * 4 + 3); + if (i > 0) { + distance.push(((radius - i) * size + (radius + j)) * 4 + 3); + } + if (j > 0) { + distance.push(((radius + i) * size + (radius - j)) * 4 + 3); + if (i > 0) { + distance.push(((radius - i) * size + (radius - j)) * 4 + 3); + } + } } } - circleArrayCache[radius] = arr; - return arr; + const pixelIndex = []; + 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; diff --git a/test/spec/ol/render/canvas/executorgroup.test.js b/test/spec/ol/render/canvas/executorgroup.test.js index 6fda89e3b5..1c8e083865 100644 --- a/test/spec/ol/render/canvas/executorgroup.test.js +++ b/test/spec/ol/render/canvas/executorgroup.test.js @@ -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('#getCircleArray_', function () { - it('creates an array with a pixelated circle marked with true', function () { + describe('#getPixelIndexArray', function () { + it('creates an array with every index within distance', function () { 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 maxRadiusSq = Math.pow(radius + Math.SQRT2, 2); - const circleArray = getCircleArray(radius); - const size = radius * 2 + 1; expect(circleArray.length).to.be(size); 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); + }); }); });