diff --git a/src/ol/render/canvas/Replay.js b/src/ol/render/canvas/Replay.js index 143997d5f2..f7938df948 100644 --- a/src/ol/render/canvas/Replay.js +++ b/src/ol/render/canvas/Replay.js @@ -1,30 +1,22 @@ /** * @module ol/render/canvas/Replay */ -import {getUid} from '../../util.js'; import {equals, reverseSubArray} from '../../array.js'; import {asColorLike} from '../../colorlike.js'; -import {buffer, clone, coordinateRelationship, createEmpty, createOrUpdate, - createOrUpdateEmpty, extend, extendCoordinate, intersects} from '../../extent.js'; +import {buffer, clone, coordinateRelationship, + createOrUpdateEmpty} from '../../extent.js'; import Relationship from '../../extent/Relationship.js'; import GeometryType from '../../geom/GeometryType.js'; import {inflateCoordinates, inflateCoordinatesArray, inflateMultiCoordinatesArray} from '../../geom/flat/inflate.js'; -import {lineStringLength} from '../../geom/flat/length.js'; -import {drawTextOnPath} from '../../geom/flat/textpath.js'; -import {transform2D} from '../../geom/flat/transform.js'; import {CANVAS_LINE_DASH} from '../../has.js'; -import {isEmpty} from '../../obj.js'; import VectorContext from '../VectorContext.js'; -import {drawImage, resetTransform, defaultPadding, defaultFillStyle, defaultStrokeStyle, +import {drawImage, resetTransform, defaultFillStyle, defaultStrokeStyle, defaultMiterLimit, defaultLineWidth, defaultLineJoin, defaultLineDashOffset, defaultLineDash, defaultLineCap} from '../canvas.js'; import CanvasInstruction from './Instruction.js'; -import {TEXT_ALIGN} from '../replay.js'; import { create as createTransform, - compose as composeTransform, - apply as applyTransform, - setFromArray as transformSetFromArray + apply as applyTransform } from '../../transform.js'; @@ -38,17 +30,6 @@ import { * @property {!Object} strokeStates The stroke states (decluttering). */ -/** - * @type {import("../../extent.js").Extent} - */ -const tmpExtent = createEmpty(); - - -/** - * @type {!import("../../transform.js").Transform} - */ -const tmpTransform = createTransform(); - class CanvasReplay extends VectorContext { /** @@ -179,147 +160,6 @@ class CanvasReplay extends VectorContext { } - /** - * @param {CanvasRenderingContext2D} context Context. - * @param {import("../../coordinate.js").Coordinate} p1 1st point of the background box. - * @param {import("../../coordinate.js").Coordinate} p2 2nd point of the background box. - * @param {import("../../coordinate.js").Coordinate} p3 3rd point of the background box. - * @param {import("../../coordinate.js").Coordinate} p4 4th point of the background box. - * @param {Array<*>} fillInstruction Fill instruction. - * @param {Array<*>} strokeInstruction Stroke instruction. - */ - replayTextBackground_(context, p1, p2, p3, p4, fillInstruction, strokeInstruction) { - context.beginPath(); - context.moveTo.apply(context, p1); - context.lineTo.apply(context, p2); - context.lineTo.apply(context, p3); - context.lineTo.apply(context, p4); - context.lineTo.apply(context, p1); - if (fillInstruction) { - this.alignFill_ = /** @type {boolean} */ (fillInstruction[2]); - this.fill_(context); - } - if (strokeInstruction) { - this.setStrokeStyle_(context, /** @type {Array<*>} */ (strokeInstruction)); - context.stroke(); - } - } - - /** - * @param {CanvasRenderingContext2D} context Context. - * @param {number} x X. - * @param {number} y Y. - * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image. - * @param {number} anchorX Anchor X. - * @param {number} anchorY Anchor Y. - * @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group. - * @param {number} height Height. - * @param {number} opacity Opacity. - * @param {number} originX Origin X. - * @param {number} originY Origin Y. - * @param {number} rotation Rotation. - * @param {number} scale Scale. - * @param {boolean} snapToPixel Snap to pixel. - * @param {number} width Width. - * @param {Array} padding Padding. - * @param {Array<*>} fillInstruction Fill instruction. - * @param {Array<*>} strokeInstruction Stroke instruction. - */ - replayImage_( - context, - x, - y, - image, - anchorX, - anchorY, - declutterGroup, - height, - opacity, - originX, - originY, - rotation, - scale, - snapToPixel, - width, - padding, - fillInstruction, - strokeInstruction - ) { - const fillStroke = fillInstruction || strokeInstruction; - anchorX *= scale; - anchorY *= scale; - x -= anchorX; - y -= anchorY; - - const w = (width + originX > image.width) ? image.width - originX : width; - const h = (height + originY > image.height) ? image.height - originY : height; - const boxW = padding[3] + w * scale + padding[1]; - const boxH = padding[0] + h * scale + padding[2]; - const boxX = x - padding[3]; - const boxY = y - padding[0]; - - /** @type {import("../../coordinate.js").Coordinate} */ - let p1; - /** @type {import("../../coordinate.js").Coordinate} */ - let p2; - /** @type {import("../../coordinate.js").Coordinate} */ - let p3; - /** @type {import("../../coordinate.js").Coordinate} */ - let p4; - if (fillStroke || rotation !== 0) { - p1 = [boxX, boxY]; - p2 = [boxX + boxW, boxY]; - p3 = [boxX + boxW, boxY + boxH]; - p4 = [boxX, boxY + boxH]; - } - - let transform = null; - if (rotation !== 0) { - const centerX = x + anchorX; - const centerY = y + anchorY; - transform = composeTransform(tmpTransform, centerX, centerY, 1, 1, rotation, -centerX, -centerY); - - createOrUpdateEmpty(tmpExtent); - extendCoordinate(tmpExtent, applyTransform(tmpTransform, p1)); - extendCoordinate(tmpExtent, applyTransform(tmpTransform, p2)); - extendCoordinate(tmpExtent, applyTransform(tmpTransform, p3)); - extendCoordinate(tmpExtent, applyTransform(tmpTransform, p4)); - } else { - createOrUpdate(boxX, boxY, boxX + boxW, boxY + boxH, tmpExtent); - } - const canvas = context.canvas; - const strokePadding = strokeInstruction ? (strokeInstruction[2] * scale / 2) : 0; - const intersects = - tmpExtent[0] - strokePadding <= canvas.width && tmpExtent[2] + strokePadding >= 0 && - tmpExtent[1] - strokePadding <= canvas.height && tmpExtent[3] + strokePadding >= 0; - - if (snapToPixel) { - x = Math.round(x); - y = Math.round(y); - } - - if (declutterGroup) { - if (!intersects && declutterGroup[4] == 1) { - return; - } - extend(declutterGroup, tmpExtent); - const declutterArgs = intersects ? - [context, transform ? transform.slice(0) : null, opacity, image, originX, originY, w, h, x, y, scale] : - null; - if (declutterArgs && fillStroke) { - declutterArgs.push(fillInstruction, strokeInstruction, p1, p2, p3, p4); - } - declutterGroup.push(declutterArgs); - } else if (intersects) { - if (fillStroke) { - this.replayTextBackground_(context, p1, p2, p3, p4, - /** @type {Array<*>} */ (fillInstruction), - /** @type {Array<*>} */ (strokeInstruction)); - } - drawImage(context, transform, opacity, image, originX, originY, w, h, x, y, scale); - } - } - /** * @protected * @param {Array} dashArray Dash array. @@ -546,368 +386,6 @@ class CanvasReplay extends VectorContext { } } - /** - * @private - * @param {CanvasRenderingContext2D} context Context. - * @param {import("../../transform.js").Transform} transform Transform. - * @param {Object} skippedFeaturesHash Ids of features - * to skip. - * @param {Array<*>} instructions Instructions array. - * @param {boolean} snapToPixel Snap point symbols and text to integer pixels. - * @param {function((import("../../Feature.js").default|import("../Feature.js").default)): T|undefined} featureCallback Feature callback. - * @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this - * extent. - * @return {T|undefined} Callback result. - * @template T - */ - replay_( - context, - transform, - skippedFeaturesHash, - instructions, - snapToPixel, - featureCallback, - opt_hitExtent - ) { - /** @type {Array} */ - let pixelCoordinates; - if (this.pixelCoordinates_ && equals(transform, this.renderedTransform_)) { - pixelCoordinates = this.pixelCoordinates_; - } else { - if (!this.pixelCoordinates_) { - this.pixelCoordinates_ = []; - } - pixelCoordinates = transform2D( - this.coordinates, 0, this.coordinates.length, 2, - transform, this.pixelCoordinates_); - transformSetFromArray(this.renderedTransform_, transform); - } - const skipFeatures = !isEmpty(skippedFeaturesHash); - let i = 0; // instruction index - const ii = instructions.length; // end of instructions - let d = 0; // data index - let dd; // end of per-instruction data - let anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup, image; - let pendingFill = 0; - let pendingStroke = 0; - let lastFillInstruction = null; - let lastStrokeInstruction = null; - const coordinateCache = this.coordinateCache_; - const viewRotation = this.viewRotation_; - - const state = /** @type {import("../../render.js").State} */ ({ - context: context, - pixelRatio: this.pixelRatio, - resolution: this.resolution, - rotation: viewRotation - }); - - // When the batch size gets too big, performance decreases. 200 is a good - // balance between batch size and number of fill/stroke instructions. - const batchSize = this.instructions != instructions || this.overlaps ? 0 : 200; - let /** @type {import("../../Feature.js").default|import("../Feature.js").default} */ feature; - let x, y; - while (i < ii) { - const instruction = instructions[i]; - const type = /** @type {CanvasInstruction} */ (instruction[0]); - switch (type) { - case CanvasInstruction.BEGIN_GEOMETRY: - feature = /** @type {import("../../Feature.js").default|import("../Feature.js").default} */ (instruction[1]); - if ((skipFeatures && skippedFeaturesHash[getUid(feature)]) || !feature.getGeometry()) { - i = /** @type {number} */ (instruction[2]); - } else if (opt_hitExtent !== undefined && !intersects( - opt_hitExtent, feature.getGeometry().getExtent())) { - i = /** @type {number} */ (instruction[2]) + 1; - } else { - ++i; - } - break; - case CanvasInstruction.BEGIN_PATH: - if (pendingFill > batchSize) { - this.fill_(context); - pendingFill = 0; - } - if (pendingStroke > batchSize) { - context.stroke(); - pendingStroke = 0; - } - if (!pendingFill && !pendingStroke) { - context.beginPath(); - prevX = prevY = NaN; - } - ++i; - break; - case CanvasInstruction.CIRCLE: - d = /** @type {number} */ (instruction[1]); - const x1 = pixelCoordinates[d]; - const y1 = pixelCoordinates[d + 1]; - const x2 = pixelCoordinates[d + 2]; - const y2 = pixelCoordinates[d + 3]; - const dx = x2 - x1; - const dy = y2 - y1; - const r = Math.sqrt(dx * dx + dy * dy); - context.moveTo(x1 + r, y1); - context.arc(x1, y1, r, 0, 2 * Math.PI, true); - ++i; - break; - case CanvasInstruction.CLOSE_PATH: - context.closePath(); - ++i; - break; - case CanvasInstruction.CUSTOM: - d = /** @type {number} */ (instruction[1]); - dd = instruction[2]; - const geometry = /** @type {import("../../geom/SimpleGeometry.js").default} */ (instruction[3]); - const renderer = instruction[4]; - const fn = instruction.length == 6 ? instruction[5] : undefined; - state.geometry = geometry; - state.feature = feature; - if (!(i in coordinateCache)) { - coordinateCache[i] = []; - } - const coords = coordinateCache[i]; - if (fn) { - fn(pixelCoordinates, d, dd, 2, coords); - } else { - coords[0] = pixelCoordinates[d]; - coords[1] = pixelCoordinates[d + 1]; - coords.length = 2; - } - renderer(coords, state); - ++i; - break; - case CanvasInstruction.DRAW_IMAGE: - d = /** @type {number} */ (instruction[1]); - dd = /** @type {number} */ (instruction[2]); - image = /** @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} */ - (instruction[3]); - // Remaining arguments in DRAW_IMAGE are in alphabetical order - anchorX = /** @type {number} */ (instruction[4]); - anchorY = /** @type {number} */ (instruction[5]); - declutterGroup = featureCallback ? null : /** @type {import("../canvas.js").DeclutterGroup} */ (instruction[6]); - const height = /** @type {number} */ (instruction[7]); - const opacity = /** @type {number} */ (instruction[8]); - const originX = /** @type {number} */ (instruction[9]); - const originY = /** @type {number} */ (instruction[10]); - const rotateWithView = /** @type {boolean} */ (instruction[11]); - let rotation = /** @type {number} */ (instruction[12]); - const scale = /** @type {number} */ (instruction[13]); - const width = /** @type {number} */ (instruction[14]); - - let padding, backgroundFill, backgroundStroke; - if (instruction.length > 16) { - padding = /** @type {Array} */ (instruction[15]); - backgroundFill = /** @type {boolean} */ (instruction[16]); - backgroundStroke = /** @type {boolean} */ (instruction[17]); - } else { - padding = defaultPadding; - backgroundFill = backgroundStroke = false; - } - - if (rotateWithView) { - rotation += viewRotation; - } - for (; d < dd; d += 2) { - this.replayImage_(context, - pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY, - declutterGroup, height, opacity, originX, originY, rotation, scale, - snapToPixel, width, padding, - backgroundFill ? /** @type {Array<*>} */ (lastFillInstruction) : null, - backgroundStroke ? /** @type {Array<*>} */ (lastStrokeInstruction) : null); - } - this.renderDeclutter_(declutterGroup, feature); - ++i; - break; - case CanvasInstruction.DRAW_CHARS: - const begin = /** @type {number} */ (instruction[1]); - const end = /** @type {number} */ (instruction[2]); - const baseline = /** @type {number} */ (instruction[3]); - declutterGroup = featureCallback ? null : /** @type {import("../canvas.js").DeclutterGroup} */ (instruction[4]); - const overflow = /** @type {number} */ (instruction[5]); - const fillKey = /** @type {string} */ (instruction[6]); - const maxAngle = /** @type {number} */ (instruction[7]); - const measure = /** @type {function(string):number} */ (instruction[8]); - const offsetY = /** @type {number} */ (instruction[9]); - const strokeKey = /** @type {string} */ (instruction[10]); - const strokeWidth = /** @type {number} */ (instruction[11]); - const text = /** @type {string} */ (instruction[12]); - const textKey = /** @type {string} */ (instruction[13]); - const textScale = /** @type {number} */ (instruction[14]); - - const pathLength = lineStringLength(pixelCoordinates, begin, end, 2); - const textLength = measure(text); - if (overflow || textLength <= pathLength) { - /** @type {import("./TextReplay.js").default} */ - const textReplay = /** @type {?} */ (this); - const textAlign = textReplay.textStates[textKey].textAlign; - const startM = (pathLength - textLength) * TEXT_ALIGN[textAlign]; - const parts = drawTextOnPath( - pixelCoordinates, begin, end, 2, text, measure, startM, maxAngle); - if (parts) { - let c, cc, chars, label, part; - if (strokeKey) { - for (c = 0, cc = parts.length; c < cc; ++c) { - part = parts[c]; // x, y, anchorX, rotation, chunk - chars = /** @type {string} */ (part[4]); - label = textReplay.getImage(chars, textKey, '', strokeKey); - anchorX = /** @type {number} */ (part[2]) + strokeWidth; - anchorY = baseline * label.height + (0.5 - baseline) * 2 * strokeWidth - offsetY; - this.replayImage_(context, - /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label, - anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, - /** @type {number} */ (part[3]), textScale, false, label.width, - defaultPadding, null, null); - } - } - if (fillKey) { - for (c = 0, cc = parts.length; c < cc; ++c) { - part = parts[c]; // x, y, anchorX, rotation, chunk - chars = /** @type {string} */ (part[4]); - label = textReplay.getImage(chars, textKey, fillKey, ''); - anchorX = /** @type {number} */ (part[2]); - anchorY = baseline * label.height - offsetY; - this.replayImage_(context, - /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label, - anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, - /** @type {number} */ (part[3]), textScale, false, label.width, - defaultPadding, null, null); - } - } - } - } - this.renderDeclutter_(declutterGroup, feature); - ++i; - break; - case CanvasInstruction.END_GEOMETRY: - if (featureCallback !== undefined) { - feature = /** @type {import("../../Feature.js").default|import("../Feature.js").default} */ (instruction[1]); - const result = featureCallback(feature); - if (result) { - return result; - } - } - ++i; - break; - case CanvasInstruction.FILL: - if (batchSize) { - pendingFill++; - } else { - this.fill_(context); - } - ++i; - break; - case CanvasInstruction.MOVE_TO_LINE_TO: - d = /** @type {number} */ (instruction[1]); - dd = /** @type {number} */ (instruction[2]); - x = pixelCoordinates[d]; - y = pixelCoordinates[d + 1]; - roundX = (x + 0.5) | 0; - roundY = (y + 0.5) | 0; - if (roundX !== prevX || roundY !== prevY) { - context.moveTo(x, y); - prevX = roundX; - prevY = roundY; - } - for (d += 2; d < dd; d += 2) { - x = pixelCoordinates[d]; - y = pixelCoordinates[d + 1]; - roundX = (x + 0.5) | 0; - roundY = (y + 0.5) | 0; - if (d == dd - 2 || roundX !== prevX || roundY !== prevY) { - context.lineTo(x, y); - prevX = roundX; - prevY = roundY; - } - } - ++i; - break; - case CanvasInstruction.SET_FILL_STYLE: - lastFillInstruction = instruction; - this.alignFill_ = instruction[2]; - - if (pendingFill) { - this.fill_(context); - pendingFill = 0; - if (pendingStroke) { - context.stroke(); - pendingStroke = 0; - } - } - - context.fillStyle = /** @type {import("../../colorlike.js").ColorLike} */ (instruction[1]); - ++i; - break; - case CanvasInstruction.SET_STROKE_STYLE: - lastStrokeInstruction = instruction; - if (pendingStroke) { - context.stroke(); - pendingStroke = 0; - } - this.setStrokeStyle_(context, /** @type {Array<*>} */ (instruction)); - ++i; - break; - case CanvasInstruction.STROKE: - if (batchSize) { - pendingStroke++; - } else { - context.stroke(); - } - ++i; - break; - default: - ++i; // consume the instruction anyway, to avoid an infinite loop - break; - } - } - if (pendingFill) { - this.fill_(context); - } - if (pendingStroke) { - context.stroke(); - } - return undefined; - } - - /** - * @param {CanvasRenderingContext2D} context Context. - * @param {import("../../transform.js").Transform} transform Transform. - * @param {number} viewRotation View rotation. - * @param {Object} skippedFeaturesHash Ids of features - * to skip. - * @param {boolean} snapToPixel Snap point symbols and text to integer pixels. - */ - replay(context, transform, viewRotation, skippedFeaturesHash, snapToPixel) { - this.viewRotation_ = viewRotation; - this.replay_(context, transform, - skippedFeaturesHash, this.instructions, snapToPixel, undefined, undefined); - } - - /** - * @param {CanvasRenderingContext2D} context Context. - * @param {import("../../transform.js").Transform} transform Transform. - * @param {number} viewRotation View rotation. - * @param {Object} skippedFeaturesHash Ids of features - * to skip. - * @param {function((import("../../Feature.js").default|import("../Feature.js").default)): T=} opt_featureCallback - * Feature callback. - * @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this - * extent. - * @return {T|undefined} Callback result. - * @template T - */ - replayHitDetection( - context, - transform, - viewRotation, - skippedFeaturesHash, - opt_featureCallback, - opt_hitExtent - ) { - this.viewRotation_ = viewRotation; - return this.replay_(context, transform, skippedFeaturesHash, - this.hitDetectionInstructions, true, opt_featureCallback, opt_hitExtent); - } - /** * Reverse the hit detection instructions. */