Implement getDataAtPixel for all layer renderers

This commit is contained in:
Tim Schaub
2018-11-16 10:34:03 +01:00
parent 832dadb3af
commit 5ad73f8bbd
6 changed files with 92 additions and 47 deletions
+1 -2
View File
@@ -623,8 +623,7 @@ class PluggableMap extends BaseObject {
const hitTolerance = options.hitTolerance !== undefined ? const hitTolerance = options.hitTolerance !== undefined ?
opt_options.hitTolerance * this.frameState_.pixelRatio : 0; opt_options.hitTolerance * this.frameState_.pixelRatio : 0;
const layerFilter = options.layerFilter || TRUE; const layerFilter = options.layerFilter || TRUE;
return this.renderer_.forEachLayerAtPixel( return this.renderer_.forEachLayerAtPixel(pixel, this.frameState_, hitTolerance, callback, layerFilter);
pixel, this.frameState_, hitTolerance, callback, null, layerFilter, null);
} }
/** /**
+8 -10
View File
@@ -1,7 +1,6 @@
/** /**
* @module ol/renderer/canvas/Map * @module ol/renderer/canvas/Map
*/ */
import {apply as applyTransform} from '../transform.js';
import {stableSort} from '../array.js'; import {stableSort} from '../array.js';
import {CLASS_UNSELECTABLE} from '../css.js'; import {CLASS_UNSELECTABLE} from '../css.js';
import {visibleAtResolution} from '../layer/Layer.js'; import {visibleAtResolution} from '../layer/Layer.js';
@@ -111,28 +110,27 @@ class CompositeMapRenderer extends MapRenderer {
/** /**
* @inheritDoc * @inheritDoc
*/ */
forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) { forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, layerFilter) {
let result;
const viewState = frameState.viewState; const viewState = frameState.viewState;
const viewResolution = viewState.resolution; const viewResolution = viewState.resolution;
const layerStates = frameState.layerStatesArray; const layerStates = frameState.layerStatesArray;
const numLayers = layerStates.length; const numLayers = layerStates.length;
const coordinate = applyTransform(
frameState.pixelToCoordinateTransform, pixel.slice());
for (let i = numLayers - 1; i >= 0; --i) { for (let i = numLayers - 1; i >= 0; --i) {
const layerState = layerStates[i]; const layerState = layerStates[i];
const layer = layerState.layer; const layer = layerState.layer;
if (visibleAtResolution(layerState, viewResolution) && layerFilter.call(thisArg2, layer)) { if (visibleAtResolution(layerState, viewResolution) && layerFilter(layer)) {
const layerRenderer = this.getLayerRenderer(layer); const layerRenderer = this.getLayerRenderer(layer);
if (!layerRenderer) { if (!layerRenderer) {
continue; continue;
} }
result = layerRenderer.forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg); const data = layerRenderer.getDataAtPixel(pixel, frameState, hitTolerance);
if (result) { if (data) {
return result; const result = callback(layer, data);
if (result) {
return result;
}
} }
} }
} }
+5 -7
View File
@@ -92,16 +92,14 @@ class LayerRenderer extends Observable {
/** /**
* @abstract * @abstract
* @param {import("../coordinate.js").Coordinate} coordinate Coordinate. * @param {import("../pixel.js").Pixel} pixel Pixel.
* @param {import("../PluggableMap.js").FrameState} frameState FrameState. * @param {import("../PluggableMap.js").FrameState} frameState FrameState.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(this: S, import("../layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback Layer * @return {Uint8ClampedArray|Uint8Array} The result. If there is no data at the pixel
* callback. * location, null will be returned. If there is data, but pixel values cannot be
* @param {S} thisArg Value to use as `this` when executing `callback`. * returned, and empty array will be returned.
* @return {T|undefined} Callback result.
* @template S,T
*/ */
forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { getDataAtPixel(pixel, frameState, hitTolerance) {
return abstract(); return abstract();
} }
+1 -3
View File
@@ -161,16 +161,14 @@ class MapRenderer extends Disposable {
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(this: S, import("../layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback Layer * @param {function(this: S, import("../layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback Layer
* callback. * 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 * @param {function(this: U, import("../layer/Layer.js").default): boolean} layerFilter Layer filter
* function, only layers which are visible and for which this function * function, only layers which are visible and for which this function
* returns `true` will be tested for features. By default, all visible * returns `true` will be tested for features. By default, all visible
* layers will be tested. * layers will be tested.
* @param {U} thisArg2 Value to use as `this` when executing `layerFilter`.
* @return {T|undefined} Callback result. * @return {T|undefined} Callback result.
* @template S,T,U * @template S,T,U
*/ */
forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) { forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, layerFilter) {
return abstract(); return abstract();
} }
+30 -22
View File
@@ -3,12 +3,11 @@
*/ */
import {getBottomLeft, getBottomRight, getTopLeft, getTopRight} from '../../extent.js'; import {getBottomLeft, getBottomRight, getTopLeft, getTopRight} from '../../extent.js';
import {createCanvasContext2D} from '../../dom.js'; import {createCanvasContext2D} from '../../dom.js';
import {TRUE} from '../../functions.js';
import RenderEvent from '../../render/Event.js'; import RenderEvent from '../../render/Event.js';
import RenderEventType from '../../render/EventType.js'; import RenderEventType from '../../render/EventType.js';
import {rotateAtOffset} from '../../render/canvas.js'; import {rotateAtOffset} from '../../render/canvas.js';
import LayerRenderer from '../Layer.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 * @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 {CanvasRenderingContext2D} context Context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @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); 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; export default CanvasLayerRenderer;
+47 -3
View File
@@ -19,11 +19,14 @@ import {ORDER} from '../../render/replay.js';
import CanvasTileLayerRenderer from './TileLayer.js'; import CanvasTileLayerRenderer from './TileLayer.js';
import {getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js'; import {getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js';
import { import {
apply as applyTransform,
create as createTransform, create as createTransform,
compose as composeTransform, compose as composeTransform,
invert as invertTransform,
reset as resetTransform, reset as resetTransform,
scale as scaleTransform, scale as scaleTransform,
translate as translateTransform translate as translateTransform,
toString as transformToString
} from '../../transform.js'; } from '../../transform.js';
import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js';
@@ -71,6 +74,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const overlayCanvas = this.overlayContext_.canvas; const overlayCanvas = this.overlayContext_.canvas;
overlayCanvas.style.position = 'absolute'; overlayCanvas.style.position = 'absolute';
overlayCanvas.style.transformOrigin = 'top left';
const container = document.createElement('div'); const container = document.createElement('div');
const style = container.style; const style = container.style;
@@ -87,6 +91,13 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
*/ */
this.container_ = container; 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. * Declutter tree.
* @private * @private
@@ -377,11 +388,15 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const canvas = context.canvas; const canvas = context.canvas;
const width = Math.round(size[0] * pixelRatio); const width = Math.round(size[0] * pixelRatio);
const height = Math.round(size[1] * 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) { if (canvas.width != width || canvas.height != height) {
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
canvas.style.width = (width / pixelRatio) + 'px'; const canvasTransform = transformToString(this.overlayPixelTransform_);
canvas.style.height = (height / pixelRatio) + 'px'; if (canvas.style.transform !== canvasTransform) {
canvas.style.transform = canvasTransform;
}
} else { } else {
context.clearRect(0, 0, width, height); 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;
}
} }