ol
Version:
OpenLayers mapping library
435 lines (402 loc) • 12.7 kB
JavaScript
/**
* @module ol/renderer/canvas/Layer
*/
import {equals} from '../../array.js';
import {asArray} from '../../color.js';
import {createCanvasContext2D, createMockDiv, isCanvas} from '../../dom.js';
import {
getBottomLeft,
getBottomRight,
getHeight,
getTopLeft,
getTopRight,
getWidth,
} from '../../extent.js';
import {WORKER_OFFSCREEN_CANVAS} from '../../has.js';
import RenderEvent from '../../render/Event.js';
import RenderEventType from '../../render/EventType.js';
import ZIndexContext from '../../render/canvas/ZIndexContext.js';
import {
apply as applyTransform,
compose as composeTransform,
create as createTransform,
equivalent,
makeInverse,
toString as toTransformString,
} from '../../transform.js';
import LayerRenderer from '../Layer.js';
/**
* @type {Array<HTMLCanvasElement|OffscreenCanvas>}
*/
export const canvasPool = [];
/**
* @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
*/
let pixelContext = null;
function createPixelContext() {
pixelContext = createCanvasContext2D(1, 1, undefined, {
willReadFrequently: true,
});
}
/**
* @abstract
* @template {import("../../layer/Layer.js").default} LayerType
* @extends {LayerRenderer<LayerType>}
*/
class CanvasLayerRenderer extends LayerRenderer {
/**
* @param {LayerType} layer Layer.
*/
constructor(layer) {
super(layer);
/**
* HTMLElement container for the layer to be rendered in.
* @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.
* @protected
* @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();
/**
* @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
*/
this.context = null;
/**
* @private
* @type {ZIndexContext}
*/
this.deferredContext_ = null;
/**
* true if the container has been reused from the previous renderer
* @type {boolean}
*/
this.containerReused = false;
/**
* @protected
* @type {import("../../Map.js").FrameState|null}
*/
this.frameState = null;
}
/**
* @param {import('../../DataTile.js').ImageLike} image Image.
* @param {number} col The column index.
* @param {number} row The row index.
* @return {Uint8ClampedArray|null} The image data.
*/
getImageData(image, col, row) {
if (!pixelContext) {
createPixelContext();
}
pixelContext.clearRect(0, 0, 1, 1);
let data;
try {
pixelContext.drawImage(image, col, row, 1, 1, 0, 0, 1, 1);
data = pixelContext.getImageData(0, 0, 1, 1).data;
} catch {
pixelContext = null;
return null;
}
return data;
}
/**
* @param {import('../../Map.js').FrameState} frameState Frame state.
* @return {string} Background color.
*/
getBackground(frameState) {
const layer = this.getLayer();
let background = layer.getBackground();
if (typeof background === 'function') {
background = background(frameState.viewState.resolution);
}
return background || undefined;
}
/**
* Get a rendering container from an existing target, if compatible.
* @param {HTMLElement} target Potential render target.
* @param {string} transform CSS transform matrix.
* @param {string} [backgroundColor] Background color.
*/
useContainer(target, transform, backgroundColor) {
// renderer canvas to target canvas
const layerClassName = this.getLayer().getClassName();
let container, context;
if (
target &&
target.className === layerClassName &&
(!backgroundColor ||
(target &&
target.style.backgroundColor &&
equals(
asArray(target.style.backgroundColor),
asArray(backgroundColor),
)))
) {
const canvas = target.firstElementChild;
if (isCanvas(canvas)) {
context = canvas.getContext('2d');
}
}
if (context && equivalent(context.canvas.style.transform, 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;
} else if (this.container) {
this.container.style.backgroundColor = null;
}
if (!this.container) {
container = WORKER_OFFSCREEN_CANVAS
? createMockDiv()
: document.createElement('div');
container.className = layerClassName;
let style = container.style;
style.position = 'absolute';
style.width = '100%';
style.height = '100%';
context = createCanvasContext2D();
const canvas = /** @type {HTMLCanvasElement} */ (context.canvas);
container.appendChild(canvas);
style = canvas.style;
style.position = 'absolute';
style.left = '0';
style.transformOrigin = 'top left';
this.container = container;
this.context = context;
}
if (
!this.containerReused &&
backgroundColor &&
!this.container.style.backgroundColor
) {
this.container.style.backgroundColor = backgroundColor;
}
}
/**
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
* @param {import("../../Map.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("../../Map.js").FrameState} frameState Frame state.
* @param {HTMLElement} target Target that may be used to render content to.
* @protected
*/
prepareContainer(frameState, target) {
const extent = frameState.extent;
const resolution = frameState.viewState.resolution;
const rotation = frameState.viewState.rotation;
const pixelRatio = frameState.pixelRatio;
const width = Math.round((getWidth(extent) / resolution) * pixelRatio);
const height = Math.round((getHeight(extent) / resolution) * pixelRatio);
// set forward and inverse pixel transforms
composeTransform(
this.pixelTransform,
frameState.size[0] / 2,
frameState.size[1] / 2,
1 / pixelRatio,
1 / pixelRatio,
rotation,
-width / 2,
-height / 2,
);
makeInverse(this.inversePixelTransform, this.pixelTransform);
const canvasTransform = toTransformString(this.pixelTransform);
this.useContainer(target, canvasTransform, this.getBackground(frameState));
if (!this.containerReused) {
const canvas = this.context.canvas;
if (canvas.width != width || canvas.height != height) {
canvas.width = width;
canvas.height = height;
} else {
this.context.clearRect(0, 0, width, height);
}
if (
canvasTransform !==
/** @type {HTMLCanvasElement} */ (canvas).style.transform
) {
/** @type {HTMLCanvasElement} */ (canvas).style.transform =
canvasTransform;
}
}
}
/**
* @param {import("../../render/EventType.js").default} type Event type.
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
* @param {import("../../Map.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|OffscreenCanvasRenderingContext2D} context Context.
* @param {import("../../Map.js").FrameState} frameState Frame state.
* @protected
*/
preRender(context, frameState) {
this.frameState = frameState;
if (frameState.declutter) {
return;
}
this.dispatchRenderEvent_(RenderEventType.PRERENDER, context, frameState);
}
/**
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
* @param {import("../../Map.js").FrameState} frameState Frame state.
* @protected
*/
postRender(context, frameState) {
if (frameState.declutter) {
return;
}
this.dispatchRenderEvent_(RenderEventType.POSTRENDER, context, frameState);
}
/**
* @param {import("../../Map.js").FrameState} frameState Frame state.
*/
renderDeferredInternal(frameState) {}
/**
* @param {import("../../Map.js").FrameState} frameState Frame state.
* @return {import('../../render/canvas/ZIndexContext.js').ZIndexContextProxy} Context.
*/
getRenderContext(frameState) {
if (frameState.declutter && !this.deferredContext_) {
this.deferredContext_ = new ZIndexContext();
}
return frameState.declutter
? this.deferredContext_.getContext()
: this.context;
}
/**
* @param {import("../../Map.js").FrameState} frameState Frame state.
* @override
*/
renderDeferred(frameState) {
if (!frameState.declutter) {
return;
}
this.dispatchRenderEvent_(
RenderEventType.PRERENDER,
this.context,
frameState,
);
if (frameState.declutter && this.deferredContext_) {
this.deferredContext_.draw(this.context);
this.deferredContext_.clear();
}
this.renderDeferredInternal(frameState);
this.dispatchRenderEvent_(
RenderEventType.POSTRENDER,
this.context,
frameState,
);
}
/**
* Creates a transform for rendering to an element that will be rotated after rendering.
* @param {import("../../coordinate.js").Coordinate} center Center.
* @param {number} resolution Resolution.
* @param {number} rotation Rotation.
* @param {number} pixelRatio Pixel ratio.
* @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(
center,
resolution,
rotation,
pixelRatio,
width,
height,
offsetX,
) {
const dx1 = width / 2;
const dy1 = height / 2;
const sx = pixelRatio / resolution;
const sy = -sx;
const dx2 = -center[0] + offsetX;
const dy2 = -center[1];
return composeTransform(
this.tempTransform,
dx1,
dy1,
sx,
sy,
-rotation,
dx2,
dy2,
);
}
/**
* Clean up.
* @override
*/
disposeInternal() {
delete this.frameState;
super.disposeInternal();
}
}
export default CanvasLayerRenderer;