package.renderer.canvas.Layer.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ol Show documentation
Show all versions of ol Show documentation
OpenLayers mapping library
The newest version!
/**
* @module ol/renderer/canvas/Layer
*/
import LayerRenderer from '../Layer.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,
makeInverse,
toString as toTransformString,
} from '../../transform.js';
import {asArray} from '../../color.js';
import {createCanvasContext2D} from '../../dom.js';
import {equals} from '../../array.js';
import {
getBottomLeft,
getBottomRight,
getHeight,
getTopLeft,
getTopRight,
getWidth,
} from '../../extent.js';
/**
* @type {Array}
*/
export const canvasPool = [];
/**
* @type {CanvasRenderingContext2D}
*/
let pixelContext = null;
function createPixelContext() {
pixelContext = createCanvasContext2D(1, 1, undefined, {
willReadFrequently: true,
});
}
/**
* @abstract
* @template {import("../../layer/Layer.js").default} LayerType
* @extends {LayerRenderer}
*/
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.
* @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}
*/
this.context = null;
/**
* @private
* @type {ZIndexContext}
*/
this.deferredContext_ = null;
/**
* @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 (err) {
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.
* @param {string} [backgroundColor] Background color.
*/
useContainer(target, transform, backgroundColor) {
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 (canvas instanceof HTMLCanvasElement) {
context = canvas.getContext('2d');
}
}
if (context && 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 = 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.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} 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 !== canvas.style.transform) {
canvas.style.transform = canvasTransform;
}
}
}
/**
* @param {import("../../render/EventType.js").default} type Event type.
* @param {CanvasRenderingContext2D} 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} 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} 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;