diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index f33d5bb238..38474eff8b 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -623,8 +623,7 @@ class PluggableMap extends BaseObject { const hitTolerance = options.hitTolerance !== undefined ? opt_options.hitTolerance * this.frameState_.pixelRatio : 0; const layerFilter = options.layerFilter || TRUE; - return this.renderer_.forEachLayerAtPixel( - pixel, this.frameState_, hitTolerance, callback, null, layerFilter, null); + return this.renderer_.forEachLayerAtPixel(pixel, this.frameState_, hitTolerance, callback, layerFilter); } /** diff --git a/src/ol/renderer/Composite.js b/src/ol/renderer/Composite.js index 67bbb8a501..8e6f00c2da 100644 --- a/src/ol/renderer/Composite.js +++ b/src/ol/renderer/Composite.js @@ -1,7 +1,6 @@ /** * @module ol/renderer/canvas/Map */ -import {apply as applyTransform} from '../transform.js'; import {stableSort} from '../array.js'; import {CLASS_UNSELECTABLE} from '../css.js'; import {visibleAtResolution} from '../layer/Layer.js'; @@ -111,28 +110,27 @@ class CompositeMapRenderer extends MapRenderer { /** * @inheritDoc */ - forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) { - let result; + forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, layerFilter) { const viewState = frameState.viewState; const viewResolution = viewState.resolution; const layerStates = frameState.layerStatesArray; const numLayers = layerStates.length; - const coordinate = applyTransform( - frameState.pixelToCoordinateTransform, pixel.slice()); - for (let i = numLayers - 1; i >= 0; --i) { const layerState = layerStates[i]; const layer = layerState.layer; - if (visibleAtResolution(layerState, viewResolution) && layerFilter.call(thisArg2, layer)) { + if (visibleAtResolution(layerState, viewResolution) && layerFilter(layer)) { const layerRenderer = this.getLayerRenderer(layer); if (!layerRenderer) { continue; } - result = layerRenderer.forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg); - if (result) { - return result; + const data = layerRenderer.getDataAtPixel(pixel, frameState, hitTolerance); + if (data) { + const result = callback(layer, data); + if (result) { + return result; + } } } } diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index 9307a187c5..3d20b415ac 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -92,16 +92,14 @@ class LayerRenderer extends Observable { /** * @abstract - * @param {import("../coordinate.js").Coordinate} coordinate Coordinate. + * @param {import("../pixel.js").Pixel} pixel Pixel. * @param {import("../PluggableMap.js").FrameState} frameState FrameState. * @param {number} hitTolerance Hit tolerance in pixels. - * @param {function(this: S, import("../layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback Layer - * callback. - * @param {S} thisArg Value to use as `this` when executing `callback`. - * @return {T|undefined} Callback result. - * @template S,T + * @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. */ - forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + getDataAtPixel(pixel, frameState, hitTolerance) { return abstract(); } diff --git a/src/ol/renderer/Map.js b/src/ol/renderer/Map.js index a07f84354f..281d1eb070 100644 --- a/src/ol/renderer/Map.js +++ b/src/ol/renderer/Map.js @@ -161,16 +161,14 @@ class MapRenderer extends Disposable { * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(this: S, import("../layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback Layer * callback. - * @param {S} thisArg Value to use as `this` when executing `callback`. * @param {function(this: U, import("../layer/Layer.js").default): boolean} layerFilter Layer filter * function, only layers which are visible and for which this function * returns `true` will be tested for features. By default, all visible * layers will be tested. - * @param {U} thisArg2 Value to use as `this` when executing `layerFilter`. * @return {T|undefined} Callback result. * @template S,T,U */ - forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) { + forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, layerFilter) { return abstract(); } diff --git a/src/ol/renderer/canvas/Layer.js b/src/ol/renderer/canvas/Layer.js index fc302d514d..63b3c475ee 100644 --- a/src/ol/renderer/canvas/Layer.js +++ b/src/ol/renderer/canvas/Layer.js @@ -3,12 +3,11 @@ */ import {getBottomLeft, getBottomRight, getTopLeft, getTopRight} from '../../extent.js'; import {createCanvasContext2D} from '../../dom.js'; -import {TRUE} from '../../functions.js'; import RenderEvent from '../../render/Event.js'; import RenderEventType from '../../render/EventType.js'; import {rotateAtOffset} from '../../render/canvas.js'; import LayerRenderer from '../Layer.js'; -import {create as createTransform, apply as applyTransform, compose as composeTransform} from '../../transform.js'; +import {invert as invertTransform, create as createTransform, apply as applyTransform, compose as composeTransform} from '../../transform.js'; /** * @abstract @@ -102,26 +101,6 @@ class CanvasLayerRenderer extends LayerRenderer { } } - /** - * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate. - * @param {import("../../PluggableMap.js").FrameState} frameState FrameState. - * @param {number} hitTolerance Hit tolerance in pixels. - * @param {function(this: S, import("../../layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback Layer - * callback. - * @param {S} thisArg Value to use as `this` when executing `callback`. - * @return {T|undefined} Callback result. - * @template S,T,U - */ - forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { - const hasFeature = this.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, TRUE); - - if (hasFeature) { - return callback.call(thisArg, this.getLayer(), null); - } else { - return undefined; - } - } - /** * @param {CanvasRenderingContext2D} context Context. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. @@ -173,6 +152,35 @@ class CanvasLayerRenderer extends LayerRenderer { return composeTransform(this.tempTransform_, dx1, dy1, sx, sy, -viewState.rotation, dx2, dy2); } + /** + * @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(invertTransform(this.pixelTransform_.slice()), pixel.slice()); + const context = this.context; + + let data; + try { + data = context.getImageData(Math.round(renderPixel[0]), Math.round(renderPixel[1]), 1, 1).data; + } catch (err) { + if (err.name === 'SecurityError') { + // tainted canvas, we assume there is data at the given pixel (although there might not be) + return new Uint8Array(); + } + return data; + } + + if (data[3] === 0) { + return null; + } + return data; + } + } export default CanvasLayerRenderer; diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 68fbb50ad5..c066a522f0 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -19,11 +19,14 @@ import {ORDER} from '../../render/replay.js'; import CanvasTileLayerRenderer from './TileLayer.js'; import {getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js'; import { + apply as applyTransform, create as createTransform, compose as composeTransform, + invert as invertTransform, reset as resetTransform, scale as scaleTransform, - translate as translateTransform + translate as translateTransform, + toString as transformToString } from '../../transform.js'; import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; @@ -71,6 +74,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const overlayCanvas = this.overlayContext_.canvas; overlayCanvas.style.position = 'absolute'; + overlayCanvas.style.transformOrigin = 'top left'; const container = document.createElement('div'); const style = container.style; @@ -87,6 +91,13 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { */ this.container_ = container; + /** + * The transform for rendered pixels to viewport CSS pixels for the overlay canvas. + * @private + * @type {import("../../transform.js").Transform} + */ + this.overlayPixelTransform_ = createTransform(); + /** * Declutter tree. * @private @@ -377,11 +388,15 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const canvas = context.canvas; const width = Math.round(size[0] * pixelRatio); const height = Math.round(size[1] * pixelRatio); + this.overlayPixelTransform_[0] = 1 / pixelRatio; + this.overlayPixelTransform_[3] = 1 / pixelRatio; if (canvas.width != width || canvas.height != height) { canvas.width = width; canvas.height = height; - canvas.style.width = (width / pixelRatio) + 'px'; - canvas.style.height = (height / pixelRatio) + 'px'; + const canvasTransform = transformToString(this.overlayPixelTransform_); + if (canvas.style.transform !== canvasTransform) { + canvas.style.transform = canvasTransform; + } } else { context.clearRect(0, 0, width, height); } @@ -519,6 +534,35 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } } } + + /** + * @inheritdoc + */ + getDataAtPixel(pixel, frameState, hitTolerance) { + let data = super.getDataAtPixel(pixel, frameState, hitTolerance); + if (data) { + return data; + } + + const renderPixel = applyTransform(invertTransform(this.overlayPixelTransform_.slice()), pixel.slice()); + const context = this.overlayContext_; + + try { + data = context.getImageData(Math.round(renderPixel[0]), Math.round(renderPixel[1]), 1, 1).data; + } catch (err) { + if (err.name === 'SecurityError') { + // tainted canvas, we assume there is data at the given pixel (although there might not be) + return new Uint8Array(); + } + return data; + } + + if (data[3] === 0) { + return null; + } + return data; + } + }