diff --git a/src/ol/VectorTile.js b/src/ol/VectorTile.js index 3a59e2fce4..d4e0f4ab7c 100644 --- a/src/ol/VectorTile.js +++ b/src/ol/VectorTile.js @@ -65,9 +65,9 @@ class VectorTile extends Tile { /** * @private - * @type {Object} + * @type {Object} */ - this.replayGroups_ = {}; + this.executorGroups_ = {}; /** * @private @@ -88,7 +88,7 @@ class VectorTile extends Tile { */ disposeInternal() { this.features_ = null; - this.replayGroups_ = {}; + this.executorGroups_ = {}; this.state = TileState.ABORT; this.changed(); super.disposeInternal(); @@ -142,10 +142,10 @@ class VectorTile extends Tile { /** * @param {import("./layer/Layer.js").default} layer Layer. * @param {string} key Key. - * @return {import("./render/ReplayGroup.js").default} Replay group. + * @return {import("./render/ExecutorGroup.js").default} Executor group. */ - getReplayGroup(layer, key) { - return this.replayGroups_[getUid(layer) + ',' + key]; + getExecutorGroup(layer, key) { + return this.executorGroups_[getUid(layer) + ',' + key]; } /** @@ -153,17 +153,17 @@ class VectorTile extends Tile { * @param {import("./layer/Layer").default} layer Layer. * @param {number} zoom Zoom. * @param {import("./extent").Extent} extent Extent. - * @return {import("./render/ReplayGroup.js").default} Replay groups. + * @return {import("./render/ExecutorGroup.js").default} Executor groups. */ - getLowResReplayGroup(layer, zoom, extent) { + getLowResExecutorGroup(layer, zoom, extent) { const layerId = getUid(layer); let bestZoom = 0; let replayGroup = null; - for (const key in this.replayGroups_) { + for (const key in this.executorGroups_) { const keyData = key.split(','); const candidateZoom = Number(keyData[1]); if (keyData[0] === layerId && candidateZoom <= zoom) { - const candidate = this.replayGroups_[key]; + const candidate = this.executorGroups_[key]; if (containsExtent(candidate.getMaxExtent(), extent) && candidateZoom > bestZoom) { replayGroup = candidate; bestZoom = candidateZoom; @@ -244,10 +244,10 @@ class VectorTile extends Tile { /** * @param {import("./layer/Layer.js").default} layer Layer. * @param {string} key Key. - * @param {import("./render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("./render/ExecutorGroup.js").default} executorGroup Executor group. */ - setReplayGroup(layer, key, replayGroup) { - this.replayGroups_[getUid(layer) + ',' + key] = replayGroup; + setExecutorGroup(layer, key, executorGroup) { + this.executorGroups_[getUid(layer) + ',' + key] = executorGroup; } /** diff --git a/src/ol/render/ReplayGroup.js b/src/ol/render/BuilderGroup.js similarity index 71% rename from src/ol/render/ReplayGroup.js rename to src/ol/render/BuilderGroup.js index 0cf176a3f5..6809455f30 100644 --- a/src/ol/render/ReplayGroup.js +++ b/src/ol/render/BuilderGroup.js @@ -1,19 +1,19 @@ /** - * @module ol/render/ReplayGroup + * @module ol/render/BuilderGroup */ import {abstract} from '../util.js'; /** - * Base class for replay groups. + * Base class for builder groups. */ -class ReplayGroup { +class BuilderGroup { /** * @abstract * @param {number|undefined} zIndex Z index. * @param {import("./ReplayType.js").default} replayType Replay type. * @return {import("./VectorContext.js").default} Replay. */ - getReplay(zIndex, replayType) { + getBuilder(zIndex, replayType) { return abstract(); } @@ -27,7 +27,7 @@ class ReplayGroup { /** * @abstract - * @param {boolean} group Group with previous replay + * @param {boolean} group Group with previous builder * @return {Array<*>} The resulting instruction group */ addDeclutter(group) { @@ -35,4 +35,4 @@ class ReplayGroup { } } -export default ReplayGroup; +export default BuilderGroup; diff --git a/src/ol/render/ExecutorGroup.js b/src/ol/render/ExecutorGroup.js new file mode 100644 index 0000000000..fb53545268 --- /dev/null +++ b/src/ol/render/ExecutorGroup.js @@ -0,0 +1,45 @@ +/** + * @module ol/render/ExecutorGroup + */ +import {abstract} from '../util.js'; + +/** + * Base class for replay groups. + */ +class ExecutorGroup { + /** + * @abstract + * @param {number|undefined} zIndex Z index. + * @param {import("./ReplayType.js").default} replayType Replay type. + * @return {import("./VectorContext.js").default} Executor. + */ + getExecutor(zIndex, replayType) { + return abstract(); + } + + /** + * @abstract + * @return {boolean} Is empty. + */ + isEmpty() { + return abstract(); + } + + /** + * @return {import("../extent.js").Extent} The extent of the group. + */ + getMaxExtent() { + return abstract(); + } + + /** + * @abstract + * @param {boolean} group Group with previous executor + * @return {Array<*>} The resulting instruction group + */ + addDeclutter(group) { + return abstract(); + } +} + +export default ExecutorGroup; diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index f338e47d7f..43bdc72fc0 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -324,6 +324,25 @@ export function measureTextWidth(font, text) { } +/** + * @param {string} font Font to use for measuring. + * @param {Array} lines Lines to measure. + * @param {Array} widths Array will be populated with the widths of + * each line. + * @return {number} Width of the whole text. + */ +export function measureTextWidths(font, lines, widths) { + const numLines = lines.length; + let width = 0; + for (let i = 0; i < numLines; ++i) { + const currentWidth = measureTextWidth(font, lines[i]); + width = Math.max(width, currentWidth); + widths.push(currentWidth); + } + return width; +} + + /** * @param {CanvasRenderingContext2D} context Context. * @param {number} rotation Rotation. diff --git a/src/ol/render/canvas/Builder.js b/src/ol/render/canvas/Builder.js new file mode 100644 index 0000000000..1a0a915e8e --- /dev/null +++ b/src/ol/render/canvas/Builder.js @@ -0,0 +1,546 @@ +/** + * @module ol/render/canvas/Builder + */ +import {equals, reverseSubArray} from '../../array.js'; +import {asColorLike} from '../../colorlike.js'; +import {buffer, clone, coordinateRelationship} 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 {CANVAS_LINE_DASH} from '../../has.js'; +import VectorContext from '../VectorContext.js'; +import {resetTransform, defaultFillStyle, defaultStrokeStyle, + defaultMiterLimit, defaultLineWidth, defaultLineJoin, defaultLineDashOffset, + defaultLineDash, defaultLineCap} from '../canvas.js'; +import CanvasInstruction from './Instruction.js'; +import { + create as createTransform, + apply as applyTransform +} from '../../transform.js'; + + +/** + * @typedef {Object} SerializableInstructions + * @property {Array<*>} instructions The rendering instructions. + * @property {Array<*>} hitDetectionInstructions The rendering hit detection instructions. + * @property {Array} coordinates The array of all coordinates. + * @property {!Object} [textStates] The text states (decluttering). + * @property {!Object} [fillStates] The fill states (decluttering). + * @property {!Object} [strokeStates] The stroke states (decluttering). + */ + + +class CanvasBuilder extends VectorContext { + /** + * @param {number} tolerance Tolerance. + * @param {import("../../extent.js").Extent} maxExtent Maximum extent. + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @param {boolean} overlaps The builder can have overlapping geometries. + * @param {?} declutterTree Declutter tree. + */ + constructor(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { + super(); + + /** + * @type {?} + */ + this.declutterTree = declutterTree; + + /** + * @protected + * @type {number} + */ + this.tolerance = tolerance; + + /** + * @protected + * @const + * @type {import("../../extent.js").Extent} + */ + this.maxExtent = maxExtent; + + /** + * @protected + * @type {boolean} + */ + this.overlaps = overlaps; + + /** + * @protected + * @type {number} + */ + this.pixelRatio = pixelRatio; + + /** + * @protected + * @type {number} + */ + this.maxLineWidth = 0; + + /** + * @protected + * @const + * @type {number} + */ + this.resolution = resolution; + + /** + * @private + * @type {boolean} + */ + this.alignFill_; + + /** + * @private + * @type {Array<*>} + */ + this.beginGeometryInstruction1_ = null; + + /** + * @private + * @type {Array<*>} + */ + this.beginGeometryInstruction2_ = null; + + /** + * @private + * @type {import("../../extent.js").Extent} + */ + this.bufferedMaxExtent_ = null; + + /** + * @protected + * @type {Array<*>} + */ + this.instructions = []; + + /** + * @protected + * @type {Array} + */ + this.coordinates = []; + + /** + * @private + * @type {!Object|Array>>} + */ + this.coordinateCache_ = {}; + + /** + * @private + * @type {!import("../../transform.js").Transform} + */ + this.renderedTransform_ = createTransform(); + + /** + * @protected + * @type {Array<*>} + */ + this.hitDetectionInstructions = []; + + /** + * @private + * @type {Array} + */ + this.pixelCoordinates_ = null; + + /** + * @protected + * @type {import("../canvas.js").FillStrokeState} + */ + this.state = /** @type {import("../canvas.js").FillStrokeState} */ ({}); + + /** + * @private + * @type {number} + */ + this.viewRotation_ = 0; + + } + + /** + * @protected + * @param {Array} dashArray Dash array. + * @return {Array} Dash array with pixel ratio applied + */ + applyPixelRatio(dashArray) { + const pixelRatio = this.pixelRatio; + return pixelRatio == 1 ? dashArray : dashArray.map(function(dash) { + return dash * pixelRatio; + }); + } + + /** + * @param {Array} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @param {boolean} closed Last input coordinate equals first. + * @param {boolean} skipFirst Skip first coordinate. + * @protected + * @return {number} My end. + */ + appendFlatCoordinates(flatCoordinates, offset, end, stride, closed, skipFirst) { + + let myEnd = this.coordinates.length; + const extent = this.getBufferedMaxExtent(); + if (skipFirst) { + offset += stride; + } + const lastCoord = [flatCoordinates[offset], flatCoordinates[offset + 1]]; + const nextCoord = [NaN, NaN]; + let skipped = true; + + let i, lastRel, nextRel; + for (i = offset + stride; i < end; i += stride) { + nextCoord[0] = flatCoordinates[i]; + nextCoord[1] = flatCoordinates[i + 1]; + nextRel = coordinateRelationship(extent, nextCoord); + if (nextRel !== lastRel) { + if (skipped) { + this.coordinates[myEnd++] = lastCoord[0]; + this.coordinates[myEnd++] = lastCoord[1]; + } + this.coordinates[myEnd++] = nextCoord[0]; + this.coordinates[myEnd++] = nextCoord[1]; + skipped = false; + } else if (nextRel === Relationship.INTERSECTING) { + this.coordinates[myEnd++] = nextCoord[0]; + this.coordinates[myEnd++] = nextCoord[1]; + skipped = false; + } else { + skipped = true; + } + lastCoord[0] = nextCoord[0]; + lastCoord[1] = nextCoord[1]; + lastRel = nextRel; + } + + // Last coordinate equals first or only one point to append: + if ((closed && skipped) || i === offset + stride) { + this.coordinates[myEnd++] = lastCoord[0]; + this.coordinates[myEnd++] = lastCoord[1]; + } + return myEnd; + } + + /** + * @param {Array} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {Array} ends Ends. + * @param {number} stride Stride. + * @param {Array} builderEnds Builder ends. + * @return {number} Offset. + */ + drawCustomCoordinates_(flatCoordinates, offset, ends, stride, builderEnds) { + for (let i = 0, ii = ends.length; i < ii; ++i) { + const end = ends[i]; + const builderEnd = this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false, false); + builderEnds.push(builderEnd); + offset = end; + } + return offset; + } + + /** + * @inheritDoc. + */ + drawCustom(geometry, feature, renderer) { + this.beginGeometry(geometry, feature); + const type = geometry.getType(); + const stride = geometry.getStride(); + const builderBegin = this.coordinates.length; + let flatCoordinates, builderEnd, builderEnds, builderEndss; + let offset; + if (type == GeometryType.MULTI_POLYGON) { + geometry = /** @type {import("../../geom/MultiPolygon.js").default} */ (geometry); + flatCoordinates = geometry.getOrientedFlatCoordinates(); + builderEndss = []; + const endss = geometry.getEndss(); + offset = 0; + for (let i = 0, ii = endss.length; i < ii; ++i) { + const myEnds = []; + offset = this.drawCustomCoordinates_(flatCoordinates, offset, endss[i], stride, myEnds); + builderEndss.push(myEnds); + } + this.instructions.push([CanvasInstruction.CUSTOM, + builderBegin, builderEndss, geometry, renderer, inflateMultiCoordinatesArray]); + } else if (type == GeometryType.POLYGON || type == GeometryType.MULTI_LINE_STRING) { + builderEnds = []; + flatCoordinates = (type == GeometryType.POLYGON) ? + /** @type {import("../../geom/Polygon.js").default} */ (geometry).getOrientedFlatCoordinates() : + geometry.getFlatCoordinates(); + offset = this.drawCustomCoordinates_(flatCoordinates, 0, + /** @type {import("../../geom/Polygon.js").default|import("../../geom/MultiLineString.js").default} */ (geometry).getEnds(), + stride, builderEnds); + this.instructions.push([CanvasInstruction.CUSTOM, + builderBegin, builderEnds, geometry, renderer, inflateCoordinatesArray]); + } else if (type == GeometryType.LINE_STRING || type == GeometryType.MULTI_POINT) { + flatCoordinates = geometry.getFlatCoordinates(); + builderEnd = this.appendFlatCoordinates( + flatCoordinates, 0, flatCoordinates.length, stride, false, false); + this.instructions.push([CanvasInstruction.CUSTOM, + builderBegin, builderEnd, geometry, renderer, inflateCoordinates]); + } else if (type == GeometryType.POINT) { + flatCoordinates = geometry.getFlatCoordinates(); + this.coordinates.push(flatCoordinates[0], flatCoordinates[1]); + builderEnd = this.coordinates.length; + this.instructions.push([CanvasInstruction.CUSTOM, + builderBegin, builderEnd, geometry, renderer]); + } + this.endGeometry(geometry, feature); + } + + /** + * @protected + * @param {import("../../geom/Geometry.js").default|import("../Feature.js").default} geometry Geometry. + * @param {import("../../Feature.js").default|import("../Feature.js").default} feature Feature. + */ + beginGeometry(geometry, feature) { + this.beginGeometryInstruction1_ = [CanvasInstruction.BEGIN_GEOMETRY, feature, 0]; + this.instructions.push(this.beginGeometryInstruction1_); + this.beginGeometryInstruction2_ = [CanvasInstruction.BEGIN_GEOMETRY, feature, 0]; + this.hitDetectionInstructions.push(this.beginGeometryInstruction2_); + } + + /** + * @return {SerializableInstructions} the serializable instructions. + */ + finish() { + return { + instructions: this.instructions, + hitDetectionInstructions: this.hitDetectionInstructions, + coordinates: this.coordinates + }; + } + + /** + * @private + * @param {CanvasRenderingContext2D} context Context. + */ + fill_(context) { + if (this.alignFill_) { + const origin = applyTransform(this.renderedTransform_, [0, 0]); + const repeatSize = 512 * this.pixelRatio; + context.translate(origin[0] % repeatSize, origin[1] % repeatSize); + context.rotate(this.viewRotation_); + } + context.fill(); + if (this.alignFill_) { + context.setTransform.apply(context, resetTransform); + } + } + + /** + * @private + * @param {CanvasRenderingContext2D} context Context. + * @param {Array<*>} instruction Instruction. + */ + setStrokeStyle_(context, instruction) { + context.strokeStyle = /** @type {import("../../colorlike.js").ColorLike} */ (instruction[1]); + context.lineWidth = /** @type {number} */ (instruction[2]); + context.lineCap = /** @type {CanvasLineCap} */ (instruction[3]); + context.lineJoin = /** @type {CanvasLineJoin} */ (instruction[4]); + context.miterLimit = /** @type {number} */ (instruction[5]); + if (CANVAS_LINE_DASH) { + context.lineDashOffset = /** @type {number} */ (instruction[7]); + context.setLineDash(/** @type {Array} */ (instruction[6])); + } + } + + /** + * Reverse the hit detection instructions. + */ + reverseHitDetectionInstructions() { + const hitDetectionInstructions = this.hitDetectionInstructions; + // step 1 - reverse array + hitDetectionInstructions.reverse(); + // step 2 - reverse instructions within geometry blocks + let i; + const n = hitDetectionInstructions.length; + let instruction; + let type; + let begin = -1; + for (i = 0; i < n; ++i) { + instruction = hitDetectionInstructions[i]; + type = /** @type {CanvasInstruction} */ (instruction[0]); + if (type == CanvasInstruction.END_GEOMETRY) { + begin = i; + } else if (type == CanvasInstruction.BEGIN_GEOMETRY) { + instruction[2] = i; + reverseSubArray(this.hitDetectionInstructions, begin, i); + begin = -1; + } + } + } + + /** + * @inheritDoc + */ + setFillStrokeStyle(fillStyle, strokeStyle) { + const state = this.state; + if (fillStyle) { + const fillStyleColor = fillStyle.getColor(); + state.fillStyle = asColorLike(fillStyleColor ? + fillStyleColor : defaultFillStyle); + } else { + state.fillStyle = undefined; + } + if (strokeStyle) { + const strokeStyleColor = strokeStyle.getColor(); + state.strokeStyle = asColorLike(strokeStyleColor ? + strokeStyleColor : defaultStrokeStyle); + const strokeStyleLineCap = strokeStyle.getLineCap(); + state.lineCap = strokeStyleLineCap !== undefined ? + strokeStyleLineCap : defaultLineCap; + const strokeStyleLineDash = strokeStyle.getLineDash(); + state.lineDash = strokeStyleLineDash ? + strokeStyleLineDash.slice() : defaultLineDash; + const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset(); + state.lineDashOffset = strokeStyleLineDashOffset ? + strokeStyleLineDashOffset : defaultLineDashOffset; + const strokeStyleLineJoin = strokeStyle.getLineJoin(); + state.lineJoin = strokeStyleLineJoin !== undefined ? + strokeStyleLineJoin : defaultLineJoin; + const strokeStyleWidth = strokeStyle.getWidth(); + state.lineWidth = strokeStyleWidth !== undefined ? + strokeStyleWidth : defaultLineWidth; + const strokeStyleMiterLimit = strokeStyle.getMiterLimit(); + state.miterLimit = strokeStyleMiterLimit !== undefined ? + strokeStyleMiterLimit : defaultMiterLimit; + + if (state.lineWidth > this.maxLineWidth) { + this.maxLineWidth = state.lineWidth; + // invalidate the buffered max extent cache + this.bufferedMaxExtent_ = null; + } + } else { + state.strokeStyle = undefined; + state.lineCap = undefined; + state.lineDash = null; + state.lineDashOffset = undefined; + state.lineJoin = undefined; + state.lineWidth = undefined; + state.miterLimit = undefined; + } + } + + /** + * @param {import("../canvas.js").FillStrokeState} state State. + * @param {import("../../geom/Geometry.js").default|import("../Feature.js").default} geometry Geometry. + * @return {Array<*>} Fill instruction. + */ + createFill(state, geometry) { + const fillStyle = state.fillStyle; + /** @type {Array<*>} */ + const fillInstruction = [CanvasInstruction.SET_FILL_STYLE, fillStyle]; + if (typeof fillStyle !== 'string') { + // Fill is a pattern or gradient - align it! + fillInstruction.push(true); + } + return fillInstruction; + } + + /** + * @param {import("../canvas.js").FillStrokeState} state State. + */ + applyStroke(state) { + this.instructions.push(this.createStroke(state)); + } + + /** + * @param {import("../canvas.js").FillStrokeState} state State. + * @return {Array<*>} Stroke instruction. + */ + createStroke(state) { + return [ + CanvasInstruction.SET_STROKE_STYLE, + state.strokeStyle, state.lineWidth * this.pixelRatio, state.lineCap, + state.lineJoin, state.miterLimit, + this.applyPixelRatio(state.lineDash), state.lineDashOffset * this.pixelRatio + ]; + } + + /** + * @param {import("../canvas.js").FillStrokeState} state State. + * @param {function(this:CanvasBuilder, import("../canvas.js").FillStrokeState, (import("../../geom/Geometry.js").default|import("../Feature.js").default)):Array<*>} createFill Create fill. + * @param {import("../../geom/Geometry.js").default|import("../Feature.js").default} geometry Geometry. + */ + updateFillStyle(state, createFill, geometry) { + const fillStyle = state.fillStyle; + if (typeof fillStyle !== 'string' || state.currentFillStyle != fillStyle) { + if (fillStyle !== undefined) { + this.instructions.push(createFill.call(this, state, geometry)); + } + state.currentFillStyle = fillStyle; + } + } + + /** + * @param {import("../canvas.js").FillStrokeState} state State. + * @param {function(this:CanvasBuilder, import("../canvas.js").FillStrokeState)} applyStroke Apply stroke. + */ + updateStrokeStyle(state, applyStroke) { + const strokeStyle = state.strokeStyle; + const lineCap = state.lineCap; + const lineDash = state.lineDash; + const lineDashOffset = state.lineDashOffset; + const lineJoin = state.lineJoin; + const lineWidth = state.lineWidth; + const miterLimit = state.miterLimit; + if (state.currentStrokeStyle != strokeStyle || + state.currentLineCap != lineCap || + (lineDash != state.currentLineDash && !equals(state.currentLineDash, lineDash)) || + state.currentLineDashOffset != lineDashOffset || + state.currentLineJoin != lineJoin || + state.currentLineWidth != lineWidth || + state.currentMiterLimit != miterLimit) { + if (strokeStyle !== undefined) { + applyStroke.call(this, state); + } + state.currentStrokeStyle = strokeStyle; + state.currentLineCap = lineCap; + state.currentLineDash = lineDash; + state.currentLineDashOffset = lineDashOffset; + state.currentLineJoin = lineJoin; + state.currentLineWidth = lineWidth; + state.currentMiterLimit = miterLimit; + } + } + + /** + * @param {import("../../geom/Geometry.js").default|import("../Feature.js").default} geometry Geometry. + * @param {import("../../Feature.js").default|import("../Feature.js").default} feature Feature. + */ + endGeometry(geometry, feature) { + this.beginGeometryInstruction1_[2] = this.instructions.length; + this.beginGeometryInstruction1_ = null; + this.beginGeometryInstruction2_[2] = this.hitDetectionInstructions.length; + this.beginGeometryInstruction2_ = null; + const endGeometryInstruction = [CanvasInstruction.END_GEOMETRY, feature]; + this.instructions.push(endGeometryInstruction); + this.hitDetectionInstructions.push(endGeometryInstruction); + } + + /** + * Get the buffered rendering extent. Rendering will be clipped to the extent + * provided to the constructor. To account for symbolizers that may intersect + * this extent, we calculate a buffered extent (e.g. based on stroke width). + * @return {import("../../extent.js").Extent} The buffered rendering extent. + * @protected + */ + getBufferedMaxExtent() { + if (!this.bufferedMaxExtent_) { + this.bufferedMaxExtent_ = clone(this.maxExtent); + if (this.maxLineWidth > 0) { + const width = this.resolution * (this.maxLineWidth + 1) / 2; + buffer(this.bufferedMaxExtent_, width, this.bufferedMaxExtent_); + } + } + return this.bufferedMaxExtent_; + } +} + + +export default CanvasBuilder; diff --git a/src/ol/render/canvas/BuilderGroup.js b/src/ol/render/canvas/BuilderGroup.js new file mode 100644 index 0000000000..c36b447f4c --- /dev/null +++ b/src/ol/render/canvas/BuilderGroup.js @@ -0,0 +1,409 @@ +/** + * @module ol/render/canvas/BuilderGroup + */ + +import {numberSafeCompareFunction} from '../../array.js'; +import {createCanvasContext2D} from '../../dom.js'; +import {buffer, createEmpty, extendCoordinate} from '../../extent.js'; +import {transform2D} from '../../geom/flat/transform.js'; +import {isEmpty} from '../../obj.js'; +import BuilderGroup from '../BuilderGroup.js'; +import ReplayType from '../ReplayType.js'; +import CanvasBuilder from './Builder.js'; +import CanvasImageBuilder from './ImageBuilder.js'; +import CanvasLineStringBuilder from './LineStringBuilder.js'; +import CanvasPolygonBuilder from './PolygonBuilder.js'; +import CanvasTextBuilder from './TextBuilder.js'; +import {ORDER} from '../replay.js'; +import {create as createTransform, compose as composeTransform} from '../../transform.js'; + + +/** + * @type {Object} + */ +const BATCH_CONSTRUCTORS = { + 'Circle': CanvasPolygonBuilder, + 'Default': CanvasBuilder, + 'Image': CanvasImageBuilder, + 'LineString': CanvasLineStringBuilder, + 'Polygon': CanvasPolygonBuilder, + 'Text': CanvasTextBuilder +}; + + +class CanvasBuilderGroup extends BuilderGroup { + /** + * @param {number} tolerance Tolerance. + * @param {import("../../extent.js").Extent} maxExtent Max extent. + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @param {boolean} overlaps The builder group can have overlapping geometries. + * @param {?} declutterTree Declutter tree for declutter processing in postrender. + * @param {number=} opt_renderBuffer Optional rendering buffer. + */ + constructor( + tolerance, + maxExtent, + resolution, + pixelRatio, + overlaps, + declutterTree, + opt_renderBuffer + ) { + super(); + + /** + * Declutter tree. + * @private + */ + this.declutterTree_ = declutterTree; + + /** + * @type {import("../canvas.js").DeclutterGroup} + * @private + */ + this.declutterGroup_ = null; + + /** + * @private + * @type {number} + */ + this.tolerance_ = tolerance; + + /** + * @private + * @type {import("../../extent.js").Extent} + */ + this.maxExtent_ = maxExtent; + + /** + * @private + * @type {boolean} + */ + this.overlaps_ = overlaps; + + /** + * @private + * @type {number} + */ + this.pixelRatio_ = pixelRatio; + + /** + * @private + * @type {number} + */ + this.resolution_ = resolution; + + /** + * @private + * @type {number|undefined} + */ + this.renderBuffer_ = opt_renderBuffer; + + /** + * @private + * @type {!Object>} + */ + this.buildersByZIndex_ = {}; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.hitDetectionContext_ = createCanvasContext2D(1, 1); + + /** + * @private + * @type {import("../../transform.js").Transform} + */ + this.hitDetectionTransform_ = createTransform(); + } + + /** + * @inheritDoc + */ + addDeclutter(group) { + let declutter = null; + if (this.declutterTree_) { + if (group) { + declutter = this.declutterGroup_; + /** @type {number} */ (declutter[4])++; + } else { + declutter = this.declutterGroup_ = createEmpty(); + declutter.push(1); + } + } + return declutter; + } + + /** + * @param {CanvasRenderingContext2D} context Context. + * @param {import("../../transform.js").Transform} transform Transform. + */ + clip(context, transform) { + const flatClipCoords = this.getClipCoords(transform); + context.beginPath(); + context.moveTo(flatClipCoords[0], flatClipCoords[1]); + context.lineTo(flatClipCoords[2], flatClipCoords[3]); + context.lineTo(flatClipCoords[4], flatClipCoords[5]); + context.lineTo(flatClipCoords[6], flatClipCoords[7]); + context.clip(); + } + + /** + * @return {!Object>} The serializable instructions + */ + finish() { + const builderInstructions = {}; + for (const zKey in this.buildersByZIndex_) { + builderInstructions[zKey] = builderInstructions[zKey] || {}; + const builders = this.buildersByZIndex_[zKey]; + for (const builderKey in builders) { + const builderInstruction = builders[builderKey].finish(); + builderInstructions[zKey][builderKey] = builderInstruction; + } + } + return builderInstructions; + } + + /** + * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {number} hitTolerance Hit tolerance in pixels. + * @param {Object} skippedFeaturesHash Ids of features to skip. + * @param {function((import("../../Feature.js").default|import("../Feature.js").default)): T} callback Feature callback. + * @param {Object} declutterReplays Declutter replays. + * @return {T|undefined} Callback result. + * @template T + */ + forEachFeatureAtCoordinate( + coordinate, + resolution, + rotation, + hitTolerance, + skippedFeaturesHash, + callback, + declutterReplays + ) { + + hitTolerance = Math.round(hitTolerance); + const contextSize = hitTolerance * 2 + 1; + const transform = composeTransform(this.hitDetectionTransform_, + hitTolerance + 0.5, hitTolerance + 0.5, + 1 / resolution, -1 / resolution, + -rotation, + -coordinate[0], -coordinate[1]); + const context = this.hitDetectionContext_; + + if (context.canvas.width !== contextSize || context.canvas.height !== contextSize) { + context.canvas.width = contextSize; + context.canvas.height = contextSize; + } else { + context.clearRect(0, 0, contextSize, contextSize); + } + + /** + * @type {import("../../extent.js").Extent} + */ + let hitExtent; + if (this.renderBuffer_ !== undefined) { + hitExtent = createEmpty(); + extendCoordinate(hitExtent, coordinate); + buffer(hitExtent, resolution * (this.renderBuffer_ + hitTolerance), hitExtent); + } + + const mask = getCircleArray(hitTolerance); + let declutteredFeatures; + if (this.declutterTree_) { + declutteredFeatures = this.declutterTree_.all().map(function(entry) { + return entry.value; + }); + } + + let replayType; + + /** + * @param {import("../../Feature.js").default|import("../Feature.js").default} feature Feature. + * @return {?} Callback result. + */ + function featureCallback(feature) { + const imageData = context.getImageData(0, 0, contextSize, contextSize).data; + for (let i = 0; i < contextSize; i++) { + for (let j = 0; j < contextSize; j++) { + if (mask[i][j]) { + if (imageData[(j * contextSize + i) * 4 + 3] > 0) { + let result; + if (!(declutteredFeatures && (replayType == ReplayType.IMAGE || replayType == ReplayType.TEXT)) || + declutteredFeatures.indexOf(feature) !== -1) { + result = callback(feature); + } + if (result) { + return result; + } else { + context.clearRect(0, 0, contextSize, contextSize); + return undefined; + } + } + } + } + } + } + + /** @type {Array} */ + const zs = Object.keys(this.buildersByZIndex_).map(Number); + zs.sort(numberSafeCompareFunction); + + let i, j, builders, builder, result; + for (i = zs.length - 1; i >= 0; --i) { + const zIndexKey = zs[i].toString(); + builders = this.buildersByZIndex_[zIndexKey]; + for (j = ORDER.length - 1; j >= 0; --j) { + replayType = ORDER[j]; + builder = builders[replayType]; + if (builder !== undefined) { + if (declutterReplays && + (replayType == ReplayType.IMAGE || replayType == ReplayType.TEXT)) { + const declutter = declutterReplays[zIndexKey]; + if (!declutter) { + declutterReplays[zIndexKey] = [builder, transform.slice(0)]; + } else { + declutter.push(builder, transform.slice(0)); + } + } else { + result = builder.executeHitDetection(context, transform, rotation, + skippedFeaturesHash, featureCallback, hitExtent); + if (result) { + return result; + } + } + } + } + } + return undefined; + } + + /** + * @param {import("../../transform.js").Transform} transform Transform. + * @return {Array} Clip coordinates. + */ + getClipCoords(transform) { + const maxExtent = this.maxExtent_; + const minX = maxExtent[0]; + const minY = maxExtent[1]; + const maxX = maxExtent[2]; + const maxY = maxExtent[3]; + const flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY]; + transform2D( + flatClipCoords, 0, 8, 2, transform, flatClipCoords); + return flatClipCoords; + } + + /** + * @inheritDoc + */ + getBuilder(zIndex, replayType) { + const zIndexKey = zIndex !== undefined ? zIndex.toString() : '0'; + let replays = this.buildersByZIndex_[zIndexKey]; + if (replays === undefined) { + replays = {}; + this.buildersByZIndex_[zIndexKey] = replays; + } + let replay = replays[replayType]; + if (replay === undefined) { + const Constructor = BATCH_CONSTRUCTORS[replayType]; + replay = new Constructor(this.tolerance_, this.maxExtent_, + this.resolution_, this.pixelRatio_, this.overlaps_, this.declutterTree_); + replays[replayType] = replay; + } + return replay; + } + + + /** + * @inheritDoc + */ + isEmpty() { + return isEmpty(this.buildersByZIndex_); + } +} + + +/** + * This cache is used for storing calculated pixel circles for increasing performance. + * It is a static property to allow each Replaygroup to access it. + * @type {Object>>} + */ +const circleArrayCache = { + 0: [[true]] +}; + + +/** + * This method fills a row in the array from the given coordinate to the + * middle with `true`. + * @param {Array>} array The array that will be altered. + * @param {number} x X coordinate. + * @param {number} y Y coordinate. + */ +function fillCircleArrayRowToMiddle(array, x, y) { + let i; + const radius = Math.floor(array.length / 2); + if (x >= radius) { + for (i = radius; i < x; i++) { + array[i][y] = true; + } + } else if (x < radius) { + for (i = x + 1; i < radius; i++) { + array[i][y] = true; + } + } +} + + +/** + * This methods creates a circle inside a fitting array. Points inside the + * circle are marked by true, points on the outside are undefined. + * It uses the midpoint circle algorithm. + * A cache is used to increase performance. + * @param {number} radius Radius. + * @returns {Array>} An array with marked circle points. + */ +export function getCircleArray(radius) { + if (circleArrayCache[radius] !== undefined) { + return circleArrayCache[radius]; + } + + const arraySize = radius * 2 + 1; + const arr = new Array(arraySize); + for (let i = 0; i < arraySize; i++) { + arr[i] = new Array(arraySize); + } + + let x = radius; + let y = 0; + let error = 0; + + while (x >= y) { + fillCircleArrayRowToMiddle(arr, radius + x, radius + y); + fillCircleArrayRowToMiddle(arr, radius + y, radius + x); + fillCircleArrayRowToMiddle(arr, radius - y, radius + x); + fillCircleArrayRowToMiddle(arr, radius - x, radius + y); + fillCircleArrayRowToMiddle(arr, radius - x, radius - y); + fillCircleArrayRowToMiddle(arr, radius - y, radius - x); + fillCircleArrayRowToMiddle(arr, radius + y, radius - x); + fillCircleArrayRowToMiddle(arr, radius + x, radius - y); + + y++; + error += 1 + 2 * y; + if (2 * (error - x) + 1 > 0) { + x -= 1; + error += 1 - 2 * x; + } + } + + circleArrayCache[radius] = arr; + return arr; +} + +export default CanvasBuilderGroup; diff --git a/src/ol/render/canvas/Replay.js b/src/ol/render/canvas/Executor.js similarity index 69% rename from src/ol/render/canvas/Replay.js rename to src/ol/render/canvas/Executor.js index 079f60575a..cb018d1e42 100644 --- a/src/ol/render/canvas/Replay.js +++ b/src/ol/render/canvas/Executor.js @@ -1,23 +1,16 @@ /** - * @module ol/render/canvas/Replay + * @module ol/render/canvas/Executor */ import {getUid} from '../../util.js'; import {equals, reverseSubArray} from '../../array.js'; -import {asColorLike} from '../../colorlike.js'; -import {buffer, clone, coordinateRelationship, createEmpty, createOrUpdate, +import {buffer, clone, createEmpty, createOrUpdate, createOrUpdateEmpty, extend, extendCoordinate, intersects} 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, - defaultMiterLimit, defaultLineWidth, defaultLineJoin, defaultLineDashOffset, - defaultLineDash, defaultLineCap} from '../canvas.js'; +import {drawImage, resetTransform, defaultPadding, defaultTextBaseline} from '../canvas.js'; import CanvasInstruction from './Instruction.js'; import {TEXT_ALIGN} from '../replay.js'; import { @@ -28,6 +21,20 @@ import { } from '../../transform.js'; +import {createCanvasContext2D} from '../../dom.js'; +import {labelCache, defaultTextAlign, measureTextHeight, measureTextWidth, measureTextWidths} from '../canvas.js'; + + +/** + * @typedef {Object} SerializableInstructions + * @property {Array<*>} instructions The rendering instructions. + * @property {Array<*>} hitDetectionInstructions The rendering hit detection instructions. + * @property {Array} coordinates The array of all coordinates. + * @property {!Object} textStates The text states (decluttering). + * @property {!Object} fillStates The fill states (decluttering). + * @property {!Object} strokeStates The stroke states (decluttering). + */ + /** * @type {import("../../extent.js").Extent} */ @@ -40,7 +47,7 @@ const tmpExtent = createEmpty(); const tmpTransform = createTransform(); -class CanvasReplay extends VectorContext { +class CanvasExecutor { /** * @param {number} tolerance Tolerance. * @param {import("../../extent.js").Extent} maxExtent Maximum extent. @@ -48,10 +55,9 @@ class CanvasReplay extends VectorContext { * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The replay can have overlapping geometries. * @param {?} declutterTree Declutter tree. + * @param {SerializableInstructions} instructions The serializable instructions */ - constructor(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { - super(); - + constructor(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree, instructions) { /** * @type {?} */ @@ -123,13 +129,13 @@ class CanvasReplay extends VectorContext { * @protected * @type {Array<*>} */ - this.instructions = []; + this.instructions = instructions.instructions; /** * @protected * @type {Array} */ - this.coordinates = []; + this.coordinates = instructions.coordinates; /** * @private @@ -147,7 +153,7 @@ class CanvasReplay extends VectorContext { * @protected * @type {Array<*>} */ - this.hitDetectionInstructions = []; + this.hitDetectionInstructions = instructions.hitDetectionInstructions; /** * @private @@ -167,6 +173,98 @@ class CanvasReplay extends VectorContext { */ this.viewRotation_ = 0; + /** + * @type {!Object} + */ + this.fillStates = instructions.fillStates || {}; + + /** + * @type {!Object} + */ + this.strokeStates = instructions.strokeStates || {}; + + /** + * @type {!Object} + */ + this.textStates = instructions.textStates || {}; + + // Adaptations + + /** + * @private + * @type {Object>} + */ + this.widths_ = {}; + } + + + /** + * @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. + */ + getTextImage(text, textKey, fillKey, strokeKey) { + let label; + const key = strokeKey + textKey + text + fillKey + this.pixelRatio; + + if (!labelCache.containsKey(key)) { + const strokeState = strokeKey ? this.strokeStates[strokeKey] : null; + const fillState = fillKey ? this.fillStates[fillKey] : null; + const textState = this.textStates[textKey]; + 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); } /** @@ -322,144 +420,6 @@ class CanvasReplay extends VectorContext { }); } - /** - * @param {Array} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {number} end End. - * @param {number} stride Stride. - * @param {boolean} closed Last input coordinate equals first. - * @param {boolean} skipFirst Skip first coordinate. - * @protected - * @return {number} My end. - */ - appendFlatCoordinates(flatCoordinates, offset, end, stride, closed, skipFirst) { - - let myEnd = this.coordinates.length; - const extent = this.getBufferedMaxExtent(); - if (skipFirst) { - offset += stride; - } - const lastCoord = [flatCoordinates[offset], flatCoordinates[offset + 1]]; - const nextCoord = [NaN, NaN]; - let skipped = true; - - let i, lastRel, nextRel; - for (i = offset + stride; i < end; i += stride) { - nextCoord[0] = flatCoordinates[i]; - nextCoord[1] = flatCoordinates[i + 1]; - nextRel = coordinateRelationship(extent, nextCoord); - if (nextRel !== lastRel) { - if (skipped) { - this.coordinates[myEnd++] = lastCoord[0]; - this.coordinates[myEnd++] = lastCoord[1]; - } - this.coordinates[myEnd++] = nextCoord[0]; - this.coordinates[myEnd++] = nextCoord[1]; - skipped = false; - } else if (nextRel === Relationship.INTERSECTING) { - this.coordinates[myEnd++] = nextCoord[0]; - this.coordinates[myEnd++] = nextCoord[1]; - skipped = false; - } else { - skipped = true; - } - lastCoord[0] = nextCoord[0]; - lastCoord[1] = nextCoord[1]; - lastRel = nextRel; - } - - // Last coordinate equals first or only one point to append: - if ((closed && skipped) || i === offset + stride) { - this.coordinates[myEnd++] = lastCoord[0]; - this.coordinates[myEnd++] = lastCoord[1]; - } - return myEnd; - } - - /** - * @param {Array} flatCoordinates Flat coordinates. - * @param {number} offset Offset. - * @param {Array} ends Ends. - * @param {number} stride Stride. - * @param {Array} replayEnds Replay ends. - * @return {number} Offset. - */ - drawCustomCoordinates_(flatCoordinates, offset, ends, stride, replayEnds) { - for (let i = 0, ii = ends.length; i < ii; ++i) { - const end = ends[i]; - const replayEnd = this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false, false); - replayEnds.push(replayEnd); - offset = end; - } - return offset; - } - - /** - * @inheritDoc. - */ - drawCustom(geometry, feature, renderer) { - this.beginGeometry(geometry, feature); - const type = geometry.getType(); - const stride = geometry.getStride(); - const replayBegin = this.coordinates.length; - let flatCoordinates, replayEnd, replayEnds, replayEndss; - let offset; - if (type == GeometryType.MULTI_POLYGON) { - geometry = /** @type {import("../../geom/MultiPolygon.js").default} */ (geometry); - flatCoordinates = geometry.getOrientedFlatCoordinates(); - replayEndss = []; - const endss = geometry.getEndss(); - offset = 0; - for (let i = 0, ii = endss.length; i < ii; ++i) { - const myEnds = []; - offset = this.drawCustomCoordinates_(flatCoordinates, offset, endss[i], stride, myEnds); - replayEndss.push(myEnds); - } - this.instructions.push([CanvasInstruction.CUSTOM, - replayBegin, replayEndss, geometry, renderer, inflateMultiCoordinatesArray]); - } else if (type == GeometryType.POLYGON || type == GeometryType.MULTI_LINE_STRING) { - replayEnds = []; - flatCoordinates = (type == GeometryType.POLYGON) ? - /** @type {import("../../geom/Polygon.js").default} */ (geometry).getOrientedFlatCoordinates() : - geometry.getFlatCoordinates(); - offset = this.drawCustomCoordinates_(flatCoordinates, 0, - /** @type {import("../../geom/Polygon.js").default|import("../../geom/MultiLineString.js").default} */ (geometry).getEnds(), - stride, replayEnds); - this.instructions.push([CanvasInstruction.CUSTOM, - replayBegin, replayEnds, geometry, renderer, inflateCoordinatesArray]); - } else if (type == GeometryType.LINE_STRING || type == GeometryType.MULTI_POINT) { - flatCoordinates = geometry.getFlatCoordinates(); - replayEnd = this.appendFlatCoordinates( - flatCoordinates, 0, flatCoordinates.length, stride, false, false); - this.instructions.push([CanvasInstruction.CUSTOM, - replayBegin, replayEnd, geometry, renderer, inflateCoordinates]); - } else if (type == GeometryType.POINT) { - flatCoordinates = geometry.getFlatCoordinates(); - this.coordinates.push(flatCoordinates[0], flatCoordinates[1]); - replayEnd = this.coordinates.length; - this.instructions.push([CanvasInstruction.CUSTOM, - replayBegin, replayEnd, geometry, renderer]); - } - this.endGeometry(geometry, feature); - } - - /** - * @protected - * @param {import("../../geom/Geometry.js").default|import("../Feature.js").default} geometry Geometry. - * @param {import("../../Feature.js").default|import("../Feature.js").default} feature Feature. - */ - beginGeometry(geometry, feature) { - this.beginGeometryInstruction1_ = [CanvasInstruction.BEGIN_GEOMETRY, feature, 0]; - this.instructions.push(this.beginGeometryInstruction1_); - this.beginGeometryInstruction2_ = [CanvasInstruction.BEGIN_GEOMETRY, feature, 0]; - this.hitDetectionInstructions.push(this.beginGeometryInstruction2_); - } - - /** - * FIXME empty description for jsdoc - */ - finish() {} - /** * @private * @param {CanvasRenderingContext2D} context Context. @@ -530,6 +490,35 @@ class CanvasReplay extends VectorContext { } } + /** + * @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.getTextImage(text, textKey, fillKey, strokeKey); + + const strokeState = this.strokeStates[strokeKey]; + const pixelRatio = this.pixelRatio; + const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign]; + const baseline = TEXT_ALIGN[textState.textBaseline || defaultTextBaseline]; + 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. @@ -544,7 +533,7 @@ class CanvasReplay extends VectorContext { * @return {T|undefined} Callback result. * @template T */ - replay_( + execute_( context, transform, skippedFeaturesHash, @@ -571,7 +560,8 @@ class CanvasReplay extends VectorContext { 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; @@ -663,20 +653,42 @@ class CanvasReplay extends VectorContext { 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 && instruction.length >= 19) { + // create label images + 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); + image = instruction[3] = labelWithAnchor.label; + const textOffsetX = /** @type {number} */ (instruction[22]); + anchorX = instruction[4] = (labelWithAnchor.anchorX - textOffsetX) * this.pixelRatio; + const textOffsetY = /** @type {number} */ (instruction[23]); + anchorY = instruction[5] = (labelWithAnchor.anchorY - textOffsetY) * this.pixelRatio; + height = instruction[7] = 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) { @@ -691,7 +703,11 @@ class CanvasReplay extends VectorContext { if (rotateWithView) { rotation += viewRotation; } + let widthIndex = 0; for (; d < dd; d += 2) { + if (geometryWidths && geometryWidths[widthIndex++] < width / this.pixelRatio) { + continue; + } this.replayImage_(context, pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY, declutterGroup, height, opacity, originX, originY, rotation, scale, @@ -708,22 +724,38 @@ class CanvasReplay extends VectorContext { 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 measure = /** @type {function(string):number} */ (instruction[8]); + 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]); - const textScale = /** @type {number} */ (instruction[14]); + text = /** @type {string} */ (instruction[12]); + textKey = /** @type {string} */ (instruction[13]); + const pixelRatioScale = /** @type {number} */ (instruction[14]); + + const textState = this.textStates[textKey]; + const font = textState.font; + const textScale = textState.scale; + + let widths = this.widths_[font]; + if (!widths) { + this.widths_[font] = widths = {}; + } + + //FIXME Do not create this function on every call + const measure = function(text) { + let width = widths[text]; + if (!width) { + width = widths[text] = measureTextWidth(font, text); + } + return width * textScale * measurePixelRatio; + }; 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 textAlign = this.textStates[textKey].textAlign; const startM = (pathLength - textLength) * TEXT_ALIGN[textAlign]; const parts = drawTextOnPath( pixelCoordinates, begin, end, 2, text, measure, startM, maxAngle); @@ -733,13 +765,13 @@ class CanvasReplay extends VectorContext { 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); + label = this.getTextImage(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, + /** @type {number} */ (part[3]), pixelRatioScale, false, label.width, defaultPadding, null, null); } } @@ -747,13 +779,13 @@ class CanvasReplay extends VectorContext { 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, ''); + label = this.getTextImage(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, + /** @type {number} */ (part[3]), pixelRatioScale, false, label.width, defaultPadding, null, null); } } @@ -860,9 +892,9 @@ class CanvasReplay extends VectorContext { * to skip. * @param {boolean} snapToPixel Snap point symbols and text to integer pixels. */ - replay(context, transform, viewRotation, skippedFeaturesHash, snapToPixel) { + execute(context, transform, viewRotation, skippedFeaturesHash, snapToPixel) { this.viewRotation_ = viewRotation; - this.replay_(context, transform, + this.execute_(context, transform, skippedFeaturesHash, this.instructions, snapToPixel, undefined, undefined); } @@ -879,7 +911,7 @@ class CanvasReplay extends VectorContext { * @return {T|undefined} Callback result. * @template T */ - replayHitDetection( + executeHitDetection( context, transform, viewRotation, @@ -888,7 +920,7 @@ class CanvasReplay extends VectorContext { opt_hitExtent ) { this.viewRotation_ = viewRotation; - return this.replay_(context, transform, skippedFeaturesHash, + return this.execute_(context, transform, skippedFeaturesHash, this.hitDetectionInstructions, true, opt_featureCallback, opt_hitExtent); } @@ -918,56 +950,6 @@ class CanvasReplay extends VectorContext { } } - /** - * @inheritDoc - */ - setFillStrokeStyle(fillStyle, strokeStyle) { - const state = this.state; - if (fillStyle) { - const fillStyleColor = fillStyle.getColor(); - state.fillStyle = asColorLike(fillStyleColor ? - fillStyleColor : defaultFillStyle); - } else { - state.fillStyle = undefined; - } - if (strokeStyle) { - const strokeStyleColor = strokeStyle.getColor(); - state.strokeStyle = asColorLike(strokeStyleColor ? - strokeStyleColor : defaultStrokeStyle); - const strokeStyleLineCap = strokeStyle.getLineCap(); - state.lineCap = strokeStyleLineCap !== undefined ? - strokeStyleLineCap : defaultLineCap; - const strokeStyleLineDash = strokeStyle.getLineDash(); - state.lineDash = strokeStyleLineDash ? - strokeStyleLineDash.slice() : defaultLineDash; - const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset(); - state.lineDashOffset = strokeStyleLineDashOffset ? - strokeStyleLineDashOffset : defaultLineDashOffset; - const strokeStyleLineJoin = strokeStyle.getLineJoin(); - state.lineJoin = strokeStyleLineJoin !== undefined ? - strokeStyleLineJoin : defaultLineJoin; - const strokeStyleWidth = strokeStyle.getWidth(); - state.lineWidth = strokeStyleWidth !== undefined ? - strokeStyleWidth : defaultLineWidth; - const strokeStyleMiterLimit = strokeStyle.getMiterLimit(); - state.miterLimit = strokeStyleMiterLimit !== undefined ? - strokeStyleMiterLimit : defaultMiterLimit; - - if (state.lineWidth > this.maxLineWidth) { - this.maxLineWidth = state.lineWidth; - // invalidate the buffered max extent cache - this.bufferedMaxExtent_ = null; - } - } else { - state.strokeStyle = undefined; - state.lineCap = undefined; - state.lineDash = null; - state.lineDashOffset = undefined; - state.lineJoin = undefined; - state.lineWidth = undefined; - state.miterLimit = undefined; - } - } /** * @param {import("../canvas.js").FillStrokeState} state State. @@ -985,12 +967,6 @@ class CanvasReplay extends VectorContext { return fillInstruction; } - /** - * @param {import("../canvas.js").FillStrokeState} state State. - */ - applyStroke(state) { - this.instructions.push(this.createStroke(state)); - } /** * @param {import("../canvas.js").FillStrokeState} state State. @@ -1005,66 +981,6 @@ class CanvasReplay extends VectorContext { ]; } - /** - * @param {import("../canvas.js").FillStrokeState} state State. - * @param {function(this:CanvasReplay, import("../canvas.js").FillStrokeState, (import("../../geom/Geometry.js").default|import("../Feature.js").default)):Array<*>} createFill Create fill. - * @param {import("../../geom/Geometry.js").default|import("../Feature.js").default} geometry Geometry. - */ - updateFillStyle(state, createFill, geometry) { - const fillStyle = state.fillStyle; - if (typeof fillStyle !== 'string' || state.currentFillStyle != fillStyle) { - if (fillStyle !== undefined) { - this.instructions.push(createFill.call(this, state, geometry)); - } - state.currentFillStyle = fillStyle; - } - } - - /** - * @param {import("../canvas.js").FillStrokeState} state State. - * @param {function(this:CanvasReplay, import("../canvas.js").FillStrokeState)} applyStroke Apply stroke. - */ - updateStrokeStyle(state, applyStroke) { - const strokeStyle = state.strokeStyle; - const lineCap = state.lineCap; - const lineDash = state.lineDash; - const lineDashOffset = state.lineDashOffset; - const lineJoin = state.lineJoin; - const lineWidth = state.lineWidth; - const miterLimit = state.miterLimit; - if (state.currentStrokeStyle != strokeStyle || - state.currentLineCap != lineCap || - (lineDash != state.currentLineDash && !equals(state.currentLineDash, lineDash)) || - state.currentLineDashOffset != lineDashOffset || - state.currentLineJoin != lineJoin || - state.currentLineWidth != lineWidth || - state.currentMiterLimit != miterLimit) { - if (strokeStyle !== undefined) { - applyStroke.call(this, state); - } - state.currentStrokeStyle = strokeStyle; - state.currentLineCap = lineCap; - state.currentLineDash = lineDash; - state.currentLineDashOffset = lineDashOffset; - state.currentLineJoin = lineJoin; - state.currentLineWidth = lineWidth; - state.currentMiterLimit = miterLimit; - } - } - - /** - * @param {import("../../geom/Geometry.js").default|import("../Feature.js").default} geometry Geometry. - * @param {import("../../Feature.js").default|import("../Feature.js").default} feature Feature. - */ - endGeometry(geometry, feature) { - this.beginGeometryInstruction1_[2] = this.instructions.length; - this.beginGeometryInstruction1_ = null; - this.beginGeometryInstruction2_[2] = this.hitDetectionInstructions.length; - this.beginGeometryInstruction2_ = null; - const endGeometryInstruction = [CanvasInstruction.END_GEOMETRY, feature]; - this.instructions.push(endGeometryInstruction); - this.hitDetectionInstructions.push(endGeometryInstruction); - } /** * Get the buffered rendering extent. Rendering will be clipped to the extent @@ -1086,4 +1002,4 @@ class CanvasReplay extends VectorContext { } -export default CanvasReplay; +export default CanvasExecutor; diff --git a/src/ol/render/canvas/ReplayGroup.js b/src/ol/render/canvas/ExecutorGroup.js similarity index 76% rename from src/ol/render/canvas/ReplayGroup.js rename to src/ol/render/canvas/ExecutorGroup.js index fa7aaaf89d..c9a0331032 100644 --- a/src/ol/render/canvas/ReplayGroup.js +++ b/src/ol/render/canvas/ExecutorGroup.js @@ -1,5 +1,5 @@ /** - * @module ol/render/canvas/ReplayGroup + * @module ol/render/canvas/ExecutorGroup */ import {numberSafeCompareFunction} from '../../array.js'; @@ -7,38 +7,23 @@ import {createCanvasContext2D} from '../../dom.js'; import {buffer, createEmpty, extendCoordinate} from '../../extent.js'; import {transform2D} from '../../geom/flat/transform.js'; import {isEmpty} from '../../obj.js'; -import ReplayGroup from '../ReplayGroup.js'; +import BaseExecutorGroup from '../ExecutorGroup.js'; import ReplayType from '../ReplayType.js'; -import CanvasReplay from './Replay.js'; -import CanvasImageReplay from './ImageReplay.js'; -import CanvasLineStringReplay from './LineStringReplay.js'; -import CanvasPolygonReplay from './PolygonReplay.js'; -import CanvasTextReplay from './TextReplay.js'; import {ORDER} from '../replay.js'; import {create as createTransform, compose as composeTransform} from '../../transform.js'; +import CanvasExecutor from './Executor.js'; -/** - * @type {Object} - */ -const BATCH_CONSTRUCTORS = { - 'Circle': CanvasPolygonReplay, - 'Default': CanvasReplay, - 'Image': CanvasImageReplay, - 'LineString': CanvasLineStringReplay, - 'Polygon': CanvasPolygonReplay, - 'Text': CanvasTextReplay -}; - - -class CanvasReplayGroup extends ReplayGroup { +class ExecutorGroup extends BaseExecutorGroup { /** * @param {number} tolerance Tolerance. * @param {import("../../extent.js").Extent} maxExtent Max extent. * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. - * @param {boolean} overlaps The replay group can have overlapping geometries. + * @param {boolean} overlaps The executor group can have overlapping geometries. * @param {?} declutterTree Declutter tree for declutter processing in postrender. + * @param {!Object>} allInstructions + * The serializable instructions. * @param {number=} opt_renderBuffer Optional rendering buffer. */ constructor( @@ -48,6 +33,7 @@ class CanvasReplayGroup extends ReplayGroup { pixelRatio, overlaps, declutterTree, + allInstructions, opt_renderBuffer ) { super(); @@ -102,9 +88,9 @@ class CanvasReplayGroup extends ReplayGroup { /** * @private - * @type {!Object>} + * @type {!Object>} */ - this.replaysByZIndex_ = {}; + this.executorsByZIndex_ = {}; /** * @private @@ -117,23 +103,8 @@ class CanvasReplayGroup extends ReplayGroup { * @type {import("../../transform.js").Transform} */ this.hitDetectionTransform_ = createTransform(); - } - /** - * @inheritDoc - */ - addDeclutter(group) { - let declutter = null; - if (this.declutterTree_) { - if (group) { - declutter = this.declutterGroup_; - /** @type {number} */ (declutter[4])++; - } else { - declutter = this.declutterGroup_ = createEmpty(); - declutter.push(1); - } - } - return declutter; + this.createExectutors_(allInstructions); } /** @@ -151,14 +122,34 @@ class CanvasReplayGroup extends ReplayGroup { } /** - * @param {Array} replays Replays. - * @return {boolean} Has replays of the provided types. + * Create executors and populate them using the provided instructions. + * @private + * @param {!Object>} allInstructions The serializable instructions */ - hasReplays(replays) { - for (const zIndex in this.replaysByZIndex_) { - const candidates = this.replaysByZIndex_[zIndex]; - for (let i = 0, ii = replays.length; i < ii; ++i) { - if (replays[i] in candidates) { + createExectutors_(allInstructions) { + for (const zIndex in allInstructions) { + let executors = this.executorsByZIndex_[zIndex]; + if (executors === undefined) { + this.executorsByZIndex_[zIndex] = executors = {}; + } + const instructionByZindex = allInstructions[zIndex]; + for (const replayType in instructionByZindex) { + const instructions = instructionByZindex[replayType]; + executors[replayType] = new CanvasExecutor(this.tolerance_, this.maxExtent_, + this.resolution_, this.pixelRatio_, this.overlaps_, this.declutterTree_, instructions); + } + } + } + + /** + * @param {Array} executors Executors. + * @return {boolean} Has executors of the provided types. + */ + hasExecutors(executors) { + for (const zIndex in this.executorsByZIndex_) { + const candidates = this.executorsByZIndex_[zIndex]; + for (let i = 0, ii = executors.length; i < ii; ++i) { + if (executors[i] in candidates) { return true; } } @@ -166,17 +157,6 @@ class CanvasReplayGroup extends ReplayGroup { return false; } - /** - * FIXME empty description for jsdoc - */ - finish() { - for (const zKey in this.replaysByZIndex_) { - const replays = this.replaysByZIndex_[zKey]; - for (const replayKey in replays) { - replays[replayKey].finish(); - } - } - } /** * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate. @@ -263,27 +243,27 @@ class CanvasReplayGroup extends ReplayGroup { } /** @type {Array} */ - const zs = Object.keys(this.replaysByZIndex_).map(Number); + const zs = Object.keys(this.executorsByZIndex_).map(Number); zs.sort(numberSafeCompareFunction); - let i, j, replays, replay, result; + let i, j, executors, executor, result; for (i = zs.length - 1; i >= 0; --i) { const zIndexKey = zs[i].toString(); - replays = this.replaysByZIndex_[zIndexKey]; + executors = this.executorsByZIndex_[zIndexKey]; for (j = ORDER.length - 1; j >= 0; --j) { replayType = ORDER[j]; - replay = replays[replayType]; - if (replay !== undefined) { + executor = executors[replayType]; + if (executor !== undefined) { if (declutterReplays && (replayType == ReplayType.IMAGE || replayType == ReplayType.TEXT)) { const declutter = declutterReplays[zIndexKey]; if (!declutter) { - declutterReplays[zIndexKey] = [replay, transform.slice(0)]; + declutterReplays[zIndexKey] = [executor, transform.slice(0)]; } else { - declutter.push(replay, transform.slice(0)); + declutter.push(executor, transform.slice(0)); } } else { - result = replay.replayHitDetection(context, transform, rotation, + result = executor.executeHitDetection(context, transform, rotation, skippedFeaturesHash, featureCallback, hitExtent); if (result) { return result; @@ -321,35 +301,40 @@ class CanvasReplayGroup extends ReplayGroup { /** * @inheritDoc */ - getReplay(zIndex, replayType) { + getExecutor(zIndex, replayType) { const zIndexKey = zIndex !== undefined ? zIndex.toString() : '0'; - let replays = this.replaysByZIndex_[zIndexKey]; - if (replays === undefined) { - replays = {}; - this.replaysByZIndex_[zIndexKey] = replays; + let executors = this.executorsByZIndex_[zIndexKey]; + if (executors === undefined) { + executors = {}; + this.executorsByZIndex_[zIndexKey] = executors; } - let replay = replays[replayType]; - if (replay === undefined) { - const Constructor = BATCH_CONSTRUCTORS[replayType]; - replay = new Constructor(this.tolerance_, this.maxExtent_, - this.resolution_, this.pixelRatio_, this.overlaps_, this.declutterTree_); - replays[replayType] = replay; + let executor = executors[replayType]; + if (executor === undefined) { + // FIXME: it should not be possible to ask for an executor that does not exist + executor = new CanvasExecutor(this.tolerance_, this.maxExtent_, + this.resolution_, this.pixelRatio_, this.overlaps_, { + instructions: [], + hitDetectionInstructions: [], + coordinates: [] + }, + this.declutterTree_); + executors[replayType] = executor; } - return replay; + return executor; } /** * @return {Object>} Replays. */ - getReplays() { - return this.replaysByZIndex_; + getExecutors() { + return this.executorsByZIndex_; } /** * @inheritDoc */ isEmpty() { - return isEmpty(this.replaysByZIndex_); + return isEmpty(this.executorsByZIndex_); } /** @@ -362,7 +347,7 @@ class CanvasReplayGroup extends ReplayGroup { * Default is {@link module:ol/render/replay~ORDER} * @param {Object=} opt_declutterReplays Declutter replays. */ - replay( + execute( context, transform, viewRotation, @@ -373,7 +358,7 @@ class CanvasReplayGroup extends ReplayGroup { ) { /** @type {Array} */ - const zs = Object.keys(this.replaysByZIndex_).map(Number); + const zs = Object.keys(this.executorsByZIndex_).map(Number); zs.sort(numberSafeCompareFunction); // setup clipping so that the parts of over-simplified geometries are not @@ -385,7 +370,7 @@ class CanvasReplayGroup extends ReplayGroup { let i, ii, j, jj, replays, replay; for (i = 0, ii = zs.length; i < ii; ++i) { const zIndexKey = zs[i].toString(); - replays = this.replaysByZIndex_[zIndexKey]; + replays = this.executorsByZIndex_[zIndexKey]; for (j = 0, jj = replayTypes.length; j < jj; ++j) { const replayType = replayTypes[j]; replay = replays[replayType]; @@ -399,7 +384,7 @@ class CanvasReplayGroup extends ReplayGroup { declutter.push(replay, transform.slice(0)); } } else { - replay.replay(context, transform, viewRotation, skippedFeaturesHash, snapToPixel); + replay.execute(context, transform, viewRotation, skippedFeaturesHash, snapToPixel); } } } @@ -498,14 +483,14 @@ export function replayDeclutter(declutterReplays, context, rotation, snapToPixel const zs = Object.keys(declutterReplays).map(Number).sort(numberSafeCompareFunction); const skippedFeatureUids = {}; for (let z = 0, zz = zs.length; z < zz; ++z) { - const replayData = declutterReplays[zs[z].toString()]; - for (let i = 0, ii = replayData.length; i < ii;) { - const replay = replayData[i++]; - const transform = replayData[i++]; - replay.replay(context, transform, rotation, skippedFeatureUids, snapToPixel); + const executorData = declutterReplays[zs[z].toString()]; + for (let i = 0, ii = executorData.length; i < ii;) { + const executor = executorData[i++]; + const transform = executorData[i++]; + executor.execute(context, transform, rotation, skippedFeatureUids, snapToPixel); } } } -export default CanvasReplayGroup; +export default ExecutorGroup; diff --git a/src/ol/render/canvas/ImageReplay.js b/src/ol/render/canvas/ImageBuilder.js similarity index 95% rename from src/ol/render/canvas/ImageReplay.js rename to src/ol/render/canvas/ImageBuilder.js index 807ea149c4..7ba5ef1acd 100644 --- a/src/ol/render/canvas/ImageReplay.js +++ b/src/ol/render/canvas/ImageBuilder.js @@ -1,16 +1,16 @@ /** - * @module ol/render/canvas/ImageReplay + * @module ol/render/canvas/ImageBuilder */ import CanvasInstruction from './Instruction.js'; -import CanvasReplay from './Replay.js'; +import CanvasBuilder from './Builder.js'; -class CanvasImageReplay extends CanvasReplay { +class CanvasImageBuilder extends CanvasBuilder { /** * @param {number} tolerance Tolerance. * @param {import("../../extent.js").Extent} maxExtent Maximum extent. * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. - * @param {boolean} overlaps The replay can have overlapping geometries. + * @param {boolean} overlaps The builder can have overlapping geometries. * @param {?} declutterTree Declutter tree. */ constructor(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { @@ -185,6 +185,7 @@ class CanvasImageReplay extends CanvasReplay { this.rotateWithView_ = undefined; this.rotation_ = undefined; this.width_ = undefined; + return super.finish(); } /** @@ -213,4 +214,4 @@ class CanvasImageReplay extends CanvasReplay { } -export default CanvasImageReplay; +export default CanvasImageBuilder; diff --git a/src/ol/render/canvas/LineStringReplay.js b/src/ol/render/canvas/LineStringBuilder.js similarity index 94% rename from src/ol/render/canvas/LineStringReplay.js rename to src/ol/render/canvas/LineStringBuilder.js index 57822418ae..2139a07add 100644 --- a/src/ol/render/canvas/LineStringReplay.js +++ b/src/ol/render/canvas/LineStringBuilder.js @@ -1,10 +1,10 @@ /** - * @module ol/render/canvas/LineStringReplay + * @module ol/render/canvas/LineStringBuilder */ import CanvasInstruction, {strokeInstruction, beginPathInstruction} from './Instruction.js'; -import CanvasReplay from './Replay.js'; +import CanvasBuilder from './Builder.js'; -class CanvasLineStringReplay extends CanvasReplay { +class CanvasLineStringBuilder extends CanvasBuilder { /** * @param {number} tolerance Tolerance. * @param {import("../../extent.js").Extent} maxExtent Maximum extent. @@ -97,6 +97,7 @@ class CanvasLineStringReplay extends CanvasReplay { } this.reverseHitDetectionInstructions(); this.state = null; + return super.finish(); } /** @@ -114,4 +115,4 @@ class CanvasLineStringReplay extends CanvasReplay { } -export default CanvasLineStringReplay; +export default CanvasLineStringBuilder; diff --git a/src/ol/render/canvas/PolygonReplay.js b/src/ol/render/canvas/PolygonBuilder.js similarity index 97% rename from src/ol/render/canvas/PolygonReplay.js rename to src/ol/render/canvas/PolygonBuilder.js index 1175a58fde..c3969e1751 100644 --- a/src/ol/render/canvas/PolygonReplay.js +++ b/src/ol/render/canvas/PolygonBuilder.js @@ -1,5 +1,5 @@ /** - * @module ol/render/canvas/PolygonReplay + * @module ol/render/canvas/PolygonBuilder */ import {asString} from '../../color.js'; import {snap} from '../../geom/flat/simplify.js'; @@ -7,10 +7,10 @@ import {defaultFillStyle} from '../canvas.js'; import CanvasInstruction, { fillInstruction, strokeInstruction, beginPathInstruction, closePathInstruction } from './Instruction.js'; -import CanvasReplay from './Replay.js'; +import CanvasBuilder from './Builder.js'; -class CanvasPolygonReplay extends CanvasReplay { +class CanvasPolygonBuilder extends CanvasBuilder { /** * @param {number} tolerance Tolerance. * @param {import("../../extent.js").Extent} maxExtent Maximum extent. @@ -192,6 +192,7 @@ class CanvasPolygonReplay extends CanvasReplay { coordinates[i] = snap(coordinates[i], tolerance); } } + return super.finish(); } /** @@ -211,4 +212,4 @@ class CanvasPolygonReplay extends CanvasReplay { } -export default CanvasPolygonReplay; +export default CanvasPolygonBuilder; diff --git a/src/ol/render/canvas/TextReplay.js b/src/ol/render/canvas/TextBuilder.js similarity index 66% rename from src/ol/render/canvas/TextReplay.js rename to src/ol/render/canvas/TextBuilder.js index e740f4a122..9c554c9f49 100644 --- a/src/ol/render/canvas/TextReplay.js +++ b/src/ol/render/canvas/TextBuilder.js @@ -1,20 +1,18 @@ /** - * @module ol/render/canvas/TextReplay + * @module ol/render/canvas/TextBuilder */ 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, measureTextWidth, 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 CanvasReplay from './Replay.js'; +import CanvasBuilder from './Builder.js'; import {TEXT_ALIGN} from '../replay.js'; import TextPlacement from '../../style/TextPlacement.js'; -class CanvasTextReplay extends CanvasReplay { +class CanvasTextBuilder extends CanvasBuilder { /** * @param {number} tolerance Tolerance. * @param {import("../../extent.js").Extent} maxExtent Maximum extent. @@ -119,14 +117,18 @@ class CanvasTextReplay extends CanvasReplay { */ this.strokeKey_ = ''; - /** - * @private - * @type {Object>} - */ - this.widths_ = {}; - labelCache.prune(); + } + /** + * @inheritdoc + */ + finish() { + const instructions = super.finish(); + instructions.textStates = this.textStates; + instructions.fillStates = this.fillStates; + instructions.strokeStates = this.strokeStates; + return instructions; } /** @@ -191,8 +193,11 @@ class CanvasTextReplay extends CanvasReplay { this.endGeometry(geometry, feature); } else { - const label = this.getImage(this.text_, this.textKey_, this.fillKey_, this.strokeKey_); - const width = label.width / this.pixelRatio; + + let geometryWidths = null; + if (!textState.overflow) { + geometryWidths = []; + } switch (geometryType) { case GeometryType.POINT: case GeometryType.MULTI_POINT: @@ -211,8 +216,8 @@ class CanvasTextReplay extends CanvasReplay { 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; @@ -220,9 +225,10 @@ class CanvasTextReplay extends CanvasReplay { 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) { @@ -232,6 +238,9 @@ class CanvasTextReplay extends CanvasReplay { 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) { @@ -243,122 +252,41 @@ class CanvasTextReplay extends CanvasReplay { 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 NaN for offsetX, offsetY, width and height, which will be computed at + // render time. + const pixelRatio = this.pixelRatio; + this.instructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, + null, NaN, NaN, this.declutterGroup_, NaN, 1, 0, 0, + this.textRotateWithView_, this.textRotation_, 1, NaN, + 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 + ]); + this.hitDetectionInstructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, + null, NaN, NaN, this.declutterGroup_, NaN, 1, 0, 0, + this.textRotateWithView_, this.textRotation_, 1 / this.pixelRatio, NaN, + textState.padding, + !!textState.backgroundFill, !!textState.backgroundStroke, + this.text_, this.textKey_, this.strokeKey_, this.fillKey_, + this.textOffsetX_, this.textOffsetY_, geometryWidths + ]); + 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_; @@ -378,10 +306,11 @@ class CanvasTextReplay extends CanvasReplay { } } 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 }); } @@ -393,41 +322,42 @@ class CanvasTextReplay extends CanvasReplay { }); } } + } + + /** + * @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]; const offsetY = this.textOffsetY_ * pixelRatio; const text = this.text_; - const font = textState.font; const textScale = textState.scale; const strokeWidth = strokeState ? strokeState.lineWidth * textScale / 2 : 0; - let widths = this.widths_[font]; - if (!widths) { - this.widths_[font] = widths = {}; - } + this.instructions.push([CanvasInstruction.DRAW_CHARS, begin, end, baseline, declutterGroup, textState.overflow, fillKey, textState.maxAngle, - function(text) { - let width = widths[text]; - if (!width) { - width = widths[text] = measureTextWidth(font, text); - } - return width * textScale * pixelRatio; - }, + pixelRatio, offsetY, strokeKey, strokeWidth * pixelRatio, text, textKey, 1 ]); this.hitDetectionInstructions.push([CanvasInstruction.DRAW_CHARS, begin, end, baseline, declutterGroup, textState.overflow, fillKey, textState.maxAngle, - function(text) { - let width = widths[text]; - if (!width) { - width = widths[text] = measureTextWidth(font, text); - } - return width * textScale; - }, + 1, offsetY, strokeKey, strokeWidth, text, textKey, 1 / pixelRatio ]); } @@ -518,23 +448,4 @@ class CanvasTextReplay extends CanvasReplay { } -/** - * @param {string} font Font to use for measuring. - * @param {Array} lines Lines to measure. - * @param {Array} widths Array will be populated with the widths of - * each line. - * @return {number} Width of the whole text. - */ -export function measureTextWidths(font, lines, widths) { - const numLines = lines.length; - let width = 0; - for (let i = 0; i < numLines; ++i) { - const currentWidth = measureTextWidth(font, lines[i]); - width = Math.max(width, currentWidth); - widths.push(currentWidth); - } - return width; -} - - -export default CanvasTextReplay; +export default CanvasTextBuilder; diff --git a/src/ol/render/webgl/Immediate.js b/src/ol/render/webgl/Immediate.js index 407cb843a7..ad0e77a78c 100644 --- a/src/ol/render/webgl/Immediate.js +++ b/src/ol/render/webgl/Immediate.js @@ -90,7 +90,7 @@ class WebGLImmediateRenderer extends VectorContext { drawText_(replayGroup, geometry) { const context = this.context_; const replay = /** @type {import("./TextReplay.js").default} */ ( - replayGroup.getReplay(0, ReplayType.TEXT)); + replayGroup.getBuilder(0, ReplayType.TEXT)); replay.setTextStyle(this.textStyle_); replay.drawText(geometry, null); replay.finish(context); @@ -191,7 +191,7 @@ class WebGLImmediateRenderer extends VectorContext { const context = this.context_; const replayGroup = new WebGLReplayGroup(1, this.extent_); const replay = /** @type {import("./ImageReplay.js").default} */ ( - replayGroup.getReplay(0, ReplayType.IMAGE)); + replayGroup.getBuilder(0, ReplayType.IMAGE)); replay.setImageStyle(this.imageStyle_); replay.drawPoint(geometry, data); replay.finish(context); @@ -218,7 +218,7 @@ class WebGLImmediateRenderer extends VectorContext { const context = this.context_; const replayGroup = new WebGLReplayGroup(1, this.extent_); const replay = /** @type {import("./ImageReplay.js").default} */ ( - replayGroup.getReplay(0, ReplayType.IMAGE)); + replayGroup.getBuilder(0, ReplayType.IMAGE)); replay.setImageStyle(this.imageStyle_); replay.drawMultiPoint(geometry, data); replay.finish(context); @@ -244,7 +244,7 @@ class WebGLImmediateRenderer extends VectorContext { const context = this.context_; const replayGroup = new WebGLReplayGroup(1, this.extent_); const replay = /** @type {import("./LineStringReplay.js").default} */ ( - replayGroup.getReplay(0, ReplayType.LINE_STRING)); + replayGroup.getBuilder(0, ReplayType.LINE_STRING)); replay.setFillStrokeStyle(null, this.strokeStyle_); replay.drawLineString(geometry, data); replay.finish(context); @@ -270,7 +270,7 @@ class WebGLImmediateRenderer extends VectorContext { const context = this.context_; const replayGroup = new WebGLReplayGroup(1, this.extent_); const replay = /** @type {import("./LineStringReplay.js").default} */ ( - replayGroup.getReplay(0, ReplayType.LINE_STRING)); + replayGroup.getBuilder(0, ReplayType.LINE_STRING)); replay.setFillStrokeStyle(null, this.strokeStyle_); replay.drawMultiLineString(geometry, data); replay.finish(context); @@ -296,7 +296,7 @@ class WebGLImmediateRenderer extends VectorContext { const context = this.context_; const replayGroup = new WebGLReplayGroup(1, this.extent_); const replay = /** @type {import("./PolygonReplay.js").default} */ ( - replayGroup.getReplay(0, ReplayType.POLYGON)); + replayGroup.getBuilder(0, ReplayType.POLYGON)); replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); replay.drawPolygon(geometry, data); replay.finish(context); @@ -322,7 +322,7 @@ class WebGLImmediateRenderer extends VectorContext { const context = this.context_; const replayGroup = new WebGLReplayGroup(1, this.extent_); const replay = /** @type {import("./PolygonReplay.js").default} */ ( - replayGroup.getReplay(0, ReplayType.POLYGON)); + replayGroup.getBuilder(0, ReplayType.POLYGON)); replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); replay.drawMultiPolygon(geometry, data); replay.finish(context); @@ -348,7 +348,7 @@ class WebGLImmediateRenderer extends VectorContext { const context = this.context_; const replayGroup = new WebGLReplayGroup(1, this.extent_); const replay = /** @type {import("./CircleReplay.js").default} */ ( - replayGroup.getReplay(0, ReplayType.CIRCLE)); + replayGroup.getBuilder(0, ReplayType.CIRCLE)); replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); replay.drawCircle(geometry, data); replay.finish(context); diff --git a/src/ol/render/webgl/ReplayGroup.js b/src/ol/render/webgl/ReplayGroup.js index 16fa92713e..207d69164e 100644 --- a/src/ol/render/webgl/ReplayGroup.js +++ b/src/ol/render/webgl/ReplayGroup.js @@ -6,7 +6,7 @@ import {numberSafeCompareFunction} from '../../array.js'; import {buffer, createOrUpdateFromCoordinate} from '../../extent.js'; import {isEmpty} from '../../obj.js'; import {ORDER} from '../replay.js'; -import ReplayGroup from '../ReplayGroup.js'; +import ReplayGroup from '../BuilderGroup.js'; import WebGLCircleReplay from './CircleReplay.js'; import WebGLImageReplay from './ImageReplay.js'; import WebGLLineStringReplay from './LineStringReplay.js'; @@ -113,7 +113,7 @@ class WebGLReplayGroup extends ReplayGroup { /** * @inheritDoc */ - getReplay(zIndex, replayType) { + getBuilder(zIndex, replayType) { const zIndexKey = zIndex !== undefined ? zIndex.toString() : '0'; let replays = this.replaysByZIndex_[zIndexKey]; if (replays === undefined) { diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index 6864bda9c9..295dcd31c5 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -10,7 +10,8 @@ import rbush from 'rbush'; import {buffer, createEmpty, containsExtent, getWidth} from '../../extent.js'; import RenderEventType from '../../render/EventType.js'; import {labelCache, rotateAtOffset} from '../../render/canvas.js'; -import CanvasReplayGroup from '../../render/canvas/ReplayGroup.js'; +import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; +import InstructionsGroupExecutor from '../../render/canvas/ExecutorGroup.js'; import CanvasLayerRenderer from './Layer.js'; import {defaultOrder as defaultRenderOrder, getTolerance as getRenderTolerance, getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js'; @@ -66,7 +67,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { /** * @private - * @type {import("../../render/canvas/ReplayGroup.js").default} + * @type {import("../../render/canvas/ExecutorGroup").default} */ this.replayGroup_ = null; @@ -165,7 +166,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const halfWidth = (frameState.size[0] * pixelRatio) / 2; const halfHeight = (frameState.size[1] * pixelRatio) / 2; rotateAtOffset(replayContext, -rotation, halfWidth, halfHeight); - replayGroup.replay(replayContext, transform, rotation, skippedFeatureUids, snapToPixel); + replayGroup.execute(replayContext, transform, rotation, skippedFeatureUids, snapToPixel); if (vectorSource.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, extent)) { let startX = extent[0]; @@ -176,7 +177,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { --world; offsetX = worldWidth * world; transform = this.getTransform(frameState, offsetX); - replayGroup.replay(replayContext, transform, rotation, skippedFeatureUids, snapToPixel); + replayGroup.execute(replayContext, transform, rotation, skippedFeatureUids, snapToPixel); startX += worldWidth; } world = 0; @@ -185,7 +186,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { ++world; offsetX = worldWidth * world; transform = this.getTransform(frameState, offsetX); - replayGroup.replay(replayContext, transform, rotation, skippedFeatureUids, snapToPixel); + replayGroup.execute(replayContext, transform, rotation, skippedFeatureUids, snapToPixel); startX -= worldWidth; } } @@ -279,7 +280,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { let transform = this.getRenderTransform(frameState, width, height, 0); const skippedFeatureUids = layerState.managed ? frameState.skippedFeatureUids : {}; - replayGroup.replay(context, transform, rotation, skippedFeatureUids, snapToPixel); + replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel); if (vectorSource.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, extent)) { let startX = extent[0]; @@ -290,7 +291,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { --world; offsetX = worldWidth * world; transform = this.getRenderTransform(frameState, width, height, offsetX); - replayGroup.replay(context, transform, rotation, skippedFeatureUids, snapToPixel); + replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel); startX += worldWidth; } world = 0; @@ -299,7 +300,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { ++world; offsetX = worldWidth * world; transform = this.getRenderTransform(frameState, width, height, offsetX); - replayGroup.replay(context, transform, rotation, skippedFeatureUids, snapToPixel); + replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel); startX -= worldWidth; } } @@ -443,7 +444,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { this.dirty_ = false; - const replayGroup = new CanvasReplayGroup( + const replayGroup = new CanvasBuilderGroup( getRenderTolerance(resolution, pixelRatio), extent, resolution, pixelRatio, vectorSource.getOverlaps(), this.declutterTree_, vectorLayer.getRenderBuffer()); vectorSource.loadFeatures(extent, resolution, projection); @@ -480,13 +481,18 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { } else { vectorSource.forEachFeatureInExtent(extent, render); } - replayGroup.finish(); + + const replayGroupInstructions = replayGroup.finish(); + const renderingExecutorGroup = new InstructionsGroupExecutor( + getRenderTolerance(resolution, pixelRatio), extent, resolution, + pixelRatio, vectorSource.getOverlaps(), this.declutterTree_, + replayGroupInstructions, vectorLayer.getRenderBuffer()); this.renderedResolution_ = resolution; this.renderedRevision_ = vectorLayerRevision; this.renderedRenderOrder_ = vectorLayerRenderOrder; this.renderedExtent_ = extent; - this.replayGroup_ = replayGroup; + this.replayGroup_ = renderingExecutorGroup; this.replayGroupChanged = true; return true; @@ -497,10 +503,10 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {import("../../style/Style.js").default|Array} styles The style or array of styles. - * @param {import("../../render/canvas/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../../render/canvas/BuilderGroup.js").default} builderGroup Builder group. * @return {boolean} `true` if an image is loading. */ - renderFeature(feature, resolution, pixelRatio, styles, replayGroup) { + renderFeature(feature, resolution, pixelRatio, styles, builderGroup) { if (!styles) { return false; } @@ -508,13 +514,13 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { if (Array.isArray(styles)) { for (let i = 0, ii = styles.length; i < ii; ++i) { loading = renderFeature( - replayGroup, feature, styles[i], + builderGroup, feature, styles[i], getSquaredRenderTolerance(resolution, pixelRatio), this.handleStyleImageChange_, this) || loading; } } else { loading = renderFeature( - replayGroup, feature, styles, + builderGroup, feature, styles, getSquaredRenderTolerance(resolution, pixelRatio), this.handleStyleImageChange_, this); } diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 2e608ea9d2..62850f0974 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -13,7 +13,7 @@ import {equivalent as equivalentProjection} from '../../proj.js'; import Units from '../../proj/Units.js'; import ReplayType from '../../render/ReplayType.js'; import {labelCache, rotateAtOffset} from '../../render/canvas.js'; -import CanvasReplayGroup, {replayDeclutter} from '../../render/canvas/ReplayGroup.js'; +import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import {ORDER} from '../../render/replay.js'; import CanvasTileLayerRenderer from './TileLayer.js'; import {getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js'; @@ -24,6 +24,7 @@ import { scale as scaleTransform, translate as translateTransform } from '../../transform.js'; +import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; /** @@ -107,7 +108,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { getTile(z, x, y, pixelRatio, projection) { const tile = super.getTile(z, x, y, pixelRatio, projection); if (tile.getState() === TileState.LOADED) { - this.createReplayGroup_(/** @type {import("../../VectorImageTile.js").default} */ (tile), pixelRatio, projection); + this.createExecutorGroup_(/** @type {import("../../VectorImageTile.js").default} */ (tile), pixelRatio, projection); if (this.context) { this.renderTileImage_(/** @type {import("../../VectorImageTile.js").default} */ (tile), pixelRatio, projection); } @@ -142,14 +143,14 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { * @param {import("../../proj/Projection.js").default} projection Projection. * @private */ - createReplayGroup_(tile, pixelRatio, projection) { + createExecutorGroup_(tile, pixelRatio, projection) { const layer = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer()); const revision = layer.getRevision(); const renderOrder = /** @type {import("../../render.js").OrderFunction} */ (layer.getRenderOrder()) || null; - const replayState = tile.getReplayState(layer); - if (!replayState.dirty && replayState.renderedRevision == revision && - replayState.renderedRenderOrder == renderOrder) { + const builderState = tile.getReplayState(layer); + if (!builderState.dirty && builderState.renderedRevision == revision && + builderState.renderedRenderOrder == renderOrder) { return; } @@ -166,10 +167,10 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { continue; } if (tile.useLoadedOnly) { - const lowResReplayGroup = sourceTile.getLowResReplayGroup(layer, zoom, tileExtent); - if (lowResReplayGroup) { + const lowResExecutorGroup = sourceTile.getLowResExecutorGroup(layer, zoom, tileExtent); + if (lowResExecutorGroup) { // reuse existing replay if we're rendering an interim tile - sourceTile.setReplayGroup(layer, tile.tileCoord.toString(), lowResReplayGroup); + sourceTile.setExecutorGroup(layer, tile.tileCoord.toString(), lowResExecutorGroup); continue; } } @@ -185,8 +186,8 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { reproject = true; sourceTile.setProjection(projection); } - replayState.dirty = false; - const replayGroup = new CanvasReplayGroup(0, sharedExtent, resolution, + builderState.dirty = false; + const builderGroup = new CanvasBuilderGroup(0, sharedExtent, resolution, pixelRatio, source.getOverlaps(), this.declutterTree_, layer.getRenderBuffer()); const squaredTolerance = getSquaredRenderTolerance(resolution, pixelRatio); @@ -201,14 +202,14 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { styles = styleFunction(feature, resolution); } if (styles) { - const dirty = this.renderFeature(feature, squaredTolerance, styles, replayGroup); + const dirty = this.renderFeature(feature, squaredTolerance, styles, builderGroup); this.dirty_ = this.dirty_ || dirty; - replayState.dirty = replayState.dirty || dirty; + builderState.dirty = builderState.dirty || dirty; } }; const features = sourceTile.getFeatures(); - if (renderOrder && renderOrder !== replayState.renderedRenderOrder) { + if (renderOrder && renderOrder !== builderState.renderedRenderOrder) { features.sort(renderOrder); } for (let i = 0, ii = features.length; i < ii; ++i) { @@ -226,11 +227,13 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { render.call(this, feature); } } - replayGroup.finish(); - sourceTile.setReplayGroup(layer, tile.tileCoord.toString(), replayGroup); + const replayGroupInstructions = builderGroup.finish(); + const renderingReplayGroup = new CanvasExecutorGroup(0, sharedExtent, resolution, + pixelRatio, source.getOverlaps(), this.declutterTree_, replayGroupInstructions, layer.getRenderBuffer()); + sourceTile.setExecutorGroup(layer, tile.tileCoord.toString(), renderingReplayGroup); } - replayState.renderedRevision = revision; - replayState.renderedRenderOrder = renderOrder; + builderState.renderedRevision = revision; + builderState.renderedRenderOrder = renderOrder; } /** @@ -259,9 +262,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (sourceTile.getState() != TileState.LOADED) { continue; } - const replayGroup = /** @type {CanvasReplayGroup} */ (sourceTile.getReplayGroup(layer, + const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(layer, tile.tileCoord.toString())); - found = found || replayGroup.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {}, + found = found || executorGroup.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {}, /** * @param {import("../../Feature.js").FeatureLike} feature Feature. * @return {?} Callback result. @@ -367,8 +370,8 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (sourceTile.getState() != TileState.LOADED) { continue; } - const replayGroup = /** @type {CanvasReplayGroup} */ (sourceTile.getReplayGroup(layer, tileCoord.toString())); - if (!replayGroup || !replayGroup.hasReplays(replayTypes)) { + const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(layer, tileCoord.toString())); + if (!executorGroup || !executorGroup.hasExecutors(replayTypes)) { // sourceTile was not yet loaded when this.createReplayGroup_() was // called, or it has no replays of the types we want to render continue; @@ -377,7 +380,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { transform = this.getTransform(frameState, worldOffset); } const currentZ = sourceTile.tileCoord[0]; - const currentClip = replayGroup.getClipCoords(transform); + const currentClip = executorGroup.getClipCoords(transform); context.save(); context.globalAlpha = layerState.opacity; // Create a clip mask for regions in this low resolution tile that are @@ -399,7 +402,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { context.clip(); } } - replayGroup.replay(context, transform, rotation, {}, snapToPixel, replayTypes, declutterReplays); + executorGroup.execute(context, transform, rotation, {}, snapToPixel, replayTypes, declutterReplays); context.restore(); clips.push(currentClip); zs.push(currentZ); @@ -420,7 +423,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { * @param {import("../../Feature.js").FeatureLike} feature Feature. * @param {number} squaredTolerance Squared tolerance. * @param {import("../../style/Style.js").default|Array} styles The style or array of styles. - * @param {import("../../render/canvas/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../../render/canvas/BuilderGroup.js").default} replayGroup Replay group. * @return {boolean} `true` if an image is loading. */ renderFeature(feature, squaredTolerance, styles, replayGroup) { @@ -474,9 +477,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const transform = resetTransform(this.tmpTransform_); scaleTransform(transform, pixelScale, -pixelScale); translateTransform(transform, -tileExtent[0], -tileExtent[3]); - const replayGroup = /** @type {CanvasReplayGroup} */ (sourceTile.getReplayGroup(layer, + const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(layer, tile.tileCoord.toString())); - replayGroup.replay(context, transform, 0, {}, true, replays); + executorGroup.execute(context, transform, 0, {}, true, replays); } } } diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index 6ebb0f32ed..356b7da3bf 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -4,7 +4,7 @@ import {getUid} from '../util.js'; import ImageState from '../ImageState.js'; import GeometryType from '../geom/GeometryType.js'; -import ReplayType from '../render/ReplayType.js'; +import BuilderType from '../render/ReplayType.js'; /** @@ -17,7 +17,7 @@ const SIMPLIFY_TOLERANCE = 0.5; /** * @const * @type {Object} */ const GEOMETRY_RENDERERS = { @@ -64,30 +64,30 @@ export function getTolerance(resolution, pixelRatio) { /** - * @param {import("../render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../render/BuilderGroup.js").default} builderGroup Builder group. * @param {import("../geom/Circle.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").default} feature Feature. */ -function renderCircleGeometry(replayGroup, geometry, style, feature) { +function renderCircleGeometry(builderGroup, geometry, style, feature) { const fillStyle = style.getFill(); const strokeStyle = style.getStroke(); if (fillStyle || strokeStyle) { - const circleReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.CIRCLE); + const circleReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.CIRCLE); circleReplay.setFillStrokeStyle(fillStyle, strokeStyle); circleReplay.drawCircle(geometry, feature); } const textStyle = style.getText(); if (textStyle) { - const textReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.TEXT); - textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false)); + const textReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.TEXT); + textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); textReplay.drawText(geometry, feature); } } /** - * @param {import("../render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../render/BuilderGroup.js").default} replayGroup Replay group. * @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../style/Style.js").default} style Style. * @param {number} squaredTolerance Squared tolerance. @@ -119,7 +119,7 @@ export function renderFeature(replayGroup, feature, style, squaredTolerance, lis /** - * @param {import("../render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../render/BuilderGroup.js").default} replayGroup Replay group. * @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../style/Style.js").default} style Style. * @param {number} squaredTolerance Squared tolerance. @@ -141,7 +141,7 @@ function renderFeatureInternal(replayGroup, feature, style, squaredTolerance) { /** - * @param {import("../render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../render/BuilderGroup.js").default} replayGroup Replay group. * @param {import("../geom/Geometry.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. @@ -154,13 +154,13 @@ function renderGeometry(replayGroup, geometry, style, feature) { } return; } - const replay = replayGroup.getReplay(style.getZIndex(), ReplayType.DEFAULT); + const replay = replayGroup.getBuilder(style.getZIndex(), BuilderType.DEFAULT); replay.drawCustom(/** @type {import("../geom/SimpleGeometry.js").default} */ (geometry), feature, style.getRenderer()); } /** - * @param {import("../render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../render/BuilderGroup.js").default} replayGroup Replay group. * @param {import("../geom/GeometryCollection.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").default} feature Feature. @@ -177,140 +177,140 @@ function renderGeometryCollectionGeometry(replayGroup, geometry, style, feature) /** - * @param {import("../render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../render/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/LineString.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. */ -function renderLineStringGeometry(replayGroup, geometry, style, feature) { +function renderLineStringGeometry(builderGroup, geometry, style, feature) { const strokeStyle = style.getStroke(); if (strokeStyle) { - const lineStringReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.LINE_STRING); + const lineStringReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.LINE_STRING); lineStringReplay.setFillStrokeStyle(null, strokeStyle); lineStringReplay.drawLineString(geometry, feature); } const textStyle = style.getText(); if (textStyle) { - const textReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.TEXT); - textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false)); + const textReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.TEXT); + textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); textReplay.drawText(geometry, feature); } } /** - * @param {import("../render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../render/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/MultiLineString.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. */ -function renderMultiLineStringGeometry(replayGroup, geometry, style, feature) { +function renderMultiLineStringGeometry(builderGroup, geometry, style, feature) { const strokeStyle = style.getStroke(); if (strokeStyle) { - const lineStringReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.LINE_STRING); + const lineStringReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.LINE_STRING); lineStringReplay.setFillStrokeStyle(null, strokeStyle); lineStringReplay.drawMultiLineString(geometry, feature); } const textStyle = style.getText(); if (textStyle) { - const textReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.TEXT); - textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false)); + const textReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.TEXT); + textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); textReplay.drawText(geometry, feature); } } /** - * @param {import("../render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../render/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/MultiPolygon.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").default} feature Feature. */ -function renderMultiPolygonGeometry(replayGroup, geometry, style, feature) { +function renderMultiPolygonGeometry(builderGroup, geometry, style, feature) { const fillStyle = style.getFill(); const strokeStyle = style.getStroke(); if (strokeStyle || fillStyle) { - const polygonReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.POLYGON); + const polygonReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.POLYGON); polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle); polygonReplay.drawMultiPolygon(geometry, feature); } const textStyle = style.getText(); if (textStyle) { - const textReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.TEXT); - textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false)); + const textReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.TEXT); + textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); textReplay.drawText(geometry, feature); } } /** - * @param {import("../render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../render/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/Point.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. */ -function renderPointGeometry(replayGroup, geometry, style, feature) { +function renderPointGeometry(builderGroup, geometry, style, feature) { const imageStyle = style.getImage(); if (imageStyle) { if (imageStyle.getImageState() != ImageState.LOADED) { return; } - const imageReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.IMAGE); - imageReplay.setImageStyle(imageStyle, replayGroup.addDeclutter(false)); + const imageReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.IMAGE); + imageReplay.setImageStyle(imageStyle, builderGroup.addDeclutter(false)); imageReplay.drawPoint(geometry, feature); } const textStyle = style.getText(); if (textStyle) { - const textReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.TEXT); - textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(!!imageStyle)); + const textReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.TEXT); + textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(!!imageStyle)); textReplay.drawText(geometry, feature); } } /** - * @param {import("../render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../render/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/MultiPoint.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. */ -function renderMultiPointGeometry(replayGroup, geometry, style, feature) { +function renderMultiPointGeometry(builderGroup, geometry, style, feature) { const imageStyle = style.getImage(); if (imageStyle) { if (imageStyle.getImageState() != ImageState.LOADED) { return; } - const imageReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.IMAGE); - imageReplay.setImageStyle(imageStyle, replayGroup.addDeclutter(false)); + const imageReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.IMAGE); + imageReplay.setImageStyle(imageStyle, builderGroup.addDeclutter(false)); imageReplay.drawMultiPoint(geometry, feature); } const textStyle = style.getText(); if (textStyle) { - const textReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.TEXT); - textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(!!imageStyle)); + const textReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.TEXT); + textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(!!imageStyle)); textReplay.drawText(geometry, feature); } } /** - * @param {import("../render/ReplayGroup.js").default} replayGroup Replay group. + * @param {import("../render/BuilderGroup.js").default} builderGroup Replay group. * @param {import("../geom/Polygon.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. */ -function renderPolygonGeometry(replayGroup, geometry, style, feature) { +function renderPolygonGeometry(builderGroup, geometry, style, feature) { const fillStyle = style.getFill(); const strokeStyle = style.getStroke(); if (fillStyle || strokeStyle) { - const polygonReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.POLYGON); + const polygonReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.POLYGON); polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle); polygonReplay.drawPolygon(geometry, feature); } const textStyle = style.getText(); if (textStyle) { - const textReplay = replayGroup.getReplay(style.getZIndex(), ReplayType.TEXT); - textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false)); + const textReplay = builderGroup.getBuilder(style.getZIndex(), BuilderType.TEXT); + textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); textReplay.drawText(geometry, feature); } } diff --git a/test/spec/ol/render/canvas/replaygroup.test.js b/test/spec/ol/render/canvas/replaygroup.test.js index ac00a62a22..7d9a3855a5 100644 --- a/test/spec/ol/render/canvas/replaygroup.test.js +++ b/test/spec/ol/render/canvas/replaygroup.test.js @@ -1,4 +1,4 @@ -import {getCircleArray} from '../../../../../src/ol/render/canvas/ReplayGroup.js'; +import {getCircleArray} from '../../../../../src/ol/render/canvas/BuilderGroup.js'; describe('ol.render.canvas.ReplayGroup', function() { diff --git a/test/spec/ol/render/canvas/textreplay.test.js b/test/spec/ol/render/canvas/textreplay.test.js index e9cce55171..590ab22cbe 100644 --- a/test/spec/ol/render/canvas/textreplay.test.js +++ b/test/spec/ol/render/canvas/textreplay.test.js @@ -1,48 +1,87 @@ import Feature from '../../../../../src/ol/Feature.js'; import MultiPolygon from '../../../../../src/ol/geom/MultiPolygon.js'; import Polygon from '../../../../../src/ol/geom/Polygon.js'; -import CanvasTextReplay from '../../../../../src/ol/render/canvas/TextReplay.js'; +import CanvasTextReplay from '../../../../../src/ol/render/canvas/TextBuilder.js'; +import InstructionExecutor from '../../../../../src/ol/render/canvas/Executor.js'; import Text from '../../../../../src/ol/style/Text.js'; +import {create as createTransform} from '../../../../../src/ol/transform.js'; + +function createBuilder() { + return new CanvasTextReplay(1, [-180, -90, 180, 90], 0.02, 1, true); +} + +function createContext() { + return { + fill: function() {}, + stroke: function() {}, + beginPath: function() {}, + clip: function() {}, + moveTo: function() {}, + lineTo: function() {}, + closePath: function() {}, + setLineDash: function() {}, + save: function() {}, + restore: function() {} + }; +} + +function executeInstructions(builder, expectedDrawTextImageCalls, expectedReplayImageCalls) { + const transform = createTransform(); + const context = createContext(); + const executor = new InstructionExecutor(1, [-180, -90, 180, 90], 0.02, 1, false, null, builder.finish()); + sinon.spy(executor, 'drawTextImageWithPointPlacement_'); + const replayImageStub = sinon.stub(executor, 'replayImage_'); + executor.execute(context, transform); + expect(executor.drawTextImageWithPointPlacement_.callCount).to.be(expectedDrawTextImageCalls); + expect(replayImageStub.callCount).to.be(expectedReplayImageCalls); +} describe('ol.render.canvas.TextReplay', function() { it('renders polygon labels only when they fit', function() { - const replay = new CanvasTextReplay(1, [-180, -90, 180, 90], 0.02, 1, true); + let builder = createBuilder(); const geometry = new Polygon([[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]]); const feature = new Feature(geometry); - replay.setTextStyle(new Text({ + builder.setTextStyle(new Text({ text: 'This is a long text' })); - replay.drawText(geometry, feature); - expect(replay.instructions.length).to.be(0); + builder.drawText(geometry, feature); + expect(builder.instructions.length).to.be(3); + executeInstructions(builder, 1, 0); - replay.setTextStyle(new Text({ + + builder = createBuilder(); + builder.setTextStyle(new Text({ text: 'short' })); - replay.drawText(geometry, feature); - expect(replay.instructions.length).to.be(3); + builder.drawText(geometry, feature); + expect(builder.instructions.length).to.be(3); + executeInstructions(builder, 1, 1); }); it('renders multipolygon labels only when they fit', function() { - const replay = new CanvasTextReplay(1, [-180, -90, 180, 90], 0.02, 1, true); const geometry = new MultiPolygon([ [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]], [[[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]] ]); const feature = new Feature(geometry); - replay.setTextStyle(new Text({ + let builder = createBuilder(); + builder.setTextStyle(new Text({ text: 'This is a long text' })); - replay.drawText(geometry, feature); - expect(replay.instructions.length).to.be(0); + builder.drawText(geometry, feature); + expect(builder.instructions.length).to.be(3); + executeInstructions(builder, 1, 0); - replay.setTextStyle(new Text({ + builder = createBuilder(); + builder.setTextStyle(new Text({ text: 'short' })); - replay.drawText(geometry, feature); - expect(replay.instructions.length).to.be(3); + builder.drawText(geometry, feature); + expect(builder.instructions.length).to.be(3); + executeInstructions(builder, 1, 2); }); }); diff --git a/test/spec/ol/renderer/canvas/replay.test.js b/test/spec/ol/renderer/canvas/replay.test.js index f449190ec3..3bfb3881fe 100644 --- a/test/spec/ol/renderer/canvas/replay.test.js +++ b/test/spec/ol/renderer/canvas/replay.test.js @@ -7,10 +7,11 @@ import MultiPoint from '../../../../../src/ol/geom/MultiPoint.js'; import MultiPolygon from '../../../../../src/ol/geom/MultiPolygon.js'; import Point from '../../../../../src/ol/geom/Point.js'; import Polygon from '../../../../../src/ol/geom/Polygon.js'; -import CanvasLineStringReplay from '../../../../../src/ol/render/canvas/LineStringReplay.js'; -import CanvasPolygonReplay from '../../../../../src/ol/render/canvas/PolygonReplay.js'; -import CanvasReplay from '../../../../../src/ol/render/canvas/Replay.js'; -import CanvasReplayGroup from '../../../../../src/ol/render/canvas/ReplayGroup.js'; +import CanvasLineStringBuilder from '../../../../../src/ol/render/canvas/LineStringBuilder.js'; +import CanvasPolygonBuilder from '../../../../../src/ol/render/canvas/PolygonBuilder.js'; +import CanvasReplay from '../../../../../src/ol/render/canvas/Builder.js'; +import CanvasInstructionsGroupBuilder from '../../../../../src/ol/render/canvas/BuilderGroup.js'; +import CanvasInstructionsGroupExecutor from '../../../../../src/ol/render/canvas/ExecutorGroup.js'; import {renderFeature} from '../../../../../src/ol/renderer/vector.js'; import Fill from '../../../../../src/ol/style/Fill.js'; import Stroke from '../../../../../src/ol/style/Stroke.js'; @@ -21,14 +22,26 @@ describe('ol.render.canvas.ReplayGroup', function() { describe('#replay', function() { - let context, replay, fillCount, transform; + let context, builder, fillCount, transform; let strokeCount, beginPathCount, moveToCount, lineToCount; let feature0, feature1, feature2, feature3; let fill0, fill1, style1, style2; + /** + * @param {CanvasInstructionsGroupBuilder} builder The builder to get instructions from. + * @param {Object=} skippedUids The ids to skip. + * @param {number=} pixelRatio The pixel ratio. + * @param {boolean=} overlaps Whether there is overlaps. + */ + function execute(builder, skippedUids, pixelRatio, overlaps) { + const executor = new CanvasInstructionsGroupExecutor(1, [-180, -90, 180, 90], 1, + pixelRatio || 1, !!overlaps, null, builder.finish()); + executor.execute(context, transform, 0, skippedUids || {}); + } + beforeEach(function() { transform = createTransform(); - replay = new CanvasReplayGroup(1, [-180, -90, 180, 90], 1, 1, false); + builder = new CanvasInstructionsGroupBuilder(1, [-180, -90, 180, 90], 1, 1, false); feature0 = new Feature(new Polygon( [[[-90, 0], [-45, 45], [0, 0], [1, 1], [0, -45], [-90, 0]]])); feature1 = new Feature(new Polygon( @@ -88,95 +101,95 @@ describe('ol.render.canvas.ReplayGroup', function() { }); it('omits lineTo for repeated coordinates', function() { - renderFeature(replay, feature0, fill0, 1); - replay.replay(context, transform, 0, {}); + renderFeature(builder, feature0, fill0, 1); + execute(builder); expect(lineToCount).to.be(4); lineToCount = 0; scaleTransform(transform, 0.25, 0.25); - replay.replay(context, transform, 0, {}); + execute(builder); expect(lineToCount).to.be(3); }); it('does not omit moveTo for repeated coordinates', function() { - renderFeature(replay, feature0, fill0, 1); - renderFeature(replay, feature1, fill1, 1); - replay.replay(context, transform, 0, {}); + renderFeature(builder, feature0, fill0, 1); + renderFeature(builder, feature1, fill1, 1); + execute(builder); expect(moveToCount).to.be(2); }); it('batches fill and stroke instructions for same style', function() { - renderFeature(replay, feature1, style1, 1); - renderFeature(replay, feature2, style1, 1); - renderFeature(replay, feature3, style1, 1); - replay.replay(context, transform, 0, {}); + renderFeature(builder, feature1, style1, 1); + renderFeature(builder, feature2, style1, 1); + renderFeature(builder, feature3, style1, 1); + execute(builder); expect(fillCount).to.be(1); expect(strokeCount).to.be(1); expect(beginPathCount).to.be(1); }); it('batches fill and stroke instructions for different styles', function() { - renderFeature(replay, feature1, style1, 1); - renderFeature(replay, feature2, style1, 1); - renderFeature(replay, feature3, style2, 1); - replay.replay(context, transform, 0, {}); + renderFeature(builder, feature1, style1, 1); + renderFeature(builder, feature2, style1, 1); + renderFeature(builder, feature3, style2, 1); + execute(builder); expect(fillCount).to.be(2); expect(strokeCount).to.be(2); expect(beginPathCount).to.be(2); }); it('batches fill and stroke instructions for changing styles', function() { - renderFeature(replay, feature1, style1, 1); - renderFeature(replay, feature2, style2, 1); - renderFeature(replay, feature3, style1, 1); - replay.replay(context, transform, 0, {}); + renderFeature(builder, feature1, style1, 1); + renderFeature(builder, feature2, style2, 1); + renderFeature(builder, feature3, style1, 1); + execute(builder); expect(fillCount).to.be(3); expect(strokeCount).to.be(3); expect(beginPathCount).to.be(3); }); it('batches fill and stroke instructions for skipped feature at the beginning', function() { - renderFeature(replay, feature1, style1, 1); - renderFeature(replay, feature2, style2, 1); - renderFeature(replay, feature3, style2, 1); + renderFeature(builder, feature1, style1, 1); + renderFeature(builder, feature2, style2, 1); + renderFeature(builder, feature3, style2, 1); const skippedUids = {}; skippedUids[getUid(feature1)] = true; - replay.replay(context, transform, 0, skippedUids); + execute(builder, skippedUids); expect(fillCount).to.be(1); expect(strokeCount).to.be(1); expect(beginPathCount).to.be(1); }); it('batches fill and stroke instructions for skipped feature at the end', function() { - renderFeature(replay, feature1, style1, 1); - renderFeature(replay, feature2, style1, 1); - renderFeature(replay, feature3, style2, 1); + renderFeature(builder, feature1, style1, 1); + renderFeature(builder, feature2, style1, 1); + renderFeature(builder, feature3, style2, 1); const skippedUids = {}; skippedUids[getUid(feature3)] = true; - replay.replay(context, transform, 0, skippedUids); + execute(builder, skippedUids); expect(fillCount).to.be(1); expect(strokeCount).to.be(1); expect(beginPathCount).to.be(1); }); it('batches fill and stroke instructions for skipped features', function() { - renderFeature(replay, feature1, style1, 1); - renderFeature(replay, feature2, style1, 1); - renderFeature(replay, feature3, style2, 1); + renderFeature(builder, feature1, style1, 1); + renderFeature(builder, feature2, style1, 1); + renderFeature(builder, feature3, style2, 1); const skippedUids = {}; skippedUids[getUid(feature1)] = true; skippedUids[getUid(feature2)] = true; - replay.replay(context, transform, 0, skippedUids); + execute(builder, skippedUids); expect(fillCount).to.be(1); expect(strokeCount).to.be(1); expect(beginPathCount).to.be(1); }); it('does not batch when overlaps is set to true', function() { - replay = new CanvasReplayGroup(1, [-180, -90, 180, 90], 1, 1, true); - renderFeature(replay, feature1, style1, 1); - renderFeature(replay, feature2, style1, 1); - renderFeature(replay, feature3, style1, 1); - replay.replay(context, transform, 0, {}); + builder = new CanvasInstructionsGroupBuilder(1, [-180, -90, 180, 90], 1, 1, true); + renderFeature(builder, feature1, style1, 1); + renderFeature(builder, feature2, style1, 1); + renderFeature(builder, feature3, style1, 1); + execute(builder, {}, 1, true); expect(fillCount).to.be(3); expect(strokeCount).to.be(3); expect(beginPathCount).to.be(3); @@ -184,7 +197,7 @@ describe('ol.render.canvas.ReplayGroup', function() { it('applies the pixelRatio to the linedash array and offset', function() { // replay with a pixelRatio of 2 - replay = new CanvasReplayGroup(1, [-180, -90, 180, 90], 1, 2, true); + builder = new CanvasInstructionsGroupBuilder(1, [-180, -90, 180, 90], 1, 2, true); let lineDash, lineDashCount = 0, lineDashOffset, lineDashOffsetCount = 0; @@ -201,9 +214,9 @@ describe('ol.render.canvas.ReplayGroup', function() { } }); - renderFeature(replay, feature1, style2, 1); - renderFeature(replay, feature2, style2, 1); - replay.replay(context, transform, 0, {}); + renderFeature(builder, feature1, style2, 1); + renderFeature(builder, feature2, style2, 1); + execute(builder, {}, 2, true); expect(lineDashCount).to.be(1); expect(style2.getStroke().getLineDash()).to.eql([3, 6]); @@ -241,16 +254,16 @@ describe('ol.render.canvas.ReplayGroup', function() { [polygon.getGeometry().getCoordinates(), polygon.getGeometry().getCoordinates()])); const geometrycollection = new Feature(new GeometryCollection( [point.getGeometry(), linestring.getGeometry(), polygon.getGeometry()])); - replay = new CanvasReplayGroup(1, [-180, -90, 180, 90], 1, 1, true); - renderFeature(replay, point, style, 1); - renderFeature(replay, multipoint, style, 1); - renderFeature(replay, linestring, style, 1); - renderFeature(replay, multilinestring, style, 1); - renderFeature(replay, polygon, style, 1); - renderFeature(replay, multipolygon, style, 1); - renderFeature(replay, geometrycollection, style, 1); + builder = new CanvasInstructionsGroupBuilder(1, [-180, -90, 180, 90], 1, 1, true); + renderFeature(builder, point, style, 1); + renderFeature(builder, multipoint, style, 1); + renderFeature(builder, linestring, style, 1); + renderFeature(builder, multilinestring, style, 1); + renderFeature(builder, polygon, style, 1); + renderFeature(builder, multipolygon, style, 1); + renderFeature(builder, geometrycollection, style, 1); scaleTransform(transform, 0.1, 0.1); - replay.replay(context, transform, 0, {}); + execute(builder, {}, 1, true); expect(calls.length).to.be(9); expect(calls[0].geometry).to.be(point.getGeometry()); expect(calls[0].feature).to.be(point); @@ -446,7 +459,7 @@ describe('ol.render.canvas.LineStringReplay', function() { const tolerance = 1; const extent = [-180, -90, 180, 90]; const resolution = 10; - const replay = new CanvasLineStringReplay(tolerance, extent, + const replay = new CanvasLineStringBuilder(tolerance, extent, resolution); const stroke = new Stroke({ width: 2 @@ -468,7 +481,7 @@ describe('ol.render.canvas.PolygonReplay', function() { const tolerance = 1; const extent = [-180, -90, 180, 90]; const resolution = 10; - replay = new CanvasPolygonReplay(tolerance, extent, + replay = new CanvasPolygonBuilder(tolerance, extent, resolution); }); diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index 7902ffbde8..4b3f6d7e1e 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -241,10 +241,8 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { }); map.addLayer(layer2); - const spy1 = sinon.spy(VectorTile.prototype, - 'getReplayGroup'); - const spy2 = sinon.spy(VectorTile.prototype, - 'setReplayGroup'); + const spy1 = sinon.spy(VectorTile.prototype, 'getExecutorGroup'); + const spy2 = sinon.spy(VectorTile.prototype, 'setExecutorGroup'); map.renderSync(); expect(spy1.callCount).to.be(4); expect(spy2.callCount).to.be(2); @@ -308,7 +306,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { }); describe('#forEachFeatureAtCoordinate', function() { - let layer, renderer, replayGroup; + let layer, renderer, executorGroup; class TileClass extends VectorImageTile { constructor() { super(...arguments); @@ -317,8 +315,8 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { const sourceTile = new VectorTile([0, 0, 0]); sourceTile.setState(TileState.LOADED); sourceTile.setProjection(getProjection('EPSG:3857')); - sourceTile.getReplayGroup = function() { - return replayGroup; + sourceTile.getExecutorGroup = function() { + return executorGroup; }; const key = sourceTile.tileCoord.toString(); this.tileKeys = [key]; @@ -329,7 +327,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { } beforeEach(function() { - replayGroup = {}; + executorGroup = {}; layer = new VectorTileLayer({ source: new VectorTileSource({ tileClass: TileClass, @@ -337,7 +335,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { }) }); renderer = new CanvasVectorTileLayerRenderer(layer); - replayGroup.forEachFeatureAtCoordinate = function(coordinate, + executorGroup.forEachFeatureAtCoordinate = function(coordinate, resolution, rotation, hitTolerance, skippedFeaturesUids, callback) { const feature = new Feature(); callback(feature); diff --git a/test/spec/ol/renderer/vector.test.js b/test/spec/ol/renderer/vector.test.js index 7d354b34e9..554079ec95 100644 --- a/test/spec/ol/renderer/vector.test.js +++ b/test/spec/ol/renderer/vector.test.js @@ -6,7 +6,7 @@ import Polygon from '../../../../src/ol/geom/Polygon.js'; import MultiLineString from '../../../../src/ol/geom/MultiLineString.js'; import MultiPoint from '../../../../src/ol/geom/MultiPoint.js'; import MultiPolygon from '../../../../src/ol/geom/MultiPolygon.js'; -import CanvasReplayGroup from '../../../../src/ol/render/canvas/ReplayGroup.js'; +import CanvasBuilderGroup from '../../../../src/ol/render/canvas/BuilderGroup.js'; import {renderFeature} from '../../../../src/ol/renderer/vector.js'; import Fill from '../../../../src/ol/style/Fill.js'; import Icon from '../../../../src/ol/style/Icon.js'; @@ -17,12 +17,12 @@ import Feature from '../../../../src/ol/Feature.js'; describe('ol.renderer.vector', function() { describe('#renderFeature', function() { - let replayGroup; + let builderGroup; let feature, iconStyle, style, squaredTolerance, listener, listenerThis; let iconStyleLoadSpy; beforeEach(function() { - replayGroup = new CanvasReplayGroup(1); + builderGroup = new CanvasBuilderGroup(1); feature = new Feature(); iconStyle = new Icon({ src: 'http://example.com/icon.png' @@ -50,7 +50,7 @@ describe('ol.renderer.vector', function() { let listeners; // call #1 - renderFeature(replayGroup, feature, + renderFeature(builderGroup, feature, style, squaredTolerance, listener, listenerThis); expect(iconStyleLoadSpy.calledOnce).to.be.ok(); @@ -59,7 +59,7 @@ describe('ol.renderer.vector', function() { expect(listeners.length).to.eql(1); // call #2 - renderFeature(replayGroup, feature, + renderFeature(builderGroup, feature, style, squaredTolerance, listener, listenerThis); expect(iconStyleLoadSpy.calledOnce).to.be.ok(); @@ -74,11 +74,11 @@ describe('ol.renderer.vector', function() { it('does not render the point', function() { feature.setGeometry(new Point([0, 0])); - const imageReplay = replayGroup.getReplay( + const imageReplay = builderGroup.getBuilder( style.getZIndex(), 'Image'); const setImageStyleSpy = sinon.spy(imageReplay, 'setImageStyle'); const drawPointSpy = sinon.stub(imageReplay, 'drawPoint').callsFake(VOID); - renderFeature(replayGroup, feature, + renderFeature(builderGroup, feature, style, squaredTolerance, listener, listenerThis); expect(setImageStyleSpy.called).to.be(false); setImageStyleSpy.restore(); @@ -87,11 +87,11 @@ describe('ol.renderer.vector', function() { it('does not render the multipoint', function() { feature.setGeometry(new MultiPoint([[0, 0], [1, 1]])); - const imageReplay = replayGroup.getReplay( + const imageReplay = builderGroup.getBuilder( style.getZIndex(), 'Image'); const setImageStyleSpy = sinon.spy(imageReplay, 'setImageStyle'); const drawMultiPointSpy = sinon.stub(imageReplay, 'drawMultiPoint').callsFake(VOID); - renderFeature(replayGroup, feature, + renderFeature(builderGroup, feature, style, squaredTolerance, listener, listenerThis); expect(setImageStyleSpy.called).to.be(false); setImageStyleSpy.restore(); @@ -100,12 +100,12 @@ describe('ol.renderer.vector', function() { it('does render the linestring', function() { feature.setGeometry(new LineString([[0, 0], [1, 1]])); - const lineStringReplay = replayGroup.getReplay( + const lineStringReplay = builderGroup.getBuilder( style.getZIndex(), 'LineString'); const setFillStrokeStyleSpy = sinon.spy(lineStringReplay, 'setFillStrokeStyle'); const drawLineStringSpy = sinon.stub(lineStringReplay, 'drawLineString').callsFake(VOID); - renderFeature(replayGroup, feature, + renderFeature(builderGroup, feature, style, squaredTolerance, listener, listenerThis); expect(setFillStrokeStyleSpy.called).to.be(true); expect(drawLineStringSpy.called).to.be(true); @@ -115,12 +115,12 @@ describe('ol.renderer.vector', function() { it('does render the multilinestring', function() { feature.setGeometry(new MultiLineString([[[0, 0], [1, 1]]])); - const lineStringReplay = replayGroup.getReplay( + const lineStringReplay = builderGroup.getBuilder( style.getZIndex(), 'LineString'); const setFillStrokeStyleSpy = sinon.spy(lineStringReplay, 'setFillStrokeStyle'); const drawMultiLineStringSpy = sinon.stub(lineStringReplay, 'drawMultiLineString').callsFake(VOID); - renderFeature(replayGroup, feature, + renderFeature(builderGroup, feature, style, squaredTolerance, listener, listenerThis); expect(setFillStrokeStyleSpy.called).to.be(true); expect(drawMultiLineStringSpy.called).to.be(true); @@ -131,12 +131,12 @@ describe('ol.renderer.vector', function() { it('does render the polygon', function() { feature.setGeometry(new Polygon( [[[0, 0], [1, 1], [1, 0], [0, 0]]])); - const polygonReplay = replayGroup.getReplay( + const polygonReplay = builderGroup.getBuilder( style.getZIndex(), 'Polygon'); const setFillStrokeStyleSpy = sinon.spy(polygonReplay, 'setFillStrokeStyle'); const drawPolygonSpy = sinon.stub(polygonReplay, 'drawPolygon').callsFake(VOID); - renderFeature(replayGroup, feature, + renderFeature(builderGroup, feature, style, squaredTolerance, listener, listenerThis); expect(setFillStrokeStyleSpy.called).to.be(true); expect(drawPolygonSpy.called).to.be(true); @@ -147,12 +147,12 @@ describe('ol.renderer.vector', function() { it('does render the multipolygon', function() { feature.setGeometry(new MultiPolygon( [[[[0, 0], [1, 1], [1, 0], [0, 0]]]])); - const polygonReplay = replayGroup.getReplay( + const polygonReplay = builderGroup.getBuilder( style.getZIndex(), 'Polygon'); const setFillStrokeStyleSpy = sinon.spy(polygonReplay, 'setFillStrokeStyle'); const drawMultiPolygonSpy = sinon.stub(polygonReplay, 'drawMultiPolygon').callsFake(VOID); - renderFeature(replayGroup, feature, + renderFeature(builderGroup, feature, style, squaredTolerance, listener, listenerThis); expect(setFillStrokeStyleSpy.called).to.be(true); expect(drawMultiPolygonSpy.called).to.be(true);