text: /**
   * @module ol/render/canvas/Executor
   */
  import {getUid} from '../../util.js';
  import {equals} from '../../array.js';
  import {createEmpty, createOrUpdate,
    createOrUpdateEmpty, extend, intersects} from '../../extent.js';
  import {lineStringLength} from '../../geom/flat/length.js';
  import {drawTextOnPath} from '../../geom/flat/textpath.js';
  import {transform2D} from '../../geom/flat/transform.js';
  import {isEmpty} from '../../obj.js';
  import {drawImage, defaultPadding, defaultTextBaseline} from '../canvas.js';
  import CanvasInstruction from './Instruction.js';
  import {TEXT_ALIGN} from './TextBuilder.js';
  import {
    create as createTransform,
    compose as composeTransform,
    apply as applyTransform,
    setFromArray as transformSetFromArray
  } from '../../transform.js';
  import {createCanvasContext2D} from '../../dom.js';
  import {labelCache, defaultTextAlign, measureTextHeight, measureAndCacheTextWidth, measureTextWidths} from '../canvas.js';
  import Disposable from '../../Disposable.js';
  import rbush from 'rbush';
  
  
  /**
   * @typedef {Object} SerializableInstructions
   * @property {Array<*>} instructions The rendering instructions.
   * @property {Array<*>} hitDetectionInstructions The rendering hit detection instructions.
   * @property {Array<number>} coordinates The array of all coordinates.
   * @property {!Object<string, import("../canvas.js").TextState>} textStates The text states (decluttering).
   * @property {!Object<string, import("../canvas.js").FillState>} fillStates The fill states (decluttering).
   * @property {!Object<string, import("../canvas.js").StrokeState>} strokeStates The stroke states (decluttering).
   */
  
  /**
   * @type {import("../../extent.js").Extent}
   */
  const tmpExtent = createEmpty();
  
  /**
   * @type {!import("../../transform.js").Transform}
   */
  const tmpTransform = createTransform();
  
  /** @type {import("../../coordinate.js").Coordinate} */
  const p1 = [];
  /** @type {import("../../coordinate.js").Coordinate} */
  const p2 = [];
  /** @type {import("../../coordinate.js").Coordinate} */
  const p3 = [];
  /** @type {import("../../coordinate.js").Coordinate} */
  const p4 = [];
  
  
  class Executor extends Disposable {
    /**
     * @param {number} resolution Resolution.
     * @param {number} pixelRatio Pixel ratio.
     * @param {boolean} overlaps The replay can have overlapping geometries.
     * @param {SerializableInstructions} instructions The serializable instructions
     */
    constructor(resolution, pixelRatio, overlaps, instructions) {
      super();
  
      /**
       * @protected
       * @type {boolean}
       */
      this.overlaps = overlaps;
  
      /**
       * @protected
       * @type {number}
       */
      this.pixelRatio = pixelRatio;
  
      /**
       * @protected
       * @const
       * @type {number}
       */
      this.resolution = resolution;
  
      /**
       * @private
       * @type {boolean}
       */
      this.alignFill_;
  
      /**
       * @type {Array<*>}
       */
      this.declutterItems = [];
  
      /**
       * @protected
       * @type {Array<*>}
       */
      this.instructions = instructions.instructions;
  
      /**
       * @protected
       * @type {Array<number>}
       */
      this.coordinates = instructions.coordinates;
  
      /**
       * @private
       * @type {!Object<number,import("../../coordinate.js").Coordinate|Array<import("../../coordinate.js").Coordinate>|Array<Array<import("../../coordinate.js").Coordinate>>>}
       */
      this.coordinateCache_ = {};
  
      /**
       * @private
       * @type {!import("../../transform.js").Transform}
       */
      this.renderedTransform_ = createTransform();
  
      /**
       * @protected
       * @type {Array<*>}
       */
      this.hitDetectionInstructions = instructions.hitDetectionInstructions;
  
      /**
       * @private
       * @type {Array<number>}
       */
      this.pixelCoordinates_ = null;
  
      /**
       * @private
       * @type {number}
       */
      this.viewRotation_ = 0;
  
      /**
       * @type {!Object<string, import("../canvas.js").FillState>}
       */
      this.fillStates = instructions.fillStates || {};
  
      /**
       * @type {!Object<string, import("../canvas.js").StrokeState>}
       */
      this.strokeStates = instructions.strokeStates || {};
  
      /**
       * @type {!Object<string, import("../canvas.js").TextState>}
       */
      this.textStates = instructions.textStates || {};
  
      /**
       * @private
       * @type {Object<string, Object<string, number>>}
       */
      this.widths_ = {};
    }
  
    /**
     * @inheritDoc
     */
    disposeInternal() {
      labelCache.release(this);
      super.disposeInternal();
    }
  
  
    /**
     * @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 (context.setLineDash && 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, this);
    }
  
    /**
     * @param {CanvasRenderingContext2D} context Context.
     * @param {import("../../coordinate.js").Coordinate} p1 1st point of the background box.
     * @param {import("../../coordinate.js").Coordinate} p2 2nd point of the background box.
     * @param {import("../../coordinate.js").Coordinate} p3 3rd point of the background box.
     * @param {import("../../coordinate.js").Coordinate} p4 4th point of the background box.
     * @param {Array<*>} fillInstruction Fill instruction.
     * @param {Array<*>} strokeInstruction Stroke instruction.
     */
    replayTextBackground_(context, p1, p2, p3, p4, fillInstruction, strokeInstruction) {
      context.beginPath();
      context.moveTo.apply(context, p1);
      context.lineTo.apply(context, p2);
      context.lineTo.apply(context, p3);
      context.lineTo.apply(context, p4);
      context.lineTo.apply(context, p1);
      if (fillInstruction) {
        this.alignFill_ = /** @type {boolean} */ (fillInstruction[2]);
        this.fill_(context);
      }
      if (strokeInstruction) {
        this.setStrokeStyle_(context, /** @type {Array<*>} */ (strokeInstruction));
        context.stroke();
      }
    }
  
    /**
     * @param {CanvasRenderingContext2D} context Context.
     * @param {number} x X.
     * @param {number} y Y.
     * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image.
     * @param {number} anchorX Anchor X.
     * @param {number} anchorY Anchor Y.
     * @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group.
     * @param {number} height Height.
     * @param {number} opacity Opacity.
     * @param {number} originX Origin X.
     * @param {number} originY Origin Y.
     * @param {number} rotation Rotation.
     * @param {number} scale Scale.
     * @param {boolean} snapToPixel Snap to pixel.
     * @param {number} width Width.
     * @param {Array<number>} padding Padding.
     * @param {Array<*>} fillInstruction Fill instruction.
     * @param {Array<*>} strokeInstruction Stroke instruction.
     */
    replayImage_(
      context,
      x,
      y,
      image,
      anchorX,
      anchorY,
      declutterGroup,
      height,
      opacity,
      originX,
      originY,
      rotation,
      scale,
      snapToPixel,
      width,
      padding,
      fillInstruction,
      strokeInstruction
    ) {
      const fillStroke = fillInstruction || strokeInstruction;
      anchorX *= scale;
      anchorY *= scale;
      x -= anchorX;
      y -= anchorY;
  
      const w = (width + originX > image.width) ? image.width - originX : width;
      const h = (height + originY > image.height) ? image.height - originY : height;
      const boxW = padding[3] + w * scale + padding[1];
      const boxH = padding[0] + h * scale + padding[2];
      const boxX = x - padding[3];
      const boxY = y - padding[0];
  
      if (fillStroke || rotation !== 0) {
        p1[0] = p4[0] = boxX;
        p1[1] = p2[1] = boxY;
        p2[0] = p3[0] = boxX + boxW;
        p3[1] = p4[1] = boxY + boxH;
      }
  
      let transform = null;
      if (rotation !== 0) {
        const centerX = x + anchorX;
        const centerY = y + anchorY;
        transform = composeTransform(tmpTransform, centerX, centerY, 1, 1, rotation, -centerX, -centerY);
  
        applyTransform(tmpTransform, p1);
        applyTransform(tmpTransform, p2);
        applyTransform(tmpTransform, p3);
        applyTransform(tmpTransform, p4);
        createOrUpdate(
          Math.min(p1[0], p2[0], p3[0], p4[0]),
          Math.min(p1[1], p2[1], p3[1], p4[1]),
          Math.max(p1[0], p2[0], p3[0], p4[0]),
          Math.max(p1[1], p2[1], p3[1], p4[1]),
          tmpExtent
        );
      } else {
        createOrUpdate(boxX, boxY, boxX + boxW, boxY + boxH, tmpExtent);
      }
      const canvas = context.canvas;
      const strokePadding = strokeInstruction ? (strokeInstruction[2] * scale / 2) : 0;
      const intersects =
          tmpExtent[0] - strokePadding <= canvas.width && tmpExtent[2] + strokePadding >= 0 &&
          tmpExtent[1] - strokePadding <= canvas.height && tmpExtent[3] + strokePadding >= 0;
  
      if (snapToPixel) {
        x = Math.round(x);
        y = Math.round(y);
      }
  
      if (declutterGroup) {
        if (!intersects && declutterGroup[4] == 1) {
          return;
        }
        extend(declutterGroup, tmpExtent);
        const declutterArgs = intersects ?
          [context, transform ? transform.slice(0) : null, opacity, image, originX, originY, w, h, x, y, scale] :
          null;
        if (declutterArgs && fillStroke) {
          declutterArgs.push(fillInstruction, strokeInstruction, p1, p2, p3, p4);
        }
        declutterGroup.push(declutterArgs);
      } else if (intersects) {
        if (fillStroke) {
          this.replayTextBackground_(context, p1, p2, p3, p4,
            /** @type {Array<*>} */ (fillInstruction),
            /** @type {Array<*>} */ (strokeInstruction));
        }
        drawImage(context, transform, opacity, image, originX, originY, w, h, x, y, scale);
      }
    }
  
    /**
     * @private
     * @param {CanvasRenderingContext2D} context Context.
     */
    fill_(context) {
      if (this.alignFill_) {
        const origin = applyTransform(this.renderedTransform_, [0, 0]);
        const repeatSize = 512 * this.pixelRatio;
        context.save();
        context.translate(origin[0] % repeatSize, origin[1] % repeatSize);
        context.rotate(this.viewRotation_);
      }
      context.fill();
      if (this.alignFill_) {
        context.restore();
      }
    }
  
    /**
     * @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 (context.setLineDash) {
        context.lineDashOffset = /** @type {number} */ (instruction[7]);
        context.setLineDash(/** @type {Array<number>} */ (instruction[6]));
      }
    }
  
    /**
     * @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group.
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     * @param {?} declutterTree Declutter tree.
     * @return {?} Declutter tree.
     */
    renderDeclutter(declutterGroup, feature, declutterTree) {
      if (declutterGroup && declutterGroup.length > 5) {
        const groupCount = declutterGroup[4];
        if (groupCount == 1 || groupCount == declutterGroup.length - 5) {
          /** @type {import("../../structs/RBush.js").Entry} */
          const box = {
            minX: /** @type {number} */ (declutterGroup[0]),
            minY: /** @type {number} */ (declutterGroup[1]),
            maxX: /** @type {number} */ (declutterGroup[2]),
            maxY: /** @type {number} */ (declutterGroup[3]),
            value: feature
          };
          if (!declutterTree) {
            declutterTree = rbush(9, undefined);
          }
          if (!declutterTree.collides(box)) {
            declutterTree.insert(box);
            for (let j = 5, jj = declutterGroup.length; j < jj; ++j) {
              const declutterData = /** @type {Array} */ (declutterGroup[j]);
              if (declutterData) {
                if (declutterData.length > 11) {
                  this.replayTextBackground_(declutterData[0],
                    declutterData[13], declutterData[14], declutterData[15], declutterData[16],
                    declutterData[11], declutterData[12]);
                }
                drawImage.apply(undefined, declutterData);
              }
            }
          }
          declutterGroup.length = 5;
          createOrUpdateEmpty(declutterGroup);
        }
      }
      return declutterTree;
    }
  
    /**
     * @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.
     * @param {import("../../transform.js").Transform} transform Transform.
     * @param {Object<string, boolean>} skippedFeaturesHash Ids of features
     *     to skip.
     * @param {Array<*>} instructions Instructions array.
     * @param {boolean} snapToPixel Snap point symbols and text to integer pixels.
     * @param {function(import("../../Feature.js").FeatureLike): T|undefined} featureCallback Feature callback.
     * @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this
     *     extent.
     * @return {T|undefined} Callback result.
     * @template T
     */
    execute_(
      context,
      transform,
      skippedFeaturesHash,
      instructions,
      snapToPixel,
      featureCallback,
      opt_hitExtent
    ) {
      this.declutterItems.length = 0;
      /** @type {Array<number>} */
      let pixelCoordinates;
      if (this.pixelCoordinates_ && equals(transform, this.renderedTransform_)) {
        pixelCoordinates = this.pixelCoordinates_;
      } else {
        if (!this.pixelCoordinates_) {
          this.pixelCoordinates_ = [];
        }
        pixelCoordinates = transform2D(
          this.coordinates, 0, this.coordinates.length, 2,
          transform, this.pixelCoordinates_);
        transformSetFromArray(this.renderedTransform_, transform);
      }
      const skipFeatures = !isEmpty(skippedFeaturesHash);
      let i = 0; // instruction index
      const ii = instructions.length; // end of instructions
      let d = 0; // data index
      let dd; // end of per-instruction data
      let anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup, image, text, textKey;
      let strokeKey, fillKey;
      let pendingFill = 0;
      let pendingStroke = 0;
      let lastFillInstruction = null;
      let lastStrokeInstruction = null;
      const coordinateCache = this.coordinateCache_;
      const viewRotation = this.viewRotation_;
  
      const state = /** @type {import("../../render.js").State} */ ({
        context: context,
        pixelRatio: this.pixelRatio,
        resolution: this.resolution,
        rotation: viewRotation
      });
  
      // When the batch size gets too big, performance decreases. 200 is a good
      // balance between batch size and number of fill/stroke instructions.
      const batchSize = this.instructions != instructions || this.overlaps ? 0 : 200;
      let /** @type {import("../../Feature.js").FeatureLike} */ feature;
      let x, y;
      while (i < ii) {
        const instruction = instructions[i];
        const type = /** @type {CanvasInstruction} */ (instruction[0]);
        switch (type) {
          case CanvasInstruction.BEGIN_GEOMETRY:
            feature = /** @type {import("../../Feature.js").FeatureLike} */ (instruction[1]);
            if ((skipFeatures && skippedFeaturesHash[getUid(feature)]) || !feature.getGeometry()) {
              i = /** @type {number} */ (instruction[2]);
            } else if (opt_hitExtent !== undefined && !intersects(
              opt_hitExtent, feature.getGeometry().getExtent())) {
              i = /** @type {number} */ (instruction[2]) + 1;
            } else {
              ++i;
            }
            break;
          case CanvasInstruction.BEGIN_PATH:
            if (pendingFill > batchSize) {
              this.fill_(context);
              pendingFill = 0;
            }
            if (pendingStroke > batchSize) {
              context.stroke();
              pendingStroke = 0;
            }
            if (!pendingFill && !pendingStroke) {
              context.beginPath();
              prevX = prevY = NaN;
            }
            ++i;
            break;
          case CanvasInstruction.CIRCLE:
            d = /** @type {number} */ (instruction[1]);
            const x1 = pixelCoordinates[d];
            const y1 = pixelCoordinates[d + 1];
            const x2 = pixelCoordinates[d + 2];
            const y2 = pixelCoordinates[d + 3];
            const dx = x2 - x1;
            const dy = y2 - y1;
            const r = Math.sqrt(dx * dx + dy * dy);
            context.moveTo(x1 + r, y1);
            context.arc(x1, y1, r, 0, 2 * Math.PI, true);
            ++i;
            break;
          case CanvasInstruction.CLOSE_PATH:
            context.closePath();
            ++i;
            break;
          case CanvasInstruction.CUSTOM:
            d = /** @type {number} */ (instruction[1]);
            dd = instruction[2];
            const geometry = /** @type {import("../../geom/SimpleGeometry.js").default} */ (instruction[3]);
            const renderer = instruction[4];
            const fn = instruction.length == 6 ? instruction[5] : undefined;
            state.geometry = geometry;
            state.feature = feature;
            if (!(i in coordinateCache)) {
              coordinateCache[i] = [];
            }
            const coords = coordinateCache[i];
            if (fn) {
              fn(pixelCoordinates, d, dd, 2, coords);
            } else {
              coords[0] = pixelCoordinates[d];
              coords[1] = pixelCoordinates[d + 1];
              coords.length = 2;
            }
            renderer(coords, state);
            ++i;
            break;
          case CanvasInstruction.DRAW_IMAGE:
            d = /** @type {number} */ (instruction[1]);
            dd = /** @type {number} */ (instruction[2]);
            image = /** @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} */ (instruction[3]);
  
            // Remaining arguments in DRAW_IMAGE are in alphabetical order
            anchorX = /** @type {number} */ (instruction[4]);
            anchorY = /** @type {number} */ (instruction[5]);
            declutterGroup = featureCallback ? null : /** @type {import("../canvas.js").DeclutterGroup} */ (instruction[6]);
            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]);
            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) {
              padding = /** @type {Array<number>} */ (instruction[15]);
              backgroundFill = /** @type {boolean} */ (instruction[16]);
              backgroundStroke = /** @type {boolean} */ (instruction[17]);
            } else {
              padding = defaultPadding;
              backgroundFill = backgroundStroke = false;
            }
  
            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,
                snapToPixel, width, padding,
                backgroundFill ? /** @type {Array<*>} */ (lastFillInstruction) : null,
                backgroundStroke ? /** @type {Array<*>} */ (lastStrokeInstruction) : null);
            }
            this.declutterItems.push(this, declutterGroup, feature);
            ++i;
            break;
          case CanvasInstruction.DRAW_CHARS:
            const begin = /** @type {number} */ (instruction[1]);
            const end = /** @type {number} */ (instruction[2]);
            const baseline = /** @type {number} */ (instruction[3]);
            declutterGroup = featureCallback ? null : /** @type {import("../canvas.js").DeclutterGroup} */ (instruction[4]);
            const overflow = /** @type {number} */ (instruction[5]);
            fillKey = /** @type {string} */ (instruction[6]);
            const maxAngle = /** @type {number} */ (instruction[7]);
            const measurePixelRatio = /** @type {number} */ (instruction[8]);
            const offsetY = /** @type {number} */ (instruction[9]);
            strokeKey = /** @type {string} */ (instruction[10]);
            const strokeWidth = /** @type {number} */ (instruction[11]);
            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 * measurePixelRatio;
  
            let cachedWidths;
            if (font in this.widths_) {
              cachedWidths = this.widths_[font];
            } else {
              cachedWidths = this.widths_[font] = {};
            }
  
            const pathLength = lineStringLength(pixelCoordinates, begin, end, 2);
            const textLength = textScale * measureAndCacheTextWidth(font, text, cachedWidths);
            if (overflow || textLength <= pathLength) {
              const textAlign = this.textStates[textKey].textAlign;
              const startM = (pathLength - textLength) * TEXT_ALIGN[textAlign];
              const parts = drawTextOnPath(
                pixelCoordinates, begin, end, 2, text, startM, maxAngle, textScale, measureAndCacheTextWidth, font, cachedWidths);
              if (parts) {
                let c, cc, chars, label, part;
                if (strokeKey) {
                  for (c = 0, cc = parts.length; c < cc; ++c) {
                    part = parts[c]; // x, y, anchorX, rotation, chunk
                    chars = /** @type {string} */ (part[4]);
                    label = 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]), pixelRatioScale, false, label.width,
                      defaultPadding, null, null);
                  }
                }
                if (fillKey) {
                  for (c = 0, cc = parts.length; c < cc; ++c) {
                    part = parts[c]; // x, y, anchorX, rotation, chunk
                    chars = /** @type {string} */ (part[4]);
                    label = 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]), pixelRatioScale, false, label.width,
                      defaultPadding, null, null);
                  }
                }
              }
            }
            this.declutterItems.push(this, declutterGroup, feature);
            ++i;
            break;
          case CanvasInstruction.END_GEOMETRY:
            if (featureCallback !== undefined) {
              feature = /** @type {import("../../Feature.js").FeatureLike} */ (instruction[1]);
              const result = featureCallback(feature);
              if (result) {
                return result;
              }
            }
            ++i;
            break;
          case CanvasInstruction.FILL:
            if (batchSize) {
              pendingFill++;
            } else {
              this.fill_(context);
            }
            ++i;
            break;
          case CanvasInstruction.MOVE_TO_LINE_TO:
            d = /** @type {number} */ (instruction[1]);
            dd = /** @type {number} */ (instruction[2]);
            x = pixelCoordinates[d];
            y = pixelCoordinates[d + 1];
            roundX = (x + 0.5) | 0;
            roundY = (y + 0.5) | 0;
            if (roundX !== prevX || roundY !== prevY) {
              context.moveTo(x, y);
              prevX = roundX;
              prevY = roundY;
            }
            for (d += 2; d < dd; d += 2) {
              x = pixelCoordinates[d];
              y = pixelCoordinates[d + 1];
              roundX = (x + 0.5) | 0;
              roundY = (y + 0.5) | 0;
              if (d == dd - 2 || roundX !== prevX || roundY !== prevY) {
                context.lineTo(x, y);
                prevX = roundX;
                prevY = roundY;
              }
            }
            ++i;
            break;
          case CanvasInstruction.SET_FILL_STYLE:
            lastFillInstruction = instruction;
            this.alignFill_ = instruction[2];
  
            if (pendingFill) {
              this.fill_(context);
              pendingFill = 0;
              if (pendingStroke) {
                context.stroke();
                pendingStroke = 0;
              }
            }
  
            context.fillStyle = /** @type {import("../../colorlike.js").ColorLike} */ (instruction[1]);
            ++i;
            break;
          case CanvasInstruction.SET_STROKE_STYLE:
            lastStrokeInstruction = instruction;
            if (pendingStroke) {
              context.stroke();
              pendingStroke = 0;
            }
            this.setStrokeStyle_(context, /** @type {Array<*>} */ (instruction));
            ++i;
            break;
          case CanvasInstruction.STROKE:
            if (batchSize) {
              pendingStroke++;
            } else {
              context.stroke();
            }
            ++i;
            break;
          default:
            ++i; // consume the instruction anyway, to avoid an infinite loop
            break;
        }
      }
      if (pendingFill) {
        this.fill_(context);
      }
      if (pendingStroke) {
        context.stroke();
      }
      return undefined;
    }
  
    /**
     * @param {CanvasRenderingContext2D} context Context.
     * @param {import("../../transform.js").Transform} transform Transform.
     * @param {number} viewRotation View rotation.
     * @param {Object<string, boolean>} skippedFeaturesHash Ids of features
     *     to skip.
     * @param {boolean} snapToPixel Snap point symbols and text to integer pixels.
     */
    execute(context, transform, viewRotation, skippedFeaturesHash, snapToPixel) {
      this.viewRotation_ = viewRotation;
      this.execute_(context, transform,
        skippedFeaturesHash, this.instructions, snapToPixel, undefined, undefined);
    }
  
    /**
     * @param {CanvasRenderingContext2D} context Context.
     * @param {import("../../transform.js").Transform} transform Transform.
     * @param {number} viewRotation View rotation.
     * @param {Object<string, boolean>} skippedFeaturesHash Ids of features
     *     to skip.
     * @param {function(import("../../Feature.js").FeatureLike): T=} opt_featureCallback
     *     Feature callback.
     * @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this
     *     extent.
     * @return {T|undefined} Callback result.
     * @template T
     */
    executeHitDetection(
      context,
      transform,
      viewRotation,
      skippedFeaturesHash,
      opt_featureCallback,
      opt_hitExtent
    ) {
      this.viewRotation_ = viewRotation;
      return this.execute_(context, transform, skippedFeaturesHash,
        this.hitDetectionInstructions, true, opt_featureCallback, opt_hitExtent);
    }
  }
  
  
  export default Executor;
  