From d85be48da29bc3a009f8ebc2812a888789b2ff42 Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 3 Jan 2022 12:22:19 +0000 Subject: [PATCH 1/3] Test getDataAtPixel() method for WebGL --- .../spec/ol/renderer/webgl/Layer.test.js | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/test/browser/spec/ol/renderer/webgl/Layer.test.js b/test/browser/spec/ol/renderer/webgl/Layer.test.js index 1d76de8a36..908c5dfd2a 100644 --- a/test/browser/spec/ol/renderer/webgl/Layer.test.js +++ b/test/browser/spec/ol/renderer/webgl/Layer.test.js @@ -1,6 +1,7 @@ import DataTileSource from '../../../../../../src/ol/source/DataTile.js'; import Layer from '../../../../../../src/ol/layer/Layer.js'; import Map from '../../../../../../src/ol/Map.js'; +import Projection from '../../../../../../src/ol/proj/Projection.js'; import TileLayer from '../../../../../../src/ol/layer/WebGLTile.js'; import VectorLayer from '../../../../../../src/ol/layer/Vector.js'; import VectorSource from '../../../../../../src/ol/source/Vector.js'; @@ -434,4 +435,176 @@ describe('ol/renderer/webgl/Layer', function () { dispose(map); }); }); + + describe('#getDataAtPixel (preserveDrawingBuffer false)', function () { + let map, target, source, layer, getContextOriginal; + beforeEach(function (done) { + getContextOriginal = HTMLCanvasElement.prototype.getContext; + HTMLCanvasElement.prototype.getContext = function (type, attributes) { + if (attributes && attributes.preserveDrawingBuffer) { + attributes.preserveDrawingBuffer = false; + } + return getContextOriginal.call(this, type, attributes); + }; + + const projection = new Projection({ + code: 'custom-image', + units: 'pixels', + extent: [0, 0, 200, 200], + }); + target = document.createElement('div'); + target.style.width = '100px'; + target.style.height = '100px'; + document.body.appendChild(target); + source = new DataTileSource({ + loader: function (z, x, y) { + return new Uint8Array(x == 0 ? [255, 0, 0, 255] : [0, 0, 0, 0]); + }, + projection: projection, + maxZoom: 0, + tileSize: 1, + maxResolution: 100, + }); + layer = new TileLayer({ + source: source, + extent: [50, 0, 150, 100], + }); + map = new Map({ + pixelRatio: 1, + target: target, + layers: [layer], + view: new View({ + projection: projection, + center: [100, 100], + zoom: 0, + }), + }); + map.once('rendercomplete', function () { + done(); + }); + }); + + afterEach(function () { + HTMLCanvasElement.prototype.getContext = getContextOriginal; + map.setLayers([]); + map.setTarget(null); + document.body.removeChild(target); + }); + + it('should not detect pixels outside of the layer extent', function () { + const pixel = [10, 10]; + const frameState = map.frameState_; + const hitTolerance = 0; + const layerRenderer = layer.getRenderer(); + const data = layerRenderer.getDataAtPixel( + pixel, + frameState, + hitTolerance + ); + expect(data).to.be(null); + }); + + it('should handle unreadable pixels in the layer extent', function () { + const pixel = [10, 60]; + const frameState = map.frameState_; + const hitTolerance = 0; + const layerRenderer = layer.getRenderer(); + const data = layerRenderer.getDataAtPixel( + pixel, + frameState, + hitTolerance + ); + expect(data.length).to.be(0); + }); + }); + + describe('#getDataAtPixel (preserveDrawingBuffer true)', function () { + let map, target, source, layer; + beforeEach(function (done) { + const projection = new Projection({ + code: 'custom-image', + units: 'pixels', + extent: [0, 0, 200, 200], + }); + target = document.createElement('div'); + target.style.width = '100px'; + target.style.height = '100px'; + document.body.appendChild(target); + source = new DataTileSource({ + loader: function (z, x, y) { + return new Uint8Array(x == 0 ? [255, 0, 0, 255] : [0, 0, 0, 0]); + }, + projection: projection, + maxZoom: 0, + tileSize: 1, + maxResolution: 100, + }); + layer = new TileLayer({ + source: source, + extent: [50, 0, 150, 100], + }); + map = new Map({ + pixelRatio: 1, + target: target, + layers: [layer], + view: new View({ + projection: projection, + center: [100, 100], + zoom: 0, + }), + }); + map.once('rendercomplete', function () { + done(); + }); + }); + + afterEach(function () { + map.setLayers([]); + map.setTarget(null); + document.body.removeChild(target); + }); + + it('should not detect pixels outside of the layer extent', function () { + const pixel = [10, 10]; + const frameState = map.frameState_; + const hitTolerance = 0; + const layerRenderer = layer.getRenderer(); + const data = layerRenderer.getDataAtPixel( + pixel, + frameState, + hitTolerance + ); + expect(data).to.be(null); + }); + + it('should detect pixels in the layer extent', function () { + const pixel = [10, 60]; + const frameState = map.frameState_; + const hitTolerance = 0; + const layerRenderer = layer.getRenderer(); + const data = layerRenderer.getDataAtPixel( + pixel, + frameState, + hitTolerance + ); + expect(data.length > 0).to.be(true); + expect(data[0]).to.be(255); + expect(data[1]).to.be(0); + expect(data[2]).to.be(0); + expect(data[3]).to.be(255); + }); + + it('should handle no data in the layer extent', function () { + const pixel = [60, 60]; + const frameState = map.frameState_; + const hitTolerance = 0; + const layerRenderer = layer.getRenderer(); + const data = layerRenderer.getDataAtPixel( + pixel, + frameState, + hitTolerance + ); + expect(data).to.be(null); + }); + }); }); From 766a336650d862153e9b3f51906a74465986fa26 Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 3 Jan 2022 12:30:49 +0000 Subject: [PATCH 2/3] Add getDataAtPixel() method for WebGL --- src/ol/renderer/webgl/Layer.js | 70 ++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js index dfce02f4df..b11bf36902 100644 --- a/src/ol/renderer/webgl/Layer.js +++ b/src/ol/renderer/webgl/Layer.js @@ -7,9 +7,11 @@ import RenderEvent from '../../render/Event.js'; import RenderEventType from '../../render/EventType.js'; import WebGLHelper from '../../webgl/Helper.js'; import { + apply as applyTransform, compose as composeTransform, create as createTransform, } from '../../transform.js'; +import {containsCoordinate} from '../../extent.js'; /** * @enum {string} @@ -71,6 +73,12 @@ class WebGLLayerRenderer extends LayerRenderer { */ this.inversePixelTransform_ = createTransform(); + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.pixelContext_ = null; + /** * @private */ @@ -270,6 +278,68 @@ class WebGLLayerRenderer extends LayerRenderer { postRender(context, frameState) { this.dispatchRenderEvent_(RenderEventType.POSTRENDER, context, frameState); } + + /** + * @param {import("../../pixel.js").Pixel} pixel Pixel. + * @param {import("../../PluggableMap.js").FrameState} frameState FrameState. + * @param {number} hitTolerance Hit tolerance in pixels. + * @return {Uint8ClampedArray|Uint8Array} The result. If there is no data at the pixel + * location, null will be returned. If there is data, but pixel values cannot be + * returned, and empty array will be returned. + */ + getDataAtPixel(pixel, frameState, hitTolerance) { + const renderPixel = applyTransform( + [frameState.pixelRatio, 0, 0, frameState.pixelRatio, 0, 0], + pixel.slice() + ); + const gl = this.helper.getGL(); + if (!gl) { + return null; + } + const layer = this.getLayer(); + const layerExtent = layer.getExtent(); + if (layerExtent) { + const renderCoordinate = applyTransform( + frameState.pixelToCoordinateTransform, + pixel.slice() + ); + + /** get only data inside of the layer extent */ + if (!containsCoordinate(layerExtent, renderCoordinate)) { + return null; + } + } + + const attributes = gl.getContextAttributes(); + if (!attributes || !attributes.preserveDrawingBuffer) { + // we assume there is data at the given pixel (although there might not be) + return new Uint8Array(); + } + + const x = Math.round(renderPixel[0]); + const y = Math.round(renderPixel[1]); + let pixelContext = this.pixelContext_; + if (!pixelContext) { + const pixelCanvas = document.createElement('canvas'); + pixelCanvas.width = 1; + pixelCanvas.height = 1; + pixelContext = pixelCanvas.getContext('2d'); + this.pixelContext_ = pixelContext; + } + pixelContext.clearRect(0, 0, 1, 1); + let data; + try { + pixelContext.drawImage(gl.canvas, x, y, 1, 1, 0, 0, 1, 1); + data = pixelContext.getImageData(0, 0, 1, 1).data; + } catch (err) { + return data; + } + + if (data[3] === 0) { + return null; + } + return data; + } } const tmpArray_ = []; From 9c955bc86d63c2bb5e29312160d5f56bbd31251a Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Fri, 7 Jan 2022 18:25:20 +0000 Subject: [PATCH 3/3] Update forEachLayerAtPixel description --- src/ol/PluggableMap.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index a880582890..c80bc2bb93 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -688,8 +688,11 @@ class PluggableMap extends BaseObject { * execute a callback with each matching layer. Layers included in the * detection can be configured through `opt_layerFilter`. * - * Note: this may give false positives unless the map layers have had different `className` - * properties assigned to them. + * Note: In maps with more than one layer, this method will typically return pixel data + * representing the composed image of all layers visible at the given pixel – because layers + * will generally share the same rendering context. To force layers to render separately, and + * to get pixel data representing only one layer at a time, you can assign each layer a unique + * `className` in its constructor. * * @param {import("./pixel.js").Pixel} pixel Pixel. * @param {function(this: S, import("./layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback