267 lines
9.2 KiB
JavaScript
267 lines
9.2 KiB
JavaScript
/**
|
|
* @module ol/renderer/canvas/Layer
|
|
*/
|
|
import {getBottomLeft, getBottomRight, getTopLeft, getTopRight} from '../../extent.js';
|
|
import {createCanvasContext2D} from '../../dom.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, toString as transformToString} from '../../transform.js';
|
|
|
|
/**
|
|
* @abstract
|
|
* @template {import("../../layer/Layer.js").default} LayerType
|
|
*/
|
|
class CanvasLayerRenderer extends LayerRenderer {
|
|
|
|
/**
|
|
* @param {LayerType} layer Layer.
|
|
*/
|
|
constructor(layer) {
|
|
|
|
super(layer);
|
|
|
|
/**
|
|
* @protected
|
|
* @type {HTMLElement}
|
|
*/
|
|
this.container = null;
|
|
|
|
/**
|
|
* @protected
|
|
* @type {number}
|
|
*/
|
|
this.renderedResolution;
|
|
|
|
/**
|
|
* A temporary transform. The values in this transform should only be used in a
|
|
* function that sets the values.
|
|
* @private
|
|
* @type {import("../../transform.js").Transform}
|
|
*/
|
|
this.tempTransform_ = createTransform();
|
|
|
|
/**
|
|
* The transform for rendered pixels to viewport CSS pixels. This transform must
|
|
* be set when rendering a frame and may be used by other functions after rendering.
|
|
* @protected
|
|
* @type {import("../../transform.js").Transform}
|
|
*/
|
|
this.pixelTransform = createTransform();
|
|
|
|
/**
|
|
* The transform for viewport CSS pixels to rendered pixels. This transform must
|
|
* be set when rendering a frame and may be used by other functions after rendering.
|
|
* @protected
|
|
* @type {import("../../transform.js").Transform}
|
|
*/
|
|
this.inversePixelTransform = createTransform();
|
|
|
|
/**
|
|
* @protected
|
|
* @type {CanvasRenderingContext2D}
|
|
*/
|
|
this.context = null;
|
|
|
|
/**
|
|
* @type {boolean}
|
|
*/
|
|
this.containerReused = false;
|
|
|
|
}
|
|
|
|
/**
|
|
* Get a rendering container from an existing target, if compatible.
|
|
* @param {HTMLElement} target Potential render target.
|
|
* @param {import("../../transform").Transform} transform Transform.
|
|
* @param {number} opacity Opacity.
|
|
*/
|
|
useContainer(target, transform, opacity) {
|
|
const layerClassName = this.getLayer().getClassName();
|
|
let container, context;
|
|
if (target && target.style.opacity === '' && target.className === layerClassName) {
|
|
const canvas = target.firstElementChild;
|
|
if (canvas instanceof HTMLCanvasElement) {
|
|
context = canvas.getContext('2d');
|
|
}
|
|
}
|
|
if (context && context.canvas.style.transform === transformToString(transform)) {
|
|
// Container of the previous layer renderer can be used.
|
|
this.container = target;
|
|
this.context = context;
|
|
this.containerReused = true;
|
|
} else if (this.containerReused) {
|
|
// Previously reused container cannot be used any more.
|
|
this.container = null;
|
|
this.context = null;
|
|
this.containerReused = false;
|
|
}
|
|
if (!this.container) {
|
|
container = document.createElement('div');
|
|
container.className = layerClassName;
|
|
let style = container.style;
|
|
style.position = 'absolute';
|
|
style.width = '100%';
|
|
style.height = '100%';
|
|
context = createCanvasContext2D();
|
|
const canvas = context.canvas;
|
|
container.appendChild(canvas);
|
|
style = canvas.style;
|
|
style.position = 'absolute';
|
|
style.transformOrigin = 'top left';
|
|
this.container = container;
|
|
this.context = context;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {CanvasRenderingContext2D} context Context.
|
|
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
|
* @param {import("../../extent.js").Extent} extent Clip extent.
|
|
* @protected
|
|
*/
|
|
clip(context, frameState, extent) {
|
|
const pixelRatio = frameState.pixelRatio;
|
|
const halfWidth = (frameState.size[0] * pixelRatio) / 2;
|
|
const halfHeight = (frameState.size[1] * pixelRatio) / 2;
|
|
const rotation = frameState.viewState.rotation;
|
|
const topLeft = getTopLeft(extent);
|
|
const topRight = getTopRight(extent);
|
|
const bottomRight = getBottomRight(extent);
|
|
const bottomLeft = getBottomLeft(extent);
|
|
|
|
applyTransform(frameState.coordinateToPixelTransform, topLeft);
|
|
applyTransform(frameState.coordinateToPixelTransform, topRight);
|
|
applyTransform(frameState.coordinateToPixelTransform, bottomRight);
|
|
applyTransform(frameState.coordinateToPixelTransform, bottomLeft);
|
|
|
|
context.save();
|
|
rotateAtOffset(context, -rotation, halfWidth, halfHeight);
|
|
context.beginPath();
|
|
context.moveTo(topLeft[0] * pixelRatio, topLeft[1] * pixelRatio);
|
|
context.lineTo(topRight[0] * pixelRatio, topRight[1] * pixelRatio);
|
|
context.lineTo(bottomRight[0] * pixelRatio, bottomRight[1] * pixelRatio);
|
|
context.lineTo(bottomLeft[0] * pixelRatio, bottomLeft[1] * pixelRatio);
|
|
context.clip();
|
|
rotateAtOffset(context, rotation, halfWidth, halfHeight);
|
|
}
|
|
|
|
/**
|
|
* @param {CanvasRenderingContext2D} context Context.
|
|
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
|
* @param {import("../../extent.js").Extent} extent Clip extent.
|
|
* @protected
|
|
*/
|
|
clipUnrotated(context, frameState, extent) {
|
|
const topLeft = getTopLeft(extent);
|
|
const topRight = getTopRight(extent);
|
|
const bottomRight = getBottomRight(extent);
|
|
const bottomLeft = getBottomLeft(extent);
|
|
|
|
applyTransform(frameState.coordinateToPixelTransform, topLeft);
|
|
applyTransform(frameState.coordinateToPixelTransform, topRight);
|
|
applyTransform(frameState.coordinateToPixelTransform, bottomRight);
|
|
applyTransform(frameState.coordinateToPixelTransform, bottomLeft);
|
|
|
|
const inverted = this.inversePixelTransform;
|
|
applyTransform(inverted, topLeft);
|
|
applyTransform(inverted, topRight);
|
|
applyTransform(inverted, bottomRight);
|
|
applyTransform(inverted, bottomLeft);
|
|
|
|
context.save();
|
|
context.beginPath();
|
|
context.moveTo(Math.round(topLeft[0]), Math.round(topLeft[1]));
|
|
context.lineTo(Math.round(topRight[0]), Math.round(topRight[1]));
|
|
context.lineTo(Math.round(bottomRight[0]), Math.round(bottomRight[1]));
|
|
context.lineTo(Math.round(bottomLeft[0]), Math.round(bottomLeft[1]));
|
|
context.clip();
|
|
}
|
|
|
|
/**
|
|
* @param {import("../../render/EventType.js").default} type Event type.
|
|
* @param {CanvasRenderingContext2D} context Context.
|
|
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
|
* @private
|
|
*/
|
|
dispatchRenderEvent_(type, context, frameState) {
|
|
const layer = this.getLayer();
|
|
if (layer.hasListener(type)) {
|
|
const event = new RenderEvent(type, this.inversePixelTransform, frameState, context);
|
|
layer.dispatchEvent(event);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {CanvasRenderingContext2D} context Context.
|
|
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
|
* @protected
|
|
*/
|
|
preRender(context, frameState) {
|
|
this.dispatchRenderEvent_(RenderEventType.PRERENDER, context, frameState);
|
|
}
|
|
|
|
/**
|
|
* @param {CanvasRenderingContext2D} context Context.
|
|
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
|
* @protected
|
|
*/
|
|
postRender(context, frameState) {
|
|
this.dispatchRenderEvent_(RenderEventType.POSTRENDER, context, frameState);
|
|
}
|
|
|
|
/**
|
|
* Creates a transform for rendering to an element that will be rotated after rendering.
|
|
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
|
* @param {number} width Width of the rendered element (in pixels).
|
|
* @param {number} height Height of the rendered element (in pixels).
|
|
* @param {number} offsetX Offset on the x-axis in view coordinates.
|
|
* @protected
|
|
* @return {!import("../../transform.js").Transform} Transform.
|
|
*/
|
|
getRenderTransform(frameState, width, height, offsetX) {
|
|
const viewState = frameState.viewState;
|
|
const pixelRatio = frameState.pixelRatio;
|
|
const dx1 = width / 2;
|
|
const dy1 = height / 2;
|
|
const sx = pixelRatio / viewState.resolution;
|
|
const sy = -sx;
|
|
const dx2 = -viewState.center[0] + offsetX;
|
|
const dy2 = -viewState.center[1];
|
|
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(this.inversePixelTransform, 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;
|