diff --git a/src/ol/render/canvas/InstructionsExecutor.js b/src/ol/render/canvas/InstructionsExecutor.js index 2ce3008acc..c2685de71a 100644 --- a/src/ol/render/canvas/InstructionsExecutor.js +++ b/src/ol/render/canvas/InstructionsExecutor.js @@ -10,7 +10,7 @@ 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 {drawImage, resetTransform, defaultPadding} from '../canvas.js'; +import {drawImage, resetTransform, defaultPadding, defaultTextBaseline} from '../canvas.js'; import CanvasInstruction from './Instruction.js'; import {TEXT_ALIGN} from '../replay.js'; import { @@ -525,6 +525,35 @@ class CanvasInstructionsExecutor { } } + /** + * @private + * @param {string} text The text to draw. + * @param {string} textKey The key of the text state. + * @param {string} strokeKey The key for the stroke state. + * @param {string} fillKey The key for the fill state. + * @return {{label: HTMLCanvasElement, anchorX: number, anchorY: number}} The text image and its anchor. + */ + drawTextImageWithPointPlacement_(text, textKey, strokeKey, fillKey) { + const textState = this.textStates[textKey]; + + const label = this.getImage(text, textKey, fillKey, strokeKey); + + const strokeState = this.strokeStates[strokeKey]; // FIXME: check if it is correct, was this.textStrokeState_; + const pixelRatio = this.pixelRatio; + const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign]; + const baseline = TEXT_ALIGN[textState.textBaseline || defaultTextBaseline]; // FIXME: why I need a default now? + const strokeWidth = strokeState && strokeState.lineWidth ? strokeState.lineWidth : 0; + + const anchorX = align * label.width / pixelRatio + 2 * (0.5 - align) * strokeWidth; + const anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth; + + return { + label: label, + anchorX: anchorX, + anchorY: anchorY + }; + } + /** * @private * @param {CanvasRenderingContext2D} context Context. @@ -566,7 +595,8 @@ class CanvasInstructionsExecutor { 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 anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup, image, text, textKey; + let strokeKey, fillKey; let pendingFill = 0; let pendingStroke = 0; let lastFillInstruction = null; @@ -658,20 +688,44 @@ class CanvasInstructionsExecutor { case CanvasInstruction.DRAW_IMAGE: d = /** @type {number} */ (instruction[1]); dd = /** @type {number} */ (instruction[2]); - image = /** @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} */ - (instruction[3]); + 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]); + let 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 width = /** @type {number} */ (instruction[14]); + + + if (!image) { + if (instruction.length < 19 || !instruction[18]) { + continue; + } + text = /** @type {string} */ (instruction[18]); + textKey = /** @type {string} */ (instruction[19]); + strokeKey = /** @type {string} */ (instruction[20]); + fillKey = /** @type {string} */ (instruction[21]); + const labelWithAnchor = this.drawTextImageWithPointPlacement_(text, textKey, strokeKey, fillKey); + const textOffsetX = /** @type {number} */ (instruction[22]); + const textOffsetY = /** @type {number} */ (instruction[23]); + image = instruction[3] = labelWithAnchor.label; + anchorX = instruction[4] = (labelWithAnchor.anchorX - textOffsetX) * this.pixelRatio; + anchorY = instruction[5] = (labelWithAnchor.anchorY - textOffsetY) * this.pixelRatio; + height = instruction[8] = image.height; + width = instruction[14] = image.width; + } + + let geometryWidths; + if (instruction.length > 24) { + geometryWidths = /** @type {number} */ (instruction[24]); + } let padding, backgroundFill, backgroundStroke; if (instruction.length > 16) { @@ -686,7 +740,13 @@ class CanvasInstructionsExecutor { if (rotateWithView) { rotation += viewRotation; } + let widthIndex = 0; for (; d < dd; d += 2) { + if (geometryWidths) { + if (geometryWidths[widthIndex++] < width) { + continue; + } + } this.replayImage_(context, pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY, declutterGroup, height, opacity, originX, originY, rotation, scale, @@ -703,14 +763,14 @@ class CanvasInstructionsExecutor { 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]); + fillKey = /** @type {string} */ (instruction[6]); const maxAngle = /** @type {number} */ (instruction[7]); const measurePixelRatio = /** @type {number} */ (instruction[8]); const offsetY = /** @type {number} */ (instruction[9]); - const strokeKey = /** @type {string} */ (instruction[10]); + strokeKey = /** @type {string} */ (instruction[10]); const strokeWidth = /** @type {number} */ (instruction[11]); - const text = /** @type {string} */ (instruction[12]); - const textKey = /** @type {string} */ (instruction[13]); + text = /** @type {string} */ (instruction[12]); + textKey = /** @type {string} */ (instruction[13]); const pixelRatioScale = /** @type {number} */ (instruction[14]); const textState = this.textStates[textKey]; diff --git a/src/ol/render/canvas/TextBuilder.js b/src/ol/render/canvas/TextBuilder.js index c516a99a86..68db8f9f95 100644 --- a/src/ol/render/canvas/TextBuilder.js +++ b/src/ol/render/canvas/TextBuilder.js @@ -3,12 +3,10 @@ */ import {getUid} from '../../util.js'; import {asColorLike} from '../../colorlike.js'; -import {createCanvasContext2D} from '../../dom.js'; import {intersects} from '../../extent.js'; import {matchingChunk} from '../../geom/flat/straightchunk.js'; import GeometryType from '../../geom/GeometryType.js'; -import {CANVAS_LINE_DASH} from '../../has.js'; -import {labelCache, measureTextWidths, defaultTextAlign, measureTextHeight, defaultPadding, defaultLineCap, defaultLineDashOffset, defaultLineDash, defaultLineJoin, defaultFillStyle, checkFont, defaultFont, defaultLineWidth, defaultMiterLimit, defaultStrokeStyle, defaultTextBaseline} from '../canvas.js'; +import {labelCache, defaultTextAlign, defaultPadding, defaultLineCap, defaultLineDashOffset, defaultLineDash, defaultLineJoin, defaultFillStyle, checkFont, defaultFont, defaultLineWidth, defaultMiterLimit, defaultStrokeStyle, defaultTextBaseline} from '../canvas.js'; import CanvasInstruction from './Instruction.js'; import CanvasInstructionsBuilder from './InstructionsBuilder.js'; import {TEXT_ALIGN} from '../replay.js'; @@ -195,8 +193,8 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder { this.endGeometry(geometry, feature); } else { - const label = this.getImage(this.text_, this.textKey_, this.fillKey_, this.strokeKey_); - const width = label.width / this.pixelRatio; + + const geometryWidths = []; switch (geometryType) { case GeometryType.POINT: case GeometryType.MULTI_POINT: @@ -215,8 +213,8 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder { break; case GeometryType.POLYGON: flatCoordinates = /** @type {import("../../geom/Polygon.js").default} */ (geometry).getFlatInteriorPoint(); - if (!textState.overflow && flatCoordinates[2] / this.resolution < width) { - return; + if (!textState.overflow) { + geometryWidths.push(flatCoordinates[2] / this.resolution); } stride = 3; break; @@ -224,9 +222,10 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder { const interiorPoints = /** @type {import("../../geom/MultiPolygon.js").default} */ (geometry).getFlatInteriorPoints(); flatCoordinates = []; for (i = 0, ii = interiorPoints.length; i < ii; i += 3) { - if (textState.overflow || interiorPoints[i + 2] / this.resolution >= width) { - flatCoordinates.push(interiorPoints[i], interiorPoints[i + 1]); + if (!textState.overflow) { + geometryWidths.push(interiorPoints[i + 2] / this.resolution); } + flatCoordinates.push(interiorPoints[i], interiorPoints[i + 1]); } end = flatCoordinates.length; if (end == 0) { @@ -236,6 +235,9 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder { default: } end = this.appendFlatCoordinates(flatCoordinates, 0, end, stride, false, false); + + this.saveTextStates_(); + if (textState.backgroundFill || textState.backgroundStroke) { this.setFillStrokeStyle(textState.backgroundFill, textState.backgroundStroke); if (textState.backgroundFill) { @@ -247,122 +249,42 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder { this.hitDetectionInstructions.push(this.createStroke(this.state)); } } + this.beginGeometry(geometry, feature); - this.drawTextImage_(label, begin, end); + + // The image is unknown at this stage so we pass null; it will be computed at render time. + // For clarity, we pass Infinity for numerical values that will be computed at render time. + const pixelRatio = this.pixelRatio; + this.instructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, + null, Infinity, Infinity, this.declutterGroup_, Infinity, 1, 0, 0, + this.textRotateWithView_, this.textRotation_, 1, Infinity, + textState.padding == defaultPadding ? + defaultPadding : textState.padding.map(function(p) { + return p * pixelRatio; + }), + !!textState.backgroundFill, !!textState.backgroundStroke, + this.text_, this.textKey_, this.strokeKey_, this.fillKey_, + this.textOffsetX_, this.textOffsetY_, + geometryWidths.length > 0 ? geometryWidths : null + ]); + this.hitDetectionInstructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, + null, Infinity, Infinity, this.declutterGroup_, Infinity, 1, 0, 0, + this.textRotateWithView_, this.textRotation_, 1 / this.pixelRatio, Infinity, + textState.padding, + !!textState.backgroundFill, !!textState.backgroundStroke, + this.text_, this.textKey_, this.strokeKey_, this.fillKey_, + this.textOffsetX_, this.textOffsetY_, + geometryWidths.length > 0 ? geometryWidths : null + ]); + this.endGeometry(geometry, feature); } } - /** - * @param {string} text Text. - * @param {string} textKey Text style key. - * @param {string} fillKey Fill style key. - * @param {string} strokeKey Stroke style key. - * @return {HTMLCanvasElement} Image. - */ - getImage(text, textKey, fillKey, strokeKey) { - let label; - const key = strokeKey + textKey + text + fillKey + this.pixelRatio; - - if (!labelCache.containsKey(key)) { - const strokeState = strokeKey ? this.strokeStates[strokeKey] || this.textStrokeState_ : null; - const fillState = fillKey ? this.fillStates[fillKey] || this.textFillState_ : null; - const textState = this.textStates[textKey] || this.textState_; - const pixelRatio = this.pixelRatio; - const scale = textState.scale * pixelRatio; - const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign]; - const strokeWidth = strokeKey && strokeState.lineWidth ? strokeState.lineWidth : 0; - - const lines = text.split('\n'); - const numLines = lines.length; - const widths = []; - const width = measureTextWidths(textState.font, lines, widths); - const lineHeight = measureTextHeight(textState.font); - const height = lineHeight * numLines; - const renderWidth = (width + strokeWidth); - const context = createCanvasContext2D( - Math.ceil(renderWidth * scale), - Math.ceil((height + strokeWidth) * scale)); - label = context.canvas; - labelCache.set(key, label); - if (scale != 1) { - context.scale(scale, scale); - } - context.font = textState.font; - if (strokeKey) { - context.strokeStyle = strokeState.strokeStyle; - context.lineWidth = strokeWidth; - context.lineCap = /** @type {CanvasLineCap} */ (strokeState.lineCap); - context.lineJoin = /** @type {CanvasLineJoin} */ (strokeState.lineJoin); - context.miterLimit = strokeState.miterLimit; - if (CANVAS_LINE_DASH && strokeState.lineDash.length) { - context.setLineDash(strokeState.lineDash); - context.lineDashOffset = strokeState.lineDashOffset; - } - } - if (fillKey) { - context.fillStyle = fillState.fillStyle; - } - context.textBaseline = 'middle'; - context.textAlign = 'center'; - const leftRight = (0.5 - align); - const x = align * label.width / scale + leftRight * strokeWidth; - let i; - if (strokeKey) { - for (i = 0; i < numLines; ++i) { - context.strokeText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight); - } - } - if (fillKey) { - for (i = 0; i < numLines; ++i) { - context.fillText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight); - } - } - } - return labelCache.get(key); - } - /** * @private - * @param {HTMLCanvasElement} label Label. - * @param {number} begin Begin. - * @param {number} end End. */ - drawTextImage_(label, begin, end) { - const textState = this.textState_; - const strokeState = this.textStrokeState_; - const pixelRatio = this.pixelRatio; - const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign]; - const baseline = TEXT_ALIGN[textState.textBaseline]; - const strokeWidth = strokeState && strokeState.lineWidth ? strokeState.lineWidth : 0; - - const anchorX = align * label.width / pixelRatio + 2 * (0.5 - align) * strokeWidth; - const anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth; - this.instructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, - label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, - this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, - 1, label.width, - textState.padding == defaultPadding ? - defaultPadding : textState.padding.map(function(p) { - return p * pixelRatio; - }), - !!textState.backgroundFill, !!textState.backgroundStroke - ]); - this.hitDetectionInstructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, - label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, - this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, - 1 / pixelRatio, label.width, textState.padding, - !!textState.backgroundFill, !!textState.backgroundStroke - ]); - } - - /** - * @private - * @param {number} begin Begin. - * @param {number} end End. - * @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group. - */ - drawChars_(begin, end, declutterGroup) { + saveTextStates_() { const strokeState = this.textStrokeState_; const textState = this.textState_; const fillState = this.textFillState_; @@ -382,10 +304,11 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder { } } const textKey = this.textKey_; - if (!(this.textKey_ in this.textStates)) { - this.textStates[this.textKey_] = /** @type {import("../canvas.js").TextState} */ ({ + if (!(textKey in this.textStates)) { + this.textStates[textKey] = /** @type {import("../canvas.js").TextState} */ ({ font: textState.font, textAlign: textState.textAlign || defaultTextAlign, + textBaseline: textState.textBaseline || defaultTextBaseline, scale: textState.scale }); } @@ -397,6 +320,23 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder { }); } } + } + + /** + * @private + * @param {number} begin Begin. + * @param {number} end End. + * @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group. + */ + drawChars_(begin, end, declutterGroup) { + const strokeState = this.textStrokeState_; + const textState = this.textState_; + + const strokeKey = this.strokeKey_; + const textKey = this.textKey_; + const fillKey = this.fillKey_; + this.saveTextStates_(); + const pixelRatio = this.pixelRatio; const baseline = TEXT_ALIGN[textState.textBaseline];