From 49c6ab716ce0c38f8ef959a5346c562c86bc60a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Sun, 7 Feb 2021 01:46:26 +0100 Subject: [PATCH 1/2] Fix VectorLayer hitdetect inaccuracy Due to rounding the hitdetection may have been off by one pixel. At map edge the pixel coordinate may exceed the map's dimensions if an decimal pixel ratio is used, this is fixed by clamping to the canvas dimensions. --- src/ol/render/canvas/hitdetect.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ol/render/canvas/hitdetect.js b/src/ol/render/canvas/hitdetect.js index 69014b87b9..a95b77d653 100644 --- a/src/ol/render/canvas/hitdetect.js +++ b/src/ol/render/canvas/hitdetect.js @@ -6,6 +6,7 @@ import CanvasImmediateRenderer from './Immediate.js'; import GeometryType from '../../geom/GeometryType.js'; import IconAnchorUnits from '../../style/IconAnchorUnits.js'; import {Icon} from '../../style.js'; +import {clamp} from '../../math.js'; import {createCanvasContext2D} from '../../dom.js'; import {intersects} from '../../extent.js'; import {numberSafeCompareFunction} from '../../array.js'; @@ -162,8 +163,13 @@ export function createHitDetectionImageData( export function hitDetect(pixel, features, imageData) { const resultFeatures = []; if (imageData) { + // The pixel coordinate is clamped down to the hit-detect canvas' size to account + // for browsers returning coordinates slightly larger than the actual canvas size + // due to a non-integer pixel ratio. const index = - (Math.round(pixel[0] / 2) + Math.round(pixel[1] / 2) * imageData.width) * + (clamp(Math.floor(Math.round(pixel[0]) / 2), 0, imageData.width - 1) + + clamp(Math.floor(Math.round(pixel[1]) / 2), 0, imageData.height - 1) * + imageData.width) * 4; const r = imageData.data[index]; const g = imageData.data[index + 1]; From 8bd081d9923b57bc2b8969f1542f7c1ae8efd216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Mon, 8 Feb 2021 20:40:07 +0100 Subject: [PATCH 2/2] Add test for hitdetect function --- test/spec/ol/render/canvas/hitdetect.test.js | 55 +++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/test/spec/ol/render/canvas/hitdetect.test.js b/test/spec/ol/render/canvas/hitdetect.test.js index 4a1efeb976..04395ba3b7 100644 --- a/test/spec/ol/render/canvas/hitdetect.test.js +++ b/test/spec/ol/render/canvas/hitdetect.test.js @@ -3,7 +3,11 @@ import Feature from '../../../../../src/ol/Feature.js'; import Point from '../../../../../src/ol/geom/Point.js'; import {Style} from '../../../../../src/ol/style.js'; import {create} from '../../../../../src/ol/transform.js'; -import {createHitDetectionImageData} from '../../../../../src/ol/render/canvas/hitdetect.js'; +import {createCanvasContext2D} from '../../../../../src/ol/dom.js'; +import { + createHitDetectionImageData, + hitDetect, +} from '../../../../../src/ol/render/canvas/hitdetect.js'; describe('hitdetect', function () { let features, styleFunction; @@ -40,4 +44,53 @@ describe('hitdetect', function () { 252, ]); }); + it('detects hit at the correct position', function () { + const context = createCanvasContext2D(3, 3); + context.fillStyle = '#ffffff'; + context.fillRect(1, 1, 1, 1); + const features = [new Feature()]; + const imageData = context.getImageData(0, 0, 3, 3); + expect(hitDetect([2, 2], features, imageData)).to.have.length(1); + expect(hitDetect([2, 3], features, imageData)).to.have.length(1); + expect(hitDetect([3, 2], features, imageData)).to.have.length(1); + expect(hitDetect([3, 3], features, imageData)).to.have.length(1); + + expect(hitDetect([1.5, 1.5], features, imageData)).to.have.length(1); + expect(hitDetect([3.4, 3.4], features, imageData)).to.have.length(1); + + expect(hitDetect([1.4, 1], features, imageData)).to.have.length(0); + expect(hitDetect([1, 2.4], features, imageData)).to.have.length(0); + expect(hitDetect([2.4, 1], features, imageData)).to.have.length(0); + + expect(hitDetect([3.5, 4.5], features, imageData)).to.have.length(0); + expect(hitDetect([5, 4], features, imageData)).to.have.length(0); + expect(hitDetect([4.5, 5], features, imageData)).to.have.length(0); + + expect(hitDetect([1.4, 3.5], features, imageData)).to.have.length(0); + expect(hitDetect([1, 4.5], features, imageData)).to.have.length(0); + expect(hitDetect([1.5, 5], features, imageData)).to.have.length(0); + }); + it('correctly detects hit for pixel exceeding canvas dimension', function () { + const features = [new Feature()]; + const context = createCanvasContext2D(2, 2); + context.fillStyle = '#ffffff'; + + context.fillRect(1, 1, 1, 1); + let imageData = context.getImageData(0, 0, 2, 2); + expect(hitDetect([4, 2], features, imageData)).to.have.length(1); + expect(hitDetect([2, 4], features, imageData)).to.have.length(1); + + expect(hitDetect([-2, 4], features, imageData)).to.have.length(0); + expect(hitDetect([4, -2], features, imageData)).to.have.length(0); + + context.clearRect(0, 0, context.canvas.width, context.canvas.height); + + context.fillRect(0, 0, 1, 1); + imageData = context.getImageData(0, 0, 2, 2); + expect(hitDetect([-2, 0], features, imageData)).to.have.length(1); + expect(hitDetect([0, -2], features, imageData)).to.have.length(1); + + expect(hitDetect([-2, 4], features, imageData)).to.have.length(0); + expect(hitDetect([4, -2], features, imageData)).to.have.length(0); + }); });