Cache hit detect indexes and check closest pixels first.

This commit is contained in:
Maximilian Krög
2020-11-28 00:17:58 +01:00
parent aec4c60c4a
commit c076d273e7
2 changed files with 103 additions and 87 deletions
+58 -82
View File
@@ -219,7 +219,7 @@ class ExecutorGroup {
); );
} }
const mask = getCircleArray(hitTolerance); const indexes = getPixelIndexArray(hitTolerance);
let builderType; let builderType;
@@ -231,31 +231,24 @@ class ExecutorGroup {
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 result = callback(feature, geometry);
builderType == BuilderType.TEXT) if (result) {
) || return result;
declutteredFeatures.indexOf(feature) !== -1
) {
result = callback(feature, geometry);
}
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 +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. * 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;
@@ -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);
});
}); });
}); });