(function () {
  'use strict';

  /**
   * @module ol/CollectionEventType
   */

  /**
   * @enum {string}
   */
  var CollectionEventType = {
    /**
     * Triggered when an item is added to the collection.
     * @event module:ol/Collection.CollectionEvent#add
     * @api
     */
    ADD: 'add',
    /**
     * Triggered when an item is removed from the collection.
     * @event module:ol/Collection.CollectionEvent#remove
     * @api
     */
    REMOVE: 'remove',
  };

  /**
   * @module ol/ObjectEventType
   */

  /**
   * @enum {string}
   */
  var ObjectEventType = {
    /**
     * Triggered when a property is changed.
     * @event module:ol/Object.ObjectEvent#propertychange
     * @api
     */
    PROPERTYCHANGE: 'propertychange',
  };

  /**
   * @typedef {'propertychange'} Types
   */

  /**
   * @module ol/events/EventType
   */

  /**
   * @enum {string}
   * @const
   */
  var EventType = {
    /**
     * Generic change event. Triggered when the revision counter is increased.
     * @event module:ol/events/Event~BaseEvent#change
     * @api
     */
    CHANGE: 'change',

    /**
     * Generic error event. Triggered when an error occurs.
     * @event module:ol/events/Event~BaseEvent#error
     * @api
     */
    ERROR: 'error',

    CONTEXTMENU: 'contextmenu',
    CLICK: 'click',
    DBLCLICK: 'dblclick',
    KEYDOWN: 'keydown',
    KEYPRESS: 'keypress',
    LOAD: 'load',
    TOUCHMOVE: 'touchmove',
    WHEEL: 'wheel',
  };

  /**
   * @module ol/Disposable
   */

  /**
   * @classdesc
   * Objects that need to clean up after themselves.
   */
  class Disposable {
    constructor() {
      /**
       * The object has already been disposed.
       * @type {boolean}
       * @protected
       */
      this.disposed = false;
    }

    /**
     * Clean up.
     */
    dispose() {
      if (!this.disposed) {
        this.disposed = true;
        this.disposeInternal();
      }
    }

    /**
     * Extension point for disposable objects.
     * @protected
     */
    disposeInternal() {}
  }

  /**
   * @module ol/array
   */

  /**
   * Performs a binary search on the provided sorted list and returns the index of the item if found. If it can't be found it'll return -1.
   * https://github.com/darkskyapp/binary-search
   *
   * @param {Array<*>} haystack Items to search through.
   * @param {*} needle The item to look for.
   * @param {Function} [comparator] Comparator function.
   * @return {number} The index of the item if found, -1 if not.
   */
  function binarySearch(haystack, needle, comparator) {
    let mid, cmp;
    comparator = comparator || ascending;
    let low = 0;
    let high = haystack.length;
    let found = false;

    while (low < high) {
      /* Note that "(low + high) >>> 1" may overflow, and results in a typecast
       * to double (which gives the wrong results). */
      mid = low + ((high - low) >> 1);
      cmp = +comparator(haystack[mid], needle);

      if (cmp < 0.0) {
        /* Too low. */
        low = mid + 1;
      } else {
        /* Key found or too high */
        high = mid;
        found = !cmp;
      }
    }

    /* Key not found. */
    return found ? low : ~low;
  }

  /**
   * Compare function sorting arrays in ascending order.  Safe to use for numeric values.
   * @param {*} a The first object to be compared.
   * @param {*} b The second object to be compared.
   * @return {number} A negative number, zero, or a positive number as the first
   *     argument is less than, equal to, or greater than the second.
   */
  function ascending(a, b) {
    return a > b ? 1 : a < b ? -1 : 0;
  }

  /**
   * Compare function sorting arrays in descending order.  Safe to use for numeric values.
   * @param {*} a The first object to be compared.
   * @param {*} b The second object to be compared.
   * @return {number} A negative number, zero, or a positive number as the first
   *     argument is greater than, equal to, or less than the second.
   */
  function descending(a, b) {
    return a < b ? 1 : a > b ? -1 : 0;
  }

  /**
   * {@link module:ol/tilegrid/TileGrid~TileGrid#getZForResolution} can use a function
   * of this type to determine which nearest resolution to use.
   *
   * This function takes a `{number}` representing a value between two array entries,
   * a `{number}` representing the value of the nearest higher entry and
   * a `{number}` representing the value of the nearest lower entry
   * as arguments and returns a `{number}`. If a negative number or zero is returned
   * the lower value will be used, if a positive number is returned the higher value
   * will be used.
   * @typedef {function(number, number, number): number} NearestDirectionFunction
   * @api
   */

  /**
   * @param {Array<number>} arr Array in descending order.
   * @param {number} target Target.
   * @param {number|NearestDirectionFunction} direction
   *    0 means return the nearest,
   *    > 0 means return the largest nearest,
   *    < 0 means return the smallest nearest.
   * @return {number} Index.
   */
  function linearFindNearest(arr, target, direction) {
    if (arr[0] <= target) {
      return 0;
    }

    const n = arr.length;
    if (target <= arr[n - 1]) {
      return n - 1;
    }

    if (typeof direction === 'function') {
      for (let i = 1; i < n; ++i) {
        const candidate = arr[i];
        if (candidate === target) {
          return i;
        }
        if (candidate < target) {
          if (direction(target, arr[i - 1], candidate) > 0) {
            return i - 1;
          }
          return i;
        }
      }
      return n - 1;
    }

    if (direction > 0) {
      for (let i = 1; i < n; ++i) {
        if (arr[i] < target) {
          return i - 1;
        }
      }
      return n - 1;
    }

    if (direction < 0) {
      for (let i = 1; i < n; ++i) {
        if (arr[i] <= target) {
          return i;
        }
      }
      return n - 1;
    }

    for (let i = 1; i < n; ++i) {
      if (arr[i] == target) {
        return i;
      }
      if (arr[i] < target) {
        if (arr[i - 1] - target < target - arr[i]) {
          return i - 1;
        }
        return i;
      }
    }
    return n - 1;
  }

  /**
   * @param {Array<*>} arr Array.
   * @param {number} begin Begin index.
   * @param {number} end End index.
   */
  function reverseSubArray(arr, begin, end) {
    while (begin < end) {
      const tmp = arr[begin];
      arr[begin] = arr[end];
      arr[end] = tmp;
      ++begin;
      --end;
    }
  }

  /**
   * @param {Array<VALUE>} arr The array to modify.
   * @param {!Array<VALUE>|VALUE} data The elements or arrays of elements to add to arr.
   * @template VALUE
   */
  function extend$2(arr, data) {
    const extension = Array.isArray(data) ? data : [data];
    const length = extension.length;
    for (let i = 0; i < length; i++) {
      arr[arr.length] = extension[i];
    }
  }

  /**
   * @param {Array<any>|Uint8ClampedArray} arr1 The first array to compare.
   * @param {Array<any>|Uint8ClampedArray} arr2 The second array to compare.
   * @return {boolean} Whether the two arrays are equal.
   */
  function equals$2(arr1, arr2) {
    const len1 = arr1.length;
    if (len1 !== arr2.length) {
      return false;
    }
    for (let i = 0; i < len1; i++) {
      if (arr1[i] !== arr2[i]) {
        return false;
      }
    }
    return true;
  }

  /**
   * @param {Array<*>} arr The array to test.
   * @param {Function} [func] Comparison function.
   * @param {boolean} [strict] Strictly sorted (default false).
   * @return {boolean} Return index.
   */
  function isSorted(arr, func, strict) {
    const compare = func || ascending;
    return arr.every(function (currentVal, index) {
      if (index === 0) {
        return true;
      }
      const res = compare(arr[index - 1], currentVal);
      return !(res > 0 || (res === 0));
    });
  }

  /**
   * @module ol/functions
   */


  /**
   * Always returns true.
   * @return {boolean} true.
   */
  function TRUE() {
    return true;
  }

  /**
   * Always returns false.
   * @return {boolean} false.
   */
  function FALSE() {
    return false;
  }

  /**
   * A reusable function, used e.g. as a default for callbacks.
   *
   * @return {void} Nothing.
   */
  function VOID() {}

  /**
   * Wrap a function in another function that remembers the last return.  If the
   * returned function is called twice in a row with the same arguments and the same
   * this object, it will return the value from the first call in the second call.
   *
   * @param {function(...any): ReturnType} fn The function to memoize.
   * @return {function(...any): ReturnType} The memoized function.
   * @template ReturnType
   */
  function memoizeOne(fn) {
    /** @type {ReturnType} */
    let lastResult;

    /** @type {Array<any>|undefined} */
    let lastArgs;

    let lastThis;

    /**
     * @this {*} Only need to know if `this` changed, don't care what type
     * @return {ReturnType} Memoized value
     */
    return function () {
      const nextArgs = Array.prototype.slice.call(arguments);
      if (!lastArgs || this !== lastThis || !equals$2(nextArgs, lastArgs)) {
        lastThis = this;
        lastArgs = nextArgs;
        lastResult = fn.apply(this, arguments);
      }
      return lastResult;
    };
  }

  /**
   * @template T
   * @param {function(): (T | Promise<T>)} getter A function that returns a value or a promise for a value.
   * @return {Promise<T>} A promise for the value.
   */
  function toPromise(getter) {
    function promiseGetter() {
      let value;
      try {
        value = getter();
      } catch (err) {
        return Promise.reject(err);
      }
      if (value instanceof Promise) {
        return value;
      }
      return Promise.resolve(value);
    }
    return promiseGetter();
  }

  /**
   * @module ol/obj
   */

  /**
   * Removes all properties from an object.
   * @param {Object<string, unknown>} object The object to clear.
   */
  function clear(object) {
    for (const property in object) {
      delete object[property];
    }
  }

  /**
   * Determine if an object has any properties.
   * @param {Object} object The object to check.
   * @return {boolean} The object is empty.
   */
  function isEmpty$1(object) {
    let property;
    for (property in object) {
      return false;
    }
    return !property;
  }

  /**
   * @module ol/events/Event
   */

  /**
   * @classdesc
   * Stripped down implementation of the W3C DOM Level 2 Event interface.
   * See https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-interface.
   *
   * This implementation only provides `type` and `target` properties, and
   * `stopPropagation` and `preventDefault` methods. It is meant as base class
   * for higher level events defined in the library, and works with
   * {@link module:ol/events/Target~Target}.
   */
  class BaseEvent {
    /**
     * @param {string} type Type.
     */
    constructor(type) {
      /**
       * @type {boolean}
       */
      this.propagationStopped;

      /**
       * @type {boolean}
       */
      this.defaultPrevented;

      /**
       * The event type.
       * @type {string}
       * @api
       */
      this.type = type;

      /**
       * The event target.
       * @type {Object}
       * @api
       */
      this.target = null;
    }

    /**
     * Prevent default. This means that no emulated `click`, `singleclick` or `doubleclick` events
     * will be fired.
     * @api
     */
    preventDefault() {
      this.defaultPrevented = true;
    }

    /**
     * Stop event propagation.
     * @api
     */
    stopPropagation() {
      this.propagationStopped = true;
    }
  }

  /**
   * @module ol/events/Target
   */

  /**
   * @typedef {EventTarget|Target} EventTargetLike
   */

  /**
   * @classdesc
   * A simplified implementation of the W3C DOM Level 2 EventTarget interface.
   * See https://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget.
   *
   * There are two important simplifications compared to the specification:
   *
   * 1. The handling of `useCapture` in `addEventListener` and
   *    `removeEventListener`. There is no real capture model.
   * 2. The handling of `stopPropagation` and `preventDefault` on `dispatchEvent`.
   *    There is no event target hierarchy. When a listener calls
   *    `stopPropagation` or `preventDefault` on an event object, it means that no
   *    more listeners after this one will be called. Same as when the listener
   *    returns false.
   */
  class Target extends Disposable {
    /**
     * @param {*} [target] Default event target for dispatched events.
     */
    constructor(target) {
      super();

      /**
       * @private
       * @type {*}
       */
      this.eventTarget_ = target;

      /**
       * @private
       * @type {Object<string, number>|null}
       */
      this.pendingRemovals_ = null;

      /**
       * @private
       * @type {Object<string, number>|null}
       */
      this.dispatching_ = null;

      /**
       * @private
       * @type {Object<string, Array<import("../events.js").Listener>>|null}
       */
      this.listeners_ = null;
    }

    /**
     * @param {string} type Type.
     * @param {import("../events.js").Listener} listener Listener.
     */
    addEventListener(type, listener) {
      if (!type || !listener) {
        return;
      }
      const listeners = this.listeners_ || (this.listeners_ = {});
      const listenersForType = listeners[type] || (listeners[type] = []);
      if (!listenersForType.includes(listener)) {
        listenersForType.push(listener);
      }
    }

    /**
     * Dispatches an event and calls all listeners listening for events
     * of this type. The event parameter can either be a string or an
     * Object with a `type` property.
     *
     * @param {import("./Event.js").default|string} event Event object.
     * @return {boolean|undefined} `false` if anyone called preventDefault on the
     *     event object or if any of the listeners returned false.
     * @api
     */
    dispatchEvent(event) {
      const isString = typeof event === 'string';
      const type = isString ? event : event.type;
      const listeners = this.listeners_ && this.listeners_[type];
      if (!listeners) {
        return;
      }

      const evt = isString ? new BaseEvent(event) : /** @type {Event} */ (event);
      if (!evt.target) {
        evt.target = this.eventTarget_ || this;
      }
      const dispatching = this.dispatching_ || (this.dispatching_ = {});
      const pendingRemovals =
        this.pendingRemovals_ || (this.pendingRemovals_ = {});
      if (!(type in dispatching)) {
        dispatching[type] = 0;
        pendingRemovals[type] = 0;
      }
      ++dispatching[type];
      let propagate;
      for (let i = 0, ii = listeners.length; i < ii; ++i) {
        if ('handleEvent' in listeners[i]) {
          propagate = /** @type {import("../events.js").ListenerObject} */ (
            listeners[i]
          ).handleEvent(evt);
        } else {
          propagate = /** @type {import("../events.js").ListenerFunction} */ (
            listeners[i]
          ).call(this, evt);
        }
        if (propagate === false || evt.propagationStopped) {
          propagate = false;
          break;
        }
      }
      if (--dispatching[type] === 0) {
        let pr = pendingRemovals[type];
        delete pendingRemovals[type];
        while (pr--) {
          this.removeEventListener(type, VOID);
        }
        delete dispatching[type];
      }
      return propagate;
    }

    /**
     * Clean up.
     * @override
     */
    disposeInternal() {
      this.listeners_ && clear(this.listeners_);
    }

    /**
     * Get the listeners for a specified event type. Listeners are returned in the
     * order that they will be called in.
     *
     * @param {string} type Type.
     * @return {Array<import("../events.js").Listener>|undefined} Listeners.
     */
    getListeners(type) {
      return (this.listeners_ && this.listeners_[type]) || undefined;
    }

    /**
     * @param {string} [type] Type. If not provided,
     *     `true` will be returned if this event target has any listeners.
     * @return {boolean} Has listeners.
     */
    hasListener(type) {
      if (!this.listeners_) {
        return false;
      }
      return type
        ? type in this.listeners_
        : Object.keys(this.listeners_).length > 0;
    }

    /**
     * @param {string} type Type.
     * @param {import("../events.js").Listener} listener Listener.
     */
    removeEventListener(type, listener) {
      if (!this.listeners_) {
        return;
      }
      const listeners = this.listeners_[type];
      if (!listeners) {
        return;
      }
      const index = listeners.indexOf(listener);
      if (index !== -1) {
        if (this.pendingRemovals_ && type in this.pendingRemovals_) {
          // make listener a no-op, and remove later in #dispatchEvent()
          listeners[index] = VOID;
          ++this.pendingRemovals_[type];
        } else {
          listeners.splice(index, 1);
          if (listeners.length === 0) {
            delete this.listeners_[type];
          }
        }
      }
    }
  }

  /**
   * @module ol/events
   */

  /**
   * Key to use with {@link module:ol/Observable.unByKey}.
   * @typedef {Object} EventsKey
   * @property {ListenerFunction} listener Listener.
   * @property {import("./events/Target.js").EventTargetLike} target Target.
   * @property {string} type Type.
   * @api
   */

  /**
   * Listener function. This function is called with an event object as argument.
   * When the function returns `false`, event propagation will stop.
   *
   * @typedef {function((Event|import("./events/Event.js").default)): (void|boolean)} ListenerFunction
   * @api
   */

  /**
   * @typedef {Object} ListenerObject
   * @property {ListenerFunction} handleEvent HandleEvent listener function.
   */

  /**
   * @typedef {ListenerFunction|ListenerObject} Listener
   */

  /**
   * Registers an event listener on an event target. Inspired by
   * https://google.github.io/closure-library/api/source/closure/goog/events/events.js.src.html
   *
   * This function efficiently binds a `listener` to a `this` object, and returns
   * a key for use with {@link module:ol/events.unlistenByKey}.
   *
   * @param {import("./events/Target.js").EventTargetLike} target Event target.
   * @param {string} type Event type.
   * @param {ListenerFunction} listener Listener.
   * @param {Object} [thisArg] Object referenced by the `this` keyword in the
   *     listener. Default is the `target`.
   * @param {boolean} [once] If true, add the listener as one-off listener.
   * @return {EventsKey} Unique key for the listener.
   */
  function listen(target, type, listener, thisArg, once) {
    if (once) {
      const originalListener = listener;
      /**
       * @param {Event|import('./events/Event.js').default} event The event
       * @return {void|boolean} When the function returns `false`, event propagation will stop.
       * @this {typeof target}
       */
      listener = function (event) {
        target.removeEventListener(type, listener);
        return originalListener.call(thisArg ?? this, event);
      };
    } else if (thisArg && thisArg !== target) {
      listener = listener.bind(thisArg);
    }
    const eventsKey = {
      target: target,
      type: type,
      listener: listener,
    };
    target.addEventListener(type, listener);
    return eventsKey;
  }

  /**
   * Registers a one-off event listener on an event target. Inspired by
   * https://google.github.io/closure-library/api/source/closure/goog/events/events.js.src.html
   *
   * This function efficiently binds a `listener` as self-unregistering listener
   * to a `this` object, and returns a key for use with
   * {@link module:ol/events.unlistenByKey} in case the listener needs to be
   * unregistered before it is called.
   *
   * When {@link module:ol/events.listen} is called with the same arguments after this
   * function, the self-unregistering listener will be turned into a permanent
   * listener.
   *
   * @param {import("./events/Target.js").EventTargetLike} target Event target.
   * @param {string} type Event type.
   * @param {ListenerFunction} listener Listener.
   * @param {Object} [thisArg] Object referenced by the `this` keyword in the
   *     listener. Default is the `target`.
   * @return {EventsKey} Key for unlistenByKey.
   */
  function listenOnce(target, type, listener, thisArg) {
    return listen(target, type, listener, thisArg, true);
  }

  /**
   * Unregisters event listeners on an event target. Inspired by
   * https://google.github.io/closure-library/api/source/closure/goog/events/events.js.src.html
   *
   * The argument passed to this function is the key returned from
   * {@link module:ol/events.listen} or {@link module:ol/events.listenOnce}.
   *
   * @param {EventsKey} key The key.
   */
  function unlistenByKey(key) {
    if (key && key.target) {
      key.target.removeEventListener(key.type, key.listener);
      clear(key);
    }
  }

  /**
   * @module ol/Observable
   */

  /***
   * @template {string} Type
   * @template {Event|import("./events/Event.js").default} EventClass
   * @template Return
   * @typedef {(type: Type, listener: (event: EventClass) => ?) => Return} OnSignature
   */

  /***
   * @template {string} Type
   * @template Return
   * @typedef {(type: Type[], listener: (event: Event|import("./events/Event").default) => ?) => Return extends void ? void : Return[]} CombinedOnSignature
   */

  /**
   * @typedef {'change'|'error'} EventTypes
   */

  /***
   * @template Return
   * @typedef {OnSignature<EventTypes, import("./events/Event.js").default, Return> & CombinedOnSignature<EventTypes, Return>} ObservableOnSignature
   */

  /**
   * @classdesc
   * Abstract base class; normally only used for creating subclasses and not
   * instantiated in apps.
   * An event target providing convenient methods for listener registration
   * and unregistration. A generic `change` event is always available through
   * {@link module:ol/Observable~Observable#changed}.
   *
   * @fires import("./events/Event.js").default
   * @api
   */
  class Observable extends Target {
    constructor() {
      super();

      this.on =
        /** @type {ObservableOnSignature<import("./events").EventsKey>} */ (
          this.onInternal
        );

      this.once =
        /** @type {ObservableOnSignature<import("./events").EventsKey>} */ (
          this.onceInternal
        );

      this.un = /** @type {ObservableOnSignature<void>} */ (this.unInternal);

      /**
       * @private
       * @type {number}
       */
      this.revision_ = 0;
    }

    /**
     * Increases the revision counter and dispatches a 'change' event.
     * @api
     */
    changed() {
      ++this.revision_;
      this.dispatchEvent(EventType.CHANGE);
    }

    /**
     * Get the version number for this object.  Each time the object is modified,
     * its version number will be incremented.
     * @return {number} Revision.
     * @api
     */
    getRevision() {
      return this.revision_;
    }

    /**
     * @param {string|Array<string>} type Type.
     * @param {function((Event|import("./events/Event").default)): ?} listener Listener.
     * @return {import("./events.js").EventsKey|Array<import("./events.js").EventsKey>} Event key.
     * @protected
     */
    onInternal(type, listener) {
      if (Array.isArray(type)) {
        const len = type.length;
        const keys = new Array(len);
        for (let i = 0; i < len; ++i) {
          keys[i] = listen(this, type[i], listener);
        }
        return keys;
      }
      return listen(this, /** @type {string} */ (type), listener);
    }

    /**
     * @param {string|Array<string>} type Type.
     * @param {function((Event|import("./events/Event").default)): ?} listener Listener.
     * @return {import("./events.js").EventsKey|Array<import("./events.js").EventsKey>} Event key.
     * @protected
     */
    onceInternal(type, listener) {
      let key;
      if (Array.isArray(type)) {
        const len = type.length;
        key = new Array(len);
        for (let i = 0; i < len; ++i) {
          key[i] = listenOnce(this, type[i], listener);
        }
      } else {
        key = listenOnce(this, /** @type {string} */ (type), listener);
      }
      /** @type {Object} */ (listener).ol_key = key;
      return key;
    }

    /**
     * Unlisten for a certain type of event.
     * @param {string|Array<string>} type Type.
     * @param {function((Event|import("./events/Event").default)): ?} listener Listener.
     * @protected
     */
    unInternal(type, listener) {
      const key = /** @type {Object} */ (listener).ol_key;
      if (key) {
        unByKey(key);
      } else if (Array.isArray(type)) {
        for (let i = 0, ii = type.length; i < ii; ++i) {
          this.removeEventListener(type[i], listener);
        }
      } else {
        this.removeEventListener(type, listener);
      }
    }
  }

  /**
   * Listen for a certain type of event.
   * @function
   * @param {string|Array<string>} type The event type or array of event types.
   * @param {function((Event|import("./events/Event").default)): ?} listener The listener function.
   * @return {import("./events.js").EventsKey|Array<import("./events.js").EventsKey>} Unique key for the listener. If
   *     called with an array of event types as the first argument, the return
   *     will be an array of keys.
   * @api
   */
  Observable.prototype.on;

  /**
   * Listen once for a certain type of event.
   * @function
   * @param {string|Array<string>} type The event type or array of event types.
   * @param {function((Event|import("./events/Event").default)): ?} listener The listener function.
   * @return {import("./events.js").EventsKey|Array<import("./events.js").EventsKey>} Unique key for the listener. If
   *     called with an array of event types as the first argument, the return
   *     will be an array of keys.
   * @api
   */
  Observable.prototype.once;

  /**
   * Unlisten for a certain type of event.
   * @function
   * @param {string|Array<string>} type The event type or array of event types.
   * @param {function((Event|import("./events/Event").default)): ?} listener The listener function.
   * @api
   */
  Observable.prototype.un;

  /**
   * Removes an event listener using the key returned by `on()` or `once()`.
   * @param {import("./events.js").EventsKey|Array<import("./events.js").EventsKey>} key The key returned by `on()`
   *     or `once()` (or an array of keys).
   * @api
   */
  function unByKey(key) {
    if (Array.isArray(key)) {
      for (let i = 0, ii = key.length; i < ii; ++i) {
        unlistenByKey(key[i]);
      }
    } else {
      unlistenByKey(/** @type {import("./events.js").EventsKey} */ (key));
    }
  }

  /**
   * @module ol/util
   */

  /**
   * @return {never} Any return.
   */
  function abstract() {
    throw new Error('Unimplemented abstract method.');
  }

  /**
   * Counter for getUid.
   * @type {number}
   * @private
   */
  let uidCounter_ = 0;

  /**
   * Gets a unique ID for an object. This mutates the object so that further calls
   * with the same object as a parameter returns the same value. Unique IDs are generated
   * as a strictly increasing sequence. Adapted from goog.getUid.
   *
   * @param {Object} obj The object to get the unique ID for.
   * @return {string} The unique ID for the object.
   * @api
   */
  function getUid(obj) {
    return obj.ol_uid || (obj.ol_uid = String(++uidCounter_));
  }

  /**
   * @module ol/Object
   */

  /**
   * @classdesc
   * Events emitted by {@link module:ol/Object~BaseObject} instances are instances of this type.
   */
  class ObjectEvent extends BaseEvent {
    /**
     * @param {string} type The event type.
     * @param {string} key The property name.
     * @param {*} oldValue The old value for `key`.
     */
    constructor(type, key, oldValue) {
      super(type);

      /**
       * The name of the property whose value is changing.
       * @type {string}
       * @api
       */
      this.key = key;

      /**
       * The old value. To get the new value use `e.target.get(e.key)` where
       * `e` is the event object.
       * @type {*}
       * @api
       */
      this.oldValue = oldValue;
    }
  }

  /***
   * @template Return
   * @typedef {import("./Observable").OnSignature<import("./Observable").EventTypes, import("./events/Event.js").default, Return> &
   *    import("./Observable").OnSignature<import("./ObjectEventType").Types, ObjectEvent, Return> &
   *    import("./Observable").CombinedOnSignature<import("./Observable").EventTypes|import("./ObjectEventType").Types, Return>} ObjectOnSignature
   */

  /**
   * @classdesc
   * Abstract base class; normally only used for creating subclasses and not
   * instantiated in apps.
   * Most non-trivial classes inherit from this.
   *
   * This extends {@link module:ol/Observable~Observable} with observable
   * properties, where each property is observable as well as the object as a
   * whole.
   *
   * Classes that inherit from this have pre-defined properties, to which you can
   * add your owns. The pre-defined properties are listed in this documentation as
   * 'Observable Properties', and have their own accessors; for example,
   * {@link module:ol/Map~Map} has a `target` property, accessed with
   * `getTarget()` and changed with `setTarget()`. Not all properties are however
   * settable. There are also general-purpose accessors `get()` and `set()`. For
   * example, `get('target')` is equivalent to `getTarget()`.
   *
   * The `set` accessors trigger a change event, and you can monitor this by
   * registering a listener. For example, {@link module:ol/View~View} has a
   * `center` property, so `view.on('change:center', function(evt) {...});` would
   * call the function whenever the value of the center property changes. Within
   * the function, `evt.target` would be the view, so `evt.target.getCenter()`
   * would return the new center.
   *
   * You can add your own observable properties with
   * `object.set('prop', 'value')`, and retrieve that with `object.get('prop')`.
   * You can listen for changes on that property value with
   * `object.on('change:prop', listener)`. You can get a list of all
   * properties with {@link module:ol/Object~BaseObject#getProperties}.
   *
   * Note that the observable properties are separate from standard JS properties.
   * You can, for example, give your map object a title with
   * `map.title='New title'` and with `map.set('title', 'Another title')`. The
   * first will be a `hasOwnProperty`; the second will appear in
   * `getProperties()`. Only the second is observable.
   *
   * Properties can be deleted by using the unset method. E.g.
   * object.unset('foo').
   *
   * @fires ObjectEvent
   * @template {Object<string, *>} [Properties=Object<string, *>]
   * @api
   */
  class BaseObject extends Observable {
    /**
     * @param {NoInfer<Properties>} [values] An object with key-value pairs.
     */
    constructor(values) {
      super();

      /***
       * @type {ObjectOnSignature<import("./events").EventsKey>}
       */
      this.on;

      /***
       * @type {ObjectOnSignature<import("./events").EventsKey>}
       */
      this.once;

      /***
       * @type {ObjectOnSignature<void>}
       */
      this.un;

      // Call {@link module:ol/util.getUid} to ensure that the order of objects' ids is
      // the same as the order in which they were created.  This also helps to
      // ensure that object properties are always added in the same order, which
      // helps many JavaScript engines generate faster code.
      getUid(this);

      /**
       * @private
       * @type {Partial<NoInfer<Properties>>|null}
       */
      this.values_ = null;

      if (values !== undefined) {
        this.setProperties(values);
      }
    }

    /**
     * Gets a value.
     * @param {string} key Key name.
     * @return {*} Value.
     * @api
     */
    get(key) {
      let value;
      if (this.values_ && this.values_.hasOwnProperty(key)) {
        value = this.values_[key];
      }
      return value;
    }

    /**
     * Get a list of object property names.
     * @return {Array<string>} List of property names.
     * @api
     */
    getKeys() {
      return (this.values_ && Object.keys(this.values_)) || [];
    }

    /**
     * Get an object of all property names and values.
     * @return {NoInfer<Properties>} Object.
     * @api
     */
    getProperties() {
      return /** @type {NoInfer<Properties>} */ (
        (this.values_ && Object.assign({}, this.values_)) || {}
      );
    }

    /**
     * Get an object of all property names and values.
     * @return {Partial<NoInfer<Properties>>?} Object.
     */
    getPropertiesInternal() {
      return this.values_;
    }

    /**
     * @return {boolean} The object has properties.
     */
    hasProperties() {
      return !!this.values_;
    }

    /**
     * @param {string} key Key name.
     * @param {*} oldValue Old value.
     */
    notify(key, oldValue) {
      let eventType;
      eventType = `change:${key}`;
      if (this.hasListener(eventType)) {
        this.dispatchEvent(new ObjectEvent(eventType, key, oldValue));
      }
      eventType = ObjectEventType.PROPERTYCHANGE;
      if (this.hasListener(eventType)) {
        this.dispatchEvent(new ObjectEvent(eventType, key, oldValue));
      }
    }

    /**
     * @param {string} key Key name.
     * @param {import("./events.js").Listener} listener Listener.
     */
    addChangeListener(key, listener) {
      this.addEventListener(`change:${key}`, listener);
    }

    /**
     * @param {string} key Key name.
     * @param {import("./events.js").Listener} listener Listener.
     */
    removeChangeListener(key, listener) {
      this.removeEventListener(`change:${key}`, listener);
    }

    /**
     * Sets a value.
     * @param {string} key Key name.
     * @param {*} value Value.
     * @param {boolean} [silent] Update without triggering an event.
     * @api
     */
    set(key, value, silent) {
      const values = this.values_ || (this.values_ = {});
      if (silent) {
        values[key] = value;
      } else {
        const oldValue = values[key];
        values[key] = value;
        if (oldValue !== value) {
          this.notify(key, oldValue);
        }
      }
    }

    /**
     * Sets a collection of key-value pairs.  Note that this changes any existing
     * properties and adds new ones (it does not remove any existing properties).
     * @param {Partial<NoInfer<Properties>>} values Values.
     * @param {boolean} [silent] Update without triggering an event.
     * @api
     */
    setProperties(values, silent) {
      for (const key in values) {
        this.set(key, values[key], silent);
      }
    }

    /**
     * Apply any properties from another object without triggering events.
     * @param {BaseObject} source The source object.
     * @protected
     */
    applyProperties(source) {
      if (!source.values_) {
        return;
      }
      Object.assign(this.values_ || (this.values_ = {}), source.values_);
    }

    /**
     * Unsets a property.
     * @param {string} key Key name.
     * @param {boolean} [silent] Unset without triggering an event.
     * @api
     */
    unset(key, silent) {
      if (this.values_ && key in this.values_) {
        const oldValue = this.values_[key];
        delete this.values_[key];
        if (isEmpty$1(this.values_)) {
          this.values_ = null;
        }
        if (!silent) {
          this.notify(key, oldValue);
        }
      }
    }
  }

  /**
   * @module ol/Collection
   */

  /**
   * @enum {string}
   * @private
   */
  const Property$3 = {
    LENGTH: 'length',
  };

  /**
   * @classdesc
   * Events emitted by {@link module:ol/Collection~Collection} instances are instances of this
   * type.
   * @template T
   */
  class CollectionEvent extends BaseEvent {
    /**
     * @param {import("./CollectionEventType.js").default} type Type.
     * @param {T} element Element.
     * @param {number} index The index of the added or removed element.
     */
    constructor(type, element, index) {
      super(type);

      /**
       * The element that is added to or removed from the collection.
       * @type {T}
       * @api
       */
      this.element = element;

      /**
       * The index of the added or removed element.
       * @type {number}
       * @api
       */
      this.index = index;
    }
  }

  /***
   * @template T
   * @template Return
   * @typedef {import("./Observable").OnSignature<import("./Observable").EventTypes, import("./events/Event.js").default, Return> &
   *   import("./Observable").OnSignature<import("./ObjectEventType").Types|'change:length', import("./Object").ObjectEvent, Return> &
   *   import("./Observable").OnSignature<'add'|'remove', CollectionEvent<T>, Return> &
   *   import("./Observable").CombinedOnSignature<import("./Observable").EventTypes|import("./ObjectEventType").Types|
   *     'change:length'|'add'|'remove',Return>} CollectionOnSignature
   */

  /**
   * @typedef {Object} Options
   * @property {boolean} [unique=false] Disallow the same item from being added to
   * the collection twice.
   */

  /**
   * @classdesc
   * An expanded version of standard JS Array, adding convenience methods for
   * manipulation. Add and remove changes to the Collection trigger a Collection
   * event. Note that this does not cover changes to the objects _within_ the
   * Collection; they trigger events on the appropriate object, not on the
   * Collection as a whole.
   *
   * @fires CollectionEvent
   *
   * @template T
   * @api
   */
  class Collection extends BaseObject {
    /**
     * @param {Array<T>} [array] Array.
     * @param {Options} [options] Collection options.
     */
    constructor(array, options) {
      super();

      /***
       * @type {CollectionOnSignature<T, import("./events").EventsKey>}
       */
      this.on;

      /***
       * @type {CollectionOnSignature<T, import("./events").EventsKey>}
       */
      this.once;

      /***
       * @type {CollectionOnSignature<T, void>}
       */
      this.un;

      options = options || {};

      /**
       * @private
       * @type {boolean}
       */
      this.unique_ = !!options.unique;

      /**
       * @private
       * @type {!Array<T>}
       */
      this.array_ = array ?? [];

      if (this.unique_) {
        for (let i = 1, ii = this.array_.length; i < ii; ++i) {
          this.assertUnique_(this.array_[i], i);
        }
      }

      this.updateLength_();
    }

    /**
     * Remove all elements from the collection.
     * @api
     */
    clear() {
      while (this.getLength() > 0) {
        this.pop();
      }
    }

    /**
     * Add elements to the collection.  This pushes each item in the provided array
     * to the end of the collection.
     * @param {!Array<T>} arr Array.
     * @return {Collection<T>} This collection.
     * @api
     */
    extend(arr) {
      for (let i = 0, ii = arr.length; i < ii; ++i) {
        this.push(arr[i]);
      }
      return this;
    }

    /**
     * Iterate over each element, calling the provided callback.
     * @param {function(T, number, Array<T>): *} f The function to call
     *     for every element. This function takes 3 arguments (the element, the
     *     index and the array). The return value is ignored.
     * @api
     */
    forEach(f) {
      const array = this.array_;
      for (let i = 0, ii = array.length; i < ii; ++i) {
        f(array[i], i, array);
      }
    }

    /**
     * Get a reference to the underlying Array object. Warning: if the array
     * is mutated, no events will be dispatched by the collection, and the
     * collection's "length" property won't be in sync with the actual length
     * of the array.
     * @return {!Array<T>} Array.
     * @api
     */
    getArray() {
      return this.array_;
    }

    /**
     * Get the element at the provided index.
     * @param {number} index Index.
     * @return {T} Element.
     * @api
     */
    item(index) {
      return this.array_[index];
    }

    /**
     * Get the length of this collection.
     * @return {number} The length of the array.
     * @observable
     * @api
     */
    getLength() {
      return this.get(Property$3.LENGTH);
    }

    /**
     * Insert an element at the provided index.
     * @param {number} index Index.
     * @param {T} elem Element.
     * @api
     */
    insertAt(index, elem) {
      if (index < 0 || index > this.getLength()) {
        throw new Error('Index out of bounds: ' + index);
      }
      if (this.unique_) {
        this.assertUnique_(elem);
      }
      this.array_.splice(index, 0, elem);
      this.updateLength_();
      this.dispatchEvent(
        new CollectionEvent(CollectionEventType.ADD, elem, index),
      );
    }

    /**
     * Remove the last element of the collection and return it.
     * Return `undefined` if the collection is empty.
     * @return {T|undefined} Element.
     * @api
     */
    pop() {
      return this.removeAt(this.getLength() - 1);
    }

    /**
     * Insert the provided element at the end of the collection.
     * @param {T} elem Element.
     * @return {number} New length of the collection.
     * @api
     */
    push(elem) {
      const n = this.getLength();
      this.insertAt(n, elem);
      return this.getLength();
    }

    /**
     * Remove the first occurrence of an element from the collection.
     * @param {T} elem Element.
     * @return {T|undefined} The removed element or undefined if none found.
     * @api
     */
    remove(elem) {
      const arr = this.array_;
      for (let i = 0, ii = arr.length; i < ii; ++i) {
        if (arr[i] === elem) {
          return this.removeAt(i);
        }
      }
      return undefined;
    }

    /**
     * Remove the element at the provided index and return it.
     * Return `undefined` if the collection does not contain this index.
     * @param {number} index Index.
     * @return {T|undefined} Value.
     * @api
     */
    removeAt(index) {
      if (index < 0 || index >= this.getLength()) {
        return undefined;
      }
      const prev = this.array_[index];
      this.array_.splice(index, 1);
      this.updateLength_();
      this.dispatchEvent(
        /** @type {CollectionEvent<T>} */ (
          new CollectionEvent(CollectionEventType.REMOVE, prev, index)
        ),
      );
      return prev;
    }

    /**
     * Set the element at the provided index.
     * @param {number} index Index.
     * @param {T} elem Element.
     * @api
     */
    setAt(index, elem) {
      const n = this.getLength();
      if (index >= n) {
        this.insertAt(index, elem);
        return;
      }
      if (index < 0) {
        throw new Error('Index out of bounds: ' + index);
      }
      if (this.unique_) {
        this.assertUnique_(elem, index);
      }
      const prev = this.array_[index];
      this.array_[index] = elem;
      this.dispatchEvent(
        /** @type {CollectionEvent<T>} */ (
          new CollectionEvent(CollectionEventType.REMOVE, prev, index)
        ),
      );
      this.dispatchEvent(
        /** @type {CollectionEvent<T>} */ (
          new CollectionEvent(CollectionEventType.ADD, elem, index)
        ),
      );
    }

    /**
     * @private
     */
    updateLength_() {
      this.set(Property$3.LENGTH, this.array_.length);
    }

    /**
     * @private
     * @param {T} elem Element.
     * @param {number} [except] Optional index to ignore.
     */
    assertUnique_(elem, except) {
      const array = this.array_;
      for (let i = 0, ii = array.length; i < ii; ++i) {
        if (array[i] === elem && i !== except) {
          throw new Error('Duplicate item added to a unique collection');
        }
      }
    }
  }

  /**
   * @module ol/asserts
   */

  /**
   * @param {*} assertion Assertion we expected to be truthy.
   * @param {string} errorMessage Error message.
   */
  function assert(assertion, errorMessage) {
    if (!assertion) {
      throw new Error(errorMessage);
    }
  }

  /**
   * @module ol/Feature
   */

  /**
   * @typedef {typeof Feature|typeof import("./render/Feature.js").default} FeatureClass
   */

  /**
   * @typedef {Feature|import("./render/Feature.js").default} FeatureLike
   */

  /***
   * @template Return
   * @typedef {import("./Observable").OnSignature<import("./Observable").EventTypes, import("./events/Event.js").default, Return> &
   *   import("./Observable").OnSignature<import("./ObjectEventType").Types|'change:geometry', import("./Object").ObjectEvent, Return> &
   *   import("./Observable").CombinedOnSignature<import("./Observable").EventTypes|import("./ObjectEventType").Types
   *     |'change:geometry', Return>} FeatureOnSignature
   */

  /***
   * @template {import("./geom/Geometry.js").default} [Geometry=import("./geom/Geometry.js").default]
   * @template {Object<string, *>} [Properties=Object<string, *>]
   * @typedef {Properties & { geometry?: Geometry }} ObjectWithGeometry
   */

  /**
   * @classdesc
   * A vector object for geographic features with a geometry and other
   * attribute properties, similar to the features in vector file formats like
   * GeoJSON.
   *
   * Features can be styled individually with `setStyle`; otherwise they use the
   * style of their vector layer.
   *
   * Note that attribute properties are set as {@link module:ol/Object~BaseObject} properties on
   * the feature object, so they are observable, and have get/set accessors.
   *
   * Typically, a feature has a single geometry property. You can set the
   * geometry using the `setGeometry` method and get it with `getGeometry`.
   * It is possible to store more than one geometry on a feature using attribute
   * properties. By default, the geometry used for rendering is identified by
   * the property name `geometry`. If you want to use another geometry property
   * for rendering, use the `setGeometryName` method to change the attribute
   * property associated with the geometry for the feature.  For example:
   *
   * ```js
   *
   * import Feature from 'ol/Feature.js';
   * import Polygon from 'ol/geom/Polygon.js';
   * import Point from 'ol/geom/Point.js';
   *
   * const feature = new Feature({
   *   geometry: new Polygon(polyCoords),
   *   labelPoint: new Point(labelCoords),
   *   name: 'My Polygon',
   * });
   *
   * // get the polygon geometry
   * const poly = feature.getGeometry();
   *
   * // Render the feature as a point using the coordinates from labelPoint
   * feature.setGeometryName('labelPoint');
   *
   * // get the point geometry
   * const point = feature.getGeometry();
   * ```
   *
   * @api
   * @template {import("./geom/Geometry.js").default} [Geometry=import("./geom/Geometry.js").default]
   * @template {Object<string, *>} [Properties=Object<string, *>]
   * @extends {BaseObject<NoInfer<Properties>>}
   */
  class Feature extends BaseObject {
    /**
     * @param {Geometry|ObjectWithGeometry<Geometry, NoInfer<Properties>>} [geometryOrProperties]
     *     You may pass a Geometry object directly, or an object literal containing
     *     properties. If you pass an object literal, you may include a Geometry
     *     associated with a `geometry` key.
     */
    constructor(geometryOrProperties) {
      super();

      /***
       * @type {FeatureOnSignature<import("./events").EventsKey>}
       */
      this.on;

      /***
       * @type {FeatureOnSignature<import("./events").EventsKey>}
       */
      this.once;

      /***
       * @type {FeatureOnSignature<void>}
       */
      this.un;

      /**
       * @private
       * @type {number|string|undefined}
       */
      this.id_ = undefined;

      /**
       * @type {string}
       * @private
       */
      this.geometryName_ = 'geometry';

      /**
       * User provided style.
       * @private
       * @type {import("./style/Style.js").StyleLike}
       */
      this.style_ = null;

      /**
       * @private
       * @type {import("./style/Style.js").StyleFunction|undefined}
       */
      this.styleFunction_ = undefined;

      /**
       * @private
       * @type {?import("./events.js").EventsKey}
       */
      this.geometryChangeKey_ = null;

      this.addChangeListener(this.geometryName_, this.handleGeometryChanged_);

      if (geometryOrProperties) {
        if (
          typeof (
            /** @type {?} */ (geometryOrProperties).getSimplifiedGeometry
          ) === 'function'
        ) {
          const geometry = /** @type {Geometry} */ (geometryOrProperties);
          this.setGeometry(geometry);
        } else {
          /** @type {?} */
          const properties = geometryOrProperties;
          this.setProperties(properties);
        }
      }
    }

    /**
     * Clone this feature. If the original feature has a geometry it
     * is also cloned. The feature id is not set in the clone.
     * @return {Feature<Geometry>} The clone.
     * @api
     */
    clone() {
      const clone = /** @type {Feature<Geometry>} */ (
        new Feature(this.hasProperties() ? this.getProperties() : null)
      );
      clone.setGeometryName(this.getGeometryName());
      const geometry = this.getGeometry();
      if (geometry) {
        clone.setGeometry(/** @type {Geometry} */ (geometry.clone()));
      }
      const style = this.getStyle();
      if (style) {
        clone.setStyle(style);
      }
      return clone;
    }

    /**
     * Get the feature's default geometry.  A feature may have any number of named
     * geometries.  The "default" geometry (the one that is rendered by default) is
     * set when calling {@link module:ol/Feature~Feature#setGeometry}.
     * @return {Geometry|undefined} The default geometry for the feature.
     * @api
     * @observable
     */
    getGeometry() {
      return /** @type {Geometry|undefined} */ (this.get(this.geometryName_));
    }

    /**
     * Get the feature identifier.  This is a stable identifier for the feature and
     * is either set when reading data from a remote source or set explicitly by
     * calling {@link module:ol/Feature~Feature#setId}.
     * @return {number|string|undefined} Id.
     * @api
     */
    getId() {
      return this.id_;
    }

    /**
     * Get the name of the feature's default geometry.  By default, the default
     * geometry is named `geometry`.
     * @return {string} Get the property name associated with the default geometry
     *     for this feature.
     * @api
     */
    getGeometryName() {
      return this.geometryName_;
    }

    /**
     * Get the feature's style. Will return what was provided to the
     * {@link module:ol/Feature~Feature#setStyle} method.
     * @return {import("./style/Style.js").StyleLike|undefined} The feature style.
     * @api
     */
    getStyle() {
      return this.style_;
    }

    /**
     * Get the feature's style function.
     * @return {import("./style/Style.js").StyleFunction|undefined} Return a function
     * representing the current style of this feature.
     * @api
     */
    getStyleFunction() {
      return this.styleFunction_;
    }

    /**
     * @private
     */
    handleGeometryChange_() {
      this.changed();
    }

    /**
     * @private
     */
    handleGeometryChanged_() {
      if (this.geometryChangeKey_) {
        unlistenByKey(this.geometryChangeKey_);
        this.geometryChangeKey_ = null;
      }
      const geometry = this.getGeometry();
      if (geometry) {
        this.geometryChangeKey_ = listen(
          geometry,
          EventType.CHANGE,
          this.handleGeometryChange_,
          this,
        );
      }
      this.changed();
    }

    /**
     * Set the default geometry for the feature.  This will update the property
     * with the name returned by {@link module:ol/Feature~Feature#getGeometryName}.
     * @param {Geometry|undefined} geometry The new geometry.
     * @api
     * @observable
     */
    setGeometry(geometry) {
      this.set(this.geometryName_, geometry);
    }

    /**
     * Set the style for the feature to override the layer style.  This can be a
     * single style object, an array of styles, or a function that takes a
     * resolution and returns an array of styles. To unset the feature style, call
     * `setStyle()` without arguments or a falsey value.
     * @param {import("./style/Style.js").StyleLike} [style] Style for this feature.
     * @api
     * @fires module:ol/events/Event~BaseEvent#event:change
     */
    setStyle(style) {
      this.style_ = style;
      this.styleFunction_ = !style ? undefined : createStyleFunction(style);
      this.changed();
    }

    /**
     * Set the feature id.  The feature id is considered stable and may be used when
     * requesting features or comparing identifiers returned from a remote source.
     * The feature id can be used with the
     * {@link module:ol/source/Vector~VectorSource#getFeatureById} method.
     * @param {number|string|undefined} id The feature id.
     * @api
     * @fires module:ol/events/Event~BaseEvent#event:change
     */
    setId(id) {
      this.id_ = id;
      this.changed();
    }

    /**
     * Set the property name to be used when getting the feature's default geometry.
     * When calling {@link module:ol/Feature~Feature#getGeometry}, the value of the property with
     * this name will be returned.
     * @param {string} name The property name of the default geometry.
     * @api
     */
    setGeometryName(name) {
      this.removeChangeListener(this.geometryName_, this.handleGeometryChanged_);
      this.geometryName_ = name;
      this.addChangeListener(this.geometryName_, this.handleGeometryChanged_);
      this.handleGeometryChanged_();
    }
  }

  /**
   * Convert the provided object into a feature style function.  Functions passed
   * through unchanged.  Arrays of Style or single style objects wrapped
   * in a new feature style function.
   * @param {!import("./style/Style.js").StyleFunction|!Array<import("./style/Style.js").default>|!import("./style/Style.js").default} obj
   *     A feature style function, a single style, or an array of styles.
   * @return {import("./style/Style.js").StyleFunction} A style function.
   */
  function createStyleFunction(obj) {
    if (typeof obj === 'function') {
      return obj;
    }
    /**
     * @type {Array<import("./style/Style.js").default>}
     */
    let styles;
    if (Array.isArray(obj)) {
      styles = obj;
    } else {
      assert(
        typeof (/** @type {?} */ (obj).getZIndex) === 'function',
        'Expected an `ol/style/Style` or an array of `ol/style/Style.js`',
      );
      const style = /** @type {import("./style/Style.js").default} */ (obj);
      styles = [style];
    }
    return function () {
      return styles;
    };
  }

  /**
   * @module ol/extent/Relationship
   */

  /**
   * Relationship to an extent.
   * @enum {number}
   */
  var Relationship = {
    UNKNOWN: 0,
    INTERSECTING: 1,
    ABOVE: 2,
    RIGHT: 4,
    BELOW: 8,
    LEFT: 16,
  };

  /**
   * @module ol/extent
   */

  /**
   * An array of numbers representing an extent: `[minx, miny, maxx, maxy]`.
   * @typedef {Array<number>} Extent
   * @api
   */

  /**
   * Extent corner.
   * @typedef {'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'} Corner
   */

  /**
   * Build an extent that includes all given coordinates.
   *
   * @param {Array<import("./coordinate.js").Coordinate>} coordinates Coordinates.
   * @return {Extent} Bounding extent.
   * @api
   */
  function boundingExtent(coordinates) {
    const extent = createEmpty();
    for (let i = 0, ii = coordinates.length; i < ii; ++i) {
      extendCoordinate(extent, coordinates[i]);
    }
    return extent;
  }

  /**
   * Return extent increased by the provided value.
   * @param {Extent} extent Extent.
   * @param {number} value The amount by which the extent should be buffered.
   * @param {Extent} [dest] Extent.
   * @return {Extent} Extent.
   * @api
   */
  function buffer(extent, value, dest) {
    if (dest) {
      dest[0] = extent[0] - value;
      dest[1] = extent[1] - value;
      dest[2] = extent[2] + value;
      dest[3] = extent[3] + value;
      return dest;
    }
    return [
      extent[0] - value,
      extent[1] - value,
      extent[2] + value,
      extent[3] + value,
    ];
  }

  /**
   * Creates a clone of an extent.
   *
   * @param {Extent} extent Extent to clone.
   * @param {Extent} [dest] Extent.
   * @return {Extent} The clone.
   */
  function clone(extent, dest) {
    if (dest) {
      dest[0] = extent[0];
      dest[1] = extent[1];
      dest[2] = extent[2];
      dest[3] = extent[3];
      return dest;
    }
    return extent.slice();
  }

  /**
   * @param {Extent} extent Extent.
   * @param {number} x X.
   * @param {number} y Y.
   * @return {number} Closest squared distance.
   */
  function closestSquaredDistanceXY(extent, x, y) {
    let dx, dy;
    if (x < extent[0]) {
      dx = extent[0] - x;
    } else if (extent[2] < x) {
      dx = x - extent[2];
    } else {
      dx = 0;
    }
    if (y < extent[1]) {
      dy = extent[1] - y;
    } else if (extent[3] < y) {
      dy = y - extent[3];
    } else {
      dy = 0;
    }
    return dx * dx + dy * dy;
  }

  /**
   * Check if the passed coordinate is contained or on the edge of the extent.
   *
   * @param {Extent} extent Extent.
   * @param {import("./coordinate.js").Coordinate} coordinate Coordinate.
   * @return {boolean} The coordinate is contained in the extent.
   * @api
   */
  function containsCoordinate(extent, coordinate) {
    return containsXY(extent, coordinate[0], coordinate[1]);
  }

  /**
   * Check if one extent contains another.
   *
   * An extent is deemed contained if it lies completely within the other extent,
   * including if they share one or more edges.
   *
   * @param {Extent} extent1 Extent 1.
   * @param {Extent} extent2 Extent 2.
   * @return {boolean} The second extent is contained by or on the edge of the
   *     first.
   * @api
   */
  function containsExtent(extent1, extent2) {
    return (
      extent1[0] <= extent2[0] &&
      extent2[2] <= extent1[2] &&
      extent1[1] <= extent2[1] &&
      extent2[3] <= extent1[3]
    );
  }

  /**
   * Check if the passed coordinate is contained or on the edge of the extent.
   *
   * @param {Extent} extent Extent.
   * @param {number} x X coordinate.
   * @param {number} y Y coordinate.
   * @return {boolean} The x, y values are contained in the extent.
   * @api
   */
  function containsXY(extent, x, y) {
    return extent[0] <= x && x <= extent[2] && extent[1] <= y && y <= extent[3];
  }

  /**
   * Get the relationship between a coordinate and extent.
   * @param {Extent} extent The extent.
   * @param {import("./coordinate.js").Coordinate} coordinate The coordinate.
   * @return {import("./extent/Relationship.js").default} The relationship (bitwise compare with
   *     import("./extent/Relationship.js").Relationship).
   */
  function coordinateRelationship(extent, coordinate) {
    const minX = extent[0];
    const minY = extent[1];
    const maxX = extent[2];
    const maxY = extent[3];
    const x = coordinate[0];
    const y = coordinate[1];
    let relationship = Relationship.UNKNOWN;
    if (x < minX) {
      relationship = relationship | Relationship.LEFT;
    } else if (x > maxX) {
      relationship = relationship | Relationship.RIGHT;
    }
    if (y < minY) {
      relationship = relationship | Relationship.BELOW;
    } else if (y > maxY) {
      relationship = relationship | Relationship.ABOVE;
    }
    if (relationship === Relationship.UNKNOWN) {
      relationship = Relationship.INTERSECTING;
    }
    return relationship;
  }

  /**
   * Create an empty extent.
   * @return {Extent} Empty extent.
   * @api
   */
  function createEmpty() {
    return [Infinity, Infinity, -Infinity, -Infinity];
  }

  /**
   * Create a new extent or update the provided extent.
   * @param {number} minX Minimum X.
   * @param {number} minY Minimum Y.
   * @param {number} maxX Maximum X.
   * @param {number} maxY Maximum Y.
   * @param {Extent} [dest] Destination extent.
   * @return {Extent} Extent.
   */
  function createOrUpdate$2(minX, minY, maxX, maxY, dest) {
    if (dest) {
      dest[0] = minX;
      dest[1] = minY;
      dest[2] = maxX;
      dest[3] = maxY;
      return dest;
    }
    return [minX, minY, maxX, maxY];
  }

  /**
   * Create a new empty extent or make the provided one empty.
   * @param {Extent} [dest] Extent.
   * @return {Extent} Extent.
   */
  function createOrUpdateEmpty(dest) {
    return createOrUpdate$2(Infinity, Infinity, -Infinity, -Infinity, dest);
  }

  /**
   * @param {import("./coordinate.js").Coordinate} coordinate Coordinate.
   * @param {Extent} [dest] Extent.
   * @return {Extent} Extent.
   */
  function createOrUpdateFromCoordinate(coordinate, dest) {
    const x = coordinate[0];
    const y = coordinate[1];
    return createOrUpdate$2(x, y, x, y, dest);
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {Extent} [dest] Extent.
   * @return {Extent} Extent.
   */
  function createOrUpdateFromFlatCoordinates(
    flatCoordinates,
    offset,
    end,
    stride,
    dest,
  ) {
    const extent = createOrUpdateEmpty(dest);
    return extendFlatCoordinates(extent, flatCoordinates, offset, end, stride);
  }

  /**
   * Determine if two extents are equivalent.
   * @param {Extent} extent1 Extent 1.
   * @param {Extent} extent2 Extent 2.
   * @return {boolean} The two extents are equivalent.
   * @api
   */
  function equals$1(extent1, extent2) {
    return (
      extent1[0] == extent2[0] &&
      extent1[2] == extent2[2] &&
      extent1[1] == extent2[1] &&
      extent1[3] == extent2[3]
    );
  }

  /**
   * Modify an extent to include another extent.
   * @param {Extent} extent1 The extent to be modified.
   * @param {Extent} extent2 The extent that will be included in the first.
   * @return {Extent} A reference to the first (extended) extent.
   * @api
   */
  function extend$1(extent1, extent2) {
    if (extent2[0] < extent1[0]) {
      extent1[0] = extent2[0];
    }
    if (extent2[2] > extent1[2]) {
      extent1[2] = extent2[2];
    }
    if (extent2[1] < extent1[1]) {
      extent1[1] = extent2[1];
    }
    if (extent2[3] > extent1[3]) {
      extent1[3] = extent2[3];
    }
    return extent1;
  }

  /**
   * @param {Extent} extent Extent.
   * @param {import("./coordinate.js").Coordinate} coordinate Coordinate.
   */
  function extendCoordinate(extent, coordinate) {
    if (coordinate[0] < extent[0]) {
      extent[0] = coordinate[0];
    }
    if (coordinate[0] > extent[2]) {
      extent[2] = coordinate[0];
    }
    if (coordinate[1] < extent[1]) {
      extent[1] = coordinate[1];
    }
    if (coordinate[1] > extent[3]) {
      extent[3] = coordinate[1];
    }
  }

  /**
   * @param {Extent} extent Extent.
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @return {Extent} Extent.
   */
  function extendFlatCoordinates(
    extent,
    flatCoordinates,
    offset,
    end,
    stride,
  ) {
    for (; offset < end; offset += stride) {
      extendXY(extent, flatCoordinates[offset], flatCoordinates[offset + 1]);
    }
    return extent;
  }

  /**
   * @param {Extent} extent Extent.
   * @param {number} x X.
   * @param {number} y Y.
   */
  function extendXY(extent, x, y) {
    extent[0] = Math.min(extent[0], x);
    extent[1] = Math.min(extent[1], y);
    extent[2] = Math.max(extent[2], x);
    extent[3] = Math.max(extent[3], y);
  }

  /**
   * This function calls `callback` for each corner of the extent. If the
   * callback returns a truthy value the function returns that value
   * immediately. Otherwise the function returns `false`.
   * @param {Extent} extent Extent.
   * @param {function(import("./coordinate.js").Coordinate): S} callback Callback.
   * @return {S|boolean} Value.
   * @template S
   */
  function forEachCorner(extent, callback) {
    let val;
    val = callback(getBottomLeft(extent));
    if (val) {
      return val;
    }
    val = callback(getBottomRight(extent));
    if (val) {
      return val;
    }
    val = callback(getTopRight(extent));
    if (val) {
      return val;
    }
    val = callback(getTopLeft(extent));
    if (val) {
      return val;
    }
    return false;
  }

  /**
   * Get the size of an extent.
   * @param {Extent} extent Extent.
   * @return {number} Area.
   * @api
   */
  function getArea(extent) {
    let area = 0;
    if (!isEmpty(extent)) {
      area = getWidth(extent) * getHeight(extent);
    }
    return area;
  }

  /**
   * Get the bottom left coordinate of an extent.
   * @param {Extent} extent Extent.
   * @return {import("./coordinate.js").Coordinate} Bottom left coordinate.
   * @api
   */
  function getBottomLeft(extent) {
    return [extent[0], extent[1]];
  }

  /**
   * Get the bottom right coordinate of an extent.
   * @param {Extent} extent Extent.
   * @return {import("./coordinate.js").Coordinate} Bottom right coordinate.
   * @api
   */
  function getBottomRight(extent) {
    return [extent[2], extent[1]];
  }

  /**
   * Get the center coordinate of an extent.
   * @param {Extent} extent Extent.
   * @return {import("./coordinate.js").Coordinate} Center.
   * @api
   */
  function getCenter(extent) {
    return [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2];
  }

  /**
   * Get a corner coordinate of an extent.
   * @param {Extent} extent Extent.
   * @param {Corner} corner Corner.
   * @return {import("./coordinate.js").Coordinate} Corner coordinate.
   */
  function getCorner(extent, corner) {
    let coordinate;
    if (corner === 'bottom-left') {
      coordinate = getBottomLeft(extent);
    } else if (corner === 'bottom-right') {
      coordinate = getBottomRight(extent);
    } else if (corner === 'top-left') {
      coordinate = getTopLeft(extent);
    } else if (corner === 'top-right') {
      coordinate = getTopRight(extent);
    } else {
      throw new Error('Invalid corner');
    }
    return coordinate;
  }

  /**
   * @param {import("./coordinate.js").Coordinate} center Center.
   * @param {number} resolution Resolution.
   * @param {number} rotation Rotation.
   * @param {import("./size.js").Size} size Size.
   * @param {Extent} [dest] Destination extent.
   * @return {Extent} Extent.
   */
  function getForViewAndSize(center, resolution, rotation, size, dest) {
    const [x0, y0, x1, y1, x2, y2, x3, y3] = getRotatedViewport(
      center,
      resolution,
      rotation,
      size,
    );
    return createOrUpdate$2(
      Math.min(x0, x1, x2, x3),
      Math.min(y0, y1, y2, y3),
      Math.max(x0, x1, x2, x3),
      Math.max(y0, y1, y2, y3),
      dest,
    );
  }

  /**
   * @param {import("./coordinate.js").Coordinate} center Center.
   * @param {number} resolution Resolution.
   * @param {number} rotation Rotation.
   * @param {import("./size.js").Size} size Size.
   * @return {Array<number>} Linear ring representing the viewport.
   */
  function getRotatedViewport(center, resolution, rotation, size) {
    const dx = (resolution * size[0]) / 2;
    const dy = (resolution * size[1]) / 2;
    const cosRotation = Math.cos(rotation);
    const sinRotation = Math.sin(rotation);
    const xCos = dx * cosRotation;
    const xSin = dx * sinRotation;
    const yCos = dy * cosRotation;
    const ySin = dy * sinRotation;
    const x = center[0];
    const y = center[1];
    return [
      x - xCos + ySin,
      y - xSin - yCos,
      x - xCos - ySin,
      y - xSin + yCos,
      x + xCos - ySin,
      y + xSin + yCos,
      x + xCos + ySin,
      y + xSin - yCos,
      x - xCos + ySin,
      y - xSin - yCos,
    ];
  }

  /**
   * Get the height of an extent.
   * @param {Extent} extent Extent.
   * @return {number} Height.
   * @api
   */
  function getHeight(extent) {
    return extent[3] - extent[1];
  }

  /**
   * Get the intersection of two extents.
   * @param {Extent} extent1 Extent 1.
   * @param {Extent} extent2 Extent 2.
   * @param {Extent} [dest] Optional extent to populate with intersection.
   * @return {Extent} Intersecting extent.
   * @api
   */
  function getIntersection(extent1, extent2, dest) {
    const intersection = dest ? dest : createEmpty();
    if (intersects$1(extent1, extent2)) {
      if (extent1[0] > extent2[0]) {
        intersection[0] = extent1[0];
      } else {
        intersection[0] = extent2[0];
      }
      if (extent1[1] > extent2[1]) {
        intersection[1] = extent1[1];
      } else {
        intersection[1] = extent2[1];
      }
      if (extent1[2] < extent2[2]) {
        intersection[2] = extent1[2];
      } else {
        intersection[2] = extent2[2];
      }
      if (extent1[3] < extent2[3]) {
        intersection[3] = extent1[3];
      } else {
        intersection[3] = extent2[3];
      }
    } else {
      createOrUpdateEmpty(intersection);
    }
    return intersection;
  }

  /**
   * Get the top left coordinate of an extent.
   * @param {Extent} extent Extent.
   * @return {import("./coordinate.js").Coordinate} Top left coordinate.
   * @api
   */
  function getTopLeft(extent) {
    return [extent[0], extent[3]];
  }

  /**
   * Get the top right coordinate of an extent.
   * @param {Extent} extent Extent.
   * @return {import("./coordinate.js").Coordinate} Top right coordinate.
   * @api
   */
  function getTopRight(extent) {
    return [extent[2], extent[3]];
  }

  /**
   * Get the width of an extent.
   * @param {Extent} extent Extent.
   * @return {number} Width.
   * @api
   */
  function getWidth(extent) {
    return extent[2] - extent[0];
  }

  /**
   * Determine if one extent intersects another.
   * @param {Extent} extent1 Extent 1.
   * @param {Extent} extent2 Extent.
   * @return {boolean} The two extents intersect.
   * @api
   */
  function intersects$1(extent1, extent2) {
    return (
      extent1[0] <= extent2[2] &&
      extent1[2] >= extent2[0] &&
      extent1[1] <= extent2[3] &&
      extent1[3] >= extent2[1]
    );
  }

  /**
   * Determine if an extent is empty.
   * @param {Extent} extent Extent.
   * @return {boolean} Is empty.
   * @api
   */
  function isEmpty(extent) {
    return extent[2] < extent[0] || extent[3] < extent[1];
  }

  /**
   * @param {Extent} extent Extent.
   * @param {Extent} [dest] Extent.
   * @return {Extent} Extent.
   */
  function returnOrUpdate(extent, dest) {
    if (dest) {
      dest[0] = extent[0];
      dest[1] = extent[1];
      dest[2] = extent[2];
      dest[3] = extent[3];
      return dest;
    }
    return extent;
  }

  /**
   * Determine if the segment between two coordinates intersects (crosses,
   * touches, or is contained by) the provided extent.
   * @param {Extent} extent The extent.
   * @param {import("./coordinate.js").Coordinate} start Segment start coordinate.
   * @param {import("./coordinate.js").Coordinate} end Segment end coordinate.
   * @return {boolean} The segment intersects the extent.
   */
  function intersectsSegment(extent, start, end) {
    let intersects = false;
    const startRel = coordinateRelationship(extent, start);
    const endRel = coordinateRelationship(extent, end);
    if (
      startRel === Relationship.INTERSECTING ||
      endRel === Relationship.INTERSECTING
    ) {
      intersects = true;
    } else {
      const minX = extent[0];
      const minY = extent[1];
      const maxX = extent[2];
      const maxY = extent[3];
      const startX = start[0];
      const startY = start[1];
      const endX = end[0];
      const endY = end[1];
      const slope = (endY - startY) / (endX - startX);
      let x, y;
      if (!!(endRel & Relationship.ABOVE) && !(startRel & Relationship.ABOVE)) {
        // potentially intersects top
        x = endX - (endY - maxY) / slope;
        intersects = x >= minX && x <= maxX;
      }
      if (
        !intersects &&
        !!(endRel & Relationship.RIGHT) &&
        !(startRel & Relationship.RIGHT)
      ) {
        // potentially intersects right
        y = endY - (endX - maxX) * slope;
        intersects = y >= minY && y <= maxY;
      }
      if (
        !intersects &&
        !!(endRel & Relationship.BELOW) &&
        !(startRel & Relationship.BELOW)
      ) {
        // potentially intersects bottom
        x = endX - (endY - minY) / slope;
        intersects = x >= minX && x <= maxX;
      }
      if (
        !intersects &&
        !!(endRel & Relationship.LEFT) &&
        !(startRel & Relationship.LEFT)
      ) {
        // potentially intersects left
        y = endY - (endX - minX) * slope;
        intersects = y >= minY && y <= maxY;
      }
    }
    return intersects;
  }

  /**
   * Modifies the provided extent in-place to be within the real world
   * extent.
   *
   * @param {Extent} extent Extent.
   * @param {import("./proj/Projection.js").default} projection Projection
   * @return {Extent} The extent within the real world extent.
   */
  function wrapX$2(extent, projection) {
    const projectionExtent = projection.getExtent();
    const center = getCenter(extent);
    if (
      projection.canWrapX() &&
      (center[0] < projectionExtent[0] || center[0] >= projectionExtent[2])
    ) {
      const worldWidth = getWidth(projectionExtent);
      const worldsAway = Math.floor(
        (center[0] - projectionExtent[0]) / worldWidth,
      );
      const offset = worldsAway * worldWidth;
      extent[0] -= offset;
      extent[2] -= offset;
    }
    return extent;
  }

  /**
   * Fits the extent to the real world
   *
   * If the extent does not cross the anti meridian, this will return the extent in an array
   * If the extent crosses the anti meridian, the extent will be sliced, so each part fits within the
   * real world
   *
   *
   * @param {Extent} extent Extent.
   * @param {import("./proj/Projection.js").default} projection Projection
   * @param {boolean} [multiWorld] Return all worlds
   * @return {Array<Extent>} The extent within the real world extent.
   */
  function wrapAndSliceX(extent, projection, multiWorld) {
    if (projection.canWrapX()) {
      const projectionExtent = projection.getExtent();

      if (!isFinite(extent[0]) || !isFinite(extent[2])) {
        return [[projectionExtent[0], extent[1], projectionExtent[2], extent[3]]];
      }

      wrapX$2(extent, projection);
      const worldWidth = getWidth(projectionExtent);

      if (getWidth(extent) > worldWidth && !multiWorld) {
        // the extent wraps around on itself
        return [[projectionExtent[0], extent[1], projectionExtent[2], extent[3]]];
      }
      if (extent[0] < projectionExtent[0]) {
        // the extent crosses the anti meridian, so it needs to be sliced
        return [
          [extent[0] + worldWidth, extent[1], projectionExtent[2], extent[3]],
          [projectionExtent[0], extent[1], extent[2], extent[3]],
        ];
      }
      if (extent[2] > projectionExtent[2]) {
        // the extent crosses the anti meridian, so it needs to be sliced
        return [
          [extent[0], extent[1], projectionExtent[2], extent[3]],
          [projectionExtent[0], extent[1], extent[2] - worldWidth, extent[3]],
        ];
      }
    }

    return [extent];
  }

  /**
   * @module ol/math
   */

  /**
   * Takes a number and clamps it to within the provided bounds.
   * @param {number} value The input number.
   * @param {number} min The minimum value to return.
   * @param {number} max The maximum value to return.
   * @return {number} The input number if it is within bounds, or the nearest
   *     number within the bounds.
   */
  function clamp(value, min, max) {
    return Math.min(Math.max(value, min), max);
  }

  /**
   * Returns the square of the closest distance between the point (x, y) and the
   * line segment (x1, y1) to (x2, y2).
   * @param {number} x X.
   * @param {number} y Y.
   * @param {number} x1 X1.
   * @param {number} y1 Y1.
   * @param {number} x2 X2.
   * @param {number} y2 Y2.
   * @return {number} Squared distance.
   */
  function squaredSegmentDistance(x, y, x1, y1, x2, y2) {
    const dx = x2 - x1;
    const dy = y2 - y1;
    if (dx !== 0 || dy !== 0) {
      const t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy);
      if (t > 1) {
        x1 = x2;
        y1 = y2;
      } else if (t > 0) {
        x1 += dx * t;
        y1 += dy * t;
      }
    }
    return squaredDistance(x, y, x1, y1);
  }

  /**
   * Returns the square of the distance between the points (x1, y1) and (x2, y2).
   * @param {number} x1 X1.
   * @param {number} y1 Y1.
   * @param {number} x2 X2.
   * @param {number} y2 Y2.
   * @return {number} Squared distance.
   */
  function squaredDistance(x1, y1, x2, y2) {
    const dx = x2 - x1;
    const dy = y2 - y1;
    return dx * dx + dy * dy;
  }

  /**
   * Solves system of linear equations using Gaussian elimination method.
   *
   * @param {Array<Array<number>>} mat Augmented matrix (n x n + 1 column)
   *                                     in row-major order.
   * @return {Array<number>|null} The resulting vector.
   */
  function solveLinearSystem(mat) {
    const n = mat.length;

    for (let i = 0; i < n; i++) {
      // Find max in the i-th column (ignoring i - 1 first rows)
      let maxRow = i;
      let maxEl = Math.abs(mat[i][i]);
      for (let r = i + 1; r < n; r++) {
        const absValue = Math.abs(mat[r][i]);
        if (absValue > maxEl) {
          maxEl = absValue;
          maxRow = r;
        }
      }

      if (maxEl === 0) {
        return null; // matrix is singular
      }

      // Swap max row with i-th (current) row
      const tmp = mat[maxRow];
      mat[maxRow] = mat[i];
      mat[i] = tmp;

      // Subtract the i-th row to make all the remaining rows 0 in the i-th column
      for (let j = i + 1; j < n; j++) {
        const coef = -mat[j][i] / mat[i][i];
        for (let k = i; k < n + 1; k++) {
          if (i == k) {
            mat[j][k] = 0;
          } else {
            mat[j][k] += coef * mat[i][k];
          }
        }
      }
    }

    // Solve Ax=b for upper triangular matrix A (mat)
    const x = new Array(n);
    for (let l = n - 1; l >= 0; l--) {
      x[l] = mat[l][n] / mat[l][l];
      for (let m = l - 1; m >= 0; m--) {
        mat[m][n] -= mat[m][l] * x[l];
      }
    }
    return x;
  }

  /**
   * Converts radians to to degrees.
   *
   * @param {number} angleInRadians Angle in radians.
   * @return {number} Angle in degrees.
   */
  function toDegrees(angleInRadians) {
    return (angleInRadians * 180) / Math.PI;
  }

  /**
   * Converts degrees to radians.
   *
   * @param {number} angleInDegrees Angle in degrees.
   * @return {number} Angle in radians.
   */
  function toRadians(angleInDegrees) {
    return (angleInDegrees * Math.PI) / 180;
  }

  /**
   * Returns the modulo of a / b, depending on the sign of b.
   *
   * @param {number} a Dividend.
   * @param {number} b Divisor.
   * @return {number} Modulo.
   */
  function modulo(a, b) {
    const r = a % b;
    return r * b < 0 ? r + b : r;
  }

  /**
   * Calculates the linearly interpolated value of x between a and b.
   *
   * @param {number} a Number
   * @param {number} b Number
   * @param {number} x Value to be interpolated.
   * @return {number} Interpolated value.
   */
  function lerp(a, b, x) {
    return a + x * (b - a);
  }

  /**
   * Returns a number with a limited number of decimal digits.
   * @param {number} n The input number.
   * @param {number} decimals The maximum number of decimal digits.
   * @return {number} The input number with a limited number of decimal digits.
   */
  function toFixed(n, decimals) {
    const factor = Math.pow(10, decimals);
    return Math.round(n * factor) / factor;
  }

  /**
   * Rounds a number to the next smaller integer considering only the given number
   * of decimal digits (with rounding on the final digit).
   * @param {number} n The input number.
   * @param {number} decimals The maximum number of decimal digits.
   * @return {number} The next smaller integer.
   */
  function floor(n, decimals) {
    return Math.floor(toFixed(n, decimals));
  }

  /**
   * Rounds a number to the next bigger integer considering only the given number
   * of decimal digits (with rounding on the final digit).
   * @param {number} n The input number.
   * @param {number} decimals The maximum number of decimal digits.
   * @return {number} The next bigger integer.
   */
  function ceil(n, decimals) {
    return Math.ceil(toFixed(n, decimals));
  }

  /**
   * Wraps a number between some minimum and maximum values.
   * @param {number} n The number to wrap.
   * @param {number} min The minimum of the range (inclusive).
   * @param {number} max The maximum of the range (exclusive).
   * @return {number} The wrapped number.
   */
  function wrap(n, min, max) {
    if (n >= min && n < max) {
      return n;
    }
    const range = max - min;
    return ((((n - min) % range) + range) % range) + min;
  }

  /**
   * @module ol/sphere
   */

  /**
   * Object literal with options for the {@link getLength} or {@link getArea}
   * functions.
   * @typedef {Object} SphereMetricOptions
   * @property {import("./proj.js").ProjectionLike} [projection='EPSG:3857']
   * Projection of the  geometry.  By default, the geometry is assumed to be in
   * Web Mercator.
   * @property {number} [radius=6371008.8] Sphere radius.  By default, the
   * [mean Earth radius](https://en.wikipedia.org/wiki/Earth_radius#Mean_radius)
   * for the WGS84 ellipsoid is used.
   */

  /**
   * The mean Earth radius (1/3 * (2a + b)) for the WGS84 ellipsoid.
   * https://en.wikipedia.org/wiki/Earth_radius#Mean_radius
   * @type {number}
   */
  const DEFAULT_RADIUS = 6371008.8;

  /**
   * Get the great circle distance (in meters) between two geographic coordinates.
   * @param {Array} c1 Starting coordinate.
   * @param {Array} c2 Ending coordinate.
   * @param {number} [radius] The sphere radius to use.  Defaults to the Earth's
   *     mean radius using the WGS84 ellipsoid.
   * @return {number} The great circle distance between the points (in meters).
   * @api
   */
  function getDistance(c1, c2, radius) {
    radius = radius || DEFAULT_RADIUS;
    const lat1 = toRadians(c1[1]);
    const lat2 = toRadians(c2[1]);
    const deltaLatBy2 = (lat2 - lat1) / 2;
    const deltaLonBy2 = toRadians(c2[0] - c1[0]) / 2;
    const a =
      Math.sin(deltaLatBy2) * Math.sin(deltaLatBy2) +
      Math.sin(deltaLonBy2) *
        Math.sin(deltaLonBy2) *
        Math.cos(lat1) *
        Math.cos(lat2);
    return 2 * radius * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  }

  /**
   * @module ol/console
   */


  /**
   * @param  {...any} args Arguments to log
   */
  function warn(...args) {
    console.warn(...args); // eslint-disable-line no-console
  }

  /**
   * @module ol/coordinate
   */

  /**
   * An array of numbers representing an `xy`, `xyz` or `xyzm` coordinate.
   * Example: `[16, 48]`.
   * @typedef {Array<number>} Coordinate
   * @api
   */

  /**
   * A function that takes a {@link module:ol/coordinate~Coordinate} and
   * transforms it into a `{string}`.
   *
   * @typedef {function((Coordinate|undefined)): string} CoordinateFormat
   * @api
   */

  /**
   * Add `delta` to `coordinate`. `coordinate` is modified in place and returned
   * by the function.
   *
   * Example:
   *
   *     import {add} from 'ol/coordinate.js';
   *
   *     const coord = [7.85, 47.983333];
   *     add(coord, [-2, 4]);
   *     // coord is now [5.85, 51.983333]
   *
   * @param {Coordinate} coordinate Coordinate.
   * @param {Coordinate} delta Delta.
   * @return {Coordinate} The input coordinate adjusted by
   * the given delta.
   * @api
   */
  function add$2(coordinate, delta) {
    coordinate[0] += +delta[0];
    coordinate[1] += +delta[1];
    return coordinate;
  }

  /**
   * @param {Coordinate} coordinate1 First coordinate.
   * @param {Coordinate} coordinate2 Second coordinate.
   * @return {boolean} The two coordinates are equal.
   */
  function equals(coordinate1, coordinate2) {
    let equals = true;
    for (let i = coordinate1.length - 1; i >= 0; --i) {
      if (coordinate1[i] != coordinate2[i]) {
        equals = false;
        break;
      }
    }
    return equals;
  }

  /**
   * Rotate `coordinate` by `angle`. `coordinate` is modified in place and
   * returned by the function.
   *
   * Example:
   *
   *     import {rotate} from 'ol/coordinate.js';
   *
   *     const coord = [7.85, 47.983333];
   *     const rotateRadians = Math.PI / 2; // 90 degrees
   *     rotate(coord, rotateRadians);
   *     // coord is now [-47.983333, 7.85]
   *
   * @param {Coordinate} coordinate Coordinate.
   * @param {number} angle Angle in radian.
   * @return {Coordinate} Coordinate.
   * @api
   */
  function rotate$1(coordinate, angle) {
    const cosAngle = Math.cos(angle);
    const sinAngle = Math.sin(angle);
    const x = coordinate[0] * cosAngle - coordinate[1] * sinAngle;
    const y = coordinate[1] * cosAngle + coordinate[0] * sinAngle;
    coordinate[0] = x;
    coordinate[1] = y;
    return coordinate;
  }

  /**
   * Scale `coordinate` by `scale`. `coordinate` is modified in place and returned
   * by the function.
   *
   * Example:
   *
   *     import {scale as scaleCoordinate} from 'ol/coordinate.js';
   *
   *     const coord = [7.85, 47.983333];
   *     const scale = 1.2;
   *     scaleCoordinate(coord, scale);
   *     // coord is now [9.42, 57.5799996]
   *
   * @param {Coordinate} coordinate Coordinate.
   * @param {number} scale Scale factor.
   * @return {Coordinate} Coordinate.
   */
  function scale$2(coordinate, scale) {
    coordinate[0] *= scale;
    coordinate[1] *= scale;
    return coordinate;
  }

  /**
   * Modifies the provided coordinate in-place to be within the real world
   * extent. The lower projection extent boundary is inclusive, the upper one
   * exclusive.
   *
   * @param {Coordinate} coordinate Coordinate.
   * @param {import("./proj/Projection.js").default} projection Projection.
   * @return {Coordinate} The coordinate within the real world extent.
   */
  function wrapX$1(coordinate, projection) {
    if (projection.canWrapX()) {
      const worldWidth = getWidth(projection.getExtent());
      const worldsAway = getWorldsAway(coordinate, projection, worldWidth);
      if (worldsAway) {
        coordinate[0] -= worldsAway * worldWidth;
      }
    }
    return coordinate;
  }
  /**
   * @param {Coordinate} coordinate Coordinate.
   * @param {import("./proj/Projection.js").default} projection Projection.
   * @param {number} [sourceExtentWidth] Width of the source extent.
   * @return {number} Offset in world widths.
   */
  function getWorldsAway(coordinate, projection, sourceExtentWidth) {
    const projectionExtent = projection.getExtent();
    let worldsAway = 0;
    if (
      projection.canWrapX() &&
      (coordinate[0] < projectionExtent[0] || coordinate[0] > projectionExtent[2])
    ) {
      sourceExtentWidth = sourceExtentWidth || getWidth(projectionExtent);
      worldsAway = Math.floor(
        (coordinate[0] - projectionExtent[0]) / sourceExtentWidth,
      );
    }
    return worldsAway;
  }

  /**
   * Compute the angle between p0pA and p0pB
   * @param {Coordinate} p0 Point 0
   * @param {Coordinate} pA Point A
   * @param {Coordinate} pB Point B
   * @return {number} a value in [0, 2PI]
   */
  function angleBetween(p0, pA, pB) {
    const lenA = Math.sqrt(
      (pA[0] - p0[0]) * (pA[0] - p0[0]) + (pA[1] - p0[1]) * (pA[1] - p0[1]),
    );
    const tangentA = [(pA[0] - p0[0]) / lenA, (pA[1] - p0[1]) / lenA];
    const orthoA = [-tangentA[1], tangentA[0]];
    const lenB = Math.sqrt(
      (pB[0] - p0[0]) * (pB[0] - p0[0]) + (pB[1] - p0[1]) * (pB[1] - p0[1]),
    );
    const tangentB = [(pB[0] - p0[0]) / lenB, (pB[1] - p0[1]) / lenB];

    // this angle can be clockwise or anticlockwise; hence the computation afterwards
    let angle =
      lenA === 0 || lenB === 0
        ? 0
        : Math.acos(
            clamp(tangentB[0] * tangentA[0] + tangentB[1] * tangentA[1], -1, 1),
          );
    angle = Math.max(angle, 0.00001); // avoid a zero angle otherwise this is detected as a line cap
    const isClockwise = tangentB[0] * orthoA[0] + tangentB[1] * orthoA[1] > 0;
    return !isClockwise ? Math.PI * 2 - angle : angle;
  }

  /**
   * @module ol/proj/Units
   */


  /**
   * @typedef {Object} MetersPerUnitLookup
   * @property {number} radians Radians
   * @property {number} degrees Degrees
   * @property {number} ft  Feet
   * @property {number} m Meters
   * @property {number} us-ft US feet
   */

  /**
   * Meters per unit lookup table.
   * @const
   * @type {MetersPerUnitLookup}
   * @api
   */
  const METERS_PER_UNIT$1 = {
    // use the radius of the Normal sphere
    'radians': 6370997 / (2 * Math.PI),
    'degrees': (2 * Math.PI * 6370997) / 360,
    'ft': 0.3048,
    'm': 1,
    'us-ft': 1200 / 3937,
  };

  /**
   * @module ol/proj/Projection
   */

  /**
   * The function is called with a `number` view resolution and a
   * {@link module:ol/coordinate~Coordinate} as arguments, and returns the `number` resolution
   * in projection units at the passed coordinate.
   * @typedef {function(number, import("../coordinate.js").Coordinate):number} GetPointResolution
   * @api
   */

  /**
   * @typedef {Object} Options
   * @property {string} code The SRS identifier code, e.g. `EPSG:4326`.
   * @property {import("./Units.js").Units} [units] Units. Required unless a
   * proj4 projection is defined for `code`.
   * @property {import("../extent.js").Extent} [extent] The validity extent for the SRS.
   * @property {string} [axisOrientation='enu'] The axis orientation as specified in Proj4.
   * @property {boolean} [global=false] Whether the projection is valid for the whole globe.
   * @property {number} [metersPerUnit] The meters per unit for the SRS.
   * If not provided, the `units` are used to get the meters per unit from the {@link METERS_PER_UNIT}
   * lookup table.
   * @property {import("../extent.js").Extent} [worldExtent] The world extent for the SRS.
   * @property {GetPointResolution} [getPointResolution]
   * Function to determine resolution at a point. The function is called with a
   * `number` view resolution and a {@link module:ol/coordinate~Coordinate} as arguments, and returns
   * the `number` resolution in projection units at the passed coordinate. If this is `undefined`,
   * the default {@link module:ol/proj.getPointResolution} function will be used.
   */

  /**
   * @classdesc
   * In most cases, you should not need to create instances of this class.
   * Instead, where projection information is required, you can use a string
   * projection code or identifier (e.g. `EPSG:4326`) instead of a projection
   * instance.
   *
   * The library includes support for transforming coordinates between the following
   * projections:
   *
   *  WGS 84 / Geographic - Using codes `EPSG:4326`, `CRS:84`, `urn:ogc:def:crs:EPSG:6.6:4326`,
   *    `urn:ogc:def:crs:OGC:1.3:CRS84`, `urn:ogc:def:crs:OGC:2:84`, `http://www.opengis.net/gml/srs/epsg.xml#4326`,
   *    or `urn:x-ogc:def:crs:EPSG:4326`
   *  WGS 84 / Spherical Mercator - Using codes `EPSG:3857`, `EPSG:102100`, `EPSG:102113`, `EPSG:900913`,
   *    `urn:ogc:def:crs:EPSG:6.18:3:3857`, or `http://www.opengis.net/gml/srs/epsg.xml#3857`
   *  WGS 84 / UTM zones - Using codes `EPSG:32601` through `EPSG:32660` for northern zones
   *    and `EPSG:32701` through `EPSG:32760` for southern zones. Note that the built-in UTM transforms
   *    are lower accuracy (with errors on the order of 0.1 m) than those that you might get in a
   *    library like [proj4js](https://github.com/proj4js/proj4js).
   *
   * For additional projection support, or to use higher accuracy transforms than the built-in ones, you can use
   * the [proj4js](https://github.com/proj4js/proj4js) library. With `proj4js`, after adding any new projection
   * definitions, call the {@link module:ol/proj/proj4.register} function.
   *
   * You can use the {@link module:ol/proj.get} function to retrieve a projection instance
   * for one of the registered projections.
   *
   * @api
   */
  class Projection {
    /**
     * @param {Options} options Projection options.
     */
    constructor(options) {
      /**
       * @private
       * @type {string}
       */
      this.code_ = options.code;

      /**
       * Units of projected coordinates. When set to `TILE_PIXELS`, a
       * `this.extent_` and `this.worldExtent_` must be configured properly for each
       * tile.
       * @private
       * @type {import("./Units.js").Units}
       */
      this.units_ = /** @type {import("./Units.js").Units} */ (options.units);

      /**
       * Validity extent of the projection in projected coordinates. For projections
       * with `TILE_PIXELS` units, this is the extent of the tile in
       * tile pixel space.
       * @private
       * @type {import("../extent.js").Extent}
       */
      this.extent_ = options.extent !== undefined ? options.extent : null;

      /**
       * Extent of the world in EPSG:4326. For projections with
       * `TILE_PIXELS` units, this is the extent of the tile in
       * projected coordinate space.
       * @private
       * @type {import("../extent.js").Extent}
       */
      this.worldExtent_ =
        options.worldExtent !== undefined ? options.worldExtent : null;

      /**
       * @private
       * @type {string}
       */
      this.axisOrientation_ =
        options.axisOrientation !== undefined ? options.axisOrientation : 'enu';

      /**
       * @private
       * @type {boolean}
       */
      this.global_ = options.global !== undefined ? options.global : false;

      /**
       * @private
       * @type {boolean}
       */
      this.canWrapX_ = !!(this.global_ && this.extent_);

      /**
       * @private
       * @type {GetPointResolution|undefined}
       */
      this.getPointResolutionFunc_ = options.getPointResolution;

      /**
       * @private
       * @type {import("../tilegrid/TileGrid.js").default}
       */
      this.defaultTileGrid_ = null;

      /**
       * @private
       * @type {number|undefined}
       */
      this.metersPerUnit_ = options.metersPerUnit;
    }

    /**
     * @return {boolean} The projection is suitable for wrapping the x-axis
     */
    canWrapX() {
      return this.canWrapX_;
    }

    /**
     * Get the code for this projection, e.g. 'EPSG:4326'.
     * @return {string} Code.
     * @api
     */
    getCode() {
      return this.code_;
    }

    /**
     * Get the validity extent for this projection.
     * @return {import("../extent.js").Extent} Extent.
     * @api
     */
    getExtent() {
      return this.extent_;
    }

    /**
     * Get the units of this projection.
     * @return {import("./Units.js").Units} Units.
     * @api
     */
    getUnits() {
      return this.units_;
    }

    /**
     * Get the amount of meters per unit of this projection.  If the projection is
     * not configured with `metersPerUnit` or a units identifier, the return is
     * `undefined`.
     * @return {number|undefined} Meters.
     * @api
     */
    getMetersPerUnit() {
      return this.metersPerUnit_ || METERS_PER_UNIT$1[this.units_];
    }

    /**
     * Get the world extent for this projection.
     * @return {import("../extent.js").Extent} Extent.
     * @api
     */
    getWorldExtent() {
      return this.worldExtent_;
    }

    /**
     * Get the axis orientation of this projection.
     * Example values are:
     * enu - the default easting, northing, elevation.
     * neu - northing, easting, up - useful for "lat/long" geographic coordinates,
     *     or south orientated transverse mercator.
     * wnu - westing, northing, up - some planetary coordinate systems have
     *     "west positive" coordinate systems
     * @return {string} Axis orientation.
     * @api
     */
    getAxisOrientation() {
      return this.axisOrientation_;
    }

    /**
     * Is this projection a global projection which spans the whole world?
     * @return {boolean} Whether the projection is global.
     * @api
     */
    isGlobal() {
      return this.global_;
    }

    /**
     * Set if the projection is a global projection which spans the whole world
     * @param {boolean} global Whether the projection is global.
     * @api
     */
    setGlobal(global) {
      this.global_ = global;
      this.canWrapX_ = !!(global && this.extent_);
    }

    /**
     * @return {import("../tilegrid/TileGrid.js").default} The default tile grid.
     */
    getDefaultTileGrid() {
      return this.defaultTileGrid_;
    }

    /**
     * @param {import("../tilegrid/TileGrid.js").default} tileGrid The default tile grid.
     */
    setDefaultTileGrid(tileGrid) {
      this.defaultTileGrid_ = tileGrid;
    }

    /**
     * Set the validity extent for this projection.
     * @param {import("../extent.js").Extent} extent Extent.
     * @api
     */
    setExtent(extent) {
      this.extent_ = extent;
      this.canWrapX_ = !!(this.global_ && extent);
    }

    /**
     * Set the world extent for this projection.
     * @param {import("../extent.js").Extent} worldExtent World extent
     *     [minlon, minlat, maxlon, maxlat].
     * @api
     */
    setWorldExtent(worldExtent) {
      this.worldExtent_ = worldExtent;
    }

    /**
     * Set the getPointResolution function (see {@link module:ol/proj.getPointResolution}
     * for this projection.
     * @param {function(number, import("../coordinate.js").Coordinate):number} func Function
     * @api
     */
    setGetPointResolution(func) {
      this.getPointResolutionFunc_ = func;
    }

    /**
     * Get the custom point resolution function for this projection (if set).
     * @return {GetPointResolution|undefined} The custom point
     * resolution function (if set).
     */
    getPointResolutionFunc() {
      return this.getPointResolutionFunc_;
    }
  }

  /**
   * @module ol/proj/epsg3857
   */

  /**
   * Radius of WGS84 sphere
   *
   * @const
   * @type {number}
   */
  const RADIUS$1 = 6378137;

  /**
   * @const
   * @type {number}
   */
  const HALF_SIZE = Math.PI * RADIUS$1;

  /**
   * @const
   * @type {import("../extent.js").Extent}
   */
  const EXTENT$1 = [-HALF_SIZE, -HALF_SIZE, HALF_SIZE, HALF_SIZE];

  /**
   * @const
   * @type {import("../extent.js").Extent}
   */
  const WORLD_EXTENT = [-180, -85, 180, 85];

  /**
   * Maximum safe value in y direction
   * @const
   * @type {number}
   */
  const MAX_SAFE_Y = RADIUS$1 * Math.log(Math.tan(Math.PI / 2));

  /**
   * @classdesc
   * Projection object for web/spherical Mercator (EPSG:3857).
   */
  class EPSG3857Projection extends Projection {
    /**
     * @param {string} code Code.
     */
    constructor(code) {
      super({
        code: code,
        units: 'm',
        extent: EXTENT$1,
        global: true,
        worldExtent: WORLD_EXTENT,
        getPointResolution: function (resolution, point) {
          return resolution / Math.cosh(point[1] / RADIUS$1);
        },
      });
    }
  }

  /**
   * Projections equal to EPSG:3857.
   *
   * @const
   * @type {Array<import("./Projection.js").default>}
   */
  const PROJECTIONS$1 = [
    new EPSG3857Projection('EPSG:3857'),
    new EPSG3857Projection('EPSG:102100'),
    new EPSG3857Projection('EPSG:102113'),
    new EPSG3857Projection('EPSG:900913'),
    new EPSG3857Projection('http://www.opengis.net/def/crs/EPSG/0/3857'),
    new EPSG3857Projection('http://www.opengis.net/gml/srs/epsg.xml#3857'),
  ];

  /**
   * Transformation from EPSG:4326 to EPSG:3857.
   *
   * @param {Array<number>} input Input array of coordinate values.
   * @param {Array<number>} [output] Output array of coordinate values.
   * @param {number} [dimension] Dimension (default is `2`).
   * @param {number} [stride] Stride (default is `dimension`).
   * @return {Array<number>} Output array of coordinate values.
   */
  function fromEPSG4326(input, output, dimension, stride) {
    const length = input.length;
    dimension = dimension > 1 ? dimension : 2;
    stride = stride ?? dimension;
    if (output === undefined) {
      if (dimension > 2) {
        // preserve values beyond second dimension
        output = input.slice();
      } else {
        output = new Array(length);
      }
    }
    for (let i = 0; i < length; i += stride) {
      output[i] = (HALF_SIZE * input[i]) / 180;
      let y = RADIUS$1 * Math.log(Math.tan((Math.PI * (+input[i + 1] + 90)) / 360));
      if (y > MAX_SAFE_Y) {
        y = MAX_SAFE_Y;
      } else if (y < -MAX_SAFE_Y) {
        y = -MAX_SAFE_Y;
      }
      output[i + 1] = y;
    }
    return output;
  }

  /**
   * Transformation from EPSG:3857 to EPSG:4326.
   *
   * @param {Array<number>} input Input array of coordinate values.
   * @param {Array<number>} [output] Output array of coordinate values.
   * @param {number} [dimension] Dimension (default is `2`).
   * @param {number} [stride] Stride (default is `dimension`).
   * @return {Array<number>} Output array of coordinate values.
   */
  function toEPSG4326(input, output, dimension, stride) {
    const length = input.length;
    dimension = dimension > 1 ? dimension : 2;
    stride = stride ?? dimension;
    if (output === undefined) {
      if (dimension > 2) {
        // preserve values beyond second dimension
        output = input.slice();
      } else {
        output = new Array(length);
      }
    }
    for (let i = 0; i < length; i += stride) {
      output[i] = (180 * input[i]) / HALF_SIZE;
      output[i + 1] =
        (360 * Math.atan(Math.exp(input[i + 1] / RADIUS$1))) / Math.PI - 90;
    }
    return output;
  }

  /**
   * @module ol/proj/epsg4326
   */

  /**
   * Semi-major radius of the WGS84 ellipsoid.
   *
   * @const
   * @type {number}
   */
  const RADIUS = 6378137;

  /**
   * Extent of the EPSG:4326 projection which is the whole world.
   *
   * @const
   * @type {import("../extent.js").Extent}
   */
  const EXTENT = [-180, -90, 180, 90];

  /**
   * @const
   * @type {number}
   */
  const METERS_PER_UNIT = (Math.PI * RADIUS) / 180;

  /**
   * @classdesc
   * Projection object for WGS84 geographic coordinates (EPSG:4326).
   *
   * Note that OpenLayers does not strictly comply with the EPSG definition.
   * The EPSG registry defines 4326 as a CRS for Latitude,Longitude (y,x).
   * OpenLayers treats EPSG:4326 as a pseudo-projection, with x,y coordinates.
   */
  class EPSG4326Projection extends Projection {
    /**
     * @param {string} code Code.
     * @param {string} [axisOrientation] Axis orientation.
     */
    constructor(code, axisOrientation) {
      super({
        code: code,
        units: 'degrees',
        extent: EXTENT,
        axisOrientation: axisOrientation,
        global: true,
        metersPerUnit: METERS_PER_UNIT,
        worldExtent: EXTENT,
      });
    }
  }

  /**
   * Projections equal to EPSG:4326.
   *
   * @const
   * @type {Array<import("./Projection.js").default>}
   */
  const PROJECTIONS = [
    new EPSG4326Projection('CRS:84'),
    new EPSG4326Projection('EPSG:4326', 'neu'),
    new EPSG4326Projection('urn:ogc:def:crs:OGC:1.3:CRS84'),
    new EPSG4326Projection('urn:ogc:def:crs:OGC:2:84'),
    new EPSG4326Projection('http://www.opengis.net/def/crs/OGC/1.3/CRS84'),
    new EPSG4326Projection('http://www.opengis.net/gml/srs/epsg.xml#4326', 'neu'),
    new EPSG4326Projection('http://www.opengis.net/def/crs/EPSG/0/4326', 'neu'),
  ];

  /**
   * @module ol/proj/projections
   */

  /**
   * @type {Object<string, import("./Projection.js").default>}
   */
  let cache$1 = {};

  /**
   * Get a cached projection by code.
   * @param {string} code The code for the projection.
   * @return {import("./Projection.js").default|null} The projection (if cached).
   */
  function get$3(code) {
    return (
      cache$1[code] ||
      cache$1[code.replace(/urn:(x-)?ogc:def:crs:EPSG:(.*:)?(\w+)$/, 'EPSG:$3')] ||
      null
    );
  }

  /**
   * Add a projection to the cache.
   * @param {string} code The projection code.
   * @param {import("./Projection.js").default} projection The projection to cache.
   */
  function add$1(code, projection) {
    cache$1[code] = projection;
  }

  /**
   * @module ol/proj/transforms
   */

  /**
   * @private
   * @type {!Object<string, Object<string, import("../proj.js").TransformFunction>>}
   */
  let transforms = {};

  /**
   * Registers a conversion function to convert coordinates from the source
   * projection to the destination projection.
   *
   * @param {import("./Projection.js").default} source Source.
   * @param {import("./Projection.js").default} destination Destination.
   * @param {import("../proj.js").TransformFunction} transformFn Transform.
   */
  function add(source, destination, transformFn) {
    const sourceCode = source.getCode();
    const destinationCode = destination.getCode();
    if (!(sourceCode in transforms)) {
      transforms[sourceCode] = {};
    }
    transforms[sourceCode][destinationCode] = transformFn;
  }

  /**
   * Get a transform given a source code and a destination code.
   * @param {string} sourceCode The code for the source projection.
   * @param {string} destinationCode The code for the destination projection.
   * @return {import("../proj.js").TransformFunction|null} The transform function (if found).
   */
  function get$2(sourceCode, destinationCode) {
    if (sourceCode in transforms && destinationCode in transforms[sourceCode]) {
      return transforms[sourceCode][destinationCode];
    }
    return null;
  }

  /**
   * @module ol/proj/utm
   */


  /**
   * @typedef {Object} UTMZone
   * @property {number} number The zone number (1 - 60).
   * @property {boolean} north The northern hemisphere.
   */

  const K0 = 0.9996;

  const E = 0.00669438;
  const E2 = E * E;
  const E3 = E2 * E;
  const E_P2 = E / (1 - E);

  const SQRT_E = Math.sqrt(1 - E);
  const _E = (1 - SQRT_E) / (1 + SQRT_E);
  const _E2 = _E * _E;
  const _E3 = _E2 * _E;
  const _E4 = _E3 * _E;
  const _E5 = _E4 * _E;

  const M1 = 1 - E / 4 - (3 * E2) / 64 - (5 * E3) / 256;
  const M2 = (3 * E) / 8 + (3 * E2) / 32 + (45 * E3) / 1024;
  const M3 = (15 * E2) / 256 + (45 * E3) / 1024;
  const M4 = (35 * E3) / 3072;

  const P2 = (3 / 2) * _E - (27 / 32) * _E3 + (269 / 512) * _E5;
  const P3 = (21 / 16) * _E2 - (55 / 32) * _E4;
  const P4 = (151 / 96) * _E3 - (417 / 128) * _E5;
  const P5 = (1097 / 512) * _E4;

  const R = 6378137;

  /**
   * @param {number} easting Easting value of coordinate.
   * @param {number} northing Northing value of coordinate.
   * @param {UTMZone} zone The UTM zone.
   * @return {import("../coordinate.js").Coordinate} The transformed coordinate.
   */
  function toLonLat$1(easting, northing, zone) {
    const x = easting - 500000;
    const y = zone.north ? northing : northing - 10000000;

    const m = y / K0;
    const mu = m / (R * M1);

    const pRad =
      mu +
      P2 * Math.sin(2 * mu) +
      P3 * Math.sin(4 * mu) +
      P4 * Math.sin(6 * mu) +
      P5 * Math.sin(8 * mu);

    const pSin = Math.sin(pRad);
    const pSin2 = pSin * pSin;

    const pCos = Math.cos(pRad);

    const pTan = pSin / pCos;
    const pTan2 = pTan * pTan;
    const pTan4 = pTan2 * pTan2;

    const epSin = 1 - E * pSin2;
    const epSinSqrt = Math.sqrt(1 - E * pSin2);

    const n = R / epSinSqrt;
    const r = (1 - E) / epSin;

    const c = E_P2 * pCos ** 2;
    const c2 = c * c;

    const d = x / (n * K0);
    const d2 = d * d;
    const d3 = d2 * d;
    const d4 = d3 * d;
    const d5 = d4 * d;
    const d6 = d5 * d;

    const latitude =
      pRad -
      (pTan / r) *
        (d2 / 2 - (d4 / 24) * (5 + 3 * pTan2 + 10 * c - 4 * c2 - 9 * E_P2)) +
      (d6 / 720) * (61 + 90 * pTan2 + 298 * c + 45 * pTan4 - 252 * E_P2 - 3 * c2);

    let longitude =
      (d -
        (d3 / 6) * (1 + 2 * pTan2 + c) +
        (d5 / 120) * (5 - 2 * c + 28 * pTan2 - 3 * c2 + 8 * E_P2 + 24 * pTan4)) /
      pCos;

    longitude = wrap(
      longitude + toRadians(zoneToCentralLongitude(zone.number)),
      -Math.PI,
      Math.PI,
    );

    return [toDegrees(longitude), toDegrees(latitude)];
  }

  const MIN_LATITUDE = -80;
  const MAX_LATITUDE = 84;
  const MIN_LONGITUDE = -180;
  const MAX_LONGITUDE = 180;

  /**
   * @param {number} longitude The longitude.
   * @param {number} latitude The latitude.
   * @param {UTMZone} zone The UTM zone.
   * @return {import('../coordinate.js').Coordinate} The UTM coordinate.
   */
  function fromLonLat$1(longitude, latitude, zone) {
    longitude = wrap(longitude, MIN_LONGITUDE, MAX_LONGITUDE);

    if (latitude < MIN_LATITUDE) {
      latitude = MIN_LATITUDE;
    } else if (latitude > MAX_LATITUDE) {
      latitude = MAX_LATITUDE;
    }

    const latRad = toRadians(latitude);
    const latSin = Math.sin(latRad);
    const latCos = Math.cos(latRad);

    const latTan = latSin / latCos;
    const latTan2 = latTan * latTan;
    const latTan4 = latTan2 * latTan2;

    const lonRad = toRadians(longitude);
    const centralLon = zoneToCentralLongitude(zone.number);
    const centralLonRad = toRadians(centralLon);

    const n = R / Math.sqrt(1 - E * latSin ** 2);
    const c = E_P2 * latCos ** 2;

    const a = latCos * wrap(lonRad - centralLonRad, -Math.PI, Math.PI);
    const a2 = a * a;
    const a3 = a2 * a;
    const a4 = a3 * a;
    const a5 = a4 * a;
    const a6 = a5 * a;

    const m =
      R *
      (M1 * latRad -
        M2 * Math.sin(2 * latRad) +
        M3 * Math.sin(4 * latRad) -
        M4 * Math.sin(6 * latRad));

    const easting =
      K0 *
        n *
        (a +
          (a3 / 6) * (1 - latTan2 + c) +
          (a5 / 120) * (5 - 18 * latTan2 + latTan4 + 72 * c - 58 * E_P2)) +
      500000;

    let northing =
      K0 *
      (m +
        n *
          latTan *
          (a2 / 2 +
            (a4 / 24) * (5 - latTan2 + 9 * c + 4 * c ** 2) +
            (a6 / 720) * (61 - 58 * latTan2 + latTan4 + 600 * c - 330 * E_P2)));

    if (!zone.north) {
      northing += 10000000;
    }

    return [easting, northing];
  }

  /**
   * @param {number} zone The zone number.
   * @return {number} The central longitude in degrees.
   */
  function zoneToCentralLongitude(zone) {
    return (zone - 1) * 6 - 180 + 3;
  }

  /**
   * @type {Array<RegExp>}
   */
  const epsgRegExes = [
    /^EPSG:(\d+)$/,
    /^urn:ogc:def:crs:EPSG::(\d+)$/,
    /^http:\/\/www\.opengis\.net\/def\/crs\/EPSG\/0\/(\d+)$/,
  ];

  /**
   * @param {string} code The projection code.
   * @return {UTMZone|null} The UTM zone info (or null if not UTM).
   */
  function zoneFromCode(code) {
    let epsgId = 0;
    for (const re of epsgRegExes) {
      const match = code.match(re);
      if (match) {
        epsgId = parseInt(match[1]);
        break;
      }
    }
    if (!epsgId) {
      return null;
    }

    let number = 0;
    let north = false;
    if (epsgId > 32700 && epsgId < 32761) {
      number = epsgId - 32700;
    } else if (epsgId > 32600 && epsgId < 32661) {
      north = true;
      number = epsgId - 32600;
    }
    if (!number) {
      return null;
    }

    return {number, north};
  }

  /**
   * @param {function(number, number, UTMZone): import('../coordinate.js').Coordinate} transformer The transformer.
   * @param {UTMZone} zone The UTM zone.
   * @return {import('../proj.js').TransformFunction} The transform function.
   */
  function makeTransformFunction(transformer, zone) {
    return function (input, output, dimension, stride) {
      const length = input.length;
      dimension = dimension > 1 ? dimension : 2;
      stride = stride ?? dimension;
      if (!output) {
        if (dimension > 2) {
          output = input.slice();
        } else {
          output = new Array(length);
        }
      }
      for (let i = 0; i < length; i += stride) {
        const x = input[i];
        const y = input[i + 1];
        const coord = transformer(x, y, zone);
        output[i] = coord[0];
        output[i + 1] = coord[1];
      }
      return output;
    };
  }

  /**
   * @param {string} code The projection code.
   * @return {import('./Projection.js').default|null} A projection or null if unable to create one.
   */
  function makeProjection(code) {
    const zone = zoneFromCode(code);
    if (!zone) {
      return null;
    }
    return new Projection({code, units: 'm'});
  }

  /**
   * @param {import('./Projection.js').default} projection The projection.
   * @return {import('../proj.js').Transforms|null} The transforms lookup or null if unable to handle projection.
   */
  function makeTransforms(projection) {
    const zone = zoneFromCode(projection.getCode());
    if (!zone) {
      return null;
    }

    return {
      forward: makeTransformFunction(fromLonLat$1, zone),
      inverse: makeTransformFunction(toLonLat$1, zone),
    };
  }

  /**
   * @module ol/proj
   */


  /**
   * A projection as {@link module:ol/proj/Projection~Projection}, SRS identifier
   * string or undefined.
   * @typedef {Projection|string|undefined} ProjectionLike
   * @api
   */

  /**
   * @typedef {Object} Transforms
   * @property {TransformFunction} forward The forward transform (from geographic).
   * @property {TransformFunction} inverse The inverse transform (to geographic).
   */

  /**
   * @type {Array<function(Projection): Transforms|null>}
   */
  const transformFactories = [makeTransforms];

  /**
   * @type {Array<function(string): Projection|null>}
   */
  const projectionFactories = [makeProjection];

  let showCoordinateWarning = true;

  /**
   * @param {boolean} [disable] Disable console info about `useGeographic()`
   */
  function disableCoordinateWarning(disable) {
    showCoordinateWarning = false;
  }

  /**
   * @param {Array<number>} input Input coordinate array.
   * @param {Array<number>} [output] Output array of coordinate values.
   * @return {Array<number>} Output coordinate array (new array, same coordinate
   *     values).
   */
  function cloneTransform(input, output) {
    if (output !== undefined) {
      for (let i = 0, ii = input.length; i < ii; ++i) {
        output[i] = input[i];
      }
      output = output;
    } else {
      output = input.slice();
    }
    return output;
  }

  /**
   * Add a Projection object to the list of supported projections that can be
   * looked up by their code.
   *
   * @param {Projection} projection Projection instance.
   * @api
   */
  function addProjection(projection) {
    add$1(projection.getCode(), projection);
    add(projection, projection, cloneTransform);
  }

  /**
   * @param {Array<Projection>} projections Projections.
   */
  function addProjections(projections) {
    projections.forEach(addProjection);
  }

  /**
   * Fetches a Projection object for the code specified.
   *
   * @param {ProjectionLike} projectionLike Either a code string which is
   *     a combination of authority and identifier such as "EPSG:4326", or an
   *     existing projection object, or undefined.
   * @return {Projection|null} Projection object, or null if not in list.
   * @api
   */
  function get$1(projectionLike) {
    if (!(typeof projectionLike === 'string')) {
      return projectionLike;
    }
    const projection = get$3(projectionLike);
    if (projection) {
      return projection;
    }
    for (const makeProjection of projectionFactories) {
      const projection = makeProjection(projectionLike);
      if (projection) {
        return projection;
      }
    }
    return null;
  }

  /**
   * Get the resolution of the point in degrees or distance units.
   * For projections with degrees as the unit this will simply return the
   * provided resolution. For other projections the point resolution is
   * by default estimated by transforming the `point` pixel to EPSG:4326,
   * measuring its width and height on the normal sphere,
   * and taking the average of the width and height.
   * A custom function can be provided for a specific projection, either
   * by setting the `getPointResolution` option in the
   * {@link module:ol/proj/Projection~Projection} constructor or by using
   * {@link module:ol/proj/Projection~Projection#setGetPointResolution} to change an existing
   * projection object.
   * @param {ProjectionLike} projection The projection.
   * @param {number} resolution Nominal resolution in projection units.
   * @param {import("./coordinate.js").Coordinate} point Point to find adjusted resolution at.
   * @param {import("./proj/Units.js").Units} [units] Units to get the point resolution in.
   * Default is the projection's units.
   * @return {number} Point resolution.
   * @api
   */
  function getPointResolution(projection, resolution, point, units) {
    projection = get$1(projection);
    let pointResolution;
    const getter = projection.getPointResolutionFunc();
    if (getter) {
      pointResolution = getter(resolution, point);
    } else {
      const projUnits = projection.getUnits();
      if ((projUnits == 'degrees' && !units) || units == 'degrees') {
        pointResolution = resolution;
      } else {
        // Estimate point resolution by transforming the center pixel to EPSG:4326,
        // measuring its width and height on the normal sphere, and taking the
        // average of the width and height.
        const toEPSG4326 = getTransformFromProjections(
          projection,
          get$1('EPSG:4326'),
        );
        if (!toEPSG4326 && projUnits !== 'degrees') {
          // no transform is available
          pointResolution = resolution * projection.getMetersPerUnit();
        } else {
          let vertices = [
            point[0] - resolution / 2,
            point[1],
            point[0] + resolution / 2,
            point[1],
            point[0],
            point[1] - resolution / 2,
            point[0],
            point[1] + resolution / 2,
          ];
          vertices = toEPSG4326(vertices, vertices, 2);
          const width = getDistance(vertices.slice(0, 2), vertices.slice(2, 4));
          const height = getDistance(vertices.slice(4, 6), vertices.slice(6, 8));
          pointResolution = (width + height) / 2;
        }
        const metersPerUnit = projection.getMetersPerUnit();
        if (metersPerUnit !== undefined) {
          pointResolution /= metersPerUnit;
        }
      }
    }
    return pointResolution;
  }

  /**
   * Registers transformation functions that don't alter coordinates. Those allow
   * to transform between projections with equal meaning.
   *
   * @param {Array<Projection>} projections Projections.
   * @api
   */
  function addEquivalentProjections(projections) {
    addProjections(projections);
    projections.forEach(function (source) {
      projections.forEach(function (destination) {
        if (source !== destination) {
          add(source, destination, cloneTransform);
        }
      });
    });
  }

  /**
   * Registers transformation functions to convert coordinates in any projection
   * in projection1 to any projection in projection2.
   *
   * @param {Array<Projection>} projections1 Projections with equal
   *     meaning.
   * @param {Array<Projection>} projections2 Projections with equal
   *     meaning.
   * @param {TransformFunction} forwardTransform Transformation from any
   *   projection in projection1 to any projection in projection2.
   * @param {TransformFunction} inverseTransform Transform from any projection
   *   in projection2 to any projection in projection1..
   */
  function addEquivalentTransforms(
    projections1,
    projections2,
    forwardTransform,
    inverseTransform,
  ) {
    projections1.forEach(function (projection1) {
      projections2.forEach(function (projection2) {
        add(projection1, projection2, forwardTransform);
        add(projection2, projection1, inverseTransform);
      });
    });
  }

  /**
   * @param {Projection|string|undefined} projection Projection.
   * @param {string} defaultCode Default code.
   * @return {Projection} Projection.
   */
  function createProjection(projection, defaultCode) {
    if (!projection) {
      return get$1(defaultCode);
    }
    if (typeof projection === 'string') {
      return get$1(projection);
    }
    return /** @type {Projection} */ (projection);
  }

  /**
   * Creates a {@link module:ol/proj~TransformFunction} from a simple 2D coordinate transform
   * function.
   * @param {function(import("./coordinate.js").Coordinate): import("./coordinate.js").Coordinate} coordTransform Coordinate
   *     transform.
   * @return {TransformFunction} Transform function.
   */
  function createTransformFromCoordinateTransform(coordTransform) {
    return (
      /**
       * @param {Array<number>} input Input.
       * @param {Array<number>} [output] Output.
       * @param {number} [dimension] Dimensions that should be transformed.
       * @param {number} [stride] Stride.
       * @return {Array<number>} Output.
       */
      function (input, output, dimension, stride) {
        const length = input.length;
        dimension = dimension !== undefined ? dimension : 2;
        stride = stride ?? dimension;
        output = output !== undefined ? output : new Array(length);
        for (let i = 0; i < length; i += stride) {
          const point = coordTransform(input.slice(i, i + dimension));
          const pointLength = point.length;
          for (let j = 0, jj = stride; j < jj; ++j) {
            output[i + j] = j >= pointLength ? input[i + j] : point[j];
          }
        }
        return output;
      }
    );
  }

  /**
   * Transforms a coordinate from longitude/latitude to a different projection.
   * @param {import("./coordinate.js").Coordinate} coordinate Coordinate as longitude and latitude, i.e.
   *     an array with longitude as 1st and latitude as 2nd element.
   * @param {ProjectionLike} [projection] Target projection. The
   *     default is Web Mercator, i.e. 'EPSG:3857'.
   * @return {import("./coordinate.js").Coordinate} Coordinate projected to the target projection.
   * @api
   */
  function fromLonLat(coordinate, projection) {
    disableCoordinateWarning();
    return transform(
      coordinate,
      'EPSG:4326',
      'EPSG:3857',
    );
  }

  /**
   * Transforms a coordinate to longitude/latitude.
   * @param {import("./coordinate.js").Coordinate} coordinate Projected coordinate.
   * @param {ProjectionLike} [projection] Projection of the coordinate.
   *     The default is Web Mercator, i.e. 'EPSG:3857'.
   * @return {import("./coordinate.js").Coordinate} Coordinate as longitude and latitude, i.e. an array
   *     with longitude as 1st and latitude as 2nd element.
   * @api
   */
  function toLonLat(coordinate, projection) {
    const lonLat = transform(
      coordinate,
      'EPSG:3857',
      'EPSG:4326',
    );
    const lon = lonLat[0];
    if (lon < -180 || lon > 180) {
      lonLat[0] = modulo(lon + 180, 360) - 180;
    }
    return lonLat;
  }

  /**
   * Checks if two projections are the same, that is every coordinate in one
   * projection does represent the same geographic point as the same coordinate in
   * the other projection.
   *
   * @param {Projection} projection1 Projection 1.
   * @param {Projection} projection2 Projection 2.
   * @return {boolean} Equivalent.
   * @api
   */
  function equivalent$1(projection1, projection2) {
    if (projection1 === projection2) {
      return true;
    }
    const equalUnits = projection1.getUnits() === projection2.getUnits();
    if (projection1.getCode() === projection2.getCode()) {
      return equalUnits;
    }
    const transformFunc = getTransformFromProjections(projection1, projection2);
    return transformFunc === cloneTransform && equalUnits;
  }

  /**
   * Searches in the list of transform functions for the function for converting
   * coordinates from the source projection to the destination projection.
   *
   * @param {Projection} source Source Projection object.
   * @param {Projection} destination Destination Projection
   *     object.
   * @return {TransformFunction|null} Transform function.
   */
  function getTransformFromProjections(source, destination) {
    const sourceCode = source.getCode();
    const destinationCode = destination.getCode();
    let transformFunc = get$2(sourceCode, destinationCode);
    if (transformFunc) {
      return transformFunc;
    }

    /**
     * @type {Transforms|null}
     */
    let sourceTransforms = null;

    /**
     * @type {Transforms|null}
     */
    let destinationTransforms = null;

    // lazily add projections if we have supported transforms
    for (const makeTransforms of transformFactories) {
      if (!sourceTransforms) {
        sourceTransforms = makeTransforms(source);
      }
      if (!destinationTransforms) {
        destinationTransforms = makeTransforms(destination);
      }
    }

    if (!sourceTransforms && !destinationTransforms) {
      return null;
    }

    const intermediateCode = 'EPSG:4326';
    if (!destinationTransforms) {
      const toDestination = get$2(intermediateCode, destinationCode);
      if (toDestination) {
        transformFunc = composeTransformFuncs(
          sourceTransforms.inverse,
          toDestination,
        );
      }
    } else if (!sourceTransforms) {
      const fromSource = get$2(sourceCode, intermediateCode);
      if (fromSource) {
        transformFunc = composeTransformFuncs(
          fromSource,
          destinationTransforms.forward,
        );
      }
    } else {
      transformFunc = composeTransformFuncs(
        sourceTransforms.inverse,
        destinationTransforms.forward,
      );
    }

    if (transformFunc) {
      addProjection(source);
      addProjection(destination);
      add(source, destination, transformFunc);
    }

    return transformFunc;
  }

  /**
   * @param {TransformFunction} t1 The first transform function.
   * @param {TransformFunction} t2 The second transform function.
   * @return {TransformFunction} The composed transform function.
   */
  function composeTransformFuncs(t1, t2) {
    return function (input, output, dimensions, stride) {
      output = t1(input, output, dimensions, stride);
      return t2(output, output, dimensions, stride);
    };
  }

  /**
   * Given the projection-like objects, searches for a transformation
   * function to convert a coordinates array from the source projection to the
   * destination projection.
   *
   * @param {ProjectionLike} source Source.
   * @param {ProjectionLike} destination Destination.
   * @return {TransformFunction} Transform function.
   * @api
   */
  function getTransform(source, destination) {
    const sourceProjection = get$1(source);
    const destinationProjection = get$1(destination);
    return getTransformFromProjections(sourceProjection, destinationProjection);
  }

  /**
   * Transforms a coordinate from source projection to destination projection.
   * This returns a new coordinate (and does not modify the original). If there
   * is no available transform between the two projection, the function will throw
   * an error.
   *
   * See {@link module:ol/proj.transformExtent} for extent transformation.
   * See the transform method of {@link module:ol/geom/Geometry~Geometry} and its
   * subclasses for geometry transforms.
   *
   * @param {import("./coordinate.js").Coordinate} coordinate Coordinate.
   * @param {ProjectionLike} source Source projection-like.
   * @param {ProjectionLike} destination Destination projection-like.
   * @return {import("./coordinate.js").Coordinate} Coordinate.
   * @api
   */
  function transform(coordinate, source, destination) {
    const transformFunc = getTransform(source, destination);
    if (!transformFunc) {
      const sourceCode = get$1(source).getCode();
      const destinationCode = get$1(destination).getCode();
      throw new Error(
        `No transform available between ${sourceCode} and ${destinationCode}`,
      );
    }
    return transformFunc(coordinate, undefined, coordinate.length);
  }

  /**
   * Return a coordinate transformed into the user projection.  If no user projection
   * is set, the original coordinate is returned.
   * @param {Array<number>} coordinate Input coordinate.
   * @param {ProjectionLike} sourceProjection The input coordinate projection.
   * @return {Array<number>} The input coordinate in the user projection.
   */
  function toUserCoordinate(coordinate, sourceProjection) {
    {
      return coordinate;
    }
  }

  /**
   * Return a coordinate transformed from the user projection.  If no user projection
   * is set, the original coordinate is returned.
   * @param {Array<number>} coordinate Input coordinate.
   * @param {ProjectionLike} destProjection The destination projection.
   * @return {Array<number>} The input coordinate transformed.
   */
  function fromUserCoordinate(coordinate, destProjection) {
    {
      if (
        showCoordinateWarning &&
        !equals(coordinate, [0, 0]) &&
        coordinate[0] >= -180 &&
        coordinate[0] <= 180 &&
        coordinate[1] >= -90 &&
        coordinate[1] <= 90
      ) {
        showCoordinateWarning = false;
        warn(
          'Call useGeographic() from ol/proj once to work with [longitude, latitude] coordinates.',
        );
      }
      return coordinate;
    }
  }

  /**
   * Return an extent transformed into the user projection.  If no user projection
   * is set, the original extent is returned.
   * @param {import("./extent.js").Extent} extent Input extent.
   * @param {ProjectionLike} sourceProjection The input extent projection.
   * @return {import("./extent.js").Extent} The input extent in the user projection.
   */
  function toUserExtent(extent, sourceProjection) {
    {
      return extent;
    }
  }

  /**
   * Return an extent transformed from the user projection.  If no user projection
   * is set, the original extent is returned.
   * @param {import("./extent.js").Extent} extent Input extent.
   * @param {ProjectionLike} destProjection The destination projection.
   * @return {import("./extent.js").Extent} The input extent transformed.
   */
  function fromUserExtent(extent, destProjection) {
    {
      return extent;
    }
  }

  /**
   * Add transforms to and from EPSG:4326 and EPSG:3857.  This function is called
   * by when this module is executed and should only need to be called again after
   * `clearAllProjections()` is called (e.g. in tests).
   */
  function addCommon() {
    // Add transformations that don't alter coordinates to convert within set of
    // projections with equal meaning.
    addEquivalentProjections(PROJECTIONS$1);
    addEquivalentProjections(PROJECTIONS);
    // Add transformations to convert EPSG:4326 like coordinates to EPSG:3857 like
    // coordinates and back.
    addEquivalentTransforms(
      PROJECTIONS,
      PROJECTIONS$1,
      fromEPSG4326,
      toEPSG4326,
    );
  }

  addCommon();

  /**
   * @module ol/transform
   */

  /**
   * An array representing an affine 2d transformation for use with
   * {@link module:ol/transform} functions. The array has 6 elements.
   * @typedef {!Array<number>} Transform
   * @api
   */

  /**
   * Collection of affine 2d transformation functions. The functions work on an
   * array of 6 elements. The element order is compatible with the [SVGMatrix
   * interface](https://developer.mozilla.org/en-US/docs/Web/API/SVGMatrix) and is
   * a subset (elements a to f) of a 3×3 matrix:
   * ```
   * [ a c e ]
   * [ b d f ]
   * [ 0 0 1 ]
   * ```
   */

  /**
   * @private
   * @type {Transform}
   */
  new Array(6);

  /**
   * Create an identity transform.
   * @return {!Transform} Identity transform.
   */
  function create() {
    return [1, 0, 0, 1, 0, 0];
  }

  /**
   * Set transform on one matrix from another matrix.
   * @param {!Transform} transform1 Matrix to set transform to.
   * @param {!Transform} transform2 Matrix to set transform from.
   * @return {!Transform} transform1 with transform from transform2 applied.
   */
  function setFromArray(transform1, transform2) {
    transform1[0] = transform2[0];
    transform1[1] = transform2[1];
    transform1[2] = transform2[2];
    transform1[3] = transform2[3];
    transform1[4] = transform2[4];
    transform1[5] = transform2[5];
    return transform1;
  }

  /**
   * Transforms the given coordinate with the given transform returning the
   * resulting, transformed coordinate. The coordinate will be modified in-place.
   *
   * @param {Transform} transform The transformation.
   * @param {import("./coordinate.js").Coordinate|import("./pixel.js").Pixel} coordinate The coordinate to transform.
   * @return {import("./coordinate.js").Coordinate|import("./pixel.js").Pixel} return coordinate so that operations can be
   *     chained together.
   */
  function apply(transform, coordinate) {
    const x = coordinate[0];
    const y = coordinate[1];
    coordinate[0] = transform[0] * x + transform[2] * y + transform[4];
    coordinate[1] = transform[1] * x + transform[3] * y + transform[5];
    return coordinate;
  }

  /**
   * Creates a composite transform given an initial translation, scale, rotation, and
   * final translation (in that order only, not commutative).
   * @param {!Transform} transform The transform (will be modified in place).
   * @param {number} dx1 Initial translation x.
   * @param {number} dy1 Initial translation y.
   * @param {number} sx Scale factor x.
   * @param {number} sy Scale factor y.
   * @param {number} angle Rotation (in counter-clockwise radians).
   * @param {number} dx2 Final translation x.
   * @param {number} dy2 Final translation y.
   * @return {!Transform} The composite transform.
   */
  function compose(transform, dx1, dy1, sx, sy, angle, dx2, dy2) {
    const sin = Math.sin(angle);
    const cos = Math.cos(angle);
    transform[0] = sx * cos;
    transform[1] = sy * sin;
    transform[2] = -sx * sin;
    transform[3] = sy * cos;
    transform[4] = dx2 * sx * cos - dy2 * sx * sin + dx1;
    transform[5] = dx2 * sy * sin + dy2 * sy * cos + dy1;
    return transform;
  }

  /**
   * Invert the given transform.
   * @param {!Transform} target Transform to be set as the inverse of
   *     the source transform.
   * @param {!Transform} source The source transform to invert.
   * @return {!Transform} The inverted (target) transform.
   */
  function makeInverse(target, source) {
    const det = determinant(source);
    assert(det !== 0, 'Transformation matrix cannot be inverted');

    const a = source[0];
    const b = source[1];
    const c = source[2];
    const d = source[3];
    const e = source[4];
    const f = source[5];

    target[0] = d / det;
    target[1] = -b / det;
    target[2] = -c / det;
    target[3] = a / det;
    target[4] = (c * f - d * e) / det;
    target[5] = -(a * f - b * e) / det;

    return target;
  }

  /**
   * Returns the determinant of the given matrix.
   * @param {!Transform} mat Matrix.
   * @return {number} Determinant.
   */
  function determinant(mat) {
    return mat[0] * mat[3] - mat[1] * mat[2];
  }

  /**
   * @type {Array}
   */
  const matrixPrecision = [1e5, 1e5, 1e5, 1e5, 2, 2];

  /**
   * A matrix string version of the transform.  This can be used
   * for CSS transforms.
   * @param {!Transform} mat Matrix.
   * @return {string} The transform as a string.
   */
  function toString$1(mat) {
    const transformString = 'matrix(' + mat.join(', ') + ')';
    return transformString;
  }

  /**
   * Create a transform from a CSS transform matrix string.
   * @param {string} cssTransform The CSS string to parse.
   * @return {!Transform} The transform.
   */
  function fromString$1(cssTransform) {
    const values = cssTransform.substring(7, cssTransform.length - 1).split(',');
    return values.map(parseFloat);
  }

  /**
   * Compare two matrices for equality.
   * @param {!string} cssTransform1 A CSS transform matrix string.
   * @param {!string} cssTransform2 A CSS transform matrix string.
   * @return {boolean} The two matrices are equal.
   */
  function equivalent(cssTransform1, cssTransform2) {
    const mat1 = fromString$1(cssTransform1);
    const mat2 = fromString$1(cssTransform2);
    for (let i = 0; i < 6; ++i) {
      if (Math.round((mat1[i] - mat2[i]) * matrixPrecision[i]) !== 0) {
        return false;
      }
    }
    return true;
  }

  /**
   * @module ol/geom/flat/transform
   */

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {import("../../transform.js").Transform} transform Transform.
   * @param {Array<number>} [dest] Destination.
   * @param {number} [destinationStride] Stride of destination coordinates; if unspecified, assumed to be 2.
   * @return {Array<number>} Transformed coordinates.
   */
  function transform2D(
    flatCoordinates,
    offset,
    end,
    stride,
    transform,
    dest,
    destinationStride,
  ) {
    dest = dest ? dest : [];
    destinationStride = destinationStride ? destinationStride : 2;
    let i = 0;
    for (let j = offset; j < end; j += stride) {
      const x = flatCoordinates[j];
      const y = flatCoordinates[j + 1];
      dest[i++] = transform[0] * x + transform[2] * y + transform[4];
      dest[i++] = transform[1] * x + transform[3] * y + transform[5];

      for (let k = 2; k < destinationStride; k++) {
        dest[i++] = flatCoordinates[j + k];
      }
    }

    if (dest && dest.length != i) {
      dest.length = i;
    }
    return dest;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {number} angle Angle.
   * @param {Array<number>} anchor Rotation anchor point.
   * @param {Array<number>} [dest] Destination.
   * @return {Array<number>} Transformed coordinates.
   */
  function rotate(
    flatCoordinates,
    offset,
    end,
    stride,
    angle,
    anchor,
    dest,
  ) {
    dest = dest ? dest : [];
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    const anchorX = anchor[0];
    const anchorY = anchor[1];
    let i = 0;
    for (let j = offset; j < end; j += stride) {
      const deltaX = flatCoordinates[j] - anchorX;
      const deltaY = flatCoordinates[j + 1] - anchorY;
      dest[i++] = anchorX + deltaX * cos - deltaY * sin;
      dest[i++] = anchorY + deltaX * sin + deltaY * cos;
      for (let k = j + 2; k < j + stride; ++k) {
        dest[i++] = flatCoordinates[k];
      }
    }
    if (dest && dest.length != i) {
      dest.length = i;
    }
    return dest;
  }

  /**
   * Scale the coordinates.
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {number} sx Scale factor in the x-direction.
   * @param {number} sy Scale factor in the y-direction.
   * @param {Array<number>} anchor Scale anchor point.
   * @param {Array<number>} [dest] Destination.
   * @return {Array<number>} Transformed coordinates.
   */
  function scale$1(
    flatCoordinates,
    offset,
    end,
    stride,
    sx,
    sy,
    anchor,
    dest,
  ) {
    dest = dest ? dest : [];
    const anchorX = anchor[0];
    const anchorY = anchor[1];
    let i = 0;
    for (let j = offset; j < end; j += stride) {
      const deltaX = flatCoordinates[j] - anchorX;
      const deltaY = flatCoordinates[j + 1] - anchorY;
      dest[i++] = anchorX + sx * deltaX;
      dest[i++] = anchorY + sy * deltaY;
      for (let k = j + 2; k < j + stride; ++k) {
        dest[i++] = flatCoordinates[k];
      }
    }
    if (dest && dest.length != i) {
      dest.length = i;
    }
    return dest;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {number} deltaX Delta X.
   * @param {number} deltaY Delta Y.
   * @param {Array<number>} [dest] Destination.
   * @return {Array<number>} Transformed coordinates.
   */
  function translate(
    flatCoordinates,
    offset,
    end,
    stride,
    deltaX,
    deltaY,
    dest,
  ) {
    dest = dest ? dest : [];
    let i = 0;
    for (let j = offset; j < end; j += stride) {
      dest[i++] = flatCoordinates[j] + deltaX;
      dest[i++] = flatCoordinates[j + 1] + deltaY;
      for (let k = j + 2; k < j + stride; ++k) {
        dest[i++] = flatCoordinates[k];
      }
    }
    if (dest && dest.length != i) {
      dest.length = i;
    }
    return dest;
  }

  /**
   * @module ol/geom/Geometry
   */

  /**
   * @typedef {'XY' | 'XYZ' | 'XYM' | 'XYZM'} GeometryLayout
   * The coordinate layout for geometries, indicating whether a 3rd or 4th z ('Z')
   * or measure ('M') coordinate is available.
   */

  /**
   * @typedef {'Point' | 'LineString' | 'LinearRing' | 'Polygon' | 'MultiPoint' | 'MultiLineString' | 'MultiPolygon' | 'GeometryCollection' | 'Circle'} Type
   * The geometry type.  One of `'Point'`, `'LineString'`, `'LinearRing'`,
   * `'Polygon'`, `'MultiPoint'`, `'MultiLineString'`, `'MultiPolygon'`,
   * `'GeometryCollection'`, or `'Circle'`.
   */

  /**
   * @type {import("../transform.js").Transform}
   */
  const tmpTransform$1 = create();

  /** @type {import('../coordinate.js').Coordinate} */
  const tmpPoint = [NaN, NaN];

  /**
   * @classdesc
   * Abstract base class; normally only used for creating subclasses and not
   * instantiated in apps.
   * Base class for vector geometries.
   *
   * To get notified of changes to the geometry, register a listener for the
   * generic `change` event on your geometry instance.
   *
   * @abstract
   * @api
   */
  class Geometry extends BaseObject {
    constructor() {
      super();

      /**
       * @private
       * @type {import("../extent.js").Extent}
       */
      this.extent_ = createEmpty();

      /**
       * @private
       * @type {number}
       */
      this.extentRevision_ = -1;

      /**
       * @protected
       * @type {number}
       */
      this.simplifiedGeometryMaxMinSquaredTolerance = 0;

      /**
       * @protected
       * @type {number}
       */
      this.simplifiedGeometryRevision = 0;

      /**
       * Get a transformed and simplified version of the geometry.
       * @abstract
       * @param {number} revision The geometry revision.
       * @param {number} squaredTolerance Squared tolerance.
       * @param {import("../proj.js").TransformFunction} [transform] Optional transform function.
       * @return {Geometry} Simplified geometry.
       */
      this.simplifyTransformedInternal = memoizeOne(
        (revision, squaredTolerance, transform) => {
          if (!transform) {
            return this.getSimplifiedGeometry(squaredTolerance);
          }
          const clone = this.clone();
          clone.applyTransform(transform);
          return clone.getSimplifiedGeometry(squaredTolerance);
        },
      );
    }

    /**
     * Get a transformed and simplified version of the geometry.
     * @abstract
     * @param {number} squaredTolerance Squared tolerance.
     * @param {import("../proj.js").TransformFunction} [transform] Optional transform function.
     * @return {Geometry} Simplified geometry.
     */
    simplifyTransformed(squaredTolerance, transform) {
      return this.simplifyTransformedInternal(
        this.getRevision(),
        squaredTolerance,
        transform,
      );
    }

    /**
     * Make a complete copy of the geometry.
     * @abstract
     * @return {!Geometry} Clone.
     */
    clone() {
      return abstract();
    }

    /**
     * @abstract
     * @param {number} x X.
     * @param {number} y Y.
     * @param {import("../coordinate.js").Coordinate} closestPoint Closest point.
     * @param {number} minSquaredDistance Minimum squared distance.
     * @return {number} Minimum squared distance.
     */
    closestPointXY(x, y, closestPoint, minSquaredDistance) {
      return abstract();
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @return {boolean} Contains (x, y).
     */
    containsXY(x, y) {
      return this.closestPointXY(x, y, tmpPoint, Number.MIN_VALUE) === 0;
    }

    /**
     * Return the closest point of the geometry to the passed point as
     * {@link module:ol/coordinate~Coordinate coordinate}.
     * @param {import("../coordinate.js").Coordinate} point Point.
     * @param {import("../coordinate.js").Coordinate} [closestPoint] Closest point.
     * @return {import("../coordinate.js").Coordinate} Closest point.
     * @api
     */
    getClosestPoint(point, closestPoint) {
      closestPoint = closestPoint ? closestPoint : [NaN, NaN];
      this.closestPointXY(point[0], point[1], closestPoint, Infinity);
      return closestPoint;
    }

    /**
     * Returns true if this geometry includes the specified coordinate. If the
     * coordinate is on the boundary of the geometry, returns false.
     * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
     * @return {boolean} Contains coordinate.
     * @api
     */
    intersectsCoordinate(coordinate) {
      return this.containsXY(coordinate[0], coordinate[1]);
    }

    /**
     * @abstract
     * @param {import("../extent.js").Extent} extent Extent.
     * @protected
     * @return {import("../extent.js").Extent} extent Extent.
     */
    computeExtent(extent) {
      return abstract();
    }

    /**
     * Get the extent of the geometry.
     * @param {import("../extent.js").Extent} [extent] Extent.
     * @return {import("../extent.js").Extent} extent Extent.
     * @api
     */
    getExtent(extent) {
      if (this.extentRevision_ != this.getRevision()) {
        const extent = this.computeExtent(this.extent_);
        if (isNaN(extent[0]) || isNaN(extent[1])) {
          createOrUpdateEmpty(extent);
        }
        this.extentRevision_ = this.getRevision();
      }
      return returnOrUpdate(this.extent_, extent);
    }

    /**
     * Rotate the geometry around a given coordinate. This modifies the geometry
     * coordinates in place.
     * @abstract
     * @param {number} angle Rotation angle in radians.
     * @param {import("../coordinate.js").Coordinate} anchor The rotation center.
     * @api
     */
    rotate(angle, anchor) {
      abstract();
    }

    /**
     * Scale the geometry (with an optional origin).  This modifies the geometry
     * coordinates in place.
     * @abstract
     * @param {number} sx The scaling factor in the x-direction.
     * @param {number} [sy] The scaling factor in the y-direction (defaults to sx).
     * @param {import("../coordinate.js").Coordinate} [anchor] The scale origin (defaults to the center
     *     of the geometry extent).
     * @api
     */
    scale(sx, sy, anchor) {
      abstract();
    }

    /**
     * Create a simplified version of this geometry.  For linestrings, this uses
     * the [Douglas Peucker](https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm)
     * algorithm.  For polygons, a quantization-based
     * simplification is used to preserve topology.
     * @param {number} tolerance The tolerance distance for simplification.
     * @return {Geometry} A new, simplified version of the original geometry.
     * @api
     */
    simplify(tolerance) {
      return this.getSimplifiedGeometry(tolerance * tolerance);
    }

    /**
     * Create a simplified version of this geometry using the Douglas Peucker
     * algorithm.
     * See https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm.
     * @abstract
     * @param {number} squaredTolerance Squared tolerance.
     * @return {Geometry} Simplified geometry.
     */
    getSimplifiedGeometry(squaredTolerance) {
      return abstract();
    }

    /**
     * Get the type of this geometry.
     * @abstract
     * @return {Type} Geometry type.
     */
    getType() {
      return abstract();
    }

    /**
     * Apply a transform function to the coordinates of the geometry.
     * The geometry is modified in place.
     * If you do not want the geometry modified in place, first `clone()` it and
     * then use this function on the clone.
     * @abstract
     * @param {import("../proj.js").TransformFunction} transformFn Transform function.
     * Called with a flat array of geometry coordinates.
     */
    applyTransform(transformFn) {
      abstract();
    }

    /**
     * Test if the geometry and the passed extent intersect.
     * @abstract
     * @param {import("../extent.js").Extent} extent Extent.
     * @return {boolean} `true` if the geometry and the extent intersect.
     */
    intersectsExtent(extent) {
      return abstract();
    }

    /**
     * Translate the geometry.  This modifies the geometry coordinates in place.  If
     * instead you want a new geometry, first `clone()` this geometry.
     * @abstract
     * @param {number} deltaX Delta X.
     * @param {number} deltaY Delta Y.
     * @api
     */
    translate(deltaX, deltaY) {
      abstract();
    }

    /**
     * Transform each coordinate of the geometry from one coordinate reference
     * system to another. The geometry is modified in place.
     * For example, a line will be transformed to a line and a circle to a circle.
     * If you do not want the geometry modified in place, first `clone()` it and
     * then use this function on the clone.
     *
     * @param {import("../proj.js").ProjectionLike} source The current projection.  Can be a
     *     string identifier or a {@link module:ol/proj/Projection~Projection} object.
     * @param {import("../proj.js").ProjectionLike} destination The desired projection.  Can be a
     *     string identifier or a {@link module:ol/proj/Projection~Projection} object.
     * @return {this} This geometry.  Note that original geometry is
     *     modified in place.
     * @api
     */
    transform(source, destination) {
      /** @type {import("../proj/Projection.js").default} */
      const sourceProj = get$1(source);
      const transformFn =
        sourceProj.getUnits() == 'tile-pixels'
          ? function (inCoordinates, outCoordinates, stride) {
              const pixelExtent = sourceProj.getExtent();
              const projectedExtent = sourceProj.getWorldExtent();
              const scale = getHeight(projectedExtent) / getHeight(pixelExtent);
              compose(
                tmpTransform$1,
                projectedExtent[0],
                projectedExtent[3],
                scale,
                -scale,
                0,
                0,
                0,
              );
              const transformed = transform2D(
                inCoordinates,
                0,
                inCoordinates.length,
                stride,
                tmpTransform$1,
                outCoordinates,
              );
              const projTransform = getTransform(sourceProj, destination);
              if (projTransform) {
                return projTransform(transformed, transformed, stride);
              }
              return transformed;
            }
          : getTransform(sourceProj, destination);
      this.applyTransform(transformFn);
      return this;
    }
  }

  /**
   * @module ol/geom/SimpleGeometry
   */

  /**
   * @classdesc
   * Abstract base class; only used for creating subclasses; do not instantiate
   * in apps, as cannot be rendered.
   *
   * @abstract
   * @api
   */
  class SimpleGeometry extends Geometry {
    constructor() {
      super();

      /**
       * @protected
       * @type {import("./Geometry.js").GeometryLayout}
       */
      this.layout = 'XY';

      /**
       * @protected
       * @type {number}
       */
      this.stride = 2;

      /**
       * @protected
       * @type {Array<number>}
       */
      this.flatCoordinates;
    }

    /**
     * @param {import("../extent.js").Extent} extent Extent.
     * @protected
     * @return {import("../extent.js").Extent} extent Extent.
     * @override
     */
    computeExtent(extent) {
      return createOrUpdateFromFlatCoordinates(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
        extent,
      );
    }

    /**
     * @abstract
     * @return {Array<*> | null} Coordinates.
     */
    getCoordinates() {
      return abstract();
    }

    /**
     * Return the first coordinate of the geometry.
     * @return {import("../coordinate.js").Coordinate} First coordinate.
     * @api
     */
    getFirstCoordinate() {
      return this.flatCoordinates.slice(0, this.stride);
    }

    /**
     * @return {Array<number>} Flat coordinates.
     */
    getFlatCoordinates() {
      return this.flatCoordinates;
    }

    /**
     * Return the last coordinate of the geometry.
     * @return {import("../coordinate.js").Coordinate} Last point.
     * @api
     */
    getLastCoordinate() {
      return this.flatCoordinates.slice(
        this.flatCoordinates.length - this.stride,
      );
    }

    /**
     * Return the {@link import("./Geometry.js").GeometryLayout layout} of the geometry.
     * @return {import("./Geometry.js").GeometryLayout} Layout.
     * @api
     */
    getLayout() {
      return this.layout;
    }

    /**
     * Create a simplified version of this geometry using the Douglas Peucker algorithm.
     * @param {number} squaredTolerance Squared tolerance.
     * @return {SimpleGeometry} Simplified geometry.
     * @override
     */
    getSimplifiedGeometry(squaredTolerance) {
      if (this.simplifiedGeometryRevision !== this.getRevision()) {
        this.simplifiedGeometryMaxMinSquaredTolerance = 0;
        this.simplifiedGeometryRevision = this.getRevision();
      }
      // If squaredTolerance is negative or if we know that simplification will not
      // have any effect then just return this.
      if (
        squaredTolerance < 0 ||
        (this.simplifiedGeometryMaxMinSquaredTolerance !== 0 &&
          squaredTolerance <= this.simplifiedGeometryMaxMinSquaredTolerance)
      ) {
        return this;
      }

      const simplifiedGeometry =
        this.getSimplifiedGeometryInternal(squaredTolerance);
      const simplifiedFlatCoordinates = simplifiedGeometry.getFlatCoordinates();
      if (simplifiedFlatCoordinates.length < this.flatCoordinates.length) {
        return simplifiedGeometry;
      }
      // Simplification did not actually remove any coordinates.  We now know
      // that any calls to getSimplifiedGeometry with a squaredTolerance less
      // than or equal to the current squaredTolerance will also not have any
      // effect.  This allows us to short circuit simplification (saving CPU
      // cycles) and prevents the cache of simplified geometries from filling
      // up with useless identical copies of this geometry (saving memory).
      this.simplifiedGeometryMaxMinSquaredTolerance = squaredTolerance;
      return this;
    }

    /**
     * @param {number} squaredTolerance Squared tolerance.
     * @return {SimpleGeometry} Simplified geometry.
     * @protected
     */
    getSimplifiedGeometryInternal(squaredTolerance) {
      return this;
    }

    /**
     * @return {number} Stride.
     */
    getStride() {
      return this.stride;
    }

    /**
     * @param {import("./Geometry.js").GeometryLayout} layout Layout.
     * @param {Array<number>} flatCoordinates Flat coordinates.
     */
    setFlatCoordinates(layout, flatCoordinates) {
      this.stride = getStrideForLayout(layout);
      this.layout = layout;
      this.flatCoordinates = flatCoordinates;
    }

    /**
     * @abstract
     * @param {!Array<*>} coordinates Coordinates.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     */
    setCoordinates(coordinates, layout) {
      abstract();
    }

    /**
     * @param {import("./Geometry.js").GeometryLayout|undefined} layout Layout.
     * @param {Array<*>} coordinates Coordinates.
     * @param {number} nesting Nesting.
     * @protected
     */
    setLayout(layout, coordinates, nesting) {
      let stride;
      if (layout) {
        stride = getStrideForLayout(layout);
      } else {
        for (let i = 0; i < nesting; ++i) {
          if (coordinates.length === 0) {
            this.layout = 'XY';
            this.stride = 2;
            return;
          }
          coordinates = /** @type {Array<unknown>} */ (coordinates[0]);
        }
        stride = coordinates.length;
        layout = getLayoutForStride(stride);
      }
      this.layout = layout;
      this.stride = stride;
    }

    /**
     * Apply a transform function to the coordinates of the geometry.
     * The geometry is modified in place.
     * If you do not want the geometry modified in place, first `clone()` it and
     * then use this function on the clone.
     * @param {import("../proj.js").TransformFunction} transformFn Transform function.
     * Called with a flat array of geometry coordinates.
     * @api
     * @override
     */
    applyTransform(transformFn) {
      if (this.flatCoordinates) {
        transformFn(
          this.flatCoordinates,
          this.flatCoordinates,
          this.layout.startsWith('XYZ') ? 3 : 2,
          this.stride,
        );
        this.changed();
      }
    }

    /**
     * Rotate the geometry around a given coordinate. This modifies the geometry
     * coordinates in place.
     * @param {number} angle Rotation angle in counter-clockwise radians.
     * @param {import("../coordinate.js").Coordinate} anchor The rotation center.
     * @api
     * @override
     */
    rotate(angle, anchor) {
      const flatCoordinates = this.getFlatCoordinates();
      if (flatCoordinates) {
        const stride = this.getStride();
        rotate(
          flatCoordinates,
          0,
          flatCoordinates.length,
          stride,
          angle,
          anchor,
          flatCoordinates,
        );
        this.changed();
      }
    }

    /**
     * Scale the geometry (with an optional origin).  This modifies the geometry
     * coordinates in place.
     * @param {number} sx The scaling factor in the x-direction.
     * @param {number} [sy] The scaling factor in the y-direction (defaults to sx).
     * @param {import("../coordinate.js").Coordinate} [anchor] The scale origin (defaults to the center
     *     of the geometry extent).
     * @api
     * @override
     */
    scale(sx, sy, anchor) {
      if (sy === undefined) {
        sy = sx;
      }
      if (!anchor) {
        anchor = getCenter(this.getExtent());
      }
      const flatCoordinates = this.getFlatCoordinates();
      if (flatCoordinates) {
        const stride = this.getStride();
        scale$1(
          flatCoordinates,
          0,
          flatCoordinates.length,
          stride,
          sx,
          sy,
          anchor,
          flatCoordinates,
        );
        this.changed();
      }
    }

    /**
     * Translate the geometry.  This modifies the geometry coordinates in place.  If
     * instead you want a new geometry, first `clone()` this geometry.
     * @param {number} deltaX Delta X.
     * @param {number} deltaY Delta Y.
     * @api
     * @override
     */
    translate(deltaX, deltaY) {
      const flatCoordinates = this.getFlatCoordinates();
      if (flatCoordinates) {
        const stride = this.getStride();
        translate(
          flatCoordinates,
          0,
          flatCoordinates.length,
          stride,
          deltaX,
          deltaY,
          flatCoordinates,
        );
        this.changed();
      }
    }
  }

  /**
   * @param {number} stride Stride.
   * @return {import("./Geometry.js").GeometryLayout} layout Layout.
   */
  function getLayoutForStride(stride) {
    let layout;
    if (stride == 2) {
      layout = 'XY';
    } else if (stride == 3) {
      layout = 'XYZ';
    } else if (stride == 4) {
      layout = 'XYZM';
    }
    return /** @type {import("./Geometry.js").GeometryLayout} */ (layout);
  }

  /**
   * @param {import("./Geometry.js").GeometryLayout} layout Layout.
   * @return {number} Stride.
   */
  function getStrideForLayout(layout) {
    let stride;
    if (layout == 'XY') {
      stride = 2;
    } else if (layout == 'XYZ' || layout == 'XYM') {
      stride = 3;
    } else if (layout == 'XYZM') {
      stride = 4;
    }
    return /** @type {number} */ (stride);
  }

  /**
   * @param {SimpleGeometry} simpleGeometry Simple geometry.
   * @param {import("../transform.js").Transform} transform Transform.
   * @param {Array<number>} [dest] Destination.
   * @return {Array<number>} Transformed flat coordinates.
   */
  function transformGeom2D(simpleGeometry, transform, dest) {
    const flatCoordinates = simpleGeometry.getFlatCoordinates();
    if (!flatCoordinates) {
      return null;
    }
    const stride = simpleGeometry.getStride();
    return transform2D(
      flatCoordinates,
      0,
      flatCoordinates.length,
      stride,
      transform,
      dest,
    );
  }

  /**
   * @module ol/geom/flat/area
   */

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @return {number} Area.
   */
  function linearRing(flatCoordinates, offset, end, stride) {
    let twiceArea = 0;
    const x0 = flatCoordinates[end - stride];
    const y0 = flatCoordinates[end - stride + 1];
    let dx1 = 0;
    let dy1 = 0;
    for (; offset < end; offset += stride) {
      const dx2 = flatCoordinates[offset] - x0;
      const dy2 = flatCoordinates[offset + 1] - y0;
      twiceArea += dy1 * dx2 - dx1 * dy2;
      dx1 = dx2;
      dy1 = dy2;
    }
    return twiceArea / 2;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @return {number} Area.
   */
  function linearRings(flatCoordinates, offset, ends, stride) {
    let area = 0;
    for (let i = 0, ii = ends.length; i < ii; ++i) {
      const end = ends[i];
      area += linearRing(flatCoordinates, offset, end, stride);
      offset = end;
    }
    return area;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<number>>} endss Endss.
   * @param {number} stride Stride.
   * @return {number} Area.
   */
  function linearRingss$1(flatCoordinates, offset, endss, stride) {
    let area = 0;
    for (let i = 0, ii = endss.length; i < ii; ++i) {
      const ends = endss[i];
      area += linearRings(flatCoordinates, offset, ends, stride);
      offset = ends[ends.length - 1];
    }
    return area;
  }

  /**
   * @module ol/geom/flat/closest
   */

  /**
   * Returns the point on the 2D line segment flatCoordinates[offset1] to
   * flatCoordinates[offset2] that is closest to the point (x, y).  Extra
   * dimensions are linearly interpolated.
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset1 Offset 1.
   * @param {number} offset2 Offset 2.
   * @param {number} stride Stride.
   * @param {number} x X.
   * @param {number} y Y.
   * @param {Array<number>} closestPoint Closest point.
   */
  function assignClosest(
    flatCoordinates,
    offset1,
    offset2,
    stride,
    x,
    y,
    closestPoint,
  ) {
    const x1 = flatCoordinates[offset1];
    const y1 = flatCoordinates[offset1 + 1];
    const dx = flatCoordinates[offset2] - x1;
    const dy = flatCoordinates[offset2 + 1] - y1;
    let offset;
    if (dx === 0 && dy === 0) {
      offset = offset1;
    } else {
      const t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy);
      if (t > 1) {
        offset = offset2;
      } else if (t > 0) {
        for (let i = 0; i < stride; ++i) {
          closestPoint[i] = lerp(
            flatCoordinates[offset1 + i],
            flatCoordinates[offset2 + i],
            t,
          );
        }
        closestPoint.length = stride;
        return;
      } else {
        offset = offset1;
      }
    }
    for (let i = 0; i < stride; ++i) {
      closestPoint[i] = flatCoordinates[offset + i];
    }
    closestPoint.length = stride;
  }

  /**
   * Return the squared of the largest distance between any pair of consecutive
   * coordinates.
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {number} max Max squared delta.
   * @return {number} Max squared delta.
   */
  function maxSquaredDelta(flatCoordinates, offset, end, stride, max) {
    let x1 = flatCoordinates[offset];
    let y1 = flatCoordinates[offset + 1];
    for (offset += stride; offset < end; offset += stride) {
      const x2 = flatCoordinates[offset];
      const y2 = flatCoordinates[offset + 1];
      const squaredDelta = squaredDistance(x1, y1, x2, y2);
      if (squaredDelta > max) {
        max = squaredDelta;
      }
      x1 = x2;
      y1 = y2;
    }
    return max;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @param {number} max Max squared delta.
   * @return {number} Max squared delta.
   */
  function arrayMaxSquaredDelta(
    flatCoordinates,
    offset,
    ends,
    stride,
    max,
  ) {
    for (let i = 0, ii = ends.length; i < ii; ++i) {
      const end = ends[i];
      max = maxSquaredDelta(flatCoordinates, offset, end, stride, max);
      offset = end;
    }
    return max;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<number>>} endss Endss.
   * @param {number} stride Stride.
   * @param {number} max Max squared delta.
   * @return {number} Max squared delta.
   */
  function multiArrayMaxSquaredDelta(
    flatCoordinates,
    offset,
    endss,
    stride,
    max,
  ) {
    for (let i = 0, ii = endss.length; i < ii; ++i) {
      const ends = endss[i];
      max = arrayMaxSquaredDelta(flatCoordinates, offset, ends, stride, max);
      offset = ends[ends.length - 1];
    }
    return max;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {number} maxDelta Max delta.
   * @param {boolean} isRing Is ring.
   * @param {number} x X.
   * @param {number} y Y.
   * @param {Array<number>} closestPoint Closest point.
   * @param {number} minSquaredDistance Minimum squared distance.
   * @param {Array<number>} [tmpPoint] Temporary point object.
   * @return {number} Minimum squared distance.
   */
  function assignClosestPoint(
    flatCoordinates,
    offset,
    end,
    stride,
    maxDelta,
    isRing,
    x,
    y,
    closestPoint,
    minSquaredDistance,
    tmpPoint,
  ) {
    if (offset == end) {
      return minSquaredDistance;
    }
    let i, squaredDistance$1;
    if (maxDelta === 0) {
      // All points are identical, so just test the first point.
      squaredDistance$1 = squaredDistance(
        x,
        y,
        flatCoordinates[offset],
        flatCoordinates[offset + 1],
      );
      if (squaredDistance$1 < minSquaredDistance) {
        for (i = 0; i < stride; ++i) {
          closestPoint[i] = flatCoordinates[offset + i];
        }
        closestPoint.length = stride;
        return squaredDistance$1;
      }
      return minSquaredDistance;
    }
    tmpPoint = tmpPoint ? tmpPoint : [NaN, NaN];
    let index = offset + stride;
    while (index < end) {
      assignClosest(
        flatCoordinates,
        index - stride,
        index,
        stride,
        x,
        y,
        tmpPoint,
      );
      squaredDistance$1 = squaredDistance(x, y, tmpPoint[0], tmpPoint[1]);
      if (squaredDistance$1 < minSquaredDistance) {
        minSquaredDistance = squaredDistance$1;
        for (i = 0; i < stride; ++i) {
          closestPoint[i] = tmpPoint[i];
        }
        closestPoint.length = stride;
        index += stride;
      } else {
        // Skip ahead multiple points, because we know that all the skipped
        // points cannot be any closer than the closest point we have found so
        // far.  We know this because we know how close the current point is, how
        // close the closest point we have found so far is, and the maximum
        // distance between consecutive points.  For example, if we're currently
        // at distance 10, the best we've found so far is 3, and that the maximum
        // distance between consecutive points is 2, then we'll need to skip at
        // least (10 - 3) / 2 == 3 (rounded down) points to have any chance of
        // finding a closer point.  We use Math.max(..., 1) to ensure that we
        // always advance at least one point, to avoid an infinite loop.
        index +=
          stride *
          Math.max(
            ((Math.sqrt(squaredDistance$1) - Math.sqrt(minSquaredDistance)) /
              maxDelta) |
              0,
            1,
          );
      }
    }
    if (isRing) {
      // Check the closing segment.
      assignClosest(
        flatCoordinates,
        end - stride,
        offset,
        stride,
        x,
        y,
        tmpPoint,
      );
      squaredDistance$1 = squaredDistance(x, y, tmpPoint[0], tmpPoint[1]);
      if (squaredDistance$1 < minSquaredDistance) {
        minSquaredDistance = squaredDistance$1;
        for (i = 0; i < stride; ++i) {
          closestPoint[i] = tmpPoint[i];
        }
        closestPoint.length = stride;
      }
    }
    return minSquaredDistance;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @param {number} maxDelta Max delta.
   * @param {boolean} isRing Is ring.
   * @param {number} x X.
   * @param {number} y Y.
   * @param {Array<number>} closestPoint Closest point.
   * @param {number} minSquaredDistance Minimum squared distance.
   * @param {Array<number>} [tmpPoint] Temporary point object.
   * @return {number} Minimum squared distance.
   */
  function assignClosestArrayPoint(
    flatCoordinates,
    offset,
    ends,
    stride,
    maxDelta,
    isRing,
    x,
    y,
    closestPoint,
    minSquaredDistance,
    tmpPoint,
  ) {
    tmpPoint = tmpPoint ? tmpPoint : [NaN, NaN];
    for (let i = 0, ii = ends.length; i < ii; ++i) {
      const end = ends[i];
      minSquaredDistance = assignClosestPoint(
        flatCoordinates,
        offset,
        end,
        stride,
        maxDelta,
        isRing,
        x,
        y,
        closestPoint,
        minSquaredDistance,
        tmpPoint,
      );
      offset = end;
    }
    return minSquaredDistance;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<number>>} endss Endss.
   * @param {number} stride Stride.
   * @param {number} maxDelta Max delta.
   * @param {boolean} isRing Is ring.
   * @param {number} x X.
   * @param {number} y Y.
   * @param {Array<number>} closestPoint Closest point.
   * @param {number} minSquaredDistance Minimum squared distance.
   * @param {Array<number>} [tmpPoint] Temporary point object.
   * @return {number} Minimum squared distance.
   */
  function assignClosestMultiArrayPoint(
    flatCoordinates,
    offset,
    endss,
    stride,
    maxDelta,
    isRing,
    x,
    y,
    closestPoint,
    minSquaredDistance,
    tmpPoint,
  ) {
    tmpPoint = tmpPoint ? tmpPoint : [NaN, NaN];
    for (let i = 0, ii = endss.length; i < ii; ++i) {
      const ends = endss[i];
      minSquaredDistance = assignClosestArrayPoint(
        flatCoordinates,
        offset,
        ends,
        stride,
        maxDelta,
        isRing,
        x,
        y,
        closestPoint,
        minSquaredDistance,
        tmpPoint,
      );
      offset = ends[ends.length - 1];
    }
    return minSquaredDistance;
  }

  /**
   * @module ol/geom/flat/deflate
   */

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
   * @param {number} stride Stride.
   * @return {number} offset Offset.
   */
  function deflateCoordinate(flatCoordinates, offset, coordinate, stride) {
    for (let i = 0, ii = coordinate.length; i < ii; ++i) {
      flatCoordinates[offset++] = coordinate[i];
    }
    return offset;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<import("../../coordinate.js").Coordinate>} coordinates Coordinates.
   * @param {number} stride Stride.
   * @return {number} offset Offset.
   */
  function deflateCoordinates(
    flatCoordinates,
    offset,
    coordinates,
    stride,
  ) {
    for (let i = 0, ii = coordinates.length; i < ii; ++i) {
      const coordinate = coordinates[i];
      for (let j = 0; j < stride; ++j) {
        flatCoordinates[offset++] = coordinate[j];
      }
    }
    return offset;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<import("../../coordinate.js").Coordinate>>} coordinatess Coordinatess.
   * @param {number} stride Stride.
   * @param {Array<number>} [ends] Ends.
   * @return {Array<number>} Ends.
   */
  function deflateCoordinatesArray(
    flatCoordinates,
    offset,
    coordinatess,
    stride,
    ends,
  ) {
    ends = ends ? ends : [];
    let i = 0;
    for (let j = 0, jj = coordinatess.length; j < jj; ++j) {
      const end = deflateCoordinates(
        flatCoordinates,
        offset,
        coordinatess[j],
        stride,
      );
      ends[i++] = end;
      offset = end;
    }
    ends.length = i;
    return ends;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<Array<import("../../coordinate.js").Coordinate>>>} coordinatesss Coordinatesss.
   * @param {number} stride Stride.
   * @param {Array<Array<number>>} [endss] Endss.
   * @return {Array<Array<number>>} Endss.
   */
  function deflateMultiCoordinatesArray(
    flatCoordinates,
    offset,
    coordinatesss,
    stride,
    endss,
  ) {
    endss = endss ? endss : [];
    let i = 0;
    for (let j = 0, jj = coordinatesss.length; j < jj; ++j) {
      const ends = deflateCoordinatesArray(
        flatCoordinates,
        offset,
        coordinatesss[j],
        stride,
        endss[i],
      );
      if (ends.length === 0) {
        ends[0] = offset;
      }
      endss[i++] = ends;
      offset = ends[ends.length - 1];
    }
    endss.length = i;
    return endss;
  }

  /**
   * @module ol/geom/flat/inflate
   */

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {Array<import("../../coordinate.js").Coordinate>} [coordinates] Coordinates.
   * @return {Array<import("../../coordinate.js").Coordinate>} Coordinates.
   */
  function inflateCoordinates(
    flatCoordinates,
    offset,
    end,
    stride,
    coordinates,
  ) {
    coordinates = coordinates !== undefined ? coordinates : [];
    let i = 0;
    for (let j = offset; j < end; j += stride) {
      coordinates[i++] = flatCoordinates.slice(j, j + stride);
    }
    coordinates.length = i;
    return coordinates;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @param {Array<Array<import("../../coordinate.js").Coordinate>>} [coordinatess] Coordinatess.
   * @return {Array<Array<import("../../coordinate.js").Coordinate>>} Coordinatess.
   */
  function inflateCoordinatesArray(
    flatCoordinates,
    offset,
    ends,
    stride,
    coordinatess,
  ) {
    coordinatess = coordinatess !== undefined ? coordinatess : [];
    let i = 0;
    for (let j = 0, jj = ends.length; j < jj; ++j) {
      const end = ends[j];
      coordinatess[i++] = inflateCoordinates(
        flatCoordinates,
        offset,
        end,
        stride,
        coordinatess[i],
      );
      offset = end;
    }
    coordinatess.length = i;
    return coordinatess;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<number>>} endss Endss.
   * @param {number} stride Stride.
   * @param {Array<Array<Array<import("../../coordinate.js").Coordinate>>>} [coordinatesss]
   *     Coordinatesss.
   * @return {Array<Array<Array<import("../../coordinate.js").Coordinate>>>} Coordinatesss.
   */
  function inflateMultiCoordinatesArray(
    flatCoordinates,
    offset,
    endss,
    stride,
    coordinatesss,
  ) {
    coordinatesss = coordinatesss !== undefined ? coordinatesss : [];
    let i = 0;
    for (let j = 0, jj = endss.length; j < jj; ++j) {
      const ends = endss[j];
      coordinatesss[i++] =
        ends.length === 1 && ends[0] === offset
          ? []
          : inflateCoordinatesArray(
              flatCoordinates,
              offset,
              ends,
              stride,
              coordinatesss[i],
            );
      offset = ends[ends.length - 1];
    }
    coordinatesss.length = i;
    return coordinatesss;
  }

  /**
   * @module ol/geom/flat/simplify
   */
  // Based on simplify-js https://github.com/mourner/simplify-js
  // Copyright (c) 2012, Vladimir Agafonkin
  // All rights reserved.
  //
  // Redistribution and use in source and binary forms, with or without
  // modification, are permitted provided that the following conditions are met:
  //
  //    1. Redistributions of source code must retain the above copyright notice,
  //       this list of conditions and the following disclaimer.
  //
  //    2. Redistributions in binary form must reproduce the above copyright
  //       notice, this list of conditions and the following disclaimer in the
  //       documentation and/or other materials provided with the distribution.
  //
  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  // POSSIBILITY OF SUCH DAMAGE.


  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {number} squaredTolerance Squared tolerance.
   * @param {Array<number>} simplifiedFlatCoordinates Simplified flat
   *     coordinates.
   * @param {number} simplifiedOffset Simplified offset.
   * @return {number} Simplified offset.
   */
  function douglasPeucker(
    flatCoordinates,
    offset,
    end,
    stride,
    squaredTolerance,
    simplifiedFlatCoordinates,
    simplifiedOffset,
  ) {
    const n = (end - offset) / stride;
    if (n < 3) {
      for (; offset < end; offset += stride) {
        simplifiedFlatCoordinates[simplifiedOffset++] = flatCoordinates[offset];
        simplifiedFlatCoordinates[simplifiedOffset++] =
          flatCoordinates[offset + 1];
      }
      return simplifiedOffset;
    }
    /** @type {Array<number>} */
    const markers = new Array(n);
    markers[0] = 1;
    markers[n - 1] = 1;
    /** @type {Array<number>} */
    const stack = [offset, end - stride];
    let index = 0;
    while (stack.length > 0) {
      const last = stack.pop();
      const first = stack.pop();
      let maxSquaredDistance = 0;
      const x1 = flatCoordinates[first];
      const y1 = flatCoordinates[first + 1];
      const x2 = flatCoordinates[last];
      const y2 = flatCoordinates[last + 1];
      for (let i = first + stride; i < last; i += stride) {
        const x = flatCoordinates[i];
        const y = flatCoordinates[i + 1];
        const squaredDistance = squaredSegmentDistance(x, y, x1, y1, x2, y2);
        if (squaredDistance > maxSquaredDistance) {
          index = i;
          maxSquaredDistance = squaredDistance;
        }
      }
      if (maxSquaredDistance > squaredTolerance) {
        markers[(index - offset) / stride] = 1;
        if (first + stride < index) {
          stack.push(first, index);
        }
        if (index + stride < last) {
          stack.push(index, last);
        }
      }
    }
    for (let i = 0; i < n; ++i) {
      if (markers[i]) {
        simplifiedFlatCoordinates[simplifiedOffset++] =
          flatCoordinates[offset + i * stride];
        simplifiedFlatCoordinates[simplifiedOffset++] =
          flatCoordinates[offset + i * stride + 1];
      }
    }
    return simplifiedOffset;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @param {number} squaredTolerance Squared tolerance.
   * @param {Array<number>} simplifiedFlatCoordinates Simplified flat
   *     coordinates.
   * @param {number} simplifiedOffset Simplified offset.
   * @param {Array<number>} simplifiedEnds Simplified ends.
   * @return {number} Simplified offset.
   */
  function douglasPeuckerArray(
    flatCoordinates,
    offset,
    ends,
    stride,
    squaredTolerance,
    simplifiedFlatCoordinates,
    simplifiedOffset,
    simplifiedEnds,
  ) {
    for (let i = 0, ii = ends.length; i < ii; ++i) {
      const end = ends[i];
      simplifiedOffset = douglasPeucker(
        flatCoordinates,
        offset,
        end,
        stride,
        squaredTolerance,
        simplifiedFlatCoordinates,
        simplifiedOffset,
      );
      simplifiedEnds.push(simplifiedOffset);
      offset = end;
    }
    return simplifiedOffset;
  }

  /**
   * @param {number} value Value.
   * @param {number} tolerance Tolerance.
   * @return {number} Rounded value.
   */
  function snap(value, tolerance) {
    return tolerance * Math.round(value / tolerance);
  }

  /**
   * Simplifies a line string using an algorithm designed by Tim Schaub.
   * Coordinates are snapped to the nearest value in a virtual grid and
   * consecutive duplicate coordinates are discarded.  This effectively preserves
   * topology as the simplification of any subsection of a line string is
   * independent of the rest of the line string.  This means that, for examples,
   * the common edge between two polygons will be simplified to the same line
   * string independently in both polygons.  This implementation uses a single
   * pass over the coordinates and eliminates intermediate collinear points.
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {number} tolerance Tolerance.
   * @param {Array<number>} simplifiedFlatCoordinates Simplified flat
   *     coordinates.
   * @param {number} simplifiedOffset Simplified offset.
   * @return {number} Simplified offset.
   */
  function quantize(
    flatCoordinates,
    offset,
    end,
    stride,
    tolerance,
    simplifiedFlatCoordinates,
    simplifiedOffset,
  ) {
    // do nothing if the line is empty
    if (offset == end) {
      return simplifiedOffset;
    }
    // snap the first coordinate (P1)
    let x1 = snap(flatCoordinates[offset], tolerance);
    let y1 = snap(flatCoordinates[offset + 1], tolerance);
    offset += stride;
    // add the first coordinate to the output
    simplifiedFlatCoordinates[simplifiedOffset++] = x1;
    simplifiedFlatCoordinates[simplifiedOffset++] = y1;
    // find the next coordinate that does not snap to the same value as the first
    // coordinate (P2)
    let x2, y2;
    do {
      x2 = snap(flatCoordinates[offset], tolerance);
      y2 = snap(flatCoordinates[offset + 1], tolerance);
      offset += stride;
      if (offset == end) {
        // all coordinates snap to the same value, the line collapses to a point
        // push the last snapped value anyway to ensure that the output contains
        // at least two points
        // FIXME should we really return at least two points anyway?
        simplifiedFlatCoordinates[simplifiedOffset++] = x2;
        simplifiedFlatCoordinates[simplifiedOffset++] = y2;
        return simplifiedOffset;
      }
    } while (x2 == x1 && y2 == y1);
    while (offset < end) {
      // snap the next coordinate (P3)
      const x3 = snap(flatCoordinates[offset], tolerance);
      const y3 = snap(flatCoordinates[offset + 1], tolerance);
      offset += stride;
      // skip P3 if it is equal to P2
      if (x3 == x2 && y3 == y2) {
        continue;
      }
      // calculate the delta between P1 and P2
      const dx1 = x2 - x1;
      const dy1 = y2 - y1;
      // calculate the delta between P3 and P1
      const dx2 = x3 - x1;
      const dy2 = y3 - y1;
      // if P1, P2, and P3 are colinear and P3 is further from P1 than P2 is from
      // P1 in the same direction then P2 is on the straight line between P1 and
      // P3
      if (
        dx1 * dy2 == dy1 * dx2 &&
        ((dx1 < 0 && dx2 < dx1) || dx1 == dx2 || (dx1 > 0 && dx2 > dx1)) &&
        ((dy1 < 0 && dy2 < dy1) || dy1 == dy2 || (dy1 > 0 && dy2 > dy1))
      ) {
        // discard P2 and set P2 = P3
        x2 = x3;
        y2 = y3;
        continue;
      }
      // either P1, P2, and P3 are not colinear, or they are colinear but P3 is
      // between P3 and P1 or on the opposite half of the line to P2.  add P2,
      // and continue with P1 = P2 and P2 = P3
      simplifiedFlatCoordinates[simplifiedOffset++] = x2;
      simplifiedFlatCoordinates[simplifiedOffset++] = y2;
      x1 = x2;
      y1 = y2;
      x2 = x3;
      y2 = y3;
    }
    // add the last point (P2)
    simplifiedFlatCoordinates[simplifiedOffset++] = x2;
    simplifiedFlatCoordinates[simplifiedOffset++] = y2;
    return simplifiedOffset;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @param {number} tolerance Tolerance.
   * @param {Array<number>} simplifiedFlatCoordinates Simplified flat
   *     coordinates.
   * @param {number} simplifiedOffset Simplified offset.
   * @param {Array<number>} simplifiedEnds Simplified ends.
   * @return {number} Simplified offset.
   */
  function quantizeArray(
    flatCoordinates,
    offset,
    ends,
    stride,
    tolerance,
    simplifiedFlatCoordinates,
    simplifiedOffset,
    simplifiedEnds,
  ) {
    for (let i = 0, ii = ends.length; i < ii; ++i) {
      const end = ends[i];
      simplifiedOffset = quantize(
        flatCoordinates,
        offset,
        end,
        stride,
        tolerance,
        simplifiedFlatCoordinates,
        simplifiedOffset,
      );
      simplifiedEnds.push(simplifiedOffset);
      offset = end;
    }
    return simplifiedOffset;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<number>>} endss Endss.
   * @param {number} stride Stride.
   * @param {number} tolerance Tolerance.
   * @param {Array<number>} simplifiedFlatCoordinates Simplified flat
   *     coordinates.
   * @param {number} simplifiedOffset Simplified offset.
   * @param {Array<Array<number>>} simplifiedEndss Simplified endss.
   * @return {number} Simplified offset.
   */
  function quantizeMultiArray(
    flatCoordinates,
    offset,
    endss,
    stride,
    tolerance,
    simplifiedFlatCoordinates,
    simplifiedOffset,
    simplifiedEndss,
  ) {
    for (let i = 0, ii = endss.length; i < ii; ++i) {
      const ends = endss[i];
      /** @type {Array<number>} */
      const simplifiedEnds = [];
      simplifiedOffset = quantizeArray(
        flatCoordinates,
        offset,
        ends,
        stride,
        tolerance,
        simplifiedFlatCoordinates,
        simplifiedOffset,
        simplifiedEnds,
      );
      simplifiedEndss.push(simplifiedEnds);
      offset = ends[ends.length - 1];
    }
    return simplifiedOffset;
  }

  /**
   * @module ol/geom/LinearRing
   */

  /**
   * @classdesc
   * Linear ring geometry. Only used as part of polygon; cannot be rendered
   * on its own.
   *
   * @api
   */
  class LinearRing extends SimpleGeometry {
    /**
     * @param {Array<import("../coordinate.js").Coordinate>|Array<number>} coordinates Coordinates.
     *     For internal use, flat coordinates in combination with `layout` are also accepted.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     */
    constructor(coordinates, layout) {
      super();

      /**
       * @private
       * @type {number}
       */
      this.maxDelta_ = -1;

      /**
       * @private
       * @type {number}
       */
      this.maxDeltaRevision_ = -1;

      if (layout !== undefined && !Array.isArray(coordinates[0])) {
        this.setFlatCoordinates(
          layout,
          /** @type {Array<number>} */ (coordinates),
        );
      } else {
        this.setCoordinates(
          /** @type {Array<import("../coordinate.js").Coordinate>} */ (
            coordinates
          ),
          layout,
        );
      }
    }

    /**
     * Make a complete copy of the geometry.
     * @return {!LinearRing} Clone.
     * @api
     * @override
     */
    clone() {
      return new LinearRing(this.flatCoordinates.slice(), this.layout);
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @param {import("../coordinate.js").Coordinate} closestPoint Closest point.
     * @param {number} minSquaredDistance Minimum squared distance.
     * @return {number} Minimum squared distance.
     * @override
     */
    closestPointXY(x, y, closestPoint, minSquaredDistance) {
      if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) {
        return minSquaredDistance;
      }
      if (this.maxDeltaRevision_ != this.getRevision()) {
        this.maxDelta_ = Math.sqrt(
          maxSquaredDelta(
            this.flatCoordinates,
            0,
            this.flatCoordinates.length,
            this.stride,
            0,
          ),
        );
        this.maxDeltaRevision_ = this.getRevision();
      }
      return assignClosestPoint(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
        this.maxDelta_,
        true,
        x,
        y,
        closestPoint,
        minSquaredDistance,
      );
    }

    /**
     * Return the area of the linear ring on projected plane.
     * @return {number} Area (on projected plane).
     * @api
     */
    getArea() {
      return linearRing(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
      );
    }

    /**
     * Return the coordinates of the linear ring.
     * @return {Array<import("../coordinate.js").Coordinate>} Coordinates.
     * @api
     * @override
     */
    getCoordinates() {
      return inflateCoordinates(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
      );
    }

    /**
     * @param {number} squaredTolerance Squared tolerance.
     * @return {LinearRing} Simplified LinearRing.
     * @protected
     * @override
     */
    getSimplifiedGeometryInternal(squaredTolerance) {
      /** @type {Array<number>} */
      const simplifiedFlatCoordinates = [];
      simplifiedFlatCoordinates.length = douglasPeucker(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
        squaredTolerance,
        simplifiedFlatCoordinates,
        0,
      );
      return new LinearRing(simplifiedFlatCoordinates, 'XY');
    }

    /**
     * Get the type of this geometry.
     * @return {import("./Geometry.js").Type} Geometry type.
     * @api
     * @override
     */
    getType() {
      return 'LinearRing';
    }

    /**
     * Test if the geometry and the passed extent intersect.
     * @param {import("../extent.js").Extent} extent Extent.
     * @return {boolean} `true` if the geometry and the extent intersect.
     * @api
     * @override
     */
    intersectsExtent(extent) {
      return false;
    }

    /**
     * Set the coordinates of the linear ring.
     * @param {!Array<import("../coordinate.js").Coordinate>} coordinates Coordinates.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     * @api
     * @override
     */
    setCoordinates(coordinates, layout) {
      this.setLayout(layout, coordinates, 1);
      if (!this.flatCoordinates) {
        this.flatCoordinates = [];
      }
      this.flatCoordinates.length = deflateCoordinates(
        this.flatCoordinates,
        0,
        coordinates,
        this.stride,
      );
      this.changed();
    }
  }

  /**
   * @module ol/geom/Point
   */

  /**
   * @classdesc
   * Point geometry.
   *
   * @api
   */
  class Point extends SimpleGeometry {
    /**
     * @param {import("../coordinate.js").Coordinate} coordinates Coordinates.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     */
    constructor(coordinates, layout) {
      super();
      this.setCoordinates(coordinates, layout);
    }

    /**
     * Make a complete copy of the geometry.
     * @return {!Point} Clone.
     * @api
     * @override
     */
    clone() {
      const point = new Point(this.flatCoordinates.slice(), this.layout);
      point.applyProperties(this);
      return point;
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @param {import("../coordinate.js").Coordinate} closestPoint Closest point.
     * @param {number} minSquaredDistance Minimum squared distance.
     * @return {number} Minimum squared distance.
     * @override
     */
    closestPointXY(x, y, closestPoint, minSquaredDistance) {
      const flatCoordinates = this.flatCoordinates;
      const squaredDistance$1 = squaredDistance(
        x,
        y,
        flatCoordinates[0],
        flatCoordinates[1],
      );
      if (squaredDistance$1 < minSquaredDistance) {
        const stride = this.stride;
        for (let i = 0; i < stride; ++i) {
          closestPoint[i] = flatCoordinates[i];
        }
        closestPoint.length = stride;
        return squaredDistance$1;
      }
      return minSquaredDistance;
    }

    /**
     * Return the coordinate of the point.
     * @return {import("../coordinate.js").Coordinate} Coordinates.
     * @api
     * @override
     */
    getCoordinates() {
      return this.flatCoordinates.slice();
    }

    /**
     * @param {import("../extent.js").Extent} extent Extent.
     * @protected
     * @return {import("../extent.js").Extent} extent Extent.
     * @override
     */
    computeExtent(extent) {
      return createOrUpdateFromCoordinate(this.flatCoordinates, extent);
    }

    /**
     * Get the type of this geometry.
     * @return {import("./Geometry.js").Type} Geometry type.
     * @api
     * @override
     */
    getType() {
      return 'Point';
    }

    /**
     * Test if the geometry and the passed extent intersect.
     * @param {import("../extent.js").Extent} extent Extent.
     * @return {boolean} `true` if the geometry and the extent intersect.
     * @api
     * @override
     */
    intersectsExtent(extent) {
      return containsXY(extent, this.flatCoordinates[0], this.flatCoordinates[1]);
    }

    /**
     * @param {!Array<*>} coordinates Coordinates.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     * @api
     * @override
     */
    setCoordinates(coordinates, layout) {
      this.setLayout(layout, coordinates, 0);
      if (!this.flatCoordinates) {
        this.flatCoordinates = [];
      }
      this.flatCoordinates.length = deflateCoordinate(
        this.flatCoordinates,
        0,
        coordinates,
        this.stride,
      );
      this.changed();
    }
  }

  /**
   * @module ol/geom/flat/contains
   */

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {import("../../extent.js").Extent} extent Extent.
   * @return {boolean} Contains extent.
   */
  function linearRingContainsExtent(
    flatCoordinates,
    offset,
    end,
    stride,
    extent,
  ) {
    const outside = forEachCorner(
      extent,
      /**
       * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
       * @return {boolean} Contains (x, y).
       */
      function (coordinate) {
        return !linearRingContainsXY(
          flatCoordinates,
          offset,
          end,
          stride,
          coordinate[0],
          coordinate[1],
        );
      },
    );
    return !outside;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {number} x X.
   * @param {number} y Y.
   * @return {boolean} Contains (x, y).
   */
  function linearRingContainsXY(
    flatCoordinates,
    offset,
    end,
    stride,
    x,
    y,
  ) {
    // https://web.archive.org/web/20210504233957/http://geomalgorithms.com/a03-_inclusion.html
    // Copyright 2000 softSurfer, 2012 Dan Sunday
    // This code may be freely used and modified for any purpose
    // providing that this copyright notice is included with it.
    // SoftSurfer makes no warranty for this code, and cannot be held
    // liable for any real or imagined damage resulting from its use.
    // Users of this code must verify correctness for their application.
    let wn = 0;
    let x1 = flatCoordinates[end - stride];
    let y1 = flatCoordinates[end - stride + 1];
    for (; offset < end; offset += stride) {
      const x2 = flatCoordinates[offset];
      const y2 = flatCoordinates[offset + 1];
      if (y1 <= y) {
        if (y2 > y && (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1) > 0) {
          wn++;
        }
      } else if (y2 <= y && (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1) < 0) {
        wn--;
      }
      x1 = x2;
      y1 = y2;
    }
    return wn !== 0;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @param {number} x X.
   * @param {number} y Y.
   * @return {boolean} Contains (x, y).
   */
  function linearRingsContainsXY(
    flatCoordinates,
    offset,
    ends,
    stride,
    x,
    y,
  ) {
    if (ends.length === 0) {
      return false;
    }
    if (!linearRingContainsXY(flatCoordinates, offset, ends[0], stride, x, y)) {
      return false;
    }
    for (let i = 1, ii = ends.length; i < ii; ++i) {
      if (
        linearRingContainsXY(flatCoordinates, ends[i - 1], ends[i], stride, x, y)
      ) {
        return false;
      }
    }
    return true;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<number>>} endss Endss.
   * @param {number} stride Stride.
   * @param {number} x X.
   * @param {number} y Y.
   * @return {boolean} Contains (x, y).
   */
  function linearRingssContainsXY(
    flatCoordinates,
    offset,
    endss,
    stride,
    x,
    y,
  ) {
    if (endss.length === 0) {
      return false;
    }
    for (let i = 0, ii = endss.length; i < ii; ++i) {
      const ends = endss[i];
      if (linearRingsContainsXY(flatCoordinates, offset, ends, stride, x, y)) {
        return true;
      }
      offset = ends[ends.length - 1];
    }
    return false;
  }

  /**
   * @module ol/geom/flat/interiorpoint
   */

  /**
   * Calculates a point that is likely to lie in the interior of the linear rings.
   * Inspired by JTS's com.vividsolutions.jts.geom.Geometry#getInteriorPoint.
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @param {Array<number>} flatCenters Flat centers.
   * @param {number} flatCentersOffset Flat center offset.
   * @param {Array<number>} [dest] Destination.
   * @return {Array<number>} Destination point as XYM coordinate, where M is the
   * length of the horizontal intersection that the point belongs to.
   */
  function getInteriorPointOfArray(
    flatCoordinates,
    offset,
    ends,
    stride,
    flatCenters,
    flatCentersOffset,
    dest,
  ) {
    let i, ii, x, x1, x2, y1, y2;
    const y = flatCenters[flatCentersOffset + 1];
    /** @type {Array<number>} */
    const intersections = [];
    // Calculate intersections with the horizontal line
    for (let r = 0, rr = ends.length; r < rr; ++r) {
      const end = ends[r];
      x1 = flatCoordinates[end - stride];
      y1 = flatCoordinates[end - stride + 1];
      for (i = offset; i < end; i += stride) {
        x2 = flatCoordinates[i];
        y2 = flatCoordinates[i + 1];
        if ((y <= y1 && y2 <= y) || (y1 <= y && y <= y2)) {
          x = ((y - y1) / (y2 - y1)) * (x2 - x1) + x1;
          intersections.push(x);
        }
        x1 = x2;
        y1 = y2;
      }
    }
    // Find the longest segment of the horizontal line that has its center point
    // inside the linear ring.
    let pointX = NaN;
    let maxSegmentLength = -Infinity;
    intersections.sort(ascending);
    x1 = intersections[0];
    for (i = 1, ii = intersections.length; i < ii; ++i) {
      x2 = intersections[i];
      const segmentLength = Math.abs(x2 - x1);
      if (segmentLength > maxSegmentLength) {
        x = (x1 + x2) / 2;
        if (linearRingsContainsXY(flatCoordinates, offset, ends, stride, x, y)) {
          pointX = x;
          maxSegmentLength = segmentLength;
        }
      }
      x1 = x2;
    }
    if (isNaN(pointX)) {
      // There is no horizontal line that has its center point inside the linear
      // ring.  Use the center of the the linear ring's extent.
      pointX = flatCenters[flatCentersOffset];
    }
    if (dest) {
      dest.push(pointX, y, maxSegmentLength);
      return dest;
    }
    return [pointX, y, maxSegmentLength];
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<number>>} endss Endss.
   * @param {number} stride Stride.
   * @param {Array<number>} flatCenters Flat centers.
   * @return {Array<number>} Interior points as XYM coordinates, where M is the
   * length of the horizontal intersection that the point belongs to.
   */
  function getInteriorPointsOfMultiArray(
    flatCoordinates,
    offset,
    endss,
    stride,
    flatCenters,
  ) {
    /** @type {Array<number>} */
    let interiorPoints = [];
    for (let i = 0, ii = endss.length; i < ii; ++i) {
      const ends = endss[i];
      interiorPoints = getInteriorPointOfArray(
        flatCoordinates,
        offset,
        ends,
        stride,
        flatCenters,
        2 * i,
        interiorPoints,
      );
      offset = ends[ends.length - 1];
    }
    return interiorPoints;
  }

  /**
   * @module ol/geom/flat/segments
   */

  /**
   * This function calls `callback` for each segment of the flat coordinates
   * array. If the callback returns a truthy value the function returns that
   * value immediately. Otherwise the function returns `false`.
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {function(import("../../coordinate.js").Coordinate, import("../../coordinate.js").Coordinate): T} callback Function
   *     called for each segment.
   * @return {T|boolean} Value.
   * @template T
   */
  function forEach(flatCoordinates, offset, end, stride, callback) {
    let ret;
    offset += stride;
    for (; offset < end; offset += stride) {
      ret = callback(
        flatCoordinates.slice(offset - stride, offset),
        flatCoordinates.slice(offset, offset + stride),
      );
      if (ret) {
        return ret;
      }
    }
    return false;
  }

  /**
   * @module ol/geom/flat/intersectsextent
   */

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {import("../../extent.js").Extent} extent Extent.
   * @param {import('../../extent.js').Extent} [coordinatesExtent] Coordinates extent
   * @return {boolean} True if the geometry and the extent intersect.
   */
  function intersectsLineString(
    flatCoordinates,
    offset,
    end,
    stride,
    extent,
    coordinatesExtent,
  ) {
    coordinatesExtent =
      coordinatesExtent ??
      extendFlatCoordinates(createEmpty(), flatCoordinates, offset, end, stride);
    if (!intersects$1(extent, coordinatesExtent)) {
      return false;
    }
    if (
      (coordinatesExtent[0] >= extent[0] && coordinatesExtent[2] <= extent[2]) ||
      (coordinatesExtent[1] >= extent[1] && coordinatesExtent[3] <= extent[3])
    ) {
      return true;
    }
    return forEach(
      flatCoordinates,
      offset,
      end,
      stride,
      /**
       * @param {import("../../coordinate.js").Coordinate} point1 Start point.
       * @param {import("../../coordinate.js").Coordinate} point2 End point.
       * @return {boolean} `true` if the segment and the extent intersect,
       *     `false` otherwise.
       */
      function (point1, point2) {
        return intersectsSegment(extent, point1, point2);
      },
    );
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @param {import("../../extent.js").Extent} extent Extent.
   * @return {boolean} True if the geometry and the extent intersect.
   */
  function intersectsLineStringArray(
    flatCoordinates,
    offset,
    ends,
    stride,
    extent,
  ) {
    for (let i = 0, ii = ends.length; i < ii; ++i) {
      if (
        intersectsLineString(flatCoordinates, offset, ends[i], stride, extent)
      ) {
        return true;
      }
      offset = ends[i];
    }
    return false;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {import("../../extent.js").Extent} extent Extent.
   * @return {boolean} True if the geometry and the extent intersect.
   */
  function intersectsLinearRing(
    flatCoordinates,
    offset,
    end,
    stride,
    extent,
  ) {
    if (intersectsLineString(flatCoordinates, offset, end, stride, extent)) {
      return true;
    }
    if (
      linearRingContainsXY(
        flatCoordinates,
        offset,
        end,
        stride,
        extent[0],
        extent[1],
      )
    ) {
      return true;
    }
    if (
      linearRingContainsXY(
        flatCoordinates,
        offset,
        end,
        stride,
        extent[0],
        extent[3],
      )
    ) {
      return true;
    }
    if (
      linearRingContainsXY(
        flatCoordinates,
        offset,
        end,
        stride,
        extent[2],
        extent[1],
      )
    ) {
      return true;
    }
    if (
      linearRingContainsXY(
        flatCoordinates,
        offset,
        end,
        stride,
        extent[2],
        extent[3],
      )
    ) {
      return true;
    }
    return false;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @param {import("../../extent.js").Extent} extent Extent.
   * @return {boolean} True if the geometry and the extent intersect.
   */
  function intersectsLinearRingArray(
    flatCoordinates,
    offset,
    ends,
    stride,
    extent,
  ) {
    if (!intersectsLinearRing(flatCoordinates, offset, ends[0], stride, extent)) {
      return false;
    }
    if (ends.length === 1) {
      return true;
    }
    for (let i = 1, ii = ends.length; i < ii; ++i) {
      if (
        linearRingContainsExtent(
          flatCoordinates,
          ends[i - 1],
          ends[i],
          stride,
          extent,
        )
      ) {
        if (
          !intersectsLineString(
            flatCoordinates,
            ends[i - 1],
            ends[i],
            stride,
            extent,
          )
        ) {
          return false;
        }
      }
    }
    return true;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<number>>} endss Endss.
   * @param {number} stride Stride.
   * @param {import("../../extent.js").Extent} extent Extent.
   * @return {boolean} True if the geometry and the extent intersect.
   */
  function intersectsLinearRingMultiArray(
    flatCoordinates,
    offset,
    endss,
    stride,
    extent,
  ) {
    for (let i = 0, ii = endss.length; i < ii; ++i) {
      const ends = endss[i];
      if (
        intersectsLinearRingArray(flatCoordinates, offset, ends, stride, extent)
      ) {
        return true;
      }
      offset = ends[ends.length - 1];
    }
    return false;
  }

  /**
   * @module ol/geom/flat/reverse
   */

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   */
  function coordinates(flatCoordinates, offset, end, stride) {
    while (offset < end - stride) {
      for (let i = 0; i < stride; ++i) {
        const tmp = flatCoordinates[offset + i];
        flatCoordinates[offset + i] = flatCoordinates[end - stride + i];
        flatCoordinates[end - stride + i] = tmp;
      }
      offset += stride;
      end -= stride;
    }
  }

  /**
   * @module ol/geom/flat/orient
   */

  /**
   * Is the linear ring oriented clockwise in a coordinate system with a bottom-left
   * coordinate origin? For a coordinate system with a top-left coordinate origin,
   * the ring's orientation is clockwise when this function returns false.
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @return {boolean|undefined} Is clockwise.
   */
  function linearRingIsClockwise(flatCoordinates, offset, end, stride) {
    // https://stackoverflow.com/q/1165647/clockwise-method#1165943
    // https://github.com/OSGeo/gdal/blob/master/gdal/ogr/ogrlinearring.cpp
    let edge = 0;
    let x1 = flatCoordinates[end - stride];
    let y1 = flatCoordinates[end - stride + 1];
    for (; offset < end; offset += stride) {
      const x2 = flatCoordinates[offset];
      const y2 = flatCoordinates[offset + 1];
      edge += (x2 - x1) * (y2 + y1);
      x1 = x2;
      y1 = y2;
    }
    return edge === 0 ? undefined : edge > 0;
  }

  /**
   * Determines if linear rings are oriented.  By default, left-hand orientation
   * is tested (first ring must be clockwise, remaining rings counter-clockwise).
   * To test for right-hand orientation, use the `right` argument.
   *
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Array of end indexes.
   * @param {number} stride Stride.
   * @param {boolean} [right] Test for right-hand orientation
   *     (counter-clockwise exterior ring and clockwise interior rings).
   * @return {boolean} Rings are correctly oriented.
   */
  function linearRingsAreOriented(
    flatCoordinates,
    offset,
    ends,
    stride,
    right,
  ) {
    right = right !== undefined ? right : false;
    for (let i = 0, ii = ends.length; i < ii; ++i) {
      const end = ends[i];
      const isClockwise = linearRingIsClockwise(
        flatCoordinates,
        offset,
        end,
        stride,
      );
      if (i === 0) {
        if ((right && isClockwise) || (!right && !isClockwise)) {
          return false;
        }
      } else {
        if ((right && !isClockwise) || (!right && isClockwise)) {
          return false;
        }
      }
      offset = end;
    }
    return true;
  }

  /**
   * Determines if linear rings are oriented.  By default, left-hand orientation
   * is tested (first ring must be clockwise, remaining rings counter-clockwise).
   * To test for right-hand orientation, use the `right` argument.
   *
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<number>>} endss Array of array of end indexes.
   * @param {number} stride Stride.
   * @param {boolean} [right] Test for right-hand orientation
   *     (counter-clockwise exterior ring and clockwise interior rings).
   * @return {boolean} Rings are correctly oriented.
   */
  function linearRingssAreOriented(
    flatCoordinates,
    offset,
    endss,
    stride,
    right,
  ) {
    for (let i = 0, ii = endss.length; i < ii; ++i) {
      const ends = endss[i];
      if (!linearRingsAreOriented(flatCoordinates, offset, ends, stride, right)) {
        return false;
      }
      if (ends.length) {
        offset = ends[ends.length - 1];
      }
    }
    return true;
  }

  /**
   * Orient coordinates in a flat array of linear rings.  By default, rings
   * are oriented following the left-hand rule (clockwise for exterior and
   * counter-clockwise for interior rings).  To orient according to the
   * right-hand rule, use the `right` argument.
   *
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @param {boolean} [right] Follow the right-hand rule for orientation.
   * @return {number} End.
   */
  function orientLinearRings(
    flatCoordinates,
    offset,
    ends,
    stride,
    right,
  ) {
    right = right !== undefined ? right : false;
    for (let i = 0, ii = ends.length; i < ii; ++i) {
      const end = ends[i];
      const isClockwise = linearRingIsClockwise(
        flatCoordinates,
        offset,
        end,
        stride,
      );
      const reverse =
        i === 0
          ? (right && isClockwise) || (!right && !isClockwise)
          : (right && !isClockwise) || (!right && isClockwise);
      if (reverse) {
        coordinates(flatCoordinates, offset, end, stride);
      }
      offset = end;
    }
    return offset;
  }

  /**
   * Orient coordinates in a flat array of linear rings.  By default, rings
   * are oriented following the left-hand rule (clockwise for exterior and
   * counter-clockwise for interior rings).  To orient according to the
   * right-hand rule, use the `right` argument.
   *
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<number>>} endss Array of array of end indexes.
   * @param {number} stride Stride.
   * @param {boolean} [right] Follow the right-hand rule for orientation.
   * @return {number} End.
   */
  function orientLinearRingsArray(
    flatCoordinates,
    offset,
    endss,
    stride,
    right,
  ) {
    for (let i = 0, ii = endss.length; i < ii; ++i) {
      offset = orientLinearRings(
        flatCoordinates,
        offset,
        endss[i],
        stride,
        right,
      );
    }
    return offset;
  }

  /**
   * Return a two-dimensional endss
   * @param {Array<number>} flatCoordinates Flat coordinates
   * @param {Array<number>} ends Linear ring end indexes
   * @return {Array<Array<number>>} Two dimensional endss array that can
   * be used to construct a MultiPolygon
   */
  function inflateEnds(flatCoordinates, ends) {
    const endss = [];
    let offset = 0;
    let prevEndIndex = 0;
    let startOrientation;
    for (let i = 0, ii = ends.length; i < ii; ++i) {
      const end = ends[i];
      // classifies an array of rings into polygons with outer rings and holes
      const orientation = linearRingIsClockwise(flatCoordinates, offset, end, 2);
      if (startOrientation === undefined) {
        startOrientation = orientation;
      }
      if (orientation === startOrientation) {
        endss.push(ends.slice(prevEndIndex, i + 1));
      } else {
        if (endss.length === 0) {
          continue;
        }
        endss[endss.length - 1].push(ends[prevEndIndex]);
      }
      prevEndIndex = i + 1;
      offset = end;
    }
    return endss;
  }

  /**
   * @module ol/geom/Polygon
   */

  /**
   * @classdesc
   * Polygon geometry.
   *
   * @api
   */
  class Polygon extends SimpleGeometry {
    /**
     * @param {!Array<Array<import("../coordinate.js").Coordinate>>|!Array<number>} coordinates
     *     Array of linear rings that define the polygon. The first linear ring of the
     *     array defines the outer-boundary or surface of the polygon. Each subsequent
     *     linear ring defines a hole in the surface of the polygon. A linear ring is
     *     an array of vertices' coordinates where the first coordinate and the last are
     *     equivalent. (For internal use, flat coordinates in combination with
     *     `layout` and `ends` are also accepted.)
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     * @param {Array<number>} [ends] Ends (for internal use with flat coordinates).
     */
    constructor(coordinates, layout, ends) {
      super();

      /**
       * @type {Array<number>}
       * @private
       */
      this.ends_ = [];

      /**
       * @private
       * @type {number}
       */
      this.flatInteriorPointRevision_ = -1;

      /**
       * @private
       * @type {import("../coordinate.js").Coordinate|null}
       */
      this.flatInteriorPoint_ = null;

      /**
       * @private
       * @type {number}
       */
      this.maxDelta_ = -1;

      /**
       * @private
       * @type {number}
       */
      this.maxDeltaRevision_ = -1;

      /**
       * @private
       * @type {number}
       */
      this.orientedRevision_ = -1;

      /**
       * @private
       * @type {Array<number>|null}
       */
      this.orientedFlatCoordinates_ = null;

      if (layout !== undefined && ends) {
        this.setFlatCoordinates(
          layout,
          /** @type {Array<number>} */ (coordinates),
        );
        this.ends_ = ends;
      } else {
        this.setCoordinates(
          /** @type {Array<Array<import("../coordinate.js").Coordinate>>} */ (
            coordinates
          ),
          layout,
        );
      }
    }

    /**
     * Append the passed linear ring to this polygon.
     * @param {LinearRing} linearRing Linear ring.
     * @api
     */
    appendLinearRing(linearRing) {
      if (!this.flatCoordinates) {
        this.flatCoordinates = linearRing.getFlatCoordinates().slice();
      } else {
        extend$2(this.flatCoordinates, linearRing.getFlatCoordinates());
      }
      this.ends_.push(this.flatCoordinates.length);
      this.changed();
    }

    /**
     * Make a complete copy of the geometry.
     * @return {!Polygon} Clone.
     * @api
     * @override
     */
    clone() {
      const polygon = new Polygon(
        this.flatCoordinates.slice(),
        this.layout,
        this.ends_.slice(),
      );
      polygon.applyProperties(this);
      return polygon;
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @param {import("../coordinate.js").Coordinate} closestPoint Closest point.
     * @param {number} minSquaredDistance Minimum squared distance.
     * @return {number} Minimum squared distance.
     * @override
     */
    closestPointXY(x, y, closestPoint, minSquaredDistance) {
      if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) {
        return minSquaredDistance;
      }
      if (this.maxDeltaRevision_ != this.getRevision()) {
        this.maxDelta_ = Math.sqrt(
          arrayMaxSquaredDelta(
            this.flatCoordinates,
            0,
            this.ends_,
            this.stride,
            0,
          ),
        );
        this.maxDeltaRevision_ = this.getRevision();
      }
      return assignClosestArrayPoint(
        this.flatCoordinates,
        0,
        this.ends_,
        this.stride,
        this.maxDelta_,
        true,
        x,
        y,
        closestPoint,
        minSquaredDistance,
      );
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @return {boolean} Contains (x, y).
     * @override
     */
    containsXY(x, y) {
      return linearRingsContainsXY(
        this.getOrientedFlatCoordinates(),
        0,
        this.ends_,
        this.stride,
        x,
        y,
      );
    }

    /**
     * Return the area of the polygon on projected plane.
     * @return {number} Area (on projected plane).
     * @api
     */
    getArea() {
      return linearRings(
        this.getOrientedFlatCoordinates(),
        0,
        this.ends_,
        this.stride,
      );
    }

    /**
     * Get the coordinate array for this geometry.  This array has the structure
     * of a GeoJSON coordinate array for polygons.
     *
     * @param {boolean} [right] Orient coordinates according to the right-hand
     *     rule (counter-clockwise for exterior and clockwise for interior rings).
     *     If `false`, coordinates will be oriented according to the left-hand rule
     *     (clockwise for exterior and counter-clockwise for interior rings).
     *     By default, coordinate orientation will depend on how the geometry was
     *     constructed.
     * @return {Array<Array<import("../coordinate.js").Coordinate>>} Coordinates.
     * @api
     * @override
     */
    getCoordinates(right) {
      let flatCoordinates;
      if (right !== undefined) {
        flatCoordinates = this.getOrientedFlatCoordinates().slice();
        orientLinearRings(flatCoordinates, 0, this.ends_, this.stride, right);
      } else {
        flatCoordinates = this.flatCoordinates;
      }

      return inflateCoordinatesArray(flatCoordinates, 0, this.ends_, this.stride);
    }

    /**
     * @return {Array<number>} Ends.
     */
    getEnds() {
      return this.ends_;
    }

    /**
     * @return {Array<number>} Interior point.
     */
    getFlatInteriorPoint() {
      if (this.flatInteriorPointRevision_ != this.getRevision()) {
        const flatCenter = getCenter(this.getExtent());
        this.flatInteriorPoint_ = getInteriorPointOfArray(
          this.getOrientedFlatCoordinates(),
          0,
          this.ends_,
          this.stride,
          flatCenter,
          0,
        );
        this.flatInteriorPointRevision_ = this.getRevision();
      }
      return /** @type {import("../coordinate.js").Coordinate} */ (
        this.flatInteriorPoint_
      );
    }

    /**
     * Return an interior point of the polygon.
     * @return {Point} Interior point as XYM coordinate, where M is the
     * length of the horizontal intersection that the point belongs to.
     * @api
     */
    getInteriorPoint() {
      return new Point(this.getFlatInteriorPoint(), 'XYM');
    }

    /**
     * Return the number of rings of the polygon,  this includes the exterior
     * ring and any interior rings.
     *
     * @return {number} Number of rings.
     * @api
     */
    getLinearRingCount() {
      return this.ends_.length;
    }

    /**
     * Return the Nth linear ring of the polygon geometry. Return `null` if the
     * given index is out of range.
     * The exterior linear ring is available at index `0` and the interior rings
     * at index `1` and beyond.
     *
     * @param {number} index Index.
     * @return {LinearRing|null} Linear ring.
     * @api
     */
    getLinearRing(index) {
      if (index < 0 || this.ends_.length <= index) {
        return null;
      }
      return new LinearRing(
        this.flatCoordinates.slice(
          index === 0 ? 0 : this.ends_[index - 1],
          this.ends_[index],
        ),
        this.layout,
      );
    }

    /**
     * Return the linear rings of the polygon.
     * @return {Array<LinearRing>} Linear rings.
     * @api
     */
    getLinearRings() {
      const layout = this.layout;
      const flatCoordinates = this.flatCoordinates;
      const ends = this.ends_;
      const linearRings = [];
      let offset = 0;
      for (let i = 0, ii = ends.length; i < ii; ++i) {
        const end = ends[i];
        const linearRing = new LinearRing(
          flatCoordinates.slice(offset, end),
          layout,
        );
        linearRings.push(linearRing);
        offset = end;
      }
      return linearRings;
    }

    /**
     * @return {Array<number>} Oriented flat coordinates.
     */
    getOrientedFlatCoordinates() {
      if (this.orientedRevision_ != this.getRevision()) {
        const flatCoordinates = this.flatCoordinates;
        if (linearRingsAreOriented(flatCoordinates, 0, this.ends_, this.stride)) {
          this.orientedFlatCoordinates_ = flatCoordinates;
        } else {
          this.orientedFlatCoordinates_ = flatCoordinates.slice();
          this.orientedFlatCoordinates_.length = orientLinearRings(
            this.orientedFlatCoordinates_,
            0,
            this.ends_,
            this.stride,
          );
        }
        this.orientedRevision_ = this.getRevision();
      }
      return /** @type {Array<number>} */ (this.orientedFlatCoordinates_);
    }

    /**
     * @param {number} squaredTolerance Squared tolerance.
     * @return {Polygon} Simplified Polygon.
     * @protected
     * @override
     */
    getSimplifiedGeometryInternal(squaredTolerance) {
      /** @type {Array<number>} */
      const simplifiedFlatCoordinates = [];
      /** @type {Array<number>} */
      const simplifiedEnds = [];
      simplifiedFlatCoordinates.length = quantizeArray(
        this.flatCoordinates,
        0,
        this.ends_,
        this.stride,
        Math.sqrt(squaredTolerance),
        simplifiedFlatCoordinates,
        0,
        simplifiedEnds,
      );
      return new Polygon(simplifiedFlatCoordinates, 'XY', simplifiedEnds);
    }

    /**
     * Get the type of this geometry.
     * @return {import("./Geometry.js").Type} Geometry type.
     * @api
     * @override
     */
    getType() {
      return 'Polygon';
    }

    /**
     * Test if the geometry and the passed extent intersect.
     * @param {import("../extent.js").Extent} extent Extent.
     * @return {boolean} `true` if the geometry and the extent intersect.
     * @api
     * @override
     */
    intersectsExtent(extent) {
      return intersectsLinearRingArray(
        this.getOrientedFlatCoordinates(),
        0,
        this.ends_,
        this.stride,
        extent,
      );
    }

    /**
     * Set the coordinates of the polygon.
     * @param {!Array<Array<import("../coordinate.js").Coordinate>>} coordinates Coordinates.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     * @api
     * @override
     */
    setCoordinates(coordinates, layout) {
      this.setLayout(layout, coordinates, 2);
      if (!this.flatCoordinates) {
        this.flatCoordinates = [];
      }
      const ends = deflateCoordinatesArray(
        this.flatCoordinates,
        0,
        coordinates,
        this.stride,
        this.ends_,
      );
      this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1];
      this.changed();
    }
  }

  /**
   * Create a polygon from an extent. The layout used is `XY`.
   * @param {import("../extent.js").Extent} extent The extent.
   * @return {Polygon} The polygon.
   * @api
   */
  function fromExtent(extent) {
    if (isEmpty(extent)) {
      throw new Error('Cannot create polygon from empty extent');
    }
    const minX = extent[0];
    const minY = extent[1];
    const maxX = extent[2];
    const maxY = extent[3];
    const flatCoordinates = [
      minX,
      minY,
      minX,
      maxY,
      maxX,
      maxY,
      maxX,
      minY,
      minX,
      minY,
    ];
    return new Polygon(flatCoordinates, 'XY', [flatCoordinates.length]);
  }

  /**
   * @module ol/geom/flat/interpolate
   */

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {number} fraction Fraction.
   * @param {Array<number>} [dest] Destination.
   * @param {number} [dimension] Destination dimension (default is `2`)
   * @return {Array<number>} Destination.
   */
  function interpolatePoint(
    flatCoordinates,
    offset,
    end,
    stride,
    fraction,
    dest,
    dimension,
  ) {
    let o, t;
    const n = (end - offset) / stride;
    if (n === 1) {
      o = offset;
    } else if (n === 2) {
      o = offset;
      t = fraction;
    } else if (n !== 0) {
      let x1 = flatCoordinates[offset];
      let y1 = flatCoordinates[offset + 1];
      let length = 0;
      const cumulativeLengths = [0];
      for (let i = offset + stride; i < end; i += stride) {
        const x2 = flatCoordinates[i];
        const y2 = flatCoordinates[i + 1];
        length += Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
        cumulativeLengths.push(length);
        x1 = x2;
        y1 = y2;
      }
      const target = fraction * length;
      const index = binarySearch(cumulativeLengths, target);
      if (index < 0) {
        t =
          (target - cumulativeLengths[-index - 2]) /
          (cumulativeLengths[-index - 1] - cumulativeLengths[-index - 2]);
        o = offset + (-index - 2) * stride;
      } else {
        o = offset + index * stride;
      }
    }
    dimension = dimension > 1 ? dimension : 2;
    dest = dest ? dest : new Array(dimension);
    for (let i = 0; i < dimension; ++i) {
      dest[i] =
        o === undefined
          ? NaN
          : t === undefined
            ? flatCoordinates[o + i]
            : lerp(flatCoordinates[o + i], flatCoordinates[o + stride + i], t);
    }
    return dest;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @param {number} m M.
   * @param {boolean} extrapolate Extrapolate.
   * @return {import("../../coordinate.js").Coordinate|null} Coordinate.
   */
  function lineStringCoordinateAtM(
    flatCoordinates,
    offset,
    end,
    stride,
    m,
    extrapolate,
  ) {
    if (end == offset) {
      return null;
    }
    let coordinate;
    if (m < flatCoordinates[offset + stride - 1]) {
      if (extrapolate) {
        coordinate = flatCoordinates.slice(offset, offset + stride);
        coordinate[stride - 1] = m;
        return coordinate;
      }
      return null;
    }
    if (flatCoordinates[end - 1] < m) {
      if (extrapolate) {
        coordinate = flatCoordinates.slice(end - stride, end);
        coordinate[stride - 1] = m;
        return coordinate;
      }
      return null;
    }
    // FIXME use O(1) search
    if (m == flatCoordinates[offset + stride - 1]) {
      return flatCoordinates.slice(offset, offset + stride);
    }
    let lo = offset / stride;
    let hi = end / stride;
    while (lo < hi) {
      const mid = (lo + hi) >> 1;
      if (m < flatCoordinates[(mid + 1) * stride - 1]) {
        hi = mid;
      } else {
        lo = mid + 1;
      }
    }
    const m0 = flatCoordinates[lo * stride - 1];
    if (m == m0) {
      return flatCoordinates.slice((lo - 1) * stride, (lo - 1) * stride + stride);
    }
    const m1 = flatCoordinates[(lo + 1) * stride - 1];
    const t = (m - m0) / (m1 - m0);
    coordinate = [];
    for (let i = 0; i < stride - 1; ++i) {
      coordinate.push(
        lerp(
          flatCoordinates[(lo - 1) * stride + i],
          flatCoordinates[lo * stride + i],
          t,
        ),
      );
    }
    coordinate.push(m);
    return coordinate;
  }

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<number>} ends Ends.
   * @param {number} stride Stride.
   * @param {number} m M.
   * @param {boolean} extrapolate Extrapolate.
   * @param {boolean} interpolate Interpolate.
   * @return {import("../../coordinate.js").Coordinate|null} Coordinate.
   */
  function lineStringsCoordinateAtM(
    flatCoordinates,
    offset,
    ends,
    stride,
    m,
    extrapolate,
    interpolate,
  ) {
    if (interpolate) {
      return lineStringCoordinateAtM(
        flatCoordinates,
        offset,
        ends[ends.length - 1],
        stride,
        m,
        extrapolate,
      );
    }
    let coordinate;
    if (m < flatCoordinates[stride - 1]) {
      if (extrapolate) {
        coordinate = flatCoordinates.slice(0, stride);
        coordinate[stride - 1] = m;
        return coordinate;
      }
      return null;
    }
    if (flatCoordinates[flatCoordinates.length - 1] < m) {
      if (extrapolate) {
        coordinate = flatCoordinates.slice(flatCoordinates.length - stride);
        coordinate[stride - 1] = m;
        return coordinate;
      }
      return null;
    }
    for (let i = 0, ii = ends.length; i < ii; ++i) {
      const end = ends[i];
      if (offset == end) {
        continue;
      }
      if (m < flatCoordinates[offset + stride - 1]) {
        return null;
      }
      if (m <= flatCoordinates[end - 1]) {
        return lineStringCoordinateAtM(
          flatCoordinates,
          offset,
          end,
          stride,
          m,
          false,
        );
      }
      offset = end;
    }
    return null;
  }

  /**
   * @module ol/geom/flat/length
   */

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @return {number} Length.
   */
  function lineStringLength(flatCoordinates, offset, end, stride) {
    let x1 = flatCoordinates[offset];
    let y1 = flatCoordinates[offset + 1];
    let length = 0;
    for (let i = offset + stride; i < end; i += stride) {
      const x2 = flatCoordinates[i];
      const y2 = flatCoordinates[i + 1];
      length += Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
      x1 = x2;
      y1 = y2;
    }
    return length;
  }

  /**
   * @module ol/geom/LineString
   */

  /**
   * @classdesc
   * Linestring geometry.
   *
   * @api
   */
  class LineString extends SimpleGeometry {
    /**
     * @param {Array<import("../coordinate.js").Coordinate>|Array<number>} coordinates Coordinates.
     *     For internal use, flat coordinates in combination with `layout` are also accepted.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     */
    constructor(coordinates, layout) {
      super();

      /**
       * @private
       * @type {import("../coordinate.js").Coordinate|null}
       */
      this.flatMidpoint_ = null;

      /**
       * @private
       * @type {number}
       */
      this.flatMidpointRevision_ = -1;

      /**
       * @private
       * @type {number}
       */
      this.maxDelta_ = -1;

      /**
       * @private
       * @type {number}
       */
      this.maxDeltaRevision_ = -1;

      if (layout !== undefined && !Array.isArray(coordinates[0])) {
        this.setFlatCoordinates(
          layout,
          /** @type {Array<number>} */ (coordinates),
        );
      } else {
        this.setCoordinates(
          /** @type {Array<import("../coordinate.js").Coordinate>} */ (
            coordinates
          ),
          layout,
        );
      }
    }

    /**
     * Append the passed coordinate to the coordinates of the linestring.
     * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
     * @api
     */
    appendCoordinate(coordinate) {
      extend$2(this.flatCoordinates, coordinate);
      this.changed();
    }

    /**
     * Make a complete copy of the geometry.
     * @return {!LineString} Clone.
     * @api
     * @override
     */
    clone() {
      const lineString = new LineString(
        this.flatCoordinates.slice(),
        this.layout,
      );
      lineString.applyProperties(this);
      return lineString;
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @param {import("../coordinate.js").Coordinate} closestPoint Closest point.
     * @param {number} minSquaredDistance Minimum squared distance.
     * @return {number} Minimum squared distance.
     * @override
     */
    closestPointXY(x, y, closestPoint, minSquaredDistance) {
      if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) {
        return minSquaredDistance;
      }
      if (this.maxDeltaRevision_ != this.getRevision()) {
        this.maxDelta_ = Math.sqrt(
          maxSquaredDelta(
            this.flatCoordinates,
            0,
            this.flatCoordinates.length,
            this.stride,
            0,
          ),
        );
        this.maxDeltaRevision_ = this.getRevision();
      }
      return assignClosestPoint(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
        this.maxDelta_,
        false,
        x,
        y,
        closestPoint,
        minSquaredDistance,
      );
    }

    /**
     * Iterate over each segment, calling the provided callback.
     * If the callback returns a truthy value the function returns that
     * value immediately. Otherwise the function returns `false`.
     *
     * @param {function(this: S, import("../coordinate.js").Coordinate, import("../coordinate.js").Coordinate): T} callback Function
     *     called for each segment. The function will receive two arguments, the start and end coordinates of the segment.
     * @return {T|boolean} Value.
     * @template T,S
     * @api
     */
    forEachSegment(callback) {
      return forEach(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
        callback,
      );
    }

    /**
     * Returns the coordinate at `m` using linear interpolation, or `null` if no
     * such coordinate exists.
     *
     * `extrapolate` controls extrapolation beyond the range of Ms in the
     * MultiLineString. If `extrapolate` is `true` then Ms less than the first
     * M will return the first coordinate and Ms greater than the last M will
     * return the last coordinate.
     *
     * @param {number} m M.
     * @param {boolean} [extrapolate] Extrapolate. Default is `false`.
     * @return {import("../coordinate.js").Coordinate|null} Coordinate.
     * @api
     */
    getCoordinateAtM(m, extrapolate) {
      if (this.layout != 'XYM' && this.layout != 'XYZM') {
        return null;
      }
      extrapolate = extrapolate !== undefined ? extrapolate : false;
      return lineStringCoordinateAtM(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
        m,
        extrapolate,
      );
    }

    /**
     * Return the coordinates of the linestring.
     * @return {Array<import("../coordinate.js").Coordinate>} Coordinates.
     * @api
     * @override
     */
    getCoordinates() {
      return inflateCoordinates(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
      );
    }

    /**
     * Return the coordinate at the provided fraction along the linestring.
     * The `fraction` is a number between 0 and 1, where 0 is the start of the
     * linestring and 1 is the end.
     * @param {number} fraction Fraction.
     * @param {import("../coordinate.js").Coordinate} [dest] Optional coordinate whose values will
     *     be modified. If not provided, a new coordinate will be returned.
     * @return {import("../coordinate.js").Coordinate} Coordinate of the interpolated point.
     * @api
     */
    getCoordinateAt(fraction, dest) {
      return interpolatePoint(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
        fraction,
        dest,
        this.stride,
      );
    }

    /**
     * Return the length of the linestring on projected plane.
     * @return {number} Length (on projected plane).
     * @api
     */
    getLength() {
      return lineStringLength(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
      );
    }

    /**
     * @return {Array<number>} Flat midpoint.
     */
    getFlatMidpoint() {
      if (this.flatMidpointRevision_ != this.getRevision()) {
        this.flatMidpoint_ = this.getCoordinateAt(
          0.5,
          this.flatMidpoint_ ?? undefined,
        );
        this.flatMidpointRevision_ = this.getRevision();
      }
      return /** @type {Array<number>} */ (this.flatMidpoint_);
    }

    /**
     * @param {number} squaredTolerance Squared tolerance.
     * @return {LineString} Simplified LineString.
     * @protected
     * @override
     */
    getSimplifiedGeometryInternal(squaredTolerance) {
      /** @type {Array<number>} */
      const simplifiedFlatCoordinates = [];
      simplifiedFlatCoordinates.length = douglasPeucker(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
        squaredTolerance,
        simplifiedFlatCoordinates,
        0,
      );
      return new LineString(simplifiedFlatCoordinates, 'XY');
    }

    /**
     * Get the type of this geometry.
     * @return {import("./Geometry.js").Type} Geometry type.
     * @api
     * @override
     */
    getType() {
      return 'LineString';
    }

    /**
     * Test if the geometry and the passed extent intersect.
     * @param {import("../extent.js").Extent} extent Extent.
     * @return {boolean} `true` if the geometry and the extent intersect.
     * @api
     * @override
     */
    intersectsExtent(extent) {
      return intersectsLineString(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
        extent,
        this.getExtent(),
      );
    }

    /**
     * Set the coordinates of the linestring.
     * @param {!Array<import("../coordinate.js").Coordinate>} coordinates Coordinates.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     * @api
     * @override
     */
    setCoordinates(coordinates, layout) {
      this.setLayout(layout, coordinates, 1);
      if (!this.flatCoordinates) {
        this.flatCoordinates = [];
      }
      this.flatCoordinates.length = deflateCoordinates(
        this.flatCoordinates,
        0,
        coordinates,
        this.stride,
      );
      this.changed();
    }
  }

  /**
   * @module ol/render/EventType
   */

  /**
   * @enum {string}
   */
  var RenderEventType = {
    /**
     * Triggered before a layer is rendered.
     * @event module:ol/render/Event~RenderEvent#prerender
     * @api
     */
    PRERENDER: 'prerender',

    /**
     * Triggered after a layer is rendered.
     * @event module:ol/render/Event~RenderEvent#postrender
     * @api
     */
    POSTRENDER: 'postrender',

    /**
     * Triggered before layers are composed.  When dispatched by the map, the event object will not have
     * a `context` set.  When dispatched by a layer, the event object will have a `context` set.  Only
     * WebGL layers currently dispatch this event.
     * @event module:ol/render/Event~RenderEvent#precompose
     * @api
     */
    PRECOMPOSE: 'precompose',

    /**
     * Triggered after layers are composed.  When dispatched by the map, the event object will not have
     * a `context` set.  When dispatched by a layer, the event object will have a `context` set.  Only
     * WebGL layers currently dispatch this event.
     * @event module:ol/render/Event~RenderEvent#postcompose
     * @api
     */
    POSTCOMPOSE: 'postcompose',

    /**
     * Triggered when rendering is complete, i.e. all sources and tiles have
     * finished loading for the current viewport, and all tiles are faded in.
     * The event object will not have a `context` set.
     * @event module:ol/render/Event~RenderEvent#rendercomplete
     * @api
     */
    RENDERCOMPLETE: 'rendercomplete',
  };

  /**
   * @typedef {'postrender'|'precompose'|'postcompose'|'rendercomplete'} MapRenderEventTypes
   */

  /**
   * @typedef {'postrender'|'prerender'} LayerRenderEventTypes
   */

  /**
   * @module ol/has
   */

  const ua =
    typeof navigator !== 'undefined' && typeof navigator.userAgent !== 'undefined'
      ? navigator.userAgent.toLowerCase()
      : '';

  /**
   * User agent string says we are dealing with Safari as browser.
   * @type {boolean}
   */
  const SAFARI = ua.includes('safari') && !ua.includes('chrom');

  /**
   * https://bugs.webkit.org/show_bug.cgi?id=237906
   * @type {boolean}
   */
  SAFARI &&
    (ua.includes('version/15.4') ||
      /cpu (os|iphone os) 15_4 like mac os x/.test(ua));

  /**
   * User agent string says we are dealing with a WebKit engine.
   * @type {boolean}
   */
  const WEBKIT = ua.includes('webkit') && !ua.includes('edge');

  /**
   * User agent string says we are dealing with a Mac as platform.
   * @type {boolean}
   */
  const MAC = ua.includes('macintosh');

  /**
   * The ratio between physical pixels and device-independent pixels
   * (dips) on the device (`window.devicePixelRatio`).
   * @const
   * @type {number}
   * @api
   */
  const DEVICE_PIXEL_RATIO =
    typeof devicePixelRatio !== 'undefined' ? devicePixelRatio : 1;

  /**
   * The execution context is a worker with OffscreenCanvas available.
   * @const
   * @type {boolean}
   */
  const WORKER_OFFSCREEN_CANVAS =
    typeof WorkerGlobalScope !== 'undefined' &&
    typeof OffscreenCanvas !== 'undefined' &&
    self instanceof WorkerGlobalScope; //eslint-disable-line

  /**
   * Image.prototype.decode() is supported.
   * @type {boolean}
   */
  const IMAGE_DECODE =
    typeof Image !== 'undefined' && Image.prototype.decode;

  /**
   * @type {boolean}
   */
  const PASSIVE_EVENT_LISTENERS = (function () {
    let passive = false;
    try {
      const options = Object.defineProperty({}, 'passive', {
        get: function () {
          passive = true;
        },
      });

      // @ts-ignore Ignore invalid event type '_'
      window.addEventListener('_', null, options);
      // @ts-ignore Ignore invalid event type '_'
      window.removeEventListener('_', null, options);
    } catch {
      // passive not supported
    }
    return passive;
  })();

  /**
   * @module ol/ImageState
   */

  /**
   * @enum {number}
   */
  var ImageState = {
    IDLE: 0,
    LOADING: 1,
    LOADED: 2,
    ERROR: 3};

  /**
   * @module ol/dom
   */

  /**
   * @typedef {Object} ImageAttributes
   * @property {string|null} [crossOrigin] Cross origin.
   * @property {ReferrerPolicy} [referrerPolicy]  Referrer policy.
   */

  //FIXME Move this function to the canvas module
  /**
   * Create an html canvas element and returns its 2d context.
   * @param {number} [width] Canvas width.
   * @param {number} [height] Canvas height.
   * @param {Array<HTMLCanvasElement|OffscreenCanvas>} [canvasPool] Canvas pool to take existing canvas from.
   * @param {CanvasRenderingContext2DSettings} [settings] CanvasRenderingContext2DSettings
   * @return {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} The context.
   */
  function createCanvasContext2D(width, height, canvasPool, settings) {
    /** @type {HTMLCanvasElement|OffscreenCanvas} */
    let canvas;
    if (canvasPool && canvasPool.length) {
      canvas = /** @type {HTMLCanvasElement} */ (canvasPool.shift());
    } else if (WORKER_OFFSCREEN_CANVAS) {
      canvas = new (class extends OffscreenCanvas {
        style = {};
      })(width ?? 300, height ?? 150);
    } else {
      canvas = document.createElement('canvas');
    }
    if (width) {
      canvas.width = width;
    }
    if (height) {
      canvas.height = height;
    }
    return /** @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} */ (
      canvas.getContext('2d', settings)
    );
  }

  /**
   * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
   */
  let sharedCanvasContext;

  /**
   * @return {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} Shared canvas context.
   */
  function getSharedCanvasContext2D() {
    if (!sharedCanvasContext) {
      sharedCanvasContext = createCanvasContext2D(1, 1);
    }
    return sharedCanvasContext;
  }

  /**
   * Releases canvas memory to avoid exceeding memory limits in Safari.
   * See https://pqina.nl/blog/total-canvas-memory-use-exceeds-the-maximum-limit/
   * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
   */
  function releaseCanvas(context) {
    const canvas = context.canvas;
    canvas.width = 1;
    canvas.height = 1;
    context.clearRect(0, 0, 1, 1);
  }

  /**
   * Get the current computed width for the given element including margin,
   * padding and border.
   * Equivalent to jQuery's `$(el).outerWidth(true)`.
   * @param {!HTMLElement} element Element.
   * @return {number} The width.
   */
  function outerWidth(element) {
    let width = element.offsetWidth;
    const style = getComputedStyle(element);
    width += parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10);

    return width;
  }

  /**
   * Get the current computed height for the given element including margin,
   * padding and border.
   * Equivalent to jQuery's `$(el).outerHeight(true)`.
   * @param {!HTMLElement} element Element.
   * @return {number} The height.
   */
  function outerHeight(element) {
    let height = element.offsetHeight;
    const style = getComputedStyle(element);
    height += parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10);

    return height;
  }

  /**
   * @param {Node} newNode Node to replace old node
   * @param {Node} oldNode The node to be replaced
   */
  function replaceNode(newNode, oldNode) {
    const parent = oldNode.parentNode;
    if (parent) {
      parent.replaceChild(newNode, oldNode);
    }
  }

  /**
   * @param {Node} node The node to remove the children from.
   */
  function removeChildren(node) {
    while (node.lastChild) {
      node.lastChild.remove();
    }
  }

  /**
   * Transform the children of a parent node so they match the
   * provided list of children.  This function aims to efficiently
   * remove, add, and reorder child nodes while maintaining a simple
   * implementation (it is not guaranteed to minimize DOM operations).
   * @param {Node} node The parent node whose children need reworking.
   * @param {Array<Node>} children The desired children.
   */
  function replaceChildren(node, children) {
    const oldChildren = node.childNodes;

    for (let i = 0; true; ++i) {
      const oldChild = oldChildren[i];
      const newChild = children[i];

      // check if our work is done
      if (!oldChild && !newChild) {
        break;
      }

      // check if children match
      if (oldChild === newChild) {
        continue;
      }

      // check if a new child needs to be added
      if (!oldChild) {
        node.appendChild(newChild);
        continue;
      }

      // check if an old child needs to be removed
      if (!newChild) {
        node.removeChild(oldChild);
        --i;
        continue;
      }

      // reorder
      node.insertBefore(newChild, oldChild);
    }
  }

  /**
   * Creates a minimal structure that mocks a DIV to be used by the composite and
   * layer renderer in a worker environment
   * @return {HTMLDivElement} mocked DIV
   */
  function createMockDiv() {
    const mockedDiv = new Proxy(
      {
        /**
         * @type {Array<HTMLElement>}
         */
        childNodes: [],
        /**
         * @param {HTMLElement} node html node.
         * @return {HTMLElement} html node.
         */
        appendChild: function (node) {
          this.childNodes.push(node);
          return node;
        },
        /**
         * dummy function, as this structure is not supposed to have a parent.
         */
        remove: function () {},
        /**
         * @param {HTMLElement} node html node.
         * @return {HTMLElement} html node.
         */
        removeChild: function (node) {
          const index = this.childNodes.indexOf(node);
          if (index === -1) {
            throw new Error('Node to remove was not found');
          }
          this.childNodes.splice(index, 1);
          return node;
        },
        /**
         * @param {HTMLElement} newNode new html node.
         * @param {HTMLElement} referenceNode reference html node.
         * @return {HTMLElement} new html node.
         */
        insertBefore: function (newNode, referenceNode) {
          const index = this.childNodes.indexOf(referenceNode);
          if (index === -1) {
            throw new Error('Reference node not found');
          }
          this.childNodes.splice(index, 0, newNode);
          return newNode;
        },
        style: {},
      },
      {
        get(target, prop, receiver) {
          if (prop === 'firstElementChild') {
            return target.childNodes.length > 0 ? target.childNodes[0] : null;
          }
          return Reflect.get(target, prop, receiver);
        },
      },
    );
    return /** @type {HTMLDivElement} */ (/** @type {*} */ (mockedDiv));
  }

  /***
   * @param {*} obj The object to check.
   * @return {obj is (HTMLCanvasElement | OffscreenCanvas)} The object is a canvas.
   */
  function isCanvas(obj) {
    return (
      (typeof HTMLCanvasElement !== 'undefined' &&
        obj instanceof HTMLCanvasElement) ||
      (typeof OffscreenCanvas !== 'undefined' && obj instanceof OffscreenCanvas)
    );
  }

  /**
   * @module ol/color
   */

  /**
   * A color represented as a short array [red, green, blue, alpha].
   * red, green, and blue should be integers in the range 0..255 inclusive.
   * alpha should be a float in the range 0..1 inclusive. If no alpha value is
   * given then `1` will be used.
   * @typedef {Array<number>} Color
   * @api
   */

  /**
   * Color to indicate that no color should be rendered. This is meant to be used for per-reference
   * comparisons only.
   * @type {Color}
   */
  const NO_COLOR = [NaN, NaN, NaN, 0];

  let colorParseContext;
  /**
   * @return {CanvasRenderingContext2D} The color parse context
   */
  function getColorParseContext() {
    if (!colorParseContext) {
      colorParseContext = createCanvasContext2D(1, 1, undefined, {
        willReadFrequently: true,
        desynchronized: true,
      });
    }
    return colorParseContext;
  }

  const rgbModernRegEx =
    /^rgba?\(\s*(\d+%?)\s+(\d+%?)\s+(\d+%?)(?:\s*\/\s*(\d+%|\d*\.\d+|[01]))?\s*\)$/i;
  const rgbLegacyAbsoluteRegEx =
    /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*(\d+%|\d*\.\d+|[01]))?\s*\)$/i;
  const rgbLegacyPercentageRegEx =
    /^rgba?\(\s*(\d+%)\s*,\s*(\d+%)\s*,\s*(\d+%)(?:\s*,\s*(\d+%|\d*\.\d+|[01]))?\s*\)$/i;
  const hexRegEx = /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i;

  /**
   * @param {string} s Color component as number or percentage.
   * @param {number} divider Divider for percentage.
   * @return {number} Color component.
   */
  function toColorComponent(s, divider) {
    return s.endsWith('%')
      ? Number(s.substring(0, s.length - 1)) / divider
      : Number(s);
  }

  /**
   * @param {string} color Color string.
   */
  function throwInvalidColor(color) {
    throw new Error('failed to parse "' + color + '" as color');
  }

  /**
   * @param {string} color Color string.
   * @return {Color} RGBa color array.
   */
  function parseRgba(color) {
    // Fast lane for rgb(a) colors
    if (color.toLowerCase().startsWith('rgb')) {
      const rgb =
        color.match(rgbLegacyAbsoluteRegEx) ||
        color.match(rgbModernRegEx) ||
        color.match(rgbLegacyPercentageRegEx);
      if (rgb) {
        const alpha = rgb[4];
        const rgbDivider = 100 / 255;
        return [
          clamp((toColorComponent(rgb[1], rgbDivider) + 0.5) | 0, 0, 255),
          clamp((toColorComponent(rgb[2], rgbDivider) + 0.5) | 0, 0, 255),
          clamp((toColorComponent(rgb[3], rgbDivider) + 0.5) | 0, 0, 255),
          alpha !== undefined ? clamp(toColorComponent(alpha, 100), 0, 1) : 1,
        ];
      }
      throwInvalidColor(color);
    }
    // Fast lane for hex colors (also with alpha)
    if (color.startsWith('#')) {
      if (hexRegEx.test(color)) {
        const hex = color.substring(1);
        const step = hex.length <= 4 ? 1 : 2;
        const colorFromHex = [0, 0, 0, 255];
        for (let i = 0, ii = hex.length; i < ii; i += step) {
          let colorComponent = parseInt(hex.substring(i, i + step), 16);
          if (step === 1) {
            colorComponent += colorComponent << 4;
          }
          colorFromHex[i / step] = colorComponent;
        }
        colorFromHex[3] = colorFromHex[3] / 255;
        return colorFromHex;
      }
      throwInvalidColor(color);
    }
    // Use canvas color serialization to parse the color into hex or rgba
    // See https://www.w3.org/TR/2021/SPSD-2dcontext-20210128/#serialization-of-a-color
    const context = getColorParseContext();
    context.fillStyle = '#abcdef';
    let invalidCheckFillStyle = context.fillStyle;
    context.fillStyle = color;
    if (context.fillStyle === invalidCheckFillStyle) {
      context.fillStyle = '#fedcba';
      invalidCheckFillStyle = context.fillStyle;
      context.fillStyle = color;
      if (context.fillStyle === invalidCheckFillStyle) {
        throwInvalidColor(color);
      }
    }
    const colorString = context.fillStyle;
    if (colorString.startsWith('#') || colorString.startsWith('rgba')) {
      return parseRgba(colorString);
    }
    context.clearRect(0, 0, 1, 1);
    context.fillRect(0, 0, 1, 1);
    const colorFromImage = Array.from(context.getImageData(0, 0, 1, 1).data);
    colorFromImage[3] = toFixed(colorFromImage[3] / 255, 3);
    return colorFromImage;
  }

  /**
   * Return the color as an rgba string.
   * @param {Color|string} color Color.
   * @return {string} Rgba string.
   * @api
   */
  function asString(color) {
    if (typeof color === 'string') {
      return color;
    }
    return toString(color);
  }

  /**
   * @type {number}
   */
  const MAX_CACHE_SIZE = 1024;

  /**
   * We maintain a small cache of parsed strings.  Whenever the cache grows too large,
   * we delete an arbitrary set of the entries.
   *
   * @type {Object<string, Color>}
   */
  const cache = {};

  /**
   * @type {number}
   */
  let cacheSize = 0;

  /**
   * @param {Color} color A color that may or may not have an alpha channel.
   * @return {Color} The input color with an alpha channel.  If the input color has
   * an alpha channel, the input color will be returned unchanged.  Otherwise, a new
   * array will be returned with the input color and an alpha channel of 1.
   */
  function withAlpha(color) {
    if (color.length === 4) {
      return color;
    }
    const output = color.slice();
    output[3] = 1;
    return output;
  }

  // The functions b1, b2, a1, a2, rgbaToLcha and lchaToRgba below are adapted from
  // https://stackoverflow.com/a/67219995/2389327

  /**
   * @param {number} v Input value.
   * @return {number} Output value.
   */
  function b1(v) {
    return v > 0.0031308 ? Math.pow(v, 1 / 2.4) * 269.025 - 14.025 : v * 3294.6;
  }

  /**
   * @param {number} v Input value.
   * @return {number} Output value.
   */
  function b2(v) {
    return v > 0.2068965 ? Math.pow(v, 3) : (v - 4 / 29) * (108 / 841);
  }

  /**
   * @param {number} v Input value.
   * @return {number} Output value.
   */
  function a1(v) {
    return v > 10.314724 ? Math.pow((v + 14.025) / 269.025, 2.4) : v / 3294.6;
  }

  /**
   * @param {number} v Input value.
   * @return {number} Output value.
   */
  function a2(v) {
    return v > 0.0088564 ? Math.pow(v, 1 / 3) : v / (108 / 841) + 4 / 29;
  }

  /**
   * @param {Color} color RGBA color.
   * @return {Color} LCHuv color with alpha.
   */
  function rgbaToLcha(color) {
    const r = a1(color[0]);
    const g = a1(color[1]);
    const b = a1(color[2]);
    const y = a2(r * 0.222488403 + g * 0.716873169 + b * 0.06060791);
    const l = 500 * (a2(r * 0.452247074 + g * 0.399439023 + b * 0.148375274) - y);
    const q = 200 * (y - a2(r * 0.016863605 + g * 0.117638439 + b * 0.865350722));
    const h = Math.atan2(q, l) * (180 / Math.PI);
    return [
      116 * y - 16,
      Math.sqrt(l * l + q * q),
      h < 0 ? h + 360 : h,
      color[3],
    ];
  }

  /**
   * @param {Color} color LCHuv color with alpha.
   * @return {Color} RGBA color.
   */
  function lchaToRgba(color) {
    const l = (color[0] + 16) / 116;
    const c = color[1];
    const h = (color[2] * Math.PI) / 180;
    const y = b2(l);
    const x = b2(l + (c / 500) * Math.cos(h));
    const z = b2(l - (c / 200) * Math.sin(h));
    const r = b1(x * 3.021973625 - y * 1.617392459 - z * 0.404875592);
    const g = b1(x * -0.943766287 + y * 1.916279586 + z * 0.027607165);
    const b = b1(x * 0.069407491 - y * 0.22898585 + z * 1.159737864);
    return [
      clamp((r + 0.5) | 0, 0, 255),
      clamp((g + 0.5) | 0, 0, 255),
      clamp((b + 0.5) | 0, 0, 255),
      color[3],
    ];
  }

  /**
   * @param {string} s String.
   * @return {Color} Color.
   */
  function fromString(s) {
    if (s === 'none') {
      return NO_COLOR;
    }
    if (cache.hasOwnProperty(s)) {
      return cache[s];
    }
    if (cacheSize >= MAX_CACHE_SIZE) {
      let i = 0;
      for (const key in cache) {
        if ((i++ & 3) === 0) {
          delete cache[key];
          --cacheSize;
        }
      }
    }

    const color = parseRgba(s);
    if (color.length !== 4) {
      throwInvalidColor(s);
    }
    for (const c of color) {
      if (isNaN(c)) {
        throwInvalidColor(s);
      }
    }
    cache[s] = color;
    ++cacheSize;
    return color;
  }

  /**
   * Return the color as an array. This function maintains a cache of calculated
   * arrays which means the result should not be modified.
   * @param {Color|string} color Color.
   * @return {Color} Color.
   * @api
   */
  function asArray(color) {
    if (Array.isArray(color)) {
      return color;
    }
    return fromString(color);
  }

  /**
   * @param {Color} color Color.
   * @return {string} String.
   */
  function toString(color) {
    let r = color[0];
    if (r != (r | 0)) {
      r = (r + 0.5) | 0;
    }
    let g = color[1];
    if (g != (g | 0)) {
      g = (g + 0.5) | 0;
    }
    let b = color[2];
    if (b != (b | 0)) {
      b = (b + 0.5) | 0;
    }
    const a = color[3] === undefined ? 1 : Math.round(color[3] * 1000) / 1000;
    return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
  }

  /**
   * @module ol/Image
   */

  /**
   * @param {import('./DataTile.js').ImageLike} image Image element.
   * @param {function():any} loadHandler Load callback function.
   * @param {function():any} errorHandler Error callback function.
   * @return {function():void} Callback to stop listening.
   */
  function listenImage(image, loadHandler, errorHandler) {
    const img = /** @type {HTMLImageElement} */ (image);
    let listening = true;
    let decoding = false;
    let loaded = false;

    const listenerKeys = [
      listenOnce(img, EventType.LOAD, function () {
        loaded = true;
        if (!decoding) {
          loadHandler();
        }
      }),
    ];

    if (img.src && IMAGE_DECODE) {
      decoding = true;
      img
        .decode()
        .then(function () {
          if (listening) {
            loadHandler();
          }
        })
        .catch(function (error) {
          if (listening) {
            if (loaded) {
              loadHandler();
            } else {
              errorHandler();
            }
          }
        });
    } else {
      listenerKeys.push(listenOnce(img, EventType.ERROR, errorHandler));
    }

    return function unlisten() {
      listening = false;
      listenerKeys.forEach(unlistenByKey);
    };
  }

  /**
   * Loads an image.
   * @param {HTMLImageElement} image Image, not yet loaded.
   * @param {string} [src] `src` attribute of the image. Optional, not required if already present.
   * @return {Promise<HTMLImageElement>} Promise resolving to an `HTMLImageElement`.
   * @api
   */
  function load(image, src) {
    return new Promise((resolve, reject) => {
      function handleLoad() {
        unlisten();
        resolve(image);
      }
      function handleError() {
        unlisten();
        reject(new Error('Image load error'));
      }
      function unlisten() {
        image.removeEventListener('load', handleLoad);
        image.removeEventListener('error', handleError);
      }
      image.addEventListener('load', handleLoad);
      image.addEventListener('error', handleError);
    });
  }

  /**
   * @param {HTMLImageElement} image Image, not yet loaded.
   * @param {string} [src] `src` attribute of the image. Optional, not required if already present.
   * @return {Promise<HTMLImageElement>} Promise resolving to an `HTMLImageElement`.
   */
  function decodeFallback(image, src) {
    if (src) {
      image.src = src;
    }
    return image.src && IMAGE_DECODE
      ? new Promise((resolve, reject) =>
          image
            .decode()
            .then(() => resolve(image))
            .catch((e) =>
              image.complete && image.width ? resolve(image) : reject(e),
            ),
        )
      : load(image);
  }

  /**
   * @module ol/style/IconImageCache
   */

  /**
   * @classdesc
   * Singleton class. Available through {@link module:ol/style/IconImageCache.shared}.
   */
  class IconImageCache {
    constructor() {
      /**
       * @type {!Object<string, import("./IconImage.js").default>}
       * @private
       */
      this.cache_ = {};

      /**
       * @type {!Object<string, CanvasPattern>}
       * @private
       */
      this.patternCache_ = {};

      /**
       * @type {number}
       * @private
       */
      this.cacheSize_ = 0;

      /**
       * @type {number}
       * @private
       */
      this.maxCacheSize_ = 1024;
    }

    /**
     * FIXME empty description for jsdoc
     */
    clear() {
      this.cache_ = {};
      this.patternCache_ = {};
      this.cacheSize_ = 0;
    }

    /**
     * @return {boolean} Can expire cache.
     */
    canExpireCache() {
      return this.cacheSize_ > this.maxCacheSize_;
    }

    /**
     * FIXME empty description for jsdoc
     */
    expire() {
      if (this.canExpireCache()) {
        let i = 0;
        for (const key in this.cache_) {
          const iconImage = this.cache_[key];
          if ((i++ & 3) === 0 && !iconImage.hasListener()) {
            delete this.cache_[key];
            delete this.patternCache_[key];
            --this.cacheSize_;
          }
        }
      }
    }

    /**
     * @param {string} src Src.
     * @param {import("../color.js").Color|string|null} color Color.
     * @return {import("./IconImage.js").default} Icon image.
     */
    get(src, color) {
      const key = getCacheKey$1(src, color);

      const icon = key in this.cache_ ? this.cache_[key] : null;
      return icon;
    }

    /**
     * @param {string} src Src.
     * @param {import("../color.js").Color|string|null} color Color.
     * @return {CanvasPattern} Icon image.
     */
    getPattern(src, color) {
      const key = getCacheKey$1(src, color);
      return key in this.patternCache_ ? this.patternCache_[key] : null;
    }

    /**
     * @param {string} src Src.
     * @param {import("../color.js").Color|string|null} color Color.
     * @param {import("./IconImage.js").default|null} iconImage Icon image.
     * @param {boolean} [pattern] Also cache a `'repeat'` pattern with this `iconImage`.
     */
    set(src, color, iconImage, pattern) {
      const key = getCacheKey$1(src, color);
      const update = key in this.cache_;
      this.cache_[key] = iconImage;
      if (pattern) {
        if (iconImage.getImageState() === ImageState.IDLE) {
          iconImage.load();
        }
        if (iconImage.getImageState() === ImageState.LOADING) {
          iconImage.ready().then(() => {
            this.patternCache_[key] = getSharedCanvasContext2D().createPattern(
              iconImage.getImage(1),
              'repeat',
            );
          });
        } else {
          this.patternCache_[key] = getSharedCanvasContext2D().createPattern(
            iconImage.getImage(1),
            'repeat',
          );
        }
      }
      if (!update) {
        ++this.cacheSize_;
      }
    }

    /**
     * Set the cache size of the icon cache. Default is `1024`. Change this value when
     * your map uses more than 1024 different icon images and you are not caching icon
     * styles on the application level.
     * @param {number} maxCacheSize Cache max size.
     * @api
     */
    setSize(maxCacheSize) {
      this.maxCacheSize_ = maxCacheSize;
      this.expire();
    }
  }

  /**
   * @param {string} src Src.
   * @param {import("../color.js").Color|string|null} color Color.
   * @return {string} Cache key.
   */
  function getCacheKey$1(src, color) {
    const colorString = color ? asArray(color) : 'null';
    return src + ':' + colorString;
  }

  /**
   * The {@link module:ol/style/IconImageCache~IconImageCache} for
   * {@link module:ol/style/Icon~Icon} images.
   * @api
   */
  const shared = new IconImageCache();

  /**
   * @module ol/style/IconImage
   */


  /**
   * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
   */
  let taintedTestContext = null;

  class IconImage extends Target {
    /**
     * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap|null} image Image.
     * @param {string|undefined} src Src.
     * @param {import('../dom').ImageAttributes} imageAttributes Image attributes options.
     * @param {import("../ImageState.js").default|undefined} imageState Image state.
     * @param {import("../color.js").Color|string|null} color Color.
     */
    constructor(image, src, imageAttributes, imageState, color) {
      super();

      /**
       * @private
       * @type {HTMLImageElement|OffscreenCanvas|HTMLCanvasElement|ImageBitmap}
       */
      this.hitDetectionImage_ = null;

      /**
       * @private
       * @type {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap|null}
       */
      this.image_ = image;

      /**
       * @private
       * @type {string|null}
       */
      this.crossOrigin_ = imageAttributes?.crossOrigin;

      /**
       * @private
       * @type {ReferrerPolicy}
       */
      this.referrerPolicy_ = imageAttributes?.referrerPolicy;

      /**
       * @private
       * @type {Object<number, HTMLCanvasElement|OffscreenCanvas>}
       */
      this.canvas_ = {};

      /**
       * @private
       * @type {import("../color.js").Color|string|null}
       */
      this.color_ = color;

      /**
       * @private
       * @type {import("../ImageState.js").default}
       */
      this.imageState_ = imageState === undefined ? ImageState.IDLE : imageState;

      /**
       * @private
       * @type {import("../size.js").Size|null}
       */
      this.size_ =
        image && image.width && image.height ? [image.width, image.height] : null;

      /**
       * @private
       * @type {string|undefined}
       */
      this.src_ = src;

      /**
       * @private
       */
      this.tainted_;

      /**
       * @private
       * @type {Promise<void>|null}
       */
      this.ready_ = null;
    }

    /**
     * @private
     */
    initializeImage_() {
      this.image_ = new Image();
      if (this.crossOrigin_ !== null) {
        this.image_.crossOrigin = this.crossOrigin_;
      }
      if (this.referrerPolicy_ !== undefined) {
        this.image_.referrerPolicy = this.referrerPolicy_;
      }
    }

    /**
     * @private
     * @return {boolean} The image canvas is tainted.
     */
    isTainted_() {
      if (this.tainted_ === undefined && this.imageState_ === ImageState.LOADED) {
        if (!taintedTestContext) {
          taintedTestContext = createCanvasContext2D(1, 1, undefined, {
            willReadFrequently: true,
          });
        }
        taintedTestContext.drawImage(this.image_, 0, 0);
        try {
          taintedTestContext.getImageData(0, 0, 1, 1);
          this.tainted_ = false;
        } catch {
          taintedTestContext = null;
          this.tainted_ = true;
        }
      }
      return this.tainted_ === true;
    }

    /**
     * @private
     */
    dispatchChangeEvent_() {
      this.dispatchEvent(EventType.CHANGE);
    }

    /**
     * @private
     */
    handleImageError_() {
      this.imageState_ = ImageState.ERROR;
      this.dispatchChangeEvent_();
    }

    /**
     * @private
     */
    handleImageLoad_() {
      this.imageState_ = ImageState.LOADED;
      this.size_ = [this.image_.width, this.image_.height];
      this.dispatchChangeEvent_();
    }

    /**
     * @param {number} pixelRatio Pixel ratio.
     * @return {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} Image or Canvas element or image bitmap.
     */
    getImage(pixelRatio) {
      if (!this.image_) {
        this.initializeImage_();
      }
      this.replaceColor_(pixelRatio);
      return this.canvas_[pixelRatio] ? this.canvas_[pixelRatio] : this.image_;
    }

    /**
     * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image Image.
     */
    setImage(image) {
      this.image_ = image;
    }

    /**
     * @param {number} pixelRatio Pixel ratio.
     * @return {number} Image or Canvas element.
     */
    getPixelRatio(pixelRatio) {
      this.replaceColor_(pixelRatio);
      return this.canvas_[pixelRatio] ? pixelRatio : 1;
    }

    /**
     * @return {import("../ImageState.js").default} Image state.
     */
    getImageState() {
      return this.imageState_;
    }

    /**
     * @return {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} Image element.
     */
    getHitDetectionImage() {
      if (!this.image_) {
        this.initializeImage_();
      }
      if (!this.hitDetectionImage_) {
        if (this.isTainted_()) {
          const width = this.size_[0];
          const height = this.size_[1];
          const context = createCanvasContext2D(width, height);
          context.fillRect(0, 0, width, height);
          this.hitDetectionImage_ = context.canvas;
        } else {
          this.hitDetectionImage_ = this.image_;
        }
      }
      return this.hitDetectionImage_;
    }

    /**
     * Get the size of the icon (in pixels).
     * @return {import("../size.js").Size} Image size.
     */
    getSize() {
      return this.size_;
    }

    /**
     * @return {string|undefined} Image src.
     */
    getSrc() {
      return this.src_;
    }

    /**
     * Load not yet loaded URI.
     */
    load() {
      if (this.imageState_ !== ImageState.IDLE) {
        return;
      }
      if (!this.image_) {
        this.initializeImage_();
      }

      this.imageState_ = ImageState.LOADING;
      try {
        if (this.src_ !== undefined) {
          /** @type {HTMLImageElement} */ (this.image_).src = this.src_;
        }
      } catch {
        this.handleImageError_();
      }
      if (this.image_ instanceof HTMLImageElement) {
        decodeFallback(this.image_, this.src_)
          .then((image) => {
            this.image_ = image;
            this.handleImageLoad_();
          })
          .catch(this.handleImageError_.bind(this));
      }
    }

    /**
     * @param {number} pixelRatio Pixel ratio.
     * @private
     */
    replaceColor_(pixelRatio) {
      if (
        !this.color_ ||
        this.canvas_[pixelRatio] ||
        this.imageState_ !== ImageState.LOADED
      ) {
        return;
      }

      const image = this.image_;
      const ctx = createCanvasContext2D(
        Math.ceil(image.width * pixelRatio),
        Math.ceil(image.height * pixelRatio),
      );
      const canvas = ctx.canvas;

      ctx.scale(pixelRatio, pixelRatio);
      ctx.drawImage(image, 0, 0);

      ctx.globalCompositeOperation = 'multiply';
      ctx.fillStyle = asString(this.color_);
      ctx.fillRect(0, 0, canvas.width / pixelRatio, canvas.height / pixelRatio);

      ctx.globalCompositeOperation = 'destination-in';
      ctx.drawImage(image, 0, 0);

      this.canvas_[pixelRatio] = canvas;
    }

    /**
     * @return {Promise<void>} Promise that resolves when the image is loaded.
     */
    ready() {
      if (!this.ready_) {
        this.ready_ = new Promise((resolve) => {
          if (
            this.imageState_ === ImageState.LOADED ||
            this.imageState_ === ImageState.ERROR
          ) {
            resolve();
          } else {
            const onChange = () => {
              if (
                this.imageState_ === ImageState.LOADED ||
                this.imageState_ === ImageState.ERROR
              ) {
                this.removeEventListener(EventType.CHANGE, onChange);
                resolve();
              }
            };
            this.addEventListener(EventType.CHANGE, onChange);
          }
        });
      }
      return this.ready_;
    }
  }

  /**
   * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap|null} image Image.
   * @param {string|undefined} src Src.
   * @param {import('../dom.js').ImageAttributes} imageAttributes Image attributes options.
   * @param {import("../ImageState.js").default|undefined} imageState Image state.
   * @param {import("../color.js").Color|string|null} color Color.
   * @param {boolean} [pattern] Also cache a `repeat` pattern with the icon image.
   * @return {IconImage} Icon image.
   */
  function get(image, src, imageAttributes, imageState, color, pattern) {
    let iconImage =
      src === undefined ? undefined : shared.get(src, color);
    if (!iconImage) {
      iconImage = new IconImage(
        image,
        image && 'src' in image ? image.src || undefined : src,
        imageAttributes,
        imageState,
        color,
      );
      shared.set(src, color, iconImage, pattern);
    }
    if (pattern && iconImage && !shared.getPattern(src, color)) {
      shared.set(src, color, iconImage, pattern);
    }
    return iconImage;
  }

  /**
   * @module ol/colorlike
   */

  /**
   * @typedef {Object} PatternDescriptor
   * @property {string} src Pattern image URL
   * @property {import("./color.js").Color|string} [color] Color to tint the pattern with.
   * @property {import("./size.js").Size} [size] Size of the desired slice from the pattern image.
   * Use this together with `offset` when the pattern image is a sprite sheet.
   * @property {import("./size.js").Size} [offset] Offset of the desired slice from the pattern image.
   * Use this together with `size` when the pattern image is a sprite sheet.
   */

  /**
   * A type accepted by CanvasRenderingContext2D.fillStyle
   * or CanvasRenderingContext2D.strokeStyle.
   * Represents a color, [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern),
   * or [CanvasGradient](https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient). The origin for
   * patterns and gradients as fill style is an increment of 512 css pixels from map coordinate
   * `[0, 0]`. For seamless repeat patterns, width and height of the pattern image
   * must be a factor of two (2, 4, 8, ..., 512).
   *
   * @typedef {string|CanvasPattern|CanvasGradient} ColorLike
   * @api
   */

  /**
   * @param {import("./color.js").Color|ColorLike|PatternDescriptor|null} color Color.
   * @return {ColorLike|null} The color as an {@link ol/colorlike~ColorLike}.
   * @api
   */
  function asColorLike(color) {
    if (!color) {
      return null;
    }
    if (Array.isArray(color)) {
      return toString(color);
    }
    if (typeof color === 'object' && 'src' in color) {
      return asCanvasPattern(color);
    }
    return color;
  }

  /**
   * @param {PatternDescriptor} pattern Pattern descriptor.
   * @return {CanvasPattern|null} Canvas pattern or null if the pattern referenced in the
   * PatternDescriptor was not found in the icon image cache.
   */
  function asCanvasPattern(pattern) {
    if (!pattern.offset || !pattern.size) {
      return shared.getPattern(pattern.src, pattern.color);
    }

    const cacheKey = pattern.src + ':' + pattern.offset;

    const canvasPattern = shared.getPattern(cacheKey, pattern.color);
    if (canvasPattern) {
      return canvasPattern;
    }

    const iconImage = shared.get(pattern.src, null);
    if (iconImage.getImageState() !== ImageState.LOADED) {
      return null;
    }
    const patternCanvasContext = createCanvasContext2D(
      pattern.size[0],
      pattern.size[1],
    );
    patternCanvasContext.drawImage(
      iconImage.getImage(1),
      pattern.offset[0],
      pattern.offset[1],
      pattern.size[0],
      pattern.size[1],
      0,
      0,
      pattern.size[0],
      pattern.size[1],
    );
    get(
      patternCanvasContext.canvas,
      cacheKey,
      undefined,
      ImageState.LOADED,
      pattern.color,
      true,
    );
    return shared.getPattern(cacheKey, pattern.color);
  }

  /**
   * Offsets a line string to the left / right along its segments direction.
   * Offset is applied to each segment of the line in the direciton of the segment normal (positive offset goes "right" relative to the line direction).
   * For very sharp angles between segments, the function falls back to offsetting along the segment normal direction to avoid excessively long miters.
   *
   * Coordinates and the offset should be in the same units — either pixels or the same spatial reference system as the input line coordinates.
   *
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} stride Stride.
   * @param {number} offset Offset distance along the segment normal direction.
   *   Positive values offset to the right relative to the direction of the line.
   *   Negative values offset to the left.
   * @param {boolean} isClosedRing If coordinates build a closed circle (in this the first and the last coordinate offsets will consider previous / next ring coordinate)
   * @param {Array<number>} [dest] Destination coordinate array. If not provided a new one will be created
   * @param {number} [destinationStride] Stride of destination coordinates. If unspecified, assumed to be same as the source coordinates stride.
   * @return {Array<number>} Result flat coordinates of the offset line.
   */
  function offsetLineString(
    flatCoordinates,
    stride,
    offset,
    isClosedRing,
    dest,
    destinationStride,
  ) {
    dest = dest ?? [];
    destinationStride = destinationStride ?? stride;

    const firstPointX = flatCoordinates[0];
    const firstPointY = flatCoordinates[1];
    const secondToLastPointX = flatCoordinates[flatCoordinates.length - 4];
    const secondToLastPointY = flatCoordinates[flatCoordinates.length - 3];
    let x, y, prevX, prevY, nextX, nextY, offsetX, offsetY;

    let i = 0;
    for (let j = 0; j < flatCoordinates.length; j += stride) {
      // 1. Detect previous and next coordinates of a current vertex
      prevX = x;
      prevY = y;
      nextX = undefined;
      nextY = undefined;
      if (j + stride < flatCoordinates.length) {
        nextX = flatCoordinates[j + stride];
        nextY = flatCoordinates[j + stride + 1];
      }
      // First coordinate of a closed ring -> previous coordinate is the second to last one
      if (isClosedRing && j === 0) {
        prevX = secondToLastPointX;
        prevY = secondToLastPointY;
      }
      // Last coordinate of a closed ring -> next coordinate is the first vertex of a line string
      if (isClosedRing && j === flatCoordinates.length - 2) {
        // last coordinate
        nextX = firstPointX;
        nextY = firstPointY;
      }

      // 2. Current vertex to offset
      x = flatCoordinates[j];
      y = flatCoordinates[j + 1];

      // 3. Offset the vertex
      [offsetX, offsetY] = offsetLineVertex(
        x,
        y,
        prevX,
        prevY,
        nextX,
        nextY,
        offset,
      );
      dest[i++] = offsetX;
      dest[i++] = offsetY;

      // 4. Copy over other dimension values if any
      for (let k = 2; k < destinationStride; k++) {
        dest[i++] = flatCoordinates[j + k];
      }
    }

    if (dest.length != i) {
      dest.length = i;
    }
    return dest;
  }

  /**
   * Computes the offset of a single vertex of a line string.
   *
   * The function calculates a new vertex coordinate offset along the normal/miter direction of the line at this vertex.
   * Offset is applied along the segment normal (positive offset goes "right" relative to the line direction).
   * It handles first and last vertices (caps) as well as joins between two segments (mitering).
   * For very sharp angles, the function falls back to offsetting along the segment normal direction to avoid excessively long miters.
   *
   * Coordinates and the offset should be in the same units — either pixels or the same spatial reference system as the input line coordinates.
   *
   * @param {number} x Vertex x-coordinate.
   * @param {number} y Vertex y-coordinate.
   * @param {number|undefined} prevX Previous vertex x-coordinate.
   *   Pass undefined if computing the offset for the first vertex (no previous vertex).
   * @param {number|undefined} prevY Previous vertex y-coordinate.
   *   Pass undefined if computing the offset for the first vertex (no previous vertex).
   * @param {number|undefined} nextX Next vertex x-coordinate.
   *   Pass undefined if computing the offset for the last vertex (no next vertex).
   * @param {number|undefined} nextY Next vertex y-coordinate.
   *   Pass undefined if computing the offset for the last vertex (no next vertex).
   * @param {number} offset Offset distance along the segment normal direction.
   *   Positive values offset to the right relative to the direction from previous to next vertex.
   *   Negative values offset to the left.
   * @return {import("../../coordinate.js").Coordinate} Offset vertex coordinate as `[x, y]`.
   */
  function offsetLineVertex(x, y, prevX, prevY, nextX, nextY, offset) {
    // Compute segment direction
    let nx, ny;
    if (prevX !== undefined && prevY !== undefined) {
      nx = x - prevX;
      ny = y - prevY;
    } else if (nextX !== undefined && nextY !== undefined) {
      nx = nextX - x;
      ny = nextY - y;
    } else {
      // no next, no previous point given -> just assume some default (horizontal) direction
      nx = 1;
      ny = 0;
    }

    // Normalize -> tangent
    const len = Math.hypot(nx, ny);
    const tx = nx / len;
    const ty = ny / len;

    // Rotate tangent 90° -> normal
    nx = -ty;
    ny = tx;

    // First / last vertex -> offset the point in the direction of the normal vector
    if (prevX === undefined || prevY === undefined) {
      return [x + nx * offset, y + ny * offset];
    }
    if (nextX === undefined || nextY === undefined) {
      return [x + nx * offset, y + ny * offset];
    }

    // Compute join angle - angle between 2 segments of the vertex.
    const joinAngle = angleBetween([x, y], [prevX, prevY], [nextX, nextY]);

    // Avoid huge or infinite miter joins for very sharp angles, offset in the segment direction in this case.
    if (Math.cos(joinAngle) > 0.998) {
      return [x + tx * offset, y + ty * offset];
    }

    // Compute join offset direction.
    // We rotate the normal vector by half of the join angle.
    // This gives the direction of the miter at the vertex.
    const cos = Math.cos(joinAngle / 2);
    const sin = Math.sin(joinAngle / 2);

    // Rotate the normal vector (nx, ny) by half of the join angle.
    // bx/by = bisector direction before normalization
    const bx = sin * nx + cos * ny;
    const by = -cos * nx + sin * ny;

    // Scale the bisector so that moving along it preserves the correct offset distance.
    // Dividing by sin(half of angle) converts the bisector into the true miter vector.
    // (This expands the miter for sharp angles and shortens it for wide ones.)
    const dx = bx * (1 / sin);
    const dy = by * (1 / sin);

    // Offset final vertex along miter direction
    return [x + dx * offset, y + dy * offset];
  }

  /**
   * @module ol/render/VectorContext
   */

  /**
   * @classdesc
   * Context for drawing geometries.  A vector context is available on render
   * events and does not need to be constructed directly.
   * @api
   */
  class VectorContext {
    /**
     * Render a geometry with a custom renderer.
     *
     * @param {import("../geom/SimpleGeometry.js").default} geometry Geometry.
     * @param {import("../Feature.js").FeatureLike} feature Feature.
     * @param {Function} renderer Renderer.
     * @param {Function} hitDetectionRenderer Renderer.
     * @param {number} [index] Render order index.
     */
    drawCustom(geometry, feature, renderer, hitDetectionRenderer, index) {}

    /**
     * Render a geometry.
     *
     * @param {import("../geom/Geometry.js").default} geometry The geometry to render.
     */
    drawGeometry(geometry) {}

    /**
     * Set the rendering style.
     *
     * @param {import("../style/Style.js").default} style The rendering style.
     */
    setStyle(style) {}

    /**
     * @param {import("../geom/Circle.js").default} circleGeometry Circle geometry.
     * @param {import("../Feature.js").default} feature Feature.
     * @param {number} [index] Render order index.
     */
    drawCircle(circleGeometry, feature, index) {}

    /**
     * @param {import("../Feature.js").default} feature Feature.
     * @param {import("../style/Style.js").default} style Style.
     * @param {number} [index] Render order index.
     */
    drawFeature(feature, style, index) {}

    /**
     * @param {import("../geom/GeometryCollection.js").default} geometryCollectionGeometry Geometry collection.
     * @param {import("../Feature.js").default} feature Feature.
     * @param {number} [index] Render order index.
     */
    drawGeometryCollection(geometryCollectionGeometry, feature, index) {}

    /**
     * @param {import("../geom/LineString.js").default|import("./Feature.js").default} lineStringGeometry Line string geometry.
     * @param {import("../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     */
    drawLineString(lineStringGeometry, feature, index) {}

    /**
     * @param {import("../geom/MultiLineString.js").default|import("./Feature.js").default} multiLineStringGeometry MultiLineString geometry.
     * @param {import("../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     */
    drawMultiLineString(multiLineStringGeometry, feature, index) {}

    /**
     * @param {import("../geom/MultiPoint.js").default|import("./Feature.js").default} multiPointGeometry MultiPoint geometry.
     * @param {import("../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     */
    drawMultiPoint(multiPointGeometry, feature, index) {}

    /**
     * @param {import("../geom/MultiPolygon.js").default} multiPolygonGeometry MultiPolygon geometry.
     * @param {import("../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     */
    drawMultiPolygon(multiPolygonGeometry, feature, index) {}

    /**
     * @param {import("../geom/Point.js").default|import("./Feature.js").default} pointGeometry Point geometry.
     * @param {import("../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     */
    drawPoint(pointGeometry, feature, index) {}

    /**
     * @param {import("../geom/Polygon.js").default|import("./Feature.js").default} polygonGeometry Polygon geometry.
     * @param {import("../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     */
    drawPolygon(polygonGeometry, feature, index) {}

    /**
     * @param {import("../geom/SimpleGeometry.js").default|import("./Feature.js").default} geometry Geometry.
     * @param {import("../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     */
    drawText(geometry, feature, index) {}

    /**
     * @param {import("../style/Fill.js").default} fillStyle Fill style.
     * @param {import("../style/Stroke.js").default} strokeStyle Stroke style.
     */
    setFillStrokeStyle(fillStyle, strokeStyle) {}

    /**
     * @param {import("../style/Image.js").default} imageStyle Image style.
     * @param {import("../render/canvas.js").DeclutterImageWithText} [declutterImageWithText] Shared data for combined decluttering with a text style.
     */
    setImageStyle(imageStyle, declutterImageWithText) {}

    /**
     * @param {import("../style/Text.js").default} textStyle Text style.
     * @param {import("../render/canvas.js").DeclutterImageWithText} [declutterImageWithText] Shared data for combined decluttering with an image style.
     */
    setTextStyle(textStyle, declutterImageWithText) {}
  }

  /**
   * @module ol/css
   */

  /**
   * @typedef {Object} FontParameters
   * @property {string} style Style.
   * @property {string} variant Variant.
   * @property {string} weight Weight.
   * @property {string} size Size.
   * @property {string} lineHeight LineHeight.
   * @property {string} family Family.
   * @property {Array<string>} families Families.
   */

  /**
   * The CSS class for hidden feature.
   *
   * @const
   * @type {string}
   */
  const CLASS_HIDDEN = 'ol-hidden';

  /**
   * The CSS class that we'll give the DOM elements to have them selectable.
   *
   * @const
   * @type {string}
   */
  const CLASS_SELECTABLE = 'ol-selectable';

  /**
   * The CSS class that we'll give the DOM elements to have them unselectable.
   *
   * @const
   * @type {string}
   */
  const CLASS_UNSELECTABLE = 'ol-unselectable';

  /**
   * The CSS class for controls.
   *
   * @const
   * @type {string}
   */
  const CLASS_CONTROL = 'ol-control';

  /**
   * The CSS class that we'll give the DOM elements that are collapsed, i.e.
   * to those elements which usually can be expanded.
   *
   * @const
   * @type {string}
   */
  const CLASS_COLLAPSED = 'ol-collapsed';

  /**
   * From https://stackoverflow.com/questions/10135697/regex-to-parse-any-css-font
   * @type {RegExp}
   */
  const fontRegEx = new RegExp(
    [
      '^\\s*(?=(?:(?:[-a-z]+\\s*){0,2}(italic|oblique))?)',
      '(?=(?:(?:[-a-z]+\\s*){0,2}(small-caps))?)',
      '(?=(?:(?:[-a-z]+\\s*){0,2}(bold(?:er)?|lighter|[1-9]00 ))?)',
      '(?:(?:normal|\\1|\\2|\\3)\\s*){0,3}((?:xx?-)?',
      '(?:small|large)|medium|smaller|larger|[\\.\\d]+(?:\\%|in|[cem]m|ex|p[ctx]))',
      '(?:\\s*\\/\\s*(normal|[\\.\\d]+(?:\\%|in|[cem]m|ex|p[ctx])?))',
      '?\\s*([-,\\"\\\'\\sa-z0-9]+?)\\s*$',
    ].join(''),
    'i',
  );
  /** @type {Array<'style'|'variant'|'weight'|'size'|'lineHeight'|'family'>} */
  const fontRegExMatchIndex = [
    'style',
    'variant',
    'weight',
    'size',
    'lineHeight',
    'family',
  ];

  /** @type {Object<string|number, number>} */
  const fontWeights = {
    normal: 400,
    bold: 700,
  };

  /**
   * Get the list of font families from a font spec.  Note that this doesn't work
   * for font families that have commas in them.
   * @param {string} fontSpec The CSS font property.
   * @return {FontParameters|null} The font parameters (or null if the input spec is invalid).
   */
  const getFontParameters = function (fontSpec) {
    const match = fontSpec.match(fontRegEx);
    if (!match) {
      return null;
    }
    const style = /** @type {FontParameters} */ ({
      lineHeight: 'normal',
      size: '1.2em',
      style: 'normal',
      weight: '400',
      variant: 'normal',
    });
    for (let i = 0, ii = fontRegExMatchIndex.length; i < ii; ++i) {
      const value = match[i + 1];
      if (value !== undefined) {
        style[fontRegExMatchIndex[i]] =
          typeof value === 'string' ? value.trim() : value;
      }
    }
    if (isNaN(Number(style.weight)) && style.weight in fontWeights) {
      style.weight = fontWeights[style.weight];
    }
    style.families = style.family
      .split(/,\s?/)
      .map((f) => f.trim().replace(/^['"]|['"]$/g, ''));
    return style;
  };

  /**
   * @module ol/render/canvas
   */

  /**
   * @typedef {'Circle' | 'Image' | 'LineString' | 'Polygon' | 'Text' | 'Default'} BuilderType
   */

  /**
   * @typedef {Object} FillState
   * @property {import("../colorlike.js").ColorLike} fillStyle FillStyle.
   */

  /**
   * @typedef Label
   * @property {number} width Width.
   * @property {number} height Height.
   * @property {Array<string|number>} contextInstructions ContextInstructions.
   */

  /**
   * @typedef {Object} FillStrokeState
   * @property {import("../colorlike.js").ColorLike} [currentFillStyle] Current FillStyle.
   * @property {import("../colorlike.js").ColorLike} [currentStrokeStyle] Current StrokeStyle.
   * @property {CanvasLineCap} [currentLineCap] Current LineCap.
   * @property {Array<number>} currentLineDash Current LineDash.
   * @property {number} [currentLineDashOffset] Current LineDashOffset.
   * @property {CanvasLineJoin} [currentLineJoin] Current LineJoin.
   * @property {number} [currentLineWidth] Current LineWidth.
   * @property {number} [currentMiterLimit] Current MiterLimit.
   * @property {number} [currentStrokeOffset] Current StrokeOffset.
   * @property {number} [lastStroke] Last stroke.
   * @property {import("../colorlike.js").ColorLike} [fillStyle] FillStyle.
   * @property {import("../colorlike.js").ColorLike} [strokeStyle] StrokeStyle.
   * @property {CanvasLineCap} [lineCap] LineCap.
   * @property {Array<number>} lineDash LineDash.
   * @property {number} [lineDashOffset] LineDashOffset.
   * @property {CanvasLineJoin} [lineJoin] LineJoin.
   * @property {number} [lineWidth] LineWidth.
   * @property {number} [miterLimit] MiterLimit.
   * @property {number} [strokeOffset] StrokeOffset.
   * @property {number} [fillPatternScale] Fill pattern scale.
   */

  /**
   * @typedef {Object} StrokeState
   * @property {CanvasLineCap} lineCap LineCap.
   * @property {Array<number>} lineDash LineDash.
   * @property {number} lineDashOffset LineDashOffset.
   * @property {CanvasLineJoin} lineJoin LineJoin.
   * @property {number} lineWidth LineWidth.
   * @property {number} miterLimit MiterLimit.
   * @property {number} [strokeOffset] StrokeOffset.
   * @property {import("../colorlike.js").ColorLike} strokeStyle StrokeStyle.
   */

  /**
   * @typedef {Object} TextState
   * @property {string} font Font.
   * @property {CanvasTextAlign} [textAlign] TextAlign.
   * @property {number} [repeat] Repeat.
   * @property {import("../style/Text.js").TextJustify} [justify] Justify.
   * @property {CanvasTextBaseline} textBaseline TextBaseline.
   * @property {import("../style/Text.js").TextPlacement} [placement] Placement.
   * @property {number} [maxAngle] MaxAngle.
   * @property {boolean} [overflow] Overflow.
   * @property {import("../style/Fill.js").default} [backgroundFill] BackgroundFill.
   * @property {import("../style/Stroke.js").default} [backgroundStroke] BackgroundStroke.
   * @property {import("../size.js").Size} [scale] Scale.
   * @property {Array<number>} [padding] Padding.
   */

  /**
   * @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, TextState>} [textStates] The text states (decluttering).
   * @property {!Object<string, FillState>} [fillStates] The fill states (decluttering).
   * @property {!Object<string, StrokeState>} [strokeStates] The stroke states (decluttering).
   */

  /**
   * @typedef {Object<number, import("./canvas/Executor.js").ReplayImageOrLabelArgs>} DeclutterImageWithText
   */

  /**
   * @const
   * @type {string}
   */
  const defaultFont = '10px sans-serif';

  /**
   * @const
   * @type {string}
   */
  const defaultFillStyle = '#000';

  /**
   * @const
   * @type {CanvasLineCap}
   */
  const defaultLineCap = 'round';

  /**
   * @const
   * @type {Array<number>}
   */
  const defaultLineDash = [];

  /**
   * @const
   * @type {number}
   */
  const defaultLineDashOffset = 0;

  /**
   * @const
   * @type {CanvasLineJoin}
   */
  const defaultLineJoin = 'round';

  /**
   * @const
   * @type {number}
   */
  const defaultMiterLimit = 10;

  /**
   * @const
   * @type {number}
   */
  const defaultStrokeOffset = 0;

  /**
   * @const
   * @type {import("../colorlike.js").ColorLike}
   */
  const defaultStrokeStyle = '#000';

  /**
   * @const
   * @type {CanvasTextAlign}
   */
  const defaultTextAlign = 'center';

  /**
   * @const
   * @type {CanvasTextBaseline}
   */
  const defaultTextBaseline = 'middle';

  /**
   * @const
   * @type {Array<number>}
   */
  const defaultPadding = [0, 0, 0, 0];

  /**
   * @const
   * @type {number}
   */
  const defaultLineWidth = 1;

  /**
   * @type {BaseObject}
   */
  const checkedFonts = new BaseObject();

  /**
   * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
   */
  let measureContext = null;

  /**
   * @type {string}
   */
  let measureFont;

  /**
   * @type {!Object<string, number>}
   */
  const textHeights = {};

  const genericFontFamilies = new Set([
    'serif',
    'sans-serif',
    'monospace',
    'cursive',
    'fantasy',
    'system-ui',
    'ui-serif',
    'ui-sans-serif',
    'ui-monospace',
    'ui-rounded',
    'emoji',
    'math',
    'fangsong',
  ]);

  /**
   * @param {string} style Css font-style
   * @param {string} weight Css font-weight
   * @param {string} family Css font-family
   * @return {string} Font key.
   */
  function getFontKey(style, weight, family) {
    return `${style} ${weight} 16px "${family}"`;
  }

  /**
   * Clears the label cache when a font becomes available.
   * @param {string} fontSpec CSS font spec.
   */
  const registerFont = (function () {
    const retries = 100;
    let timeout, fontFaceSet;

    /**
     * @param {string} fontSpec Css font spec
     * @return {Promise<boolean>} Font with style and weight is available
     */
    async function isAvailable(fontSpec) {
      await fontFaceSet.ready;
      const fontFaces = await fontFaceSet.load(fontSpec);
      if (fontFaces.length === 0) {
        return false;
      }
      const font = getFontParameters(fontSpec);
      const checkFamily = font.families[0].toLowerCase();
      const checkWeight = font.weight;
      return fontFaces.some(
        /**
         * @param {import('../css.js').FontParameters} f Font.
         * @return {boolean} Font matches.
         */
        (f) => {
          const family = f.family.replace(/^['"]|['"]$/g, '').toLowerCase();
          const weight = fontWeights[f.weight] || f.weight;
          return (
            family === checkFamily &&
            f.style === font.style &&
            weight == checkWeight
          );
        },
      );
    }

    async function check() {
      await fontFaceSet.ready;
      let done = true;
      const checkedFontsProperties = checkedFonts.getProperties();
      const fonts = Object.keys(checkedFontsProperties).filter(
        (key) => checkedFontsProperties[key] < retries,
      );
      for (let i = fonts.length - 1; i >= 0; --i) {
        const font = fonts[i];
        let currentRetries = checkedFontsProperties[font];
        if (currentRetries < retries) {
          if (await isAvailable(font)) {
            clear(textHeights);
            checkedFonts.set(font, retries);
          } else {
            currentRetries += 10;
            checkedFonts.set(font, currentRetries, true);
            if (currentRetries < retries) {
              done = false;
            }
          }
        }
      }
      timeout = undefined;
      if (!done) {
        timeout = setTimeout(check, 100);
      }
    }

    return async function (fontSpec) {
      if (!fontFaceSet) {
        fontFaceSet = WORKER_OFFSCREEN_CANVAS ? self.fonts : document.fonts;
      }
      const font = getFontParameters(fontSpec);
      if (!font) {
        return;
      }
      const families = font.families;
      let needCheck = false;
      for (const family of families) {
        if (genericFontFamilies.has(family)) {
          continue;
        }
        const key = getFontKey(font.style, font.weight, family);
        if (checkedFonts.get(key) !== undefined) {
          continue;
        }
        checkedFonts.set(key, 0, true);
        needCheck = true;
      }
      if (needCheck) {
        clearTimeout(timeout);
        timeout = setTimeout(check, 100);
      }
    };
  })();

  /**
   * @param {string} font Font to use for measuring.
   * @return {import("../size.js").Size} Measurement.
   */
  const measureTextHeight = (function () {
    /**
     * @type {HTMLDivElement}
     */
    let measureElement;
    return function (fontSpec) {
      let height = textHeights[fontSpec];
      if (height == undefined) {
        if (WORKER_OFFSCREEN_CANVAS) {
          const font = getFontParameters(fontSpec);
          const metrics = measureText(fontSpec, 'Žg');
          const lineHeight = isNaN(Number(font.lineHeight))
            ? 1.2
            : Number(font.lineHeight);
          height =
            lineHeight *
            (metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent);
        } else {
          if (!measureElement) {
            measureElement = document.createElement('div');
            measureElement.innerHTML = 'M';
            measureElement.style.minHeight = '0';
            measureElement.style.maxHeight = 'none';
            measureElement.style.height = 'auto';
            measureElement.style.padding = '0';
            measureElement.style.border = 'none';
            measureElement.style.position = 'absolute';
            measureElement.style.display = 'block';
            measureElement.style.left = '-99999px';
          }
          measureElement.style.font = fontSpec;
          document.body.appendChild(measureElement);
          height = measureElement.offsetHeight;
          document.body.removeChild(measureElement);
        }
        textHeights[fontSpec] = height;
      }
      return height;
    };
  })();

  /**
   * @param {string} font Font.
   * @param {string} text Text.
   * @return {TextMetrics} Text metrics.
   */
  function measureText(font, text) {
    if (!measureContext) {
      measureContext = createCanvasContext2D(1, 1);
    }
    if (font != measureFont) {
      measureContext.font = font;
      measureFont = measureContext.font;
    }
    return measureContext.measureText(text);
  }

  /**
   * @param {string} font Font.
   * @param {string} text Text.
   * @return {number} Width.
   */
  function measureTextWidth(font, text) {
    return measureText(font, text).width;
  }

  /**
   * Measure text width using a cache.
   * @param {string} font The font.
   * @param {string} text The text to measure.
   * @param {Object<string, number>} cache A lookup of cached widths by text.
   * @return {number} The text width.
   */
  function measureAndCacheTextWidth(font, text, cache) {
    if (text in cache) {
      return cache[text];
    }
    const width = text
      .split('\n')
      .reduce((prev, curr) => Math.max(prev, measureTextWidth(font, curr)), 0);
    cache[text] = width;
    return width;
  }

  /**
   * @param {TextState} baseStyle Base style.
   * @param {Array<string>} chunks Text chunks to measure.
   * @return {{width: number, height: number, widths: Array<number>, heights: Array<number>, lineWidths: Array<number>}}} Text metrics.
   */
  function getTextDimensions(baseStyle, chunks) {
    const widths = [];
    const heights = [];
    const lineWidths = [];
    let width = 0;
    let lineWidth = 0;
    let height = 0;
    let lineHeight = 0;
    for (let i = 0, ii = chunks.length; i <= ii; i += 2) {
      const text = chunks[i];
      if (text === '\n' || i === ii) {
        width = Math.max(width, lineWidth);
        lineWidths.push(lineWidth);
        lineWidth = 0;
        height += lineHeight;
        lineHeight = 0;
        continue;
      }
      const font = chunks[i + 1] || baseStyle.font;
      const currentWidth = measureTextWidth(font, text);
      widths.push(currentWidth);
      lineWidth += currentWidth;
      const currentHeight = measureTextHeight(font);
      heights.push(currentHeight);
      lineHeight = Math.max(lineHeight, currentHeight);
    }
    return {width, height, widths, heights, lineWidths};
  }

  /**
   * @param {CanvasRenderingContext2D|import("../render/canvas/ZIndexContext.js").ZIndexContextProxy} context Context.
   * @param {import("../transform.js").Transform|null} transform Transform.
   * @param {number} opacity Opacity.
   * @param {Label|HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} labelOrImage Label.
   * @param {number} originX Origin X.
   * @param {number} originY Origin Y.
   * @param {number} w Width.
   * @param {number} h Height.
   * @param {number} x X.
   * @param {number} y Y.
   * @param {import("../size.js").Size} scale Scale.
   */
  function drawImageOrLabel(
    context,
    transform,
    opacity,
    labelOrImage,
    originX,
    originY,
    w,
    h,
    x,
    y,
    scale,
  ) {
    context.save();

    if (opacity !== 1) {
      if (context.globalAlpha === undefined) {
        context.globalAlpha = (context) => (context.globalAlpha *= opacity);
      } else {
        context.globalAlpha *= opacity;
      }
    }
    if (transform) {
      context.transform.apply(context, transform);
    }

    if (/** @type {*} */ (labelOrImage).contextInstructions) {
      // label
      context.translate(x, y);
      context.scale(scale[0], scale[1]);
      executeLabelInstructions(/** @type {Label} */ (labelOrImage), context);
    } else if (scale[0] < 0 || scale[1] < 0) {
      // flipped image
      context.translate(x, y);
      context.scale(scale[0], scale[1]);
      context.drawImage(
        /** @type {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} */ (
          labelOrImage
        ),
        originX,
        originY,
        w,
        h,
        0,
        0,
        w,
        h,
      );
    } else {
      // if image not flipped translate and scale can be avoided
      context.drawImage(
        /** @type {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} */ (
          labelOrImage
        ),
        originX,
        originY,
        w,
        h,
        x,
        y,
        w * scale[0],
        h * scale[1],
      );
    }

    context.restore();
  }

  /**
   * @param {Label} label Label.
   * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
   */
  function executeLabelInstructions(label, context) {
    const contextInstructions = label.contextInstructions;
    for (let i = 0, ii = contextInstructions.length; i < ii; i += 2) {
      if (Array.isArray(contextInstructions[i + 1])) {
        context[contextInstructions[i]].apply(
          context,
          contextInstructions[i + 1],
        );
      } else {
        context[contextInstructions[i]] = contextInstructions[i + 1];
      }
    }
  }

  /**
   * @module ol/render/canvas/Immediate
   */
  // FIXME test, especially polygons with holes and multipolygons
  // FIXME need to handle large thick features (where pixel size matters)
  // FIXME add offset and end to ol/geom/flat/transform~transform2D?


  /**
   * @classdesc
   * A concrete subclass of {@link module:ol/render/VectorContext~VectorContext} that implements
   * direct rendering of features and geometries to an HTML5 Canvas context.
   * Instances of this class are created internally by the library and
   * provided to application code as vectorContext member of the
   * {@link module:ol/render/Event~RenderEvent} object associated with postcompose, precompose and
   * render events emitted by layers and maps.
   */
  class CanvasImmediateRenderer extends VectorContext {
    /**
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
     * @param {number} pixelRatio Pixel ratio.
     * @param {import("../../extent.js").Extent} extent Extent.
     * @param {import("../../transform.js").Transform} transform Transform.
     * @param {number} viewRotation View rotation.
     * @param {number} [squaredTolerance] Optional squared tolerance for simplification.
     * @param {import("../../proj.js").TransformFunction} [userTransform] Transform from user to view projection.
     */
    constructor(
      context,
      pixelRatio,
      extent,
      transform,
      viewRotation,
      squaredTolerance,
      userTransform,
    ) {
      super();

      /**
       * @private
       * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
       */
      this.context_ = context;

      /**
       * @private
       * @type {number}
       */
      this.pixelRatio_ = pixelRatio;

      /**
       * @private
       * @type {import("../../extent.js").Extent}
       */
      this.extent_ = extent;

      /**
       * @private
       * @type {import("../../transform.js").Transform}
       */
      this.transform_ = transform;

      /**
       * @private
       * @type {number}
       */
      this.transformRotation_ = transform
        ? toFixed(Math.atan2(transform[1], transform[0]), 10)
        : 0;

      /**
       * @private
       * @type {number}
       */
      this.viewRotation_ = viewRotation;

      /**
       * @private
       * @type {number}
       */
      this.squaredTolerance_ = squaredTolerance;

      /**
       * @private
       * @type {import("../../proj.js").TransformFunction}
       */
      this.userTransform_ = userTransform;

      /**
       * @private
       * @type {?import("../canvas.js").FillState}
       */
      this.contextFillState_ = null;

      /**
       * @private
       * @type {?import("../canvas.js").StrokeState}
       */
      this.contextStrokeState_ = null;

      /**
       * @private
       * @type {?import("../canvas.js").TextState}
       */
      this.contextTextState_ = null;

      /**
       * @private
       * @type {?import("../canvas.js").FillState}
       */
      this.fillState_ = null;

      /**
       * @private
       * @type {?import("../canvas.js").StrokeState}
       */
      this.strokeState_ = null;

      /**
       * @private
       * @type {import('../../DataTile.js').ImageLike}
       */
      this.image_ = null;

      /**
       * @private
       * @type {number}
       */
      this.imageAnchorX_ = 0;

      /**
       * @private
       * @type {number}
       */
      this.imageAnchorY_ = 0;

      /**
       * @private
       * @type {number}
       */
      this.imageHeight_ = 0;

      /**
       * @private
       * @type {number}
       */
      this.imageOpacity_ = 0;

      /**
       * @private
       * @type {number}
       */
      this.imageOriginX_ = 0;

      /**
       * @private
       * @type {number}
       */
      this.imageOriginY_ = 0;

      /**
       * @private
       * @type {boolean}
       */
      this.imageRotateWithView_ = false;

      /**
       * @private
       * @type {number}
       */
      this.imageRotation_ = 0;

      /**
       * @private
       * @type {import("../../size.js").Size}
       */
      this.imageScale_ = [0, 0];

      /**
       * @private
       * @type {number}
       */
      this.imageWidth_ = 0;

      /**
       * @private
       * @type {string}
       */
      this.text_ = '';

      /**
       * @private
       * @type {number}
       */
      this.textOffsetX_ = 0;

      /**
       * @private
       * @type {number}
       */
      this.textOffsetY_ = 0;

      /**
       * @private
       * @type {boolean}
       */
      this.textRotateWithView_ = false;

      /**
       * @private
       * @type {number}
       */
      this.textRotation_ = 0;

      /**
       * @private
       * @type {import("../../size.js").Size}
       */
      this.textScale_ = [0, 0];

      /**
       * @private
       * @type {?import("../canvas.js").FillState}
       */
      this.textFillState_ = null;

      /**
       * @private
       * @type {?import("../canvas.js").StrokeState}
       */
      this.textStrokeState_ = null;

      /**
       * @private
       * @type {?import("../canvas.js").TextState}
       */
      this.textState_ = null;

      /**
       * @private
       * @type {Array<number>}
       */
      this.pixelCoordinates_ = [];

      /**
       * @private
       * @type {import("../../transform.js").Transform}
       */
      this.tmpLocalTransform_ = create();
    }

    /**
     * @param {Array<number>} flatCoordinates Flat coordinates.
     * @param {number} offset Offset.
     * @param {number} end End.
     * @param {number} stride Stride.
     * @private
     */
    drawImages_(flatCoordinates, offset, end, stride) {
      if (!this.image_) {
        return;
      }
      const pixelCoordinates = transform2D(
        flatCoordinates,
        offset,
        end,
        stride,
        this.transform_,
        this.pixelCoordinates_,
      );
      const context = this.context_;
      const localTransform = this.tmpLocalTransform_;
      const alpha = context.globalAlpha;
      if (this.imageOpacity_ != 1) {
        context.globalAlpha = alpha * this.imageOpacity_;
      }
      let rotation = this.imageRotation_;
      if (this.transformRotation_ === 0) {
        rotation -= this.viewRotation_;
      }
      if (this.imageRotateWithView_) {
        rotation += this.viewRotation_;
      }
      for (let i = 0, ii = pixelCoordinates.length; i < ii; i += 2) {
        const x = pixelCoordinates[i] - this.imageAnchorX_;
        const y = pixelCoordinates[i + 1] - this.imageAnchorY_;
        if (
          rotation !== 0 ||
          this.imageScale_[0] != 1 ||
          this.imageScale_[1] != 1
        ) {
          const centerX = x + this.imageAnchorX_;
          const centerY = y + this.imageAnchorY_;
          compose(
            localTransform,
            centerX,
            centerY,
            1,
            1,
            rotation,
            -centerX,
            -centerY,
          );
          context.save();
          context.transform.apply(context, localTransform);
          context.translate(centerX, centerY);
          context.scale(this.imageScale_[0], this.imageScale_[1]);
          context.drawImage(
            this.image_,
            this.imageOriginX_,
            this.imageOriginY_,
            this.imageWidth_,
            this.imageHeight_,
            -this.imageAnchorX_,
            -this.imageAnchorY_,
            this.imageWidth_,
            this.imageHeight_,
          );
          context.restore();
        } else {
          context.drawImage(
            this.image_,
            this.imageOriginX_,
            this.imageOriginY_,
            this.imageWidth_,
            this.imageHeight_,
            x,
            y,
            this.imageWidth_,
            this.imageHeight_,
          );
        }
      }
      if (this.imageOpacity_ != 1) {
        context.globalAlpha = alpha;
      }
    }

    /**
     * @param {Array<number>} flatCoordinates Flat coordinates.
     * @param {number} offset Offset.
     * @param {number} end End.
     * @param {number} stride Stride.
     * @private
     */
    drawText_(flatCoordinates, offset, end, stride) {
      if (!this.textState_ || this.text_ === '') {
        return;
      }
      if (this.textFillState_) {
        this.setContextFillState_(this.textFillState_);
      }
      if (this.textStrokeState_) {
        this.setContextStrokeState_(this.textStrokeState_);
      }
      this.setContextTextState_(this.textState_);
      const pixelCoordinates = transform2D(
        flatCoordinates,
        offset,
        end,
        stride,
        this.transform_,
        this.pixelCoordinates_,
      );
      const context = this.context_;
      let rotation = this.textRotation_;
      if (this.transformRotation_ === 0) {
        rotation -= this.viewRotation_;
      }
      if (this.textRotateWithView_) {
        rotation += this.viewRotation_;
      }
      for (; offset < end; offset += stride) {
        const x = pixelCoordinates[offset] + this.textOffsetX_;
        const y = pixelCoordinates[offset + 1] + this.textOffsetY_;
        if (
          rotation !== 0 ||
          this.textScale_[0] != 1 ||
          this.textScale_[1] != 1
        ) {
          context.save();
          context.translate(x - this.textOffsetX_, y - this.textOffsetY_);
          context.rotate(rotation);
          context.translate(this.textOffsetX_, this.textOffsetY_);
          context.scale(this.textScale_[0], this.textScale_[1]);
          if (this.textStrokeState_) {
            context.strokeText(this.text_, 0, 0);
          }
          if (this.textFillState_) {
            context.fillText(this.text_, 0, 0);
          }
          context.restore();
        } else {
          if (this.textStrokeState_) {
            context.strokeText(this.text_, x, y);
          }
          if (this.textFillState_) {
            context.fillText(this.text_, x, y);
          }
        }
      }
    }

    /**
     * @param {Array<number>} flatCoordinates Flat coordinates.
     * @param {number} offset Offset.
     * @param {number} end End.
     * @param {number} stride Stride.
     * @param {boolean} close Close.
     * @param {number} [strokeOffset] Stroke Offset.
     * @private
     * @return {number} end End.
     */
    moveToLineTo_(flatCoordinates, offset, end, stride, close, strokeOffset) {
      const context = this.context_;
      let pixelCoordinates = transform2D(
        flatCoordinates,
        offset,
        end,
        stride,
        this.transform_,
        this.pixelCoordinates_,
      );
      if (Math.abs(strokeOffset) > 0) {
        pixelCoordinates = offsetLineString(
          pixelCoordinates,
          stride,
          strokeOffset,
          close,
          pixelCoordinates,
        );
      }
      context.moveTo(pixelCoordinates[0], pixelCoordinates[1]);
      let length = pixelCoordinates.length;
      if (close) {
        length -= 2;
      }
      for (let i = 2; i < length; i += 2) {
        context.lineTo(pixelCoordinates[i], pixelCoordinates[i + 1]);
      }
      if (close) {
        context.closePath();
      }
      return end;
    }

    /**
     * @param {Array<number>} flatCoordinates Flat coordinates.
     * @param {number} offset Offset.
     * @param {Array<number>} ends Ends.
     * @param {number} stride Stride.
     * @param {number} [strokeOffset] Stroke Offset.
     * @private
     * @return {number} End.
     */
    drawRings_(flatCoordinates, offset, ends, stride, strokeOffset) {
      for (let i = 0, ii = ends.length; i < ii; ++i) {
        offset = this.moveToLineTo_(
          flatCoordinates,
          offset,
          ends[i],
          stride,
          true,
          strokeOffset,
        );
      }
      return offset;
    }

    /**
     * Render a circle geometry into the canvas.  Rendering is immediate and uses
     * the current fill and stroke styles.
     *
     * @param {import("../../geom/Circle.js").default} geometry Circle geometry.
     * @api
     * @override
     */
    drawCircle(geometry) {
      if (this.squaredTolerance_) {
        geometry = /** @type {import("../../geom/Circle.js").default} */ (
          geometry.simplifyTransformed(
            this.squaredTolerance_,
            this.userTransform_,
          )
        );
      }
      if (!intersects$1(this.extent_, geometry.getExtent())) {
        return;
      }
      if (this.fillState_ || this.strokeState_) {
        if (this.fillState_) {
          this.setContextFillState_(this.fillState_);
        }
        if (this.strokeState_) {
          this.setContextStrokeState_(this.strokeState_);
        }
        const pixelCoordinates = transformGeom2D(
          geometry,
          this.transform_,
          this.pixelCoordinates_,
        );
        const dx = pixelCoordinates[2] - pixelCoordinates[0];
        const dy = pixelCoordinates[3] - pixelCoordinates[1];
        const radius = Math.sqrt(dx * dx + dy * dy);
        const context = this.context_;
        context.beginPath();
        context.arc(
          pixelCoordinates[0],
          pixelCoordinates[1],
          radius,
          0,
          2 * Math.PI,
        );
        if (this.fillState_) {
          context.fill();
        }
        if (this.strokeState_) {
          context.stroke();
        }
      }
      if (this.text_ !== '') {
        this.drawText_(geometry.getCenter(), 0, 2, 2);
      }
    }

    /**
     * Set the rendering style.  Note that since this is an immediate rendering API,
     * any `zIndex` on the provided style will be ignored.
     *
     * @param {import("../../style/Style.js").default} style The rendering style.
     * @api
     * @override
     */
    setStyle(style) {
      this.setFillStrokeStyle(style.getFill(), style.getStroke());
      this.setImageStyle(style.getImage());
      this.setTextStyle(style.getText());
    }

    /**
     * @param {import("../../transform.js").Transform} transform Transform.
     */
    setTransform(transform) {
      this.transform_ = transform;
    }

    /**
     * Render a geometry into the canvas.  Call
     * {@link module:ol/render/canvas/Immediate~CanvasImmediateRenderer#setStyle renderer.setStyle()} first to set the rendering style.
     *
     * @param {import("../../geom/Geometry.js").default|import("../Feature.js").default} geometry The geometry to render.
     * @api
     * @override
     */
    drawGeometry(geometry) {
      const type = geometry.getType();
      switch (type) {
        case 'Point':
          this.drawPoint(
            /** @type {import("../../geom/Point.js").default} */ (geometry),
          );
          break;
        case 'LineString':
          this.drawLineString(
            /** @type {import("../../geom/LineString.js").default} */ (geometry),
          );
          break;
        case 'Polygon':
          this.drawPolygon(
            /** @type {import("../../geom/Polygon.js").default} */ (geometry),
          );
          break;
        case 'MultiPoint':
          this.drawMultiPoint(
            /** @type {import("../../geom/MultiPoint.js").default} */ (geometry),
          );
          break;
        case 'MultiLineString':
          this.drawMultiLineString(
            /** @type {import("../../geom/MultiLineString.js").default} */ (
              geometry
            ),
          );
          break;
        case 'MultiPolygon':
          this.drawMultiPolygon(
            /** @type {import("../../geom/MultiPolygon.js").default} */ (
              geometry
            ),
          );
          break;
        case 'GeometryCollection':
          this.drawGeometryCollection(
            /** @type {import("../../geom/GeometryCollection.js").default} */ (
              geometry
            ),
          );
          break;
        case 'Circle':
          this.drawCircle(
            /** @type {import("../../geom/Circle.js").default} */ (geometry),
          );
          break;
      }
    }

    /**
     * Render a feature into the canvas.  Note that any `zIndex` on the provided
     * style will be ignored - features are rendered immediately in the order that
     * this method is called.  If you need `zIndex` support, you should be using an
     * {@link module:ol/layer/Vector~VectorLayer} instead.
     *
     * @param {import("../../Feature.js").default} feature Feature.
     * @param {import("../../style/Style.js").default} style Style.
     * @api
     * @override
     */
    drawFeature(feature, style) {
      const geometry = style.getGeometryFunction()(feature);
      if (!geometry) {
        return;
      }
      this.setStyle(style);
      this.drawGeometry(geometry);
    }

    /**
     * Render a GeometryCollection to the canvas.  Rendering is immediate and
     * uses the current styles appropriate for each geometry in the collection.
     *
     * @param {import("../../geom/GeometryCollection.js").default} geometry Geometry collection.
     * @override
     */
    drawGeometryCollection(geometry) {
      const geometries = geometry.getGeometriesArray();
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        this.drawGeometry(geometries[i]);
      }
    }

    /**
     * Render a Point geometry into the canvas.  Rendering is immediate and uses
     * the current style.
     *
     * @param {import("../../geom/Point.js").default|import("../Feature.js").default} geometry Point geometry.
     * @override
     */
    drawPoint(geometry) {
      if (this.squaredTolerance_) {
        geometry = /** @type {import("../../geom/Point.js").default} */ (
          geometry.simplifyTransformed(
            this.squaredTolerance_,
            this.userTransform_,
          )
        );
      }
      const flatCoordinates = geometry.getFlatCoordinates();
      const stride = geometry.getStride();
      if (this.image_) {
        this.drawImages_(flatCoordinates, 0, flatCoordinates.length, stride);
      }
      if (this.text_ !== '') {
        this.drawText_(flatCoordinates, 0, flatCoordinates.length, stride);
      }
    }

    /**
     * Render a MultiPoint geometry  into the canvas.  Rendering is immediate and
     * uses the current style.
     *
     * @param {import("../../geom/MultiPoint.js").default|import("../Feature.js").default} geometry MultiPoint geometry.
     * @override
     */
    drawMultiPoint(geometry) {
      if (this.squaredTolerance_) {
        geometry = /** @type {import("../../geom/MultiPoint.js").default} */ (
          geometry.simplifyTransformed(
            this.squaredTolerance_,
            this.userTransform_,
          )
        );
      }
      const flatCoordinates = geometry.getFlatCoordinates();
      const stride = geometry.getStride();
      if (this.image_) {
        this.drawImages_(flatCoordinates, 0, flatCoordinates.length, stride);
      }
      if (this.text_ !== '') {
        this.drawText_(flatCoordinates, 0, flatCoordinates.length, stride);
      }
    }

    /**
     * Render a LineString into the canvas.  Rendering is immediate and uses
     * the current style.
     *
     * @param {import("../../geom/LineString.js").default|import("../Feature.js").default} geometry LineString geometry.
     * @override
     */
    drawLineString(geometry) {
      if (this.squaredTolerance_) {
        geometry = /** @type {import("../../geom/LineString.js").default} */ (
          geometry.simplifyTransformed(
            this.squaredTolerance_,
            this.userTransform_,
          )
        );
      }
      if (!intersects$1(this.extent_, geometry.getExtent())) {
        return;
      }
      if (this.strokeState_) {
        this.setContextStrokeState_(this.strokeState_);
        const context = this.context_;
        const flatCoordinates = geometry.getFlatCoordinates();
        context.beginPath();
        this.moveToLineTo_(
          flatCoordinates,
          0,
          flatCoordinates.length,
          geometry.getStride(),
          false,
          this.strokeState_.strokeOffset,
        );
        context.stroke();
      }
      if (this.text_ !== '') {
        const flatMidpoint = geometry.getFlatMidpoint();
        this.drawText_(flatMidpoint, 0, 2, 2);
      }
    }

    /**
     * Render a MultiLineString geometry into the canvas.  Rendering is immediate
     * and uses the current style.
     *
     * @param {import("../../geom/MultiLineString.js").default|import("../Feature.js").default} geometry MultiLineString geometry.
     * @override
     */
    drawMultiLineString(geometry) {
      if (this.squaredTolerance_) {
        geometry =
          /** @type {import("../../geom/MultiLineString.js").default} */ (
            geometry.simplifyTransformed(
              this.squaredTolerance_,
              this.userTransform_,
            )
          );
      }
      const geometryExtent = geometry.getExtent();
      if (!intersects$1(this.extent_, geometryExtent)) {
        return;
      }
      if (this.strokeState_) {
        this.setContextStrokeState_(this.strokeState_);
        const context = this.context_;
        const flatCoordinates = geometry.getFlatCoordinates();
        let offset = 0;
        const ends = /** @type {Array<number>} */ (geometry.getEnds());
        const stride = geometry.getStride();
        context.beginPath();
        for (let i = 0, ii = ends.length; i < ii; ++i) {
          offset = this.moveToLineTo_(
            flatCoordinates,
            offset,
            ends[i],
            stride,
            false,
            this.strokeState_.strokeOffset,
          );
        }
        context.stroke();
      }
      if (this.text_ !== '') {
        const flatMidpoints = geometry.getFlatMidpoints();
        this.drawText_(flatMidpoints, 0, flatMidpoints.length, 2);
      }
    }

    /**
     * Render a Polygon geometry into the canvas.  Rendering is immediate and uses
     * the current style.
     *
     * @param {import("../../geom/Polygon.js").default|import("../Feature.js").default} geometry Polygon geometry.
     * @override
     */
    drawPolygon(geometry) {
      if (this.squaredTolerance_) {
        geometry = /** @type {import("../../geom/Polygon.js").default} */ (
          geometry.simplifyTransformed(
            this.squaredTolerance_,
            this.userTransform_,
          )
        );
      }
      if (!intersects$1(this.extent_, geometry.getExtent())) {
        return;
      }
      if (this.strokeState_ || this.fillState_) {
        if (this.fillState_) {
          this.setContextFillState_(this.fillState_);
        }
        if (this.strokeState_) {
          this.setContextStrokeState_(this.strokeState_);
        }
        const context = this.context_;
        context.beginPath();
        this.drawRings_(
          geometry.getOrientedFlatCoordinates(),
          0,
          /** @type {Array<number>} */ (geometry.getEnds()),
          geometry.getStride(),
          this.strokeState_?.strokeOffset,
        );
        if (this.fillState_) {
          context.fill();
        }
        if (this.strokeState_) {
          context.stroke();
        }
      }
      if (this.text_ !== '') {
        const flatInteriorPoint = geometry.getFlatInteriorPoint();
        this.drawText_(flatInteriorPoint, 0, 2, 2);
      }
    }

    /**
     * Render MultiPolygon geometry into the canvas.  Rendering is immediate and
     * uses the current style.
     * @param {import("../../geom/MultiPolygon.js").default} geometry MultiPolygon geometry.
     * @override
     */
    drawMultiPolygon(geometry) {
      if (this.squaredTolerance_) {
        geometry = /** @type {import("../../geom/MultiPolygon.js").default} */ (
          geometry.simplifyTransformed(
            this.squaredTolerance_,
            this.userTransform_,
          )
        );
      }
      if (!intersects$1(this.extent_, geometry.getExtent())) {
        return;
      }
      if (this.strokeState_ || this.fillState_) {
        if (this.fillState_) {
          this.setContextFillState_(this.fillState_);
        }
        if (this.strokeState_) {
          this.setContextStrokeState_(this.strokeState_);
        }
        const context = this.context_;
        const flatCoordinates = geometry.getOrientedFlatCoordinates();
        let offset = 0;
        const endss = geometry.getEndss();
        const stride = geometry.getStride();
        context.beginPath();
        for (let i = 0, ii = endss.length; i < ii; ++i) {
          const ends = endss[i];
          offset = this.drawRings_(
            flatCoordinates,
            offset,
            ends,
            stride,
            this.strokeState_?.strokeOffset,
          );
        }
        if (this.fillState_) {
          context.fill();
        }
        if (this.strokeState_) {
          context.stroke();
        }
      }
      if (this.text_ !== '') {
        const flatInteriorPoints = geometry.getFlatInteriorPoints();
        this.drawText_(flatInteriorPoints, 0, flatInteriorPoints.length, 2);
      }
    }

    /**
     * @param {import("../canvas.js").FillState} fillState Fill state.
     * @private
     */
    setContextFillState_(fillState) {
      const context = this.context_;
      const contextFillState = this.contextFillState_;
      if (!contextFillState) {
        context.fillStyle = fillState.fillStyle;
        this.contextFillState_ = {
          fillStyle: fillState.fillStyle,
        };
      } else {
        if (contextFillState.fillStyle != fillState.fillStyle) {
          contextFillState.fillStyle = fillState.fillStyle;
          context.fillStyle = fillState.fillStyle;
        }
      }
    }

    /**
     * @param {import("../canvas.js").StrokeState} strokeState Stroke state.
     * @private
     */
    setContextStrokeState_(strokeState) {
      const context = this.context_;
      const contextStrokeState = this.contextStrokeState_;
      if (!contextStrokeState) {
        context.lineCap = strokeState.lineCap;
        context.setLineDash(strokeState.lineDash);
        context.lineDashOffset = strokeState.lineDashOffset;
        context.lineJoin = strokeState.lineJoin;
        context.lineWidth = strokeState.lineWidth;
        context.miterLimit = strokeState.miterLimit;
        context.strokeStyle = strokeState.strokeStyle;
        this.contextStrokeState_ = {
          lineCap: strokeState.lineCap,
          lineDash: strokeState.lineDash,
          lineDashOffset: strokeState.lineDashOffset,
          lineJoin: strokeState.lineJoin,
          lineWidth: strokeState.lineWidth,
          miterLimit: strokeState.miterLimit,
          strokeStyle: strokeState.strokeStyle,
        };
      } else {
        if (contextStrokeState.lineCap != strokeState.lineCap) {
          contextStrokeState.lineCap = strokeState.lineCap;
          context.lineCap = strokeState.lineCap;
        }
        if (!equals$2(contextStrokeState.lineDash, strokeState.lineDash)) {
          context.setLineDash(
            (contextStrokeState.lineDash = strokeState.lineDash),
          );
        }
        if (contextStrokeState.lineDashOffset != strokeState.lineDashOffset) {
          contextStrokeState.lineDashOffset = strokeState.lineDashOffset;
          context.lineDashOffset = strokeState.lineDashOffset;
        }
        if (contextStrokeState.lineJoin != strokeState.lineJoin) {
          contextStrokeState.lineJoin = strokeState.lineJoin;
          context.lineJoin = strokeState.lineJoin;
        }
        if (contextStrokeState.lineWidth != strokeState.lineWidth) {
          contextStrokeState.lineWidth = strokeState.lineWidth;
          context.lineWidth = strokeState.lineWidth;
        }
        if (contextStrokeState.miterLimit != strokeState.miterLimit) {
          contextStrokeState.miterLimit = strokeState.miterLimit;
          context.miterLimit = strokeState.miterLimit;
        }
        if (contextStrokeState.strokeStyle != strokeState.strokeStyle) {
          contextStrokeState.strokeStyle = strokeState.strokeStyle;
          context.strokeStyle = strokeState.strokeStyle;
        }
      }
    }

    /**
     * @param {import("../canvas.js").TextState} textState Text state.
     * @private
     */
    setContextTextState_(textState) {
      const context = this.context_;
      const contextTextState = this.contextTextState_;
      const textAlign = textState.textAlign
        ? textState.textAlign
        : defaultTextAlign;
      if (!contextTextState) {
        context.font = textState.font;
        context.textAlign = textAlign;
        context.textBaseline = textState.textBaseline;
        this.contextTextState_ = {
          font: textState.font,
          textAlign: textAlign,
          textBaseline: textState.textBaseline,
        };
      } else {
        if (contextTextState.font != textState.font) {
          contextTextState.font = textState.font;
          context.font = textState.font;
        }
        if (contextTextState.textAlign != textAlign) {
          contextTextState.textAlign = textAlign;
          context.textAlign = textAlign;
        }
        if (contextTextState.textBaseline != textState.textBaseline) {
          contextTextState.textBaseline = textState.textBaseline;
          context.textBaseline = textState.textBaseline;
        }
      }
    }

    /**
     * Set the fill and stroke style for subsequent draw operations.  To clear
     * either fill or stroke styles, pass null for the appropriate parameter.
     *
     * @param {import("../../style/Fill.js").default} fillStyle Fill style.
     * @param {import("../../style/Stroke.js").default} strokeStyle Stroke style.
     * @override
     */
    setFillStrokeStyle(fillStyle, strokeStyle) {
      if (!fillStyle) {
        this.fillState_ = null;
      } else {
        const fillStyleColor = fillStyle.getColor();
        this.fillState_ = {
          fillStyle: asColorLike(
            fillStyleColor ? fillStyleColor : defaultFillStyle,
          ),
        };
      }
      if (!strokeStyle) {
        this.strokeState_ = null;
      } else {
        const strokeStyleColor = strokeStyle.getColor();
        const strokeStyleLineCap = strokeStyle.getLineCap();
        const strokeStyleLineDash = strokeStyle.getLineDash();
        const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset();
        const strokeStyleLineJoin = strokeStyle.getLineJoin();
        const strokeStyleWidth = strokeStyle.getWidth();
        const strokeStyleMiterLimit = strokeStyle.getMiterLimit();
        const lineDash = strokeStyleLineDash
          ? strokeStyleLineDash
          : defaultLineDash;
        const strokeOffset = strokeStyle.getOffset();
        this.strokeState_ = {
          lineCap:
            strokeStyleLineCap !== undefined
              ? strokeStyleLineCap
              : defaultLineCap,
          lineDash:
            this.pixelRatio_ === 1
              ? lineDash
              : lineDash.map((n) => n * this.pixelRatio_),
          lineDashOffset:
            (strokeStyleLineDashOffset
              ? strokeStyleLineDashOffset
              : defaultLineDashOffset) * this.pixelRatio_,
          lineJoin:
            strokeStyleLineJoin !== undefined
              ? strokeStyleLineJoin
              : defaultLineJoin,
          lineWidth:
            (strokeStyleWidth !== undefined
              ? strokeStyleWidth
              : defaultLineWidth) * this.pixelRatio_,
          miterLimit:
            strokeStyleMiterLimit !== undefined
              ? strokeStyleMiterLimit
              : defaultMiterLimit,
          strokeStyle: asColorLike(
            strokeStyleColor ? strokeStyleColor : defaultStrokeStyle,
          ),
          strokeOffset: (strokeOffset ?? 0) * this.pixelRatio_,
        };
      }
    }

    /**
     * Set the image style for subsequent draw operations.  Pass null to remove
     * the image style.
     *
     * @param {import("../../style/Image.js").default} imageStyle Image style.
     * @override
     */
    setImageStyle(imageStyle) {
      let imageSize;
      if (!imageStyle || !(imageSize = imageStyle.getSize())) {
        this.image_ = null;
        return;
      }
      const imagePixelRatio = imageStyle.getPixelRatio(this.pixelRatio_);
      const imageAnchor = imageStyle.getAnchor();
      const imageOrigin = imageStyle.getOrigin();
      this.image_ = imageStyle.getImage(this.pixelRatio_);
      this.imageAnchorX_ = imageAnchor[0] * imagePixelRatio;
      this.imageAnchorY_ = imageAnchor[1] * imagePixelRatio;
      this.imageHeight_ = imageSize[1] * imagePixelRatio;
      this.imageOpacity_ = imageStyle.getOpacity();
      this.imageOriginX_ = imageOrigin[0];
      this.imageOriginY_ = imageOrigin[1];
      this.imageRotateWithView_ = imageStyle.getRotateWithView();
      this.imageRotation_ = imageStyle.getRotation();
      const imageScale = imageStyle.getScaleArray();
      this.imageScale_ = [
        (imageScale[0] * this.pixelRatio_) / imagePixelRatio,
        (imageScale[1] * this.pixelRatio_) / imagePixelRatio,
      ];
      this.imageWidth_ = imageSize[0] * imagePixelRatio;
    }

    /**
     * Set the text style for subsequent draw operations.  Pass null to
     * remove the text style.
     *
     * @param {import("../../style/Text.js").default} textStyle Text style.
     * @override
     */
    setTextStyle(textStyle) {
      if (!textStyle) {
        this.text_ = '';
      } else {
        const textFillStyle = textStyle.getFill();
        if (!textFillStyle) {
          this.textFillState_ = null;
        } else {
          const textFillStyleColor = textFillStyle.getColor();
          this.textFillState_ = {
            fillStyle: asColorLike(
              textFillStyleColor ? textFillStyleColor : defaultFillStyle,
            ),
          };
        }
        const textStrokeStyle = textStyle.getStroke();
        if (!textStrokeStyle) {
          this.textStrokeState_ = null;
        } else {
          const textStrokeStyleColor = textStrokeStyle.getColor();
          const textStrokeStyleLineCap = textStrokeStyle.getLineCap();
          const textStrokeStyleLineDash = textStrokeStyle.getLineDash();
          const textStrokeStyleLineDashOffset =
            textStrokeStyle.getLineDashOffset();
          const textStrokeStyleLineJoin = textStrokeStyle.getLineJoin();
          const textStrokeStyleWidth = textStrokeStyle.getWidth();
          const textStrokeStyleMiterLimit = textStrokeStyle.getMiterLimit();
          this.textStrokeState_ = {
            lineCap:
              textStrokeStyleLineCap !== undefined
                ? textStrokeStyleLineCap
                : defaultLineCap,
            lineDash: textStrokeStyleLineDash
              ? textStrokeStyleLineDash
              : defaultLineDash,
            lineDashOffset: textStrokeStyleLineDashOffset
              ? textStrokeStyleLineDashOffset
              : defaultLineDashOffset,
            lineJoin:
              textStrokeStyleLineJoin !== undefined
                ? textStrokeStyleLineJoin
                : defaultLineJoin,
            lineWidth:
              textStrokeStyleWidth !== undefined
                ? textStrokeStyleWidth
                : defaultLineWidth,
            miterLimit:
              textStrokeStyleMiterLimit !== undefined
                ? textStrokeStyleMiterLimit
                : defaultMiterLimit,
            strokeStyle: asColorLike(
              textStrokeStyleColor ? textStrokeStyleColor : defaultStrokeStyle,
            ),
          };
        }
        const textFont = textStyle.getFont();
        const textOffsetX = textStyle.getOffsetX();
        const textOffsetY = textStyle.getOffsetY();
        const textRotateWithView = textStyle.getRotateWithView();
        const textRotation = textStyle.getRotation();
        const textScale = textStyle.getScaleArray();
        const textText = textStyle.getText();
        const textTextAlign = textStyle.getTextAlign();
        const textTextBaseline = textStyle.getTextBaseline();
        this.textState_ = {
          font: textFont !== undefined ? textFont : defaultFont,
          textAlign:
            textTextAlign !== undefined ? textTextAlign : defaultTextAlign,
          textBaseline:
            textTextBaseline !== undefined
              ? textTextBaseline
              : defaultTextBaseline,
        };
        this.text_ =
          textText !== undefined
            ? Array.isArray(textText)
              ? textText.reduce((acc, t, i) => (acc += i % 2 ? ' ' : t), '')
              : textText
            : '';
        this.textOffsetX_ =
          textOffsetX !== undefined ? this.pixelRatio_ * textOffsetX : 0;
        this.textOffsetY_ =
          textOffsetY !== undefined ? this.pixelRatio_ * textOffsetY : 0;
        this.textRotateWithView_ =
          textRotateWithView !== undefined ? textRotateWithView : false;
        this.textRotation_ = textRotation !== undefined ? textRotation : 0;
        this.textScale_ = [
          this.pixelRatio_ * textScale[0],
          this.pixelRatio_ * textScale[1],
        ];
      }
    }
  }

  /**
   * @module ol/renderer/vector
   */

  /**
   * Feature callback. The callback will be called with three arguments. The first
   * argument is one {@link module:ol/Feature~Feature feature} or {@link module:ol/render/Feature~RenderFeature render feature}
   * at the pixel, the second is the {@link module:ol/layer/Layer~Layer layer} of the feature and will be null for
   * unmanaged layers. The third is the {@link module:ol/geom/SimpleGeometry~SimpleGeometry} of the feature. For features
   * with a GeometryCollection geometry, it will be the first detected geometry from the collection.
   * @template T
   * @typedef {function(import("../Feature.js").FeatureLike, import("../layer/Layer.js").default<import("../source/Source").default>, import("../geom/SimpleGeometry.js").default): T} FeatureCallback
   */

  /**
   * Tolerance for geometry simplification in device pixels.
   * @type {number}
   */
  const SIMPLIFY_TOLERANCE = 0.5;

  /**
   * @const
   * @type {Object<import("../geom/Geometry.js").Type,
   *                function(import("../render/canvas/BuilderGroup.js").default, import("../geom/Geometry.js").default,
   *                         import("../style/Style.js").default, Object): void>}
   */
  const GEOMETRY_RENDERERS = {
    'Point': renderPointGeometry,
    'LineString': renderLineStringGeometry,
    'Polygon': renderPolygonGeometry,
    'MultiPoint': renderMultiPointGeometry,
    'MultiLineString': renderMultiLineStringGeometry,
    'MultiPolygon': renderMultiPolygonGeometry,
    'GeometryCollection': renderGeometryCollectionGeometry,
    'Circle': renderCircleGeometry,
  };

  /**
   * @param {import("../Feature.js").FeatureLike} feature1 Feature 1.
   * @param {import("../Feature.js").FeatureLike} feature2 Feature 2.
   * @return {number} Order.
   */
  function defaultOrder(feature1, feature2) {
    return parseInt(getUid(feature1), 10) - parseInt(getUid(feature2), 10);
  }

  /**
   * @param {number} resolution Resolution.
   * @param {number} pixelRatio Pixel ratio.
   * @return {number} Squared pixel tolerance.
   */
  function getSquaredTolerance(resolution, pixelRatio) {
    const tolerance = getTolerance(resolution, pixelRatio);
    return tolerance * tolerance;
  }

  /**
   * @param {number} resolution Resolution.
   * @param {number} pixelRatio Pixel ratio.
   * @return {number} Pixel tolerance.
   */
  function getTolerance(resolution, pixelRatio) {
    return (SIMPLIFY_TOLERANCE * resolution) / pixelRatio;
  }

  /**
   * @param {import("../render/canvas/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.
   * @param {number} [index] Render order index.
   */
  function renderCircleGeometry(builderGroup, geometry, style, feature, index) {
    const fillStyle = style.getFill();
    const strokeStyle = style.getStroke();
    if (fillStyle || strokeStyle) {
      const circleReplay = builderGroup.getBuilder(style.getZIndex(), 'Circle');
      circleReplay.setFillStrokeStyle(fillStyle, strokeStyle);
      circleReplay.drawCircle(geometry, feature, index);
    }
    const textStyle = style.getText();
    if (textStyle && textStyle.getText()) {
      const textReplay = builderGroup.getBuilder(style.getZIndex(), 'Text');
      textReplay.setTextStyle(textStyle);
      textReplay.drawText(geometry, feature);
    }
  }

  /**
   * @param {import("../render/canvas/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.
   * @param {function(import("../events/Event.js").default): void} listener Listener function.
   * @param {import("../proj.js").TransformFunction} [transform] Transform from user to view projection.
   * @param {boolean} [declutter] Enable decluttering.
   * @param {number} [index] Render order index..
   * @return {boolean} `true` if style is loading.
   */
  function renderFeature(
    replayGroup,
    feature,
    style,
    squaredTolerance,
    listener,
    transform,
    declutter,
    index,
  ) {
    const loadingPromises = [];
    const imageStyle = style.getImage();
    if (imageStyle) {
      let loading = true;
      const imageState = imageStyle.getImageState();
      if (imageState == ImageState.LOADED || imageState == ImageState.ERROR) {
        loading = false;
      } else {
        if (imageState == ImageState.IDLE) {
          imageStyle.load();
        }
      }
      if (loading) {
        loadingPromises.push(imageStyle.ready());
      }
    }
    const fillStyle = style.getFill();
    if (fillStyle && fillStyle.loading()) {
      loadingPromises.push(fillStyle.ready());
    }
    const loading = loadingPromises.length > 0;
    if (loading) {
      Promise.all(loadingPromises).then(() => listener(null));
    }
    renderFeatureInternal(
      replayGroup,
      feature,
      style,
      squaredTolerance,
      transform,
      declutter,
      index,
    );

    return loading;
  }

  /**
   * @param {import("../render/canvas/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.
   * @param {import("../proj.js").TransformFunction} [transform] Optional transform function.
   * @param {boolean} [declutter] Enable decluttering.
   * @param {number} [index] Render order index..
   */
  function renderFeatureInternal(
    replayGroup,
    feature,
    style,
    squaredTolerance,
    transform,
    declutter,
    index,
  ) {
    const geometry = style.getGeometryFunction()(feature);
    if (!geometry) {
      return;
    }
    const simplifiedGeometry = geometry.simplifyTransformed(
      squaredTolerance,
      transform,
    );
    const renderer = style.getRenderer();
    if (renderer) {
      renderGeometry(replayGroup, simplifiedGeometry, style, feature, index);
    } else {
      const geometryRenderer = GEOMETRY_RENDERERS[simplifiedGeometry.getType()];
      geometryRenderer(
        replayGroup,
        simplifiedGeometry,
        style,
        feature,
        index,
        declutter,
      );
    }
  }

  /**
   * @param {import("../render/canvas/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.
   * @param {number} [index] Render order index.
   */
  function renderGeometry(replayGroup, geometry, style, feature, index) {
    if (geometry.getType() == 'GeometryCollection') {
      const geometries =
        /** @type {import("../geom/GeometryCollection.js").default} */ (
          geometry
        ).getGeometries();
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        renderGeometry(replayGroup, geometries[i], style, feature, index);
      }
      return;
    }
    const replay = replayGroup.getBuilder(style.getZIndex(), 'Default');
    replay.drawCustom(
      /** @type {import("../geom/SimpleGeometry.js").default} */ (geometry),
      feature,
      style.getRenderer(),
      style.getHitDetectionRenderer(),
      index,
    );
  }

  /**
   * @param {import("../render/canvas/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.
   * @param {import("../render/canvas/BuilderGroup.js").default} [declutterBuilderGroup] Builder for decluttering.
   * @param {number} [index] Render order index.
   */
  function renderGeometryCollectionGeometry(
    replayGroup,
    geometry,
    style,
    feature,
    declutterBuilderGroup,
    index,
  ) {
    const geometries = geometry.getGeometriesArray();
    let i, ii;
    for (i = 0, ii = geometries.length; i < ii; ++i) {
      const geometryRenderer = GEOMETRY_RENDERERS[geometries[i].getType()];
      geometryRenderer(
        replayGroup,
        geometries[i],
        style,
        feature,
        declutterBuilderGroup,
        index,
      );
    }
  }

  /**
   * @param {import("../render/canvas/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.
   * @param {number} [index] Render order index.
   */
  function renderLineStringGeometry(
    builderGroup,
    geometry,
    style,
    feature,
    index,
  ) {
    const strokeStyle = style.getStroke();
    if (strokeStyle) {
      const lineStringReplay = builderGroup.getBuilder(
        style.getZIndex(),
        'LineString',
      );
      lineStringReplay.setFillStrokeStyle(null, strokeStyle);
      lineStringReplay.drawLineString(geometry, feature, index);
    }
    const textStyle = style.getText();
    if (textStyle && textStyle.getText()) {
      const textReplay = builderGroup.getBuilder(style.getZIndex(), 'Text');
      textReplay.setTextStyle(textStyle);
      textReplay.drawText(geometry, feature, index);
    }
  }

  /**
   * @param {import("../render/canvas/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.
   * @param {number} [index] Render order index.
   */
  function renderMultiLineStringGeometry(
    builderGroup,
    geometry,
    style,
    feature,
    index,
  ) {
    const strokeStyle = style.getStroke();
    if (strokeStyle) {
      const lineStringReplay = builderGroup.getBuilder(
        style.getZIndex(),
        'LineString',
      );
      lineStringReplay.setFillStrokeStyle(null, strokeStyle);
      lineStringReplay.drawMultiLineString(geometry, feature, index);
    }
    const textStyle = style.getText();
    if (textStyle && textStyle.getText()) {
      const textReplay = builderGroup.getBuilder(style.getZIndex(), 'Text');
      textReplay.setTextStyle(textStyle);
      textReplay.drawText(geometry, feature, index);
    }
  }

  /**
   * @param {import("../render/canvas/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.
   * @param {number} [index] Render order index.
   */
  function renderMultiPolygonGeometry(
    builderGroup,
    geometry,
    style,
    feature,
    index,
  ) {
    const fillStyle = style.getFill();
    const strokeStyle = style.getStroke();
    if (strokeStyle || fillStyle) {
      const polygonReplay = builderGroup.getBuilder(style.getZIndex(), 'Polygon');
      polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
      polygonReplay.drawMultiPolygon(geometry, feature, index);
    }
    const textStyle = style.getText();
    if (textStyle && textStyle.getText()) {
      const textReplay = builderGroup.getBuilder(style.getZIndex(), 'Text');
      textReplay.setTextStyle(textStyle);
      textReplay.drawText(geometry, feature, index);
    }
  }

  /**
   * @param {import("../render/canvas/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.
   * @param {number} [index] Render order index.
   * @param {boolean} [declutter] Enable decluttering.
   */
  function renderPointGeometry(
    builderGroup,
    geometry,
    style,
    feature,
    index,
    declutter,
  ) {
    const imageStyle = style.getImage();
    const textStyle = style.getText();
    const hasText = textStyle && textStyle.getText();
    /** @type {import("../render/canvas.js").DeclutterImageWithText} */
    const declutterImageWithText =
      declutter && imageStyle && hasText ? {} : undefined;
    if (imageStyle) {
      if (imageStyle.getImageState() != ImageState.LOADED) {
        return;
      }
      const imageReplay = builderGroup.getBuilder(style.getZIndex(), 'Image');
      imageReplay.setImageStyle(imageStyle, declutterImageWithText);
      imageReplay.drawPoint(geometry, feature, index);
    }
    if (hasText) {
      const textReplay = builderGroup.getBuilder(style.getZIndex(), 'Text');
      textReplay.setTextStyle(textStyle, declutterImageWithText);
      textReplay.drawText(geometry, feature, index);
    }
  }

  /**
   * @param {import("../render/canvas/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.
   * @param {number} [index] Render order index.
   * @param {boolean} [declutter] Enable decluttering.
   */
  function renderMultiPointGeometry(
    builderGroup,
    geometry,
    style,
    feature,
    index,
    declutter,
  ) {
    const imageStyle = style.getImage();
    const hasImage = imageStyle && imageStyle.getOpacity() !== 0;
    const textStyle = style.getText();
    const hasText = textStyle && textStyle.getText();
    /** @type {import("../render/canvas.js").DeclutterImageWithText} */
    const declutterImageWithText =
      declutter && hasImage && hasText ? {} : undefined;
    if (hasImage) {
      if (imageStyle.getImageState() != ImageState.LOADED) {
        return;
      }
      const imageReplay = builderGroup.getBuilder(style.getZIndex(), 'Image');
      imageReplay.setImageStyle(imageStyle, declutterImageWithText);
      imageReplay.drawMultiPoint(geometry, feature, index);
    }
    if (hasText) {
      const textReplay = builderGroup.getBuilder(style.getZIndex(), 'Text');
      textReplay.setTextStyle(textStyle, declutterImageWithText);
      textReplay.drawText(geometry, feature, index);
    }
  }

  /**
   * @param {import("../render/canvas/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.
   * @param {number} [index] Render order index.
   */
  function renderPolygonGeometry(builderGroup, geometry, style, feature, index) {
    const fillStyle = style.getFill();
    const strokeStyle = style.getStroke();
    if (fillStyle || strokeStyle) {
      const polygonReplay = builderGroup.getBuilder(style.getZIndex(), 'Polygon');
      polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
      polygonReplay.drawPolygon(geometry, feature, index);
    }
    const textStyle = style.getText();
    if (textStyle && textStyle.getText()) {
      const textReplay = builderGroup.getBuilder(style.getZIndex(), 'Text');
      textReplay.setTextStyle(textStyle);
      textReplay.drawText(geometry, feature, index);
    }
  }

  /**
   * @module ol/featureloader
   */

  /**
   *
   * @type {boolean}
   * @private
   */
  let withCredentials = false;

  /**
   * {@link module:ol/source/Vector~VectorSource} sources use a function of this type to
   * load features.
   *
   * This function takes up to 5 arguments. These are an {@link module:ol/extent~Extent} representing
   * the area to be loaded, a `{number}` representing the resolution (map units per pixel), a
   * {@link module:ol/proj/Projection~Projection} for the projection, an optional success callback that should get
   * the loaded features passed as an argument and an optional failure callback with no arguments. If
   * the callbacks are not used, the corresponding vector source will not fire `'featuresloadend'` and
   * `'featuresloaderror'` events. `this` within the function is bound to the
   * {@link module:ol/source/Vector~VectorSource} it's called from.
   *
   * The function is responsible for loading the features and adding them to the
   * source.
   *
   * @template {import("./Feature.js").FeatureLike} [FeatureType=import("./Feature.js").FeatureLike]
   * @typedef {(
   *           extent: import("./extent.js").Extent,
   *           resolution: number,
   *           projection: import("./proj/Projection.js").default,
   *           success?: (features: Array<FeatureType>) => void,
   *           failure?: () => void) => void} FeatureLoader
   * @api
   */

  /**
   * {@link module:ol/source/Vector~VectorSource} sources use a function of this type to
   * get the url to load features from.
   *
   * This function takes an {@link module:ol/extent~Extent} representing the area
   * to be loaded, a `{number}` representing the resolution (map units per pixel)
   * and an {@link module:ol/proj/Projection~Projection} for the projection  as
   * arguments and returns a `{string}` representing the URL.
   * @typedef {function(import("./extent.js").Extent, number, import("./proj/Projection.js").default): string} FeatureUrlFunction
   * @api
   */

  /**
   * @template {import("./Feature.js").FeatureLike} [FeatureType=import("./Feature.js").default]
   * @param {string|FeatureUrlFunction} url Feature URL service.
   * @param {import("./format/Feature.js").default<FeatureType>} format Feature format.
   * @param {import("./extent.js").Extent} extent Extent.
   * @param {number} resolution Resolution.
   * @param {import("./proj/Projection.js").default} projection Projection.
   * @param {function(Array<FeatureType>, import("./proj/Projection.js").default): void} success Success
   *      Function called with the loaded features and optionally with the data projection.
   * @param {function(): void} failure Failure
   *      Function called when loading failed.
   */
  function loadFeaturesXhr(
    url,
    format,
    extent,
    resolution,
    projection,
    success,
    failure,
  ) {
    const xhr = new XMLHttpRequest();
    xhr.open(
      'GET',
      typeof url === 'function' ? url(extent, resolution, projection) : url,
      true,
    );
    if (format.getType() == 'arraybuffer') {
      xhr.responseType = 'arraybuffer';
    }
    xhr.withCredentials = withCredentials;
    /**
     * @param {Event} event Event.
     * @private
     */
    xhr.onload = function (event) {
      // status will be 0 for file:// urls
      if (!xhr.status || (xhr.status >= 200 && xhr.status < 300)) {
        const type = format.getType();
        try {
          /** @type {Document|Node|Object|string|undefined} */
          let source;
          if (type == 'text' || type == 'json') {
            source = xhr.responseText;
          } else if (type == 'xml') {
            source = xhr.responseXML || xhr.responseText;
          } else if (type == 'arraybuffer') {
            source = /** @type {ArrayBuffer} */ (xhr.response);
          }
          if (source) {
            success(
              /** @type {Array<FeatureType>} */
              (
                format.readFeatures(source, {
                  extent: extent,
                  featureProjection: projection,
                })
              ),
              format.readProjection(source),
            );
          } else {
            failure();
          }
        } catch {
          failure();
        }
      } else {
        failure();
      }
    };
    /**
     * @private
     */
    xhr.onerror = failure;
    xhr.send();
  }

  /**
   * Create an XHR feature loader for a `url` and `format`. The feature loader
   * loads features (with XHR), parses the features, and adds them to the
   * vector source.
   *
   * @template {import("./Feature.js").FeatureLike} [FeatureType=import("./Feature.js").default]
   * @param {string|FeatureUrlFunction} url Feature URL service.
   * @param {import("./format/Feature.js").default<FeatureType>} format Feature format.
   * @return {FeatureLoader<FeatureType>} The feature loader.
   * @api
   */
  function xhr(url, format) {
    /**
     * @param {import("./extent.js").Extent} extent Extent.
     * @param {number} resolution Resolution.
     * @param {import("./proj/Projection.js").default} projection Projection.
     * @param {function(Array<FeatureType>): void} [success] Success
     *      Function called when loading succeeded.
     * @param {function(): void} [failure] Failure
     *      Function called when loading failed.
     * @this {import("./source/Vector.js").default<FeatureType>}
     */
    return function (extent, resolution, projection, success, failure) {
      loadFeaturesXhr(
        url,
        format,
        extent,
        resolution,
        projection,
        /**
         * @param {Array<FeatureType>} features The loaded features.
         * @param {import("./proj/Projection.js").default} dataProjection Data
         * projection.
         */
        (features, dataProjection) => {
          this.addFeatures(features);
          if (success !== undefined) {
            success(features);
          }
        },
        () => {
          this.changed();
          if (failure !== undefined) {
            failure();
          }
        },
      );
    };
  }

  /**
   * @module ol/loadingstrategy
   */


  /**
   * Strategy function for loading all features with a single request.
   * @param {import("./extent.js").Extent} extent Extent.
   * @param {number} resolution Resolution.
   * @return {Array<import("./extent.js").Extent>} Extents.
   * @api
   */
  function all$1(extent, resolution) {
    return [[-Infinity, -Infinity, Infinity, Infinity]];
  }

  /**
   * @module ol/geom/flat/center
   */

  /**
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {Array<Array<number>>} endss Endss.
   * @param {number} stride Stride.
   * @return {Array<number>} Flat centers.
   */
  function linearRingss(flatCoordinates, offset, endss, stride) {
    const flatCenters = [];
    let extent = createEmpty();
    for (let i = 0, ii = endss.length; i < ii; ++i) {
      const ends = endss[i];
      extent = createOrUpdateFromFlatCoordinates(
        flatCoordinates,
        offset,
        ends[0],
        stride,
      );
      flatCenters.push((extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2);
      offset = ends[ends.length - 1];
    }
    return flatCenters;
  }

  /**
   * @module ol/geom/GeometryCollection
   */

  /**
   * @classdesc
   * An array of {@link module:ol/geom/Geometry~Geometry} objects.
   *
   * @api
   */
  class GeometryCollection extends Geometry {
    /**
     * @param {Array<Geometry>} geometries Geometries.
     */
    constructor(geometries) {
      super();

      /**
       * @private
       * @type {Array<Geometry>}
       */
      this.geometries_ = geometries;

      /**
       * @private
       * @type {Array<import("../events.js").EventsKey>}
       */
      this.changeEventsKeys_ = [];

      this.listenGeometriesChange_();
    }

    /**
     * @private
     */
    unlistenGeometriesChange_() {
      this.changeEventsKeys_.forEach(unlistenByKey);
      this.changeEventsKeys_.length = 0;
    }

    /**
     * @private
     */
    listenGeometriesChange_() {
      const geometries = this.geometries_;
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        this.changeEventsKeys_.push(
          listen(geometries[i], EventType.CHANGE, this.changed, this),
        );
      }
    }

    /**
     * Make a complete copy of the geometry.
     * @return {!GeometryCollection} Clone.
     * @api
     * @override
     */
    clone() {
      const geometryCollection = new GeometryCollection(
        cloneGeometries(this.geometries_),
      );
      geometryCollection.applyProperties(this);
      return geometryCollection;
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @param {import("../coordinate.js").Coordinate} closestPoint Closest point.
     * @param {number} minSquaredDistance Minimum squared distance.
     * @return {number} Minimum squared distance.
     * @override
     */
    closestPointXY(x, y, closestPoint, minSquaredDistance) {
      if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) {
        return minSquaredDistance;
      }
      const geometries = this.geometries_;
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        minSquaredDistance = geometries[i].closestPointXY(
          x,
          y,
          closestPoint,
          minSquaredDistance,
        );
      }
      return minSquaredDistance;
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @return {boolean} Contains (x, y).
     * @override
     */
    containsXY(x, y) {
      const geometries = this.geometries_;
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        if (geometries[i].containsXY(x, y)) {
          return true;
        }
      }
      return false;
    }

    /**
     * @param {import("../extent.js").Extent} extent Extent.
     * @protected
     * @return {import("../extent.js").Extent} extent Extent.
     * @override
     */
    computeExtent(extent) {
      createOrUpdateEmpty(extent);
      const geometries = this.geometries_;
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        extend$1(extent, geometries[i].getExtent());
      }
      return extent;
    }

    /**
     * Return the geometries that make up this geometry collection.
     * @return {Array<Geometry>} Geometries.
     * @api
     */
    getGeometries() {
      return cloneGeometries(this.geometries_);
    }

    /**
     * @return {Array<Geometry>} Geometries.
     */
    getGeometriesArray() {
      return this.geometries_;
    }

    /**
     * @return {Array<Geometry>} Geometries.
     */
    getGeometriesArrayRecursive() {
      /** @type {Array<Geometry>} */
      let geometriesArray = [];
      const geometries = this.geometries_;
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        if (geometries[i].getType() === this.getType()) {
          geometriesArray = geometriesArray.concat(
            /** @type {GeometryCollection} */ (
              geometries[i]
            ).getGeometriesArrayRecursive(),
          );
        } else {
          geometriesArray.push(geometries[i]);
        }
      }
      return geometriesArray;
    }

    /**
     * Create a simplified version of this geometry using the Douglas Peucker algorithm.
     * @param {number} squaredTolerance Squared tolerance.
     * @return {GeometryCollection} Simplified GeometryCollection.
     * @override
     */
    getSimplifiedGeometry(squaredTolerance) {
      if (this.simplifiedGeometryRevision !== this.getRevision()) {
        this.simplifiedGeometryMaxMinSquaredTolerance = 0;
        this.simplifiedGeometryRevision = this.getRevision();
      }
      if (
        squaredTolerance < 0 ||
        (this.simplifiedGeometryMaxMinSquaredTolerance !== 0 &&
          squaredTolerance < this.simplifiedGeometryMaxMinSquaredTolerance)
      ) {
        return this;
      }

      const simplifiedGeometries = [];
      const geometries = this.geometries_;
      let simplified = false;
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        const geometry = geometries[i];
        const simplifiedGeometry =
          geometry.getSimplifiedGeometry(squaredTolerance);
        simplifiedGeometries.push(simplifiedGeometry);
        if (simplifiedGeometry !== geometry) {
          simplified = true;
        }
      }
      if (simplified) {
        const simplifiedGeometryCollection = new GeometryCollection(
          simplifiedGeometries,
        );
        return simplifiedGeometryCollection;
      }
      this.simplifiedGeometryMaxMinSquaredTolerance = squaredTolerance;
      return this;
    }

    /**
     * Get the type of this geometry.
     * @return {import("./Geometry.js").Type} Geometry type.
     * @api
     * @override
     */
    getType() {
      return 'GeometryCollection';
    }

    /**
     * Test if the geometry and the passed extent intersect.
     * @param {import("../extent.js").Extent} extent Extent.
     * @return {boolean} `true` if the geometry and the extent intersect.
     * @api
     * @override
     */
    intersectsExtent(extent) {
      const geometries = this.geometries_;
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        if (geometries[i].intersectsExtent(extent)) {
          return true;
        }
      }
      return false;
    }

    /**
     * @return {boolean} Is empty.
     */
    isEmpty() {
      return this.geometries_.length === 0;
    }

    /**
     * Rotate the geometry around a given coordinate. This modifies the geometry
     * coordinates in place.
     * @param {number} angle Rotation angle in radians.
     * @param {import("../coordinate.js").Coordinate} anchor The rotation center.
     * @api
     * @override
     */
    rotate(angle, anchor) {
      const geometries = this.geometries_;
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        geometries[i].rotate(angle, anchor);
      }
      this.changed();
    }

    /**
     * Scale the geometry (with an optional origin).  This modifies the geometry
     * coordinates in place.
     * @abstract
     * @param {number} sx The scaling factor in the x-direction.
     * @param {number} [sy] The scaling factor in the y-direction (defaults to sx).
     * @param {import("../coordinate.js").Coordinate} [anchor] The scale origin (defaults to the center
     *     of the geometry extent).
     * @api
     * @override
     */
    scale(sx, sy, anchor) {
      if (!anchor) {
        anchor = getCenter(this.getExtent());
      }
      const geometries = this.geometries_;
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        geometries[i].scale(sx, sy, anchor);
      }
      this.changed();
    }

    /**
     * Set the geometries that make up this geometry collection.
     * @param {Array<Geometry>} geometries Geometries.
     * @api
     */
    setGeometries(geometries) {
      this.setGeometriesArray(cloneGeometries(geometries));
    }

    /**
     * @param {Array<Geometry>} geometries Geometries.
     */
    setGeometriesArray(geometries) {
      this.unlistenGeometriesChange_();
      this.geometries_ = geometries;
      this.listenGeometriesChange_();
      this.changed();
    }

    /**
     * Apply a transform function to the coordinates of the geometry.
     * The geometry is modified in place.
     * If you do not want the geometry modified in place, first `clone()` it and
     * then use this function on the clone.
     * @param {import("../proj.js").TransformFunction} transformFn Transform function.
     * Called with a flat array of geometry coordinates.
     * @api
     * @override
     */
    applyTransform(transformFn) {
      const geometries = this.geometries_;
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        geometries[i].applyTransform(transformFn);
      }
      this.changed();
    }

    /**
     * Translate the geometry.  This modifies the geometry coordinates in place.  If
     * instead you want a new geometry, first `clone()` this geometry.
     * @param {number} deltaX Delta X.
     * @param {number} deltaY Delta Y.
     * @api
     * @override
     */
    translate(deltaX, deltaY) {
      const geometries = this.geometries_;
      for (let i = 0, ii = geometries.length; i < ii; ++i) {
        geometries[i].translate(deltaX, deltaY);
      }
      this.changed();
    }

    /**
     * Clean up.
     * @override
     */
    disposeInternal() {
      this.unlistenGeometriesChange_();
      super.disposeInternal();
    }
  }

  /**
   * @param {Array<Geometry>} geometries Geometries.
   * @return {Array<Geometry>} Cloned geometries.
   */
  function cloneGeometries(geometries) {
    return geometries.map((geometry) => geometry.clone());
  }

  /**
   * @module ol/geom/MultiLineString
   */

  /**
   * @classdesc
   * Multi-linestring geometry.
   *
   * @api
   */
  class MultiLineString extends SimpleGeometry {
    /**
     * @param {Array<Array<import("../coordinate.js").Coordinate>|LineString>|Array<number>} coordinates
     *     Coordinates or LineString geometries. (For internal use, flat coordinates in
     *     combination with `layout` and `ends` are also accepted.)
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     * @param {Array<number>} [ends] Flat coordinate ends for internal use.
     */
    constructor(coordinates, layout, ends) {
      super();

      /**
       * @type {Array<number>}
       * @private
       */
      this.ends_ = [];

      /**
       * @private
       * @type {number}
       */
      this.maxDelta_ = -1;

      /**
       * @private
       * @type {number}
       */
      this.maxDeltaRevision_ = -1;

      if (Array.isArray(coordinates[0])) {
        this.setCoordinates(
          /** @type {Array<Array<import("../coordinate.js").Coordinate>>} */ (
            coordinates
          ),
          layout,
        );
      } else if (layout !== undefined && ends) {
        this.setFlatCoordinates(
          layout,
          /** @type {Array<number>} */ (coordinates),
        );
        this.ends_ = ends;
      } else {
        const lineStrings = /** @type {Array<LineString>} */ (coordinates);
        /** @type {Array<number>} */
        const flatCoordinates = [];
        const ends = [];
        for (let i = 0, ii = lineStrings.length; i < ii; ++i) {
          const lineString = lineStrings[i];
          extend$2(flatCoordinates, lineString.getFlatCoordinates());
          ends.push(flatCoordinates.length);
        }
        const layout =
          lineStrings.length === 0
            ? this.getLayout()
            : lineStrings[0].getLayout();
        this.setFlatCoordinates(layout, flatCoordinates);
        this.ends_ = ends;
      }
    }

    /**
     * Append the passed linestring to the multilinestring.
     * @param {LineString} lineString LineString.
     * @api
     */
    appendLineString(lineString) {
      extend$2(this.flatCoordinates, lineString.getFlatCoordinates().slice());
      this.ends_.push(this.flatCoordinates.length);
      this.changed();
    }

    /**
     * Make a complete copy of the geometry.
     * @return {!MultiLineString} Clone.
     * @api
     * @override
     */
    clone() {
      const multiLineString = new MultiLineString(
        this.flatCoordinates.slice(),
        this.layout,
        this.ends_.slice(),
      );
      multiLineString.applyProperties(this);
      return multiLineString;
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @param {import("../coordinate.js").Coordinate} closestPoint Closest point.
     * @param {number} minSquaredDistance Minimum squared distance.
     * @return {number} Minimum squared distance.
     * @override
     */
    closestPointXY(x, y, closestPoint, minSquaredDistance) {
      if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) {
        return minSquaredDistance;
      }
      if (this.maxDeltaRevision_ != this.getRevision()) {
        this.maxDelta_ = Math.sqrt(
          arrayMaxSquaredDelta(
            this.flatCoordinates,
            0,
            this.ends_,
            this.stride,
            0,
          ),
        );
        this.maxDeltaRevision_ = this.getRevision();
      }
      return assignClosestArrayPoint(
        this.flatCoordinates,
        0,
        this.ends_,
        this.stride,
        this.maxDelta_,
        false,
        x,
        y,
        closestPoint,
        minSquaredDistance,
      );
    }

    /**
     * Returns the coordinate at `m` using linear interpolation, or `null` if no
     * such coordinate exists.
     *
     * `extrapolate` controls extrapolation beyond the range of Ms in the
     * MultiLineString. If `extrapolate` is `true` then Ms less than the first
     * M will return the first coordinate and Ms greater than the last M will
     * return the last coordinate.
     *
     * `interpolate` controls interpolation between consecutive LineStrings
     * within the MultiLineString. If `interpolate` is `true` the coordinates
     * will be linearly interpolated between the last coordinate of one LineString
     * and the first coordinate of the next LineString.  If `interpolate` is
     * `false` then the function will return `null` for Ms falling between
     * LineStrings.
     *
     * @param {number} m M.
     * @param {boolean} [extrapolate] Extrapolate. Default is `false`.
     * @param {boolean} [interpolate] Interpolate. Default is `false`.
     * @return {import("../coordinate.js").Coordinate|null} Coordinate.
     * @api
     */
    getCoordinateAtM(m, extrapolate, interpolate) {
      if (
        (this.layout != 'XYM' && this.layout != 'XYZM') ||
        this.flatCoordinates.length === 0
      ) {
        return null;
      }
      extrapolate = extrapolate !== undefined ? extrapolate : false;
      interpolate = interpolate !== undefined ? interpolate : false;
      return lineStringsCoordinateAtM(
        this.flatCoordinates,
        0,
        this.ends_,
        this.stride,
        m,
        extrapolate,
        interpolate,
      );
    }

    /**
     * Return the coordinates of the multilinestring.
     * @return {Array<Array<import("../coordinate.js").Coordinate>>} Coordinates.
     * @api
     * @override
     */
    getCoordinates() {
      return inflateCoordinatesArray(
        this.flatCoordinates,
        0,
        this.ends_,
        this.stride,
      );
    }

    /**
     * @return {Array<number>} Ends.
     */
    getEnds() {
      return this.ends_;
    }

    /**
     * Return the linestring at the specified index.
     * @param {number} index Index.
     * @return {LineString} LineString.
     * @api
     */
    getLineString(index) {
      if (index < 0 || this.ends_.length <= index) {
        return null;
      }
      return new LineString(
        this.flatCoordinates.slice(
          index === 0 ? 0 : this.ends_[index - 1],
          this.ends_[index],
        ),
        this.layout,
      );
    }

    /**
     * Return the linestrings of this multilinestring.
     * @return {Array<LineString>} LineStrings.
     * @api
     */
    getLineStrings() {
      const flatCoordinates = this.flatCoordinates;
      const ends = this.ends_;
      const layout = this.layout;
      /** @type {Array<LineString>} */
      const lineStrings = [];
      let offset = 0;
      for (let i = 0, ii = ends.length; i < ii; ++i) {
        const end = ends[i];
        const lineString = new LineString(
          flatCoordinates.slice(offset, end),
          layout,
        );
        lineStrings.push(lineString);
        offset = end;
      }
      return lineStrings;
    }

    /**
     * Return the sum of all line string lengths
     * @return {number} Length (on projected plane).
     * @api
     */
    getLength() {
      const ends = this.ends_;
      let start = 0;
      let length = 0;
      for (let i = 0, ii = ends.length; i < ii; ++i) {
        length += lineStringLength(
          this.flatCoordinates,
          start,
          ends[i],
          this.stride,
        );
        start = ends[i];
      }
      return length;
    }

    /**
     * @return {Array<number>} Flat midpoints.
     */
    getFlatMidpoints() {
      /** @type {Array<number>} */
      const midpoints = [];
      const flatCoordinates = this.flatCoordinates;
      let offset = 0;
      const ends = this.ends_;
      const stride = this.stride;
      for (let i = 0, ii = ends.length; i < ii; ++i) {
        const end = ends[i];
        const midpoint = interpolatePoint(
          flatCoordinates,
          offset,
          end,
          stride,
          0.5,
        );
        extend$2(midpoints, midpoint);
        offset = end;
      }
      return midpoints;
    }

    /**
     * @param {number} squaredTolerance Squared tolerance.
     * @return {MultiLineString} Simplified MultiLineString.
     * @protected
     * @override
     */
    getSimplifiedGeometryInternal(squaredTolerance) {
      /** @type {Array<number>} */
      const simplifiedFlatCoordinates = [];
      /** @type {Array<number>} */
      const simplifiedEnds = [];
      simplifiedFlatCoordinates.length = douglasPeuckerArray(
        this.flatCoordinates,
        0,
        this.ends_,
        this.stride,
        squaredTolerance,
        simplifiedFlatCoordinates,
        0,
        simplifiedEnds,
      );
      return new MultiLineString(simplifiedFlatCoordinates, 'XY', simplifiedEnds);
    }

    /**
     * Get the type of this geometry.
     * @return {import("./Geometry.js").Type} Geometry type.
     * @api
     * @override
     */
    getType() {
      return 'MultiLineString';
    }

    /**
     * Test if the geometry and the passed extent intersect.
     * @param {import("../extent.js").Extent} extent Extent.
     * @return {boolean} `true` if the geometry and the extent intersect.
     * @api
     * @override
     */
    intersectsExtent(extent) {
      return intersectsLineStringArray(
        this.flatCoordinates,
        0,
        this.ends_,
        this.stride,
        extent,
      );
    }

    /**
     * Set the coordinates of the multilinestring.
     * @param {!Array<Array<import("../coordinate.js").Coordinate>>} coordinates Coordinates.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     * @api
     * @override
     */
    setCoordinates(coordinates, layout) {
      this.setLayout(layout, coordinates, 2);
      if (!this.flatCoordinates) {
        this.flatCoordinates = [];
      }
      const ends = deflateCoordinatesArray(
        this.flatCoordinates,
        0,
        coordinates,
        this.stride,
        this.ends_,
      );
      this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1];
      this.changed();
    }
  }

  /**
   * @module ol/geom/MultiPoint
   */

  /**
   * @classdesc
   * Multi-point geometry.
   *
   * @api
   */
  class MultiPoint extends SimpleGeometry {
    /**
     * @param {Array<import("../coordinate.js").Coordinate>|Array<number>} coordinates Coordinates.
     *     For internal use, flat coordinates in combination with `layout` are also accepted.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     */
    constructor(coordinates, layout) {
      super();
      if (layout && !Array.isArray(coordinates[0])) {
        this.setFlatCoordinates(
          layout,
          /** @type {Array<number>} */ (coordinates),
        );
      } else {
        this.setCoordinates(
          /** @type {Array<import("../coordinate.js").Coordinate>} */ (
            coordinates
          ),
          layout,
        );
      }
    }

    /**
     * Append the passed point to this multipoint.
     * @param {Point} point Point.
     * @api
     */
    appendPoint(point) {
      extend$2(this.flatCoordinates, point.getFlatCoordinates());
      this.changed();
    }

    /**
     * Make a complete copy of the geometry.
     * @return {!MultiPoint} Clone.
     * @api
     * @override
     */
    clone() {
      const multiPoint = new MultiPoint(
        this.flatCoordinates.slice(),
        this.layout,
      );
      multiPoint.applyProperties(this);
      return multiPoint;
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @param {import("../coordinate.js").Coordinate} closestPoint Closest point.
     * @param {number} minSquaredDistance Minimum squared distance.
     * @return {number} Minimum squared distance.
     * @override
     */
    closestPointXY(x, y, closestPoint, minSquaredDistance) {
      if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) {
        return minSquaredDistance;
      }
      const flatCoordinates = this.flatCoordinates;
      const stride = this.stride;
      for (let i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
        const squaredDistance$1 = squaredDistance(
          x,
          y,
          flatCoordinates[i],
          flatCoordinates[i + 1],
        );
        if (squaredDistance$1 < minSquaredDistance) {
          minSquaredDistance = squaredDistance$1;
          for (let j = 0; j < stride; ++j) {
            closestPoint[j] = flatCoordinates[i + j];
          }
          closestPoint.length = stride;
        }
      }
      return minSquaredDistance;
    }

    /**
     * Return the coordinates of the multipoint.
     * @return {Array<import("../coordinate.js").Coordinate>} Coordinates.
     * @api
     * @override
     */
    getCoordinates() {
      return inflateCoordinates(
        this.flatCoordinates,
        0,
        this.flatCoordinates.length,
        this.stride,
      );
    }

    /**
     * Return the point at the specified index.
     * @param {number} index Index.
     * @return {Point} Point.
     * @api
     */
    getPoint(index) {
      const n = this.flatCoordinates.length / this.stride;
      if (index < 0 || n <= index) {
        return null;
      }
      return new Point(
        this.flatCoordinates.slice(
          index * this.stride,
          (index + 1) * this.stride,
        ),
        this.layout,
      );
    }

    /**
     * Return the points of this multipoint.
     * @return {Array<Point>} Points.
     * @api
     */
    getPoints() {
      const flatCoordinates = this.flatCoordinates;
      const layout = this.layout;
      const stride = this.stride;
      /** @type {Array<Point>} */
      const points = [];
      for (let i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
        const point = new Point(flatCoordinates.slice(i, i + stride), layout);
        points.push(point);
      }
      return points;
    }

    /**
     * Get the type of this geometry.
     * @return {import("./Geometry.js").Type} Geometry type.
     * @api
     * @override
     */
    getType() {
      return 'MultiPoint';
    }

    /**
     * Test if the geometry and the passed extent intersect.
     * @param {import("../extent.js").Extent} extent Extent.
     * @return {boolean} `true` if the geometry and the extent intersect.
     * @api
     * @override
     */
    intersectsExtent(extent) {
      const flatCoordinates = this.flatCoordinates;
      const stride = this.stride;
      for (let i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
        const x = flatCoordinates[i];
        const y = flatCoordinates[i + 1];
        if (containsXY(extent, x, y)) {
          return true;
        }
      }
      return false;
    }

    /**
     * Set the coordinates of the multipoint.
     * @param {!Array<import("../coordinate.js").Coordinate>} coordinates Coordinates.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     * @api
     * @override
     */
    setCoordinates(coordinates, layout) {
      this.setLayout(layout, coordinates, 1);
      if (!this.flatCoordinates) {
        this.flatCoordinates = [];
      }
      this.flatCoordinates.length = deflateCoordinates(
        this.flatCoordinates,
        0,
        coordinates,
        this.stride,
      );
      this.changed();
    }
  }

  /**
   * @module ol/geom/MultiPolygon
   */

  /**
   * @classdesc
   * Multi-polygon geometry.
   *
   * @api
   */
  class MultiPolygon extends SimpleGeometry {
    /**
     * @param {Array<Array<Array<import("../coordinate.js").Coordinate>>|Polygon>|Array<number>} coordinates Coordinates.
     *     For internal use, flat coordinates in combination with `layout` and `endss` are also accepted.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     * @param {Array<Array<number>>} [endss] Array of ends for internal use with flat coordinates.
     */
    constructor(coordinates, layout, endss) {
      super();

      /**
       * @type {Array<Array<number>>}
       * @private
       */
      this.endss_ = [];

      /**
       * @private
       * @type {number}
       */
      this.flatInteriorPointsRevision_ = -1;

      /**
       * @private
       * @type {Array<number>|null}
       */
      this.flatInteriorPoints_ = null;

      /**
       * @private
       * @type {number}
       */
      this.maxDelta_ = -1;

      /**
       * @private
       * @type {number}
       */
      this.maxDeltaRevision_ = -1;

      /**
       * @private
       * @type {number}
       */
      this.orientedRevision_ = -1;

      /**
       * @private
       * @type {Array<number>|null}
       */
      this.orientedFlatCoordinates_ = null;

      if (!endss && !Array.isArray(coordinates[0])) {
        const polygons = /** @type {Array<Polygon>} */ (coordinates);
        /** @type {Array<number>} */
        const flatCoordinates = [];
        const thisEndss = [];
        for (let i = 0, ii = polygons.length; i < ii; ++i) {
          const polygon = polygons[i];
          const offset = flatCoordinates.length;
          const ends = polygon.getEnds();
          for (let j = 0, jj = ends.length; j < jj; ++j) {
            ends[j] += offset;
          }
          extend$2(flatCoordinates, polygon.getFlatCoordinates());
          thisEndss.push(ends);
        }
        layout =
          polygons.length === 0 ? this.getLayout() : polygons[0].getLayout();
        coordinates = flatCoordinates;
        endss = thisEndss;
      }
      if (layout !== undefined && endss) {
        this.setFlatCoordinates(
          layout,
          /** @type {Array<number>} */ (coordinates),
        );
        this.endss_ = endss;
      } else {
        this.setCoordinates(
          /** @type {Array<Array<Array<import("../coordinate.js").Coordinate>>>} */ (
            coordinates
          ),
          layout,
        );
      }
    }

    /**
     * Append the passed polygon to this multipolygon.
     * @param {Polygon} polygon Polygon.
     * @api
     */
    appendPolygon(polygon) {
      /** @type {Array<number>} */
      let ends;
      if (!this.flatCoordinates) {
        this.flatCoordinates = polygon.getFlatCoordinates().slice();
        ends = polygon.getEnds().slice();
        this.endss_.push();
      } else {
        const offset = this.flatCoordinates.length;
        extend$2(this.flatCoordinates, polygon.getFlatCoordinates());
        ends = polygon.getEnds().slice();
        for (let i = 0, ii = ends.length; i < ii; ++i) {
          ends[i] += offset;
        }
      }
      this.endss_.push(ends);
      this.changed();
    }

    /**
     * Make a complete copy of the geometry.
     * @return {!MultiPolygon} Clone.
     * @api
     * @override
     */
    clone() {
      const len = this.endss_.length;
      const newEndss = new Array(len);
      for (let i = 0; i < len; ++i) {
        newEndss[i] = this.endss_[i].slice();
      }

      const multiPolygon = new MultiPolygon(
        this.flatCoordinates.slice(),
        this.layout,
        newEndss,
      );
      multiPolygon.applyProperties(this);

      return multiPolygon;
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @param {import("../coordinate.js").Coordinate} closestPoint Closest point.
     * @param {number} minSquaredDistance Minimum squared distance.
     * @return {number} Minimum squared distance.
     * @override
     */
    closestPointXY(x, y, closestPoint, minSquaredDistance) {
      if (minSquaredDistance < closestSquaredDistanceXY(this.getExtent(), x, y)) {
        return minSquaredDistance;
      }
      if (this.maxDeltaRevision_ != this.getRevision()) {
        this.maxDelta_ = Math.sqrt(
          multiArrayMaxSquaredDelta(
            this.flatCoordinates,
            0,
            this.endss_,
            this.stride,
            0,
          ),
        );
        this.maxDeltaRevision_ = this.getRevision();
      }
      return assignClosestMultiArrayPoint(
        this.getOrientedFlatCoordinates(),
        0,
        this.endss_,
        this.stride,
        this.maxDelta_,
        true,
        x,
        y,
        closestPoint,
        minSquaredDistance,
      );
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     * @return {boolean} Contains (x, y).
     * @override
     */
    containsXY(x, y) {
      return linearRingssContainsXY(
        this.getOrientedFlatCoordinates(),
        0,
        this.endss_,
        this.stride,
        x,
        y,
      );
    }

    /**
     * Return the area of the multipolygon on projected plane.
     * @return {number} Area (on projected plane).
     * @api
     */
    getArea() {
      return linearRingss$1(
        this.getOrientedFlatCoordinates(),
        0,
        this.endss_,
        this.stride,
      );
    }

    /**
     * Get the coordinate array for this geometry.  This array has the structure
     * of a GeoJSON coordinate array for multi-polygons.
     *
     * @param {boolean} [right] Orient coordinates according to the right-hand
     *     rule (counter-clockwise for exterior and clockwise for interior rings).
     *     If `false`, coordinates will be oriented according to the left-hand rule
     *     (clockwise for exterior and counter-clockwise for interior rings).
     *     By default, coordinate orientation will depend on how the geometry was
     *     constructed.
     * @return {Array<Array<Array<import("../coordinate.js").Coordinate>>>} Coordinates.
     * @api
     * @override
     */
    getCoordinates(right) {
      let flatCoordinates;
      if (right !== undefined) {
        flatCoordinates = this.getOrientedFlatCoordinates().slice();
        orientLinearRingsArray(
          flatCoordinates,
          0,
          this.endss_,
          this.stride,
          right,
        );
      } else {
        flatCoordinates = this.flatCoordinates;
      }

      return inflateMultiCoordinatesArray(
        flatCoordinates,
        0,
        this.endss_,
        this.stride,
      );
    }

    /**
     * @return {Array<Array<number>>} Endss.
     */
    getEndss() {
      return this.endss_;
    }

    /**
     * @return {Array<number>} Flat interior points.
     */
    getFlatInteriorPoints() {
      if (this.flatInteriorPointsRevision_ != this.getRevision()) {
        const flatCenters = linearRingss(
          this.flatCoordinates,
          0,
          this.endss_,
          this.stride,
        );
        this.flatInteriorPoints_ = getInteriorPointsOfMultiArray(
          this.getOrientedFlatCoordinates(),
          0,
          this.endss_,
          this.stride,
          flatCenters,
        );
        this.flatInteriorPointsRevision_ = this.getRevision();
      }
      return /** @type {Array<number>} */ (this.flatInteriorPoints_);
    }

    /**
     * Return the interior points as {@link module:ol/geom/MultiPoint~MultiPoint multipoint}.
     * @return {MultiPoint} Interior points as XYM coordinates, where M is
     * the length of the horizontal intersection that the point belongs to.
     * @api
     */
    getInteriorPoints() {
      return new MultiPoint(this.getFlatInteriorPoints().slice(), 'XYM');
    }

    /**
     * @return {Array<number>} Oriented flat coordinates.
     */
    getOrientedFlatCoordinates() {
      if (this.orientedRevision_ != this.getRevision()) {
        const flatCoordinates = this.flatCoordinates;
        if (
          linearRingssAreOriented(flatCoordinates, 0, this.endss_, this.stride)
        ) {
          this.orientedFlatCoordinates_ = flatCoordinates;
        } else {
          this.orientedFlatCoordinates_ = flatCoordinates.slice();
          this.orientedFlatCoordinates_.length = orientLinearRingsArray(
            this.orientedFlatCoordinates_,
            0,
            this.endss_,
            this.stride,
          );
        }
        this.orientedRevision_ = this.getRevision();
      }
      return /** @type {Array<number>} */ (this.orientedFlatCoordinates_);
    }

    /**
     * @param {number} squaredTolerance Squared tolerance.
     * @return {MultiPolygon} Simplified MultiPolygon.
     * @protected
     * @override
     */
    getSimplifiedGeometryInternal(squaredTolerance) {
      /** @type {Array<number>} */
      const simplifiedFlatCoordinates = [];
      /** @type {Array<Array<number>>} */
      const simplifiedEndss = [];
      simplifiedFlatCoordinates.length = quantizeMultiArray(
        this.flatCoordinates,
        0,
        this.endss_,
        this.stride,
        Math.sqrt(squaredTolerance),
        simplifiedFlatCoordinates,
        0,
        simplifiedEndss,
      );
      return new MultiPolygon(simplifiedFlatCoordinates, 'XY', simplifiedEndss);
    }

    /**
     * Return the polygon at the specified index.
     * @param {number} index Index.
     * @return {Polygon} Polygon.
     * @api
     */
    getPolygon(index) {
      if (index < 0 || this.endss_.length <= index) {
        return null;
      }
      let offset;
      if (index === 0) {
        offset = 0;
      } else {
        const prevEnds = this.endss_[index - 1];
        offset = prevEnds[prevEnds.length - 1];
      }
      const ends = this.endss_[index].slice();
      const end = ends[ends.length - 1];
      if (offset !== 0) {
        for (let i = 0, ii = ends.length; i < ii; ++i) {
          ends[i] -= offset;
        }
      }
      return new Polygon(
        this.flatCoordinates.slice(offset, end),
        this.layout,
        ends,
      );
    }

    /**
     * Return the polygons of this multipolygon.
     * @return {Array<Polygon>} Polygons.
     * @api
     */
    getPolygons() {
      const layout = this.layout;
      const flatCoordinates = this.flatCoordinates;
      const endss = this.endss_;
      const polygons = [];
      let offset = 0;
      for (let i = 0, ii = endss.length; i < ii; ++i) {
        const ends = endss[i].slice();
        const end = ends[ends.length - 1];
        if (offset !== 0) {
          for (let j = 0, jj = ends.length; j < jj; ++j) {
            ends[j] -= offset;
          }
        }
        const polygon = new Polygon(
          flatCoordinates.slice(offset, end),
          layout,
          ends,
        );
        polygons.push(polygon);
        offset = end;
      }
      return polygons;
    }

    /**
     * Get the type of this geometry.
     * @return {import("./Geometry.js").Type} Geometry type.
     * @api
     * @override
     */
    getType() {
      return 'MultiPolygon';
    }

    /**
     * Test if the geometry and the passed extent intersect.
     * @param {import("../extent.js").Extent} extent Extent.
     * @return {boolean} `true` if the geometry and the extent intersect.
     * @api
     * @override
     */
    intersectsExtent(extent) {
      return intersectsLinearRingMultiArray(
        this.getOrientedFlatCoordinates(),
        0,
        this.endss_,
        this.stride,
        extent,
      );
    }

    /**
     * Set the coordinates of the multipolygon.
     * @param {!Array<Array<Array<import("../coordinate.js").Coordinate>>>} coordinates Coordinates.
     * @param {import("./Geometry.js").GeometryLayout} [layout] Layout.
     * @api
     * @override
     */
    setCoordinates(coordinates, layout) {
      this.setLayout(layout, coordinates, 3);
      if (!this.flatCoordinates) {
        this.flatCoordinates = [];
      }
      const endss = deflateMultiCoordinatesArray(
        this.flatCoordinates,
        0,
        coordinates,
        this.stride,
        this.endss_,
      );
      if (endss.length === 0) {
        this.flatCoordinates.length = 0;
      } else {
        const lastEnds = endss[endss.length - 1];
        this.flatCoordinates.length =
          lastEnds.length === 0 ? 0 : lastEnds[lastEnds.length - 1];
      }
      this.changed();
    }
  }

  /**
   * @module ol/render/Feature
   */

  /**
   * @typedef {'Point' | 'LineString' | 'LinearRing' | 'Polygon' | 'MultiPoint' | 'MultiLineString'} Type
   * The geometry type.  One of `'Point'`, `'LineString'`, `'LinearRing'`,
   * `'Polygon'`, `'MultiPoint'` or 'MultiLineString'`.
   */

  /**
   * @type {import("../transform.js").Transform}
   */
  const tmpTransform = create();

  /**
   * Lightweight, read-only, {@link module:ol/Feature~Feature} and {@link module:ol/geom/Geometry~Geometry} like
   * structure, optimized for vector tile rendering and styling. Geometry access
   * through the API is limited to getting the type and extent of the geometry.
   */
  class RenderFeature {
    /**
     * @param {Type} type Geometry type.
     * @param {Array<number>} flatCoordinates Flat coordinates. These always need
     *     to be right-handed for polygons.
     * @param {Array<number>} ends Ends.
     * @param {number} stride Stride.
     * @param {Object<string, *>} properties Properties.
     * @param {number|string|undefined} id Feature id.
     */
    constructor(type, flatCoordinates, ends, stride, properties, id) {
      /**
       * @type {import("../style/Style.js").StyleFunction|undefined}
       */
      this.styleFunction;

      /**
       * @private
       * @type {import("../extent.js").Extent|undefined}
       */
      this.extent_;

      /**
       * @private
       * @type {number|string|undefined}
       */
      this.id_ = id;

      /**
       * @private
       * @type {Type}
       */
      this.type_ = type;

      /**
       * @private
       * @type {Array<number>}
       */
      this.flatCoordinates_ = flatCoordinates;

      /**
       * @private
       * @type {Array<number>}
       */
      this.flatInteriorPoints_ = null;

      /**
       * @private
       * @type {Array<number>}
       */
      this.flatMidpoints_ = null;

      /**
       * @private
       * @type {Array<number>|null}
       */
      this.ends_ = ends || null;

      /**
       * @private
       * @type {Object<string, *>}
       */
      this.properties_ = properties;

      /**
       * @private
       * @type {number}
       */
      this.squaredTolerance_;

      /**
       * @private
       * @type {number}
       */
      this.stride_ = stride;

      /**
       * @private
       * @type {RenderFeature}
       */
      this.simplifiedGeometry_;
    }

    /**
     * Get a feature property by its key.
     * @param {string} key Key
     * @return {*} Value for the requested key.
     * @api
     */
    get(key) {
      return this.properties_[key];
    }

    /**
     * Get the extent of this feature's geometry.
     * @return {import("../extent.js").Extent} Extent.
     * @api
     */
    getExtent() {
      if (!this.extent_) {
        this.extent_ =
          this.type_ === 'Point'
            ? createOrUpdateFromCoordinate(this.flatCoordinates_)
            : createOrUpdateFromFlatCoordinates(
                this.flatCoordinates_,
                0,
                this.flatCoordinates_.length,
                2,
              );
      }
      return this.extent_;
    }

    /**
     * @return {Array<number>} Flat interior points.
     */
    getFlatInteriorPoint() {
      if (!this.flatInteriorPoints_) {
        const flatCenter = getCenter(this.getExtent());
        this.flatInteriorPoints_ = getInteriorPointOfArray(
          this.flatCoordinates_,
          0,
          this.ends_,
          2,
          flatCenter,
          0,
        );
      }
      return this.flatInteriorPoints_;
    }

    /**
     * @return {Array<number>} Flat interior points.
     */
    getFlatInteriorPoints() {
      if (!this.flatInteriorPoints_) {
        const ends = inflateEnds(this.flatCoordinates_, this.ends_);
        const flatCenters = linearRingss(this.flatCoordinates_, 0, ends, 2);
        this.flatInteriorPoints_ = getInteriorPointsOfMultiArray(
          this.flatCoordinates_,
          0,
          ends,
          2,
          flatCenters,
        );
      }
      return this.flatInteriorPoints_;
    }

    /**
     * @return {Array<number>} Flat midpoint.
     */
    getFlatMidpoint() {
      if (!this.flatMidpoints_) {
        this.flatMidpoints_ = interpolatePoint(
          this.flatCoordinates_,
          0,
          this.flatCoordinates_.length,
          2,
          0.5,
        );
      }
      return this.flatMidpoints_;
    }

    /**
     * @return {Array<number>} Flat midpoints.
     */
    getFlatMidpoints() {
      if (!this.flatMidpoints_) {
        this.flatMidpoints_ = [];
        const flatCoordinates = this.flatCoordinates_;
        let offset = 0;
        const ends = /** @type {Array<number>} */ (this.ends_);
        for (let i = 0, ii = ends.length; i < ii; ++i) {
          const end = ends[i];
          const midpoint = interpolatePoint(flatCoordinates, offset, end, 2, 0.5);
          extend$2(this.flatMidpoints_, midpoint);
          offset = end;
        }
      }
      return this.flatMidpoints_;
    }

    /**
     * Get the feature identifier.  This is a stable identifier for the feature and
     * is set when reading data from a remote source.
     * @return {number|string|undefined} Id.
     * @api
     */
    getId() {
      return this.id_;
    }

    /**
     * @return {Array<number>} Flat coordinates.
     */
    getOrientedFlatCoordinates() {
      return this.flatCoordinates_;
    }

    /**
     * For API compatibility with {@link module:ol/Feature~Feature}, this method is useful when
     * determining the geometry type in style function (see {@link #getType}).
     * @return {RenderFeature} Feature.
     * @api
     */
    getGeometry() {
      return this;
    }

    /**
     * @param {number} squaredTolerance Squared tolerance.
     * @return {RenderFeature} Simplified geometry.
     */
    getSimplifiedGeometry(squaredTolerance) {
      return this;
    }

    /**
     * Get a transformed and simplified version of the geometry.
     * @param {number} squaredTolerance Squared tolerance.
     * @param {import("../proj.js").TransformFunction} [transform] Optional transform function.
     * @return {RenderFeature} Simplified geometry.
     */
    simplifyTransformed(squaredTolerance, transform) {
      return this;
    }

    /**
     * Get the feature properties.
     * @return {Object<string, *>} Feature properties.
     * @api
     */
    getProperties() {
      return this.properties_;
    }

    /**
     * Get an object of all property names and values.  This has the same behavior as getProperties,
     * but is here to conform with the {@link module:ol/Feature~Feature} interface.
     * @return {Object<string, *>?} Object.
     */
    getPropertiesInternal() {
      return this.properties_;
    }

    /**
     * @return {number} Stride.
     */
    getStride() {
      return this.stride_;
    }

    /**
     * @return {import('../style/Style.js').StyleFunction|undefined} Style
     */
    getStyleFunction() {
      return this.styleFunction;
    }

    /**
     * Get the type of this feature's geometry.
     * @return {Type} Geometry type.
     * @api
     */
    getType() {
      return this.type_;
    }

    /**
     * Transform geometry coordinates from tile pixel space to projected.
     *
     * @param {import("../proj.js").ProjectionLike} projection The data projection
     */
    transform(projection) {
      projection = get$1(projection);
      const pixelExtent = projection.getExtent();
      const projectedExtent = projection.getWorldExtent();
      if (pixelExtent && projectedExtent) {
        const scale = getHeight(projectedExtent) / getHeight(pixelExtent);
        compose(
          tmpTransform,
          projectedExtent[0],
          projectedExtent[3],
          scale,
          -scale,
          0,
          0,
          0,
        );
        transform2D(
          this.flatCoordinates_,
          0,
          this.flatCoordinates_.length,
          2,
          tmpTransform,
          this.flatCoordinates_,
        );
      }
    }

    /**
     * Apply a transform function to the coordinates of the geometry.
     * The geometry is modified in place.
     * If you do not want the geometry modified in place, first `clone()` it and
     * then use this function on the clone.
     * @param {import("../proj.js").TransformFunction} transformFn Transform function.
     */
    applyTransform(transformFn) {
      transformFn(this.flatCoordinates_, this.flatCoordinates_, this.stride_);
    }

    /**
     * @return {RenderFeature} A cloned render feature.
     */
    clone() {
      return new RenderFeature(
        this.type_,
        this.flatCoordinates_.slice(),
        this.ends_?.slice(),
        this.stride_,
        Object.assign({}, this.properties_),
        this.id_,
      );
    }

    /**
     * @return {Array<number>|null} Ends.
     */
    getEnds() {
      return this.ends_;
    }

    /**
     * Add transform and resolution based geometry simplification to this instance.
     * @return {RenderFeature} This render feature.
     */
    enableSimplifyTransformed() {
      this.simplifyTransformed = memoizeOne((squaredTolerance, transform) => {
        if (squaredTolerance === this.squaredTolerance_) {
          return this.simplifiedGeometry_;
        }
        this.simplifiedGeometry_ = this.clone();
        if (transform) {
          this.simplifiedGeometry_.applyTransform(transform);
        }
        const simplifiedFlatCoordinates =
          this.simplifiedGeometry_.getFlatCoordinates();
        let simplifiedEnds;
        switch (this.type_) {
          case 'LineString':
            simplifiedFlatCoordinates.length = douglasPeucker(
              simplifiedFlatCoordinates,
              0,
              this.simplifiedGeometry_.flatCoordinates_.length,
              this.simplifiedGeometry_.stride_,
              squaredTolerance,
              simplifiedFlatCoordinates,
              0,
            );
            simplifiedEnds = [simplifiedFlatCoordinates.length];
            break;
          case 'MultiLineString':
            simplifiedEnds = [];
            simplifiedFlatCoordinates.length = douglasPeuckerArray(
              simplifiedFlatCoordinates,
              0,
              this.simplifiedGeometry_.ends_,
              this.simplifiedGeometry_.stride_,
              squaredTolerance,
              simplifiedFlatCoordinates,
              0,
              simplifiedEnds,
            );
            break;
          case 'Polygon':
            simplifiedEnds = [];
            simplifiedFlatCoordinates.length = quantizeArray(
              simplifiedFlatCoordinates,
              0,
              this.simplifiedGeometry_.ends_,
              this.simplifiedGeometry_.stride_,
              Math.sqrt(squaredTolerance),
              simplifiedFlatCoordinates,
              0,
              simplifiedEnds,
            );
            break;
        }
        if (simplifiedEnds) {
          this.simplifiedGeometry_ = new RenderFeature(
            this.type_,
            simplifiedFlatCoordinates,
            simplifiedEnds,
            2,
            this.properties_,
            this.id_,
          );
        }
        this.squaredTolerance_ = squaredTolerance;
        return this.simplifiedGeometry_;
      });
      return this;
    }
  }

  /**
   * @return {Array<number>} Flat coordinates.
   */
  RenderFeature.prototype.getFlatCoordinates =
    RenderFeature.prototype.getOrientedFlatCoordinates;

  /**
   * Rearranges items so that all items in the [left, k] are the smallest.
   * The k-th element will have the (k - left + 1)-th smallest value in [left, right].
   *
   * @template T
   * @param {T[]} arr the array to partially sort (in place)
   * @param {number} k middle index for partial sorting (as defined above)
   * @param {number} [left=0] left index of the range to sort
   * @param {number} [right=arr.length-1] right index
   * @param {(a: T, b: T) => number} [compare = (a, b) => a - b] compare function
   */
  function quickselect(arr, k, left = 0, right = arr.length - 1, compare = defaultCompare) {

      while (right > left) {
          if (right - left > 600) {
              const n = right - left + 1;
              const m = k - left + 1;
              const z = Math.log(n);
              const s = 0.5 * Math.exp(2 * z / 3);
              const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
              const newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
              const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
              quickselect(arr, k, newLeft, newRight, compare);
          }

          const t = arr[k];
          let i = left;
          /** @type {number} */
          let j = right;

          swap(arr, left, k);
          if (compare(arr[right], t) > 0) swap(arr, left, right);

          while (i < j) {
              swap(arr, i, j);
              i++;
              j--;
              while (compare(arr[i], t) < 0) i++;
              while (compare(arr[j], t) > 0) j--;
          }

          if (compare(arr[left], t) === 0) swap(arr, left, j);
          else {
              j++;
              swap(arr, j, right);
          }

          if (j <= k) left = j + 1;
          if (k <= j) right = j - 1;
      }
  }

  /**
   * @template T
   * @param {T[]} arr
   * @param {number} i
   * @param {number} j
   */
  function swap(arr, i, j) {
      const tmp = arr[i];
      arr[i] = arr[j];
      arr[j] = tmp;
  }

  /**
   * @template T
   * @param {T} a
   * @param {T} b
   * @returns {number}
   */
  function defaultCompare(a, b) {
      return a < b ? -1 : a > b ? 1 : 0;
  }

  let RBush$1 = class RBush {
      constructor(maxEntries = 9) {
          // max entries in a node is 9 by default; min node fill is 40% for best performance
          this._maxEntries = Math.max(4, maxEntries);
          this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
          this.clear();
      }

      all() {
          return this._all(this.data, []);
      }

      search(bbox) {
          let node = this.data;
          const result = [];

          if (!intersects(bbox, node)) return result;

          const toBBox = this.toBBox;
          const nodesToSearch = [];

          while (node) {
              for (let i = 0; i < node.children.length; i++) {
                  const child = node.children[i];
                  const childBBox = node.leaf ? toBBox(child) : child;

                  if (intersects(bbox, childBBox)) {
                      if (node.leaf) result.push(child);
                      else if (contains(bbox, childBBox)) this._all(child, result);
                      else nodesToSearch.push(child);
                  }
              }
              node = nodesToSearch.pop();
          }

          return result;
      }

      collides(bbox) {
          let node = this.data;

          if (!intersects(bbox, node)) return false;

          const nodesToSearch = [];
          while (node) {
              for (let i = 0; i < node.children.length; i++) {
                  const child = node.children[i];
                  const childBBox = node.leaf ? this.toBBox(child) : child;

                  if (intersects(bbox, childBBox)) {
                      if (node.leaf || contains(bbox, childBBox)) return true;
                      nodesToSearch.push(child);
                  }
              }
              node = nodesToSearch.pop();
          }

          return false;
      }

      load(data) {
          if (!(data && data.length)) return this;

          if (data.length < this._minEntries) {
              for (let i = 0; i < data.length; i++) {
                  this.insert(data[i]);
              }
              return this;
          }

          // recursively build the tree with the given data from scratch using OMT algorithm
          let node = this._build(data.slice(), 0, data.length - 1, 0);

          if (!this.data.children.length) {
              // save as is if tree is empty
              this.data = node;

          } else if (this.data.height === node.height) {
              // split root if trees have the same height
              this._splitRoot(this.data, node);

          } else {
              if (this.data.height < node.height) {
                  // swap trees if inserted one is bigger
                  const tmpNode = this.data;
                  this.data = node;
                  node = tmpNode;
              }

              // insert the small tree into the large tree at appropriate level
              this._insert(node, this.data.height - node.height - 1, true);
          }

          return this;
      }

      insert(item) {
          if (item) this._insert(item, this.data.height - 1);
          return this;
      }

      clear() {
          this.data = createNode([]);
          return this;
      }

      remove(item, equalsFn) {
          if (!item) return this;

          let node = this.data;
          const bbox = this.toBBox(item);
          const path = [];
          const indexes = [];
          let i, parent, goingUp;

          // depth-first iterative tree traversal
          while (node || path.length) {

              if (!node) { // go up
                  node = path.pop();
                  parent = path[path.length - 1];
                  i = indexes.pop();
                  goingUp = true;
              }

              if (node.leaf) { // check current node
                  const index = findItem(item, node.children, equalsFn);

                  if (index !== -1) {
                      // item found, remove the item and condense tree upwards
                      node.children.splice(index, 1);
                      path.push(node);
                      this._condense(path);
                      return this;
                  }
              }

              if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
                  path.push(node);
                  indexes.push(i);
                  i = 0;
                  parent = node;
                  node = node.children[0];

              } else if (parent) { // go right
                  i++;
                  node = parent.children[i];
                  goingUp = false;

              } else node = null; // nothing found
          }

          return this;
      }

      toBBox(item) { return item; }

      compareMinX(a, b) { return a.minX - b.minX; }
      compareMinY(a, b) { return a.minY - b.minY; }

      toJSON() { return this.data; }

      fromJSON(data) {
          this.data = data;
          return this;
      }

      _all(node, result) {
          const nodesToSearch = [];
          while (node) {
              if (node.leaf) result.push(...node.children);
              else nodesToSearch.push(...node.children);

              node = nodesToSearch.pop();
          }
          return result;
      }

      _build(items, left, right, height) {

          const N = right - left + 1;
          let M = this._maxEntries;
          let node;

          if (N <= M) {
              // reached leaf level; return leaf
              node = createNode(items.slice(left, right + 1));
              calcBBox(node, this.toBBox);
              return node;
          }

          if (!height) {
              // target height of the bulk-loaded tree
              height = Math.ceil(Math.log(N) / Math.log(M));

              // target number of root entries to maximize storage utilization
              M = Math.ceil(N / Math.pow(M, height - 1));
          }

          node = createNode([]);
          node.leaf = false;
          node.height = height;

          // split the items into M mostly square tiles

          const N2 = Math.ceil(N / M);
          const N1 = N2 * Math.ceil(Math.sqrt(M));

          multiSelect(items, left, right, N1, this.compareMinX);

          for (let i = left; i <= right; i += N1) {

              const right2 = Math.min(i + N1 - 1, right);

              multiSelect(items, i, right2, N2, this.compareMinY);

              for (let j = i; j <= right2; j += N2) {

                  const right3 = Math.min(j + N2 - 1, right2);

                  // pack each entry recursively
                  node.children.push(this._build(items, j, right3, height - 1));
              }
          }

          calcBBox(node, this.toBBox);

          return node;
      }

      _chooseSubtree(bbox, node, level, path) {
          while (true) {
              path.push(node);

              if (node.leaf || path.length - 1 === level) break;

              let minArea = Infinity;
              let minEnlargement = Infinity;
              let targetNode;

              for (let i = 0; i < node.children.length; i++) {
                  const child = node.children[i];
                  const area = bboxArea(child);
                  const enlargement = enlargedArea(bbox, child) - area;

                  // choose entry with the least area enlargement
                  if (enlargement < minEnlargement) {
                      minEnlargement = enlargement;
                      minArea = area < minArea ? area : minArea;
                      targetNode = child;

                  } else if (enlargement === minEnlargement) {
                      // otherwise choose one with the smallest area
                      if (area < minArea) {
                          minArea = area;
                          targetNode = child;
                      }
                  }
              }

              node = targetNode || node.children[0];
          }

          return node;
      }

      _insert(item, level, isNode) {
          const bbox = isNode ? item : this.toBBox(item);
          const insertPath = [];

          // find the best node for accommodating the item, saving all nodes along the path too
          const node = this._chooseSubtree(bbox, this.data, level, insertPath);

          // put the item into the node
          node.children.push(item);
          extend(node, bbox);

          // split on node overflow; propagate upwards if necessary
          while (level >= 0) {
              if (insertPath[level].children.length > this._maxEntries) {
                  this._split(insertPath, level);
                  level--;
              } else break;
          }

          // adjust bboxes along the insertion path
          this._adjustParentBBoxes(bbox, insertPath, level);
      }

      // split overflowed node into two
      _split(insertPath, level) {
          const node = insertPath[level];
          const M = node.children.length;
          const m = this._minEntries;

          this._chooseSplitAxis(node, m, M);

          const splitIndex = this._chooseSplitIndex(node, m, M);

          const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
          newNode.height = node.height;
          newNode.leaf = node.leaf;

          calcBBox(node, this.toBBox);
          calcBBox(newNode, this.toBBox);

          if (level) insertPath[level - 1].children.push(newNode);
          else this._splitRoot(node, newNode);
      }

      _splitRoot(node, newNode) {
          // split root node
          this.data = createNode([node, newNode]);
          this.data.height = node.height + 1;
          this.data.leaf = false;
          calcBBox(this.data, this.toBBox);
      }

      _chooseSplitIndex(node, m, M) {
          let index;
          let minOverlap = Infinity;
          let minArea = Infinity;

          for (let i = m; i <= M - m; i++) {
              const bbox1 = distBBox(node, 0, i, this.toBBox);
              const bbox2 = distBBox(node, i, M, this.toBBox);

              const overlap = intersectionArea(bbox1, bbox2);
              const area = bboxArea(bbox1) + bboxArea(bbox2);

              // choose distribution with minimum overlap
              if (overlap < minOverlap) {
                  minOverlap = overlap;
                  index = i;

                  minArea = area < minArea ? area : minArea;

              } else if (overlap === minOverlap) {
                  // otherwise choose distribution with minimum area
                  if (area < minArea) {
                      minArea = area;
                      index = i;
                  }
              }
          }

          return index || M - m;
      }

      // sorts node children by the best axis for split
      _chooseSplitAxis(node, m, M) {
          const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
          const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
          const xMargin = this._allDistMargin(node, m, M, compareMinX);
          const yMargin = this._allDistMargin(node, m, M, compareMinY);

          // if total distributions margin value is minimal for x, sort by minX,
          // otherwise it's already sorted by minY
          if (xMargin < yMargin) node.children.sort(compareMinX);
      }

      // total margin of all possible split distributions where each node is at least m full
      _allDistMargin(node, m, M, compare) {
          node.children.sort(compare);

          const toBBox = this.toBBox;
          const leftBBox = distBBox(node, 0, m, toBBox);
          const rightBBox = distBBox(node, M - m, M, toBBox);
          let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);

          for (let i = m; i < M - m; i++) {
              const child = node.children[i];
              extend(leftBBox, node.leaf ? toBBox(child) : child);
              margin += bboxMargin(leftBBox);
          }

          for (let i = M - m - 1; i >= m; i--) {
              const child = node.children[i];
              extend(rightBBox, node.leaf ? toBBox(child) : child);
              margin += bboxMargin(rightBBox);
          }

          return margin;
      }

      _adjustParentBBoxes(bbox, path, level) {
          // adjust bboxes along the given tree path
          for (let i = level; i >= 0; i--) {
              extend(path[i], bbox);
          }
      }

      _condense(path) {
          // go through the path, removing empty nodes and updating bboxes
          for (let i = path.length - 1, siblings; i >= 0; i--) {
              if (path[i].children.length === 0) {
                  if (i > 0) {
                      siblings = path[i - 1].children;
                      siblings.splice(siblings.indexOf(path[i]), 1);

                  } else this.clear();

              } else calcBBox(path[i], this.toBBox);
          }
      }
  };

  function findItem(item, items, equalsFn) {
      if (!equalsFn) return items.indexOf(item);

      for (let i = 0; i < items.length; i++) {
          if (equalsFn(item, items[i])) return i;
      }
      return -1;
  }

  // calculate node's bbox from bboxes of its children
  function calcBBox(node, toBBox) {
      distBBox(node, 0, node.children.length, toBBox, node);
  }

  // min bounding rectangle of node children from k to p-1
  function distBBox(node, k, p, toBBox, destNode) {
      if (!destNode) destNode = createNode(null);
      destNode.minX = Infinity;
      destNode.minY = Infinity;
      destNode.maxX = -Infinity;
      destNode.maxY = -Infinity;

      for (let i = k; i < p; i++) {
          const child = node.children[i];
          extend(destNode, node.leaf ? toBBox(child) : child);
      }

      return destNode;
  }

  function extend(a, b) {
      a.minX = Math.min(a.minX, b.minX);
      a.minY = Math.min(a.minY, b.minY);
      a.maxX = Math.max(a.maxX, b.maxX);
      a.maxY = Math.max(a.maxY, b.maxY);
      return a;
  }

  function compareNodeMinX(a, b) { return a.minX - b.minX; }
  function compareNodeMinY(a, b) { return a.minY - b.minY; }

  function bboxArea(a)   { return (a.maxX - a.minX) * (a.maxY - a.minY); }
  function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }

  function enlargedArea(a, b) {
      return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
             (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
  }

  function intersectionArea(a, b) {
      const minX = Math.max(a.minX, b.minX);
      const minY = Math.max(a.minY, b.minY);
      const maxX = Math.min(a.maxX, b.maxX);
      const maxY = Math.min(a.maxY, b.maxY);

      return Math.max(0, maxX - minX) *
             Math.max(0, maxY - minY);
  }

  function contains(a, b) {
      return a.minX <= b.minX &&
             a.minY <= b.minY &&
             b.maxX <= a.maxX &&
             b.maxY <= a.maxY;
  }

  function intersects(a, b) {
      return b.minX <= a.maxX &&
             b.minY <= a.maxY &&
             b.maxX >= a.minX &&
             b.maxY >= a.minY;
  }

  function createNode(children) {
      return {
          children,
          height: 1,
          leaf: true,
          minX: Infinity,
          minY: Infinity,
          maxX: -Infinity,
          maxY: -Infinity
      };
  }

  // sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
  // combines selection algorithm with binary divide & conquer approach

  function multiSelect(arr, left, right, n, compare) {
      const stack = [left, right];

      while (stack.length) {
          right = stack.pop();
          left = stack.pop();

          if (right - left <= n) continue;

          const mid = left + Math.ceil((right - left) / n / 2) * n;
          quickselect(arr, mid, left, right, compare);

          stack.push(left, mid, mid, right);
      }
  }

  /**
   * @module ol/structs/RBush
   */

  /**
   * @typedef {import("rbush").BBox & {value: T}} Entry
   * @template T
   */

  /**
   * @classdesc
   * Wrapper around the RBush by Vladimir Agafonkin.
   * See https://github.com/mourner/rbush.
   *
   * @template {Object} T
   */
  class RBush {
    /**
     * @param {number} [maxEntries] Max entries.
     */
    constructor(maxEntries) {
      /**
       * @private
       * @type {RBush_<Entry<T>>}
       */
      this.rbush_ = new RBush$1(maxEntries);

      /**
       * A mapping between the objects added to this rbush wrapper
       * and the objects that are actually added to the internal rbush.
       * @private
       * @type {Object<string, Entry<T>>}
       */
      this.items_ = {};
    }

    /**
     * Insert a value into the RBush.
     * @param {import("../extent.js").Extent} extent Extent.
     * @param {T} value Value.
     */
    insert(extent, value) {
      /** @type {Entry<T>} */
      const item = {
        minX: extent[0],
        minY: extent[1],
        maxX: extent[2],
        maxY: extent[3],
        value: value,
      };

      this.rbush_.insert(item);
      this.items_[getUid(value)] = item;
    }

    /**
     * Bulk-insert values into the RBush.
     * @param {Array<import("../extent.js").Extent>} extents Extents.
     * @param {Array<T>} values Values.
     */
    load(extents, values) {
      const items = new Array(values.length);
      for (let i = 0, l = values.length; i < l; i++) {
        const extent = extents[i];
        const value = values[i];

        /** @type {Entry<T>} */
        const item = {
          minX: extent[0],
          minY: extent[1],
          maxX: extent[2],
          maxY: extent[3],
          value: value,
        };
        items[i] = item;
        this.items_[getUid(value)] = item;
      }
      this.rbush_.load(items);
    }

    /**
     * Remove a value from the RBush.
     * @param {T} value Value.
     * @return {boolean} Removed.
     */
    remove(value) {
      const uid = getUid(value);

      // get the object in which the value was wrapped when adding to the
      // internal rbush. then use that object to do the removal.
      const item = this.items_[uid];
      delete this.items_[uid];
      return this.rbush_.remove(item) !== null;
    }

    /**
     * Update the extent of a value in the RBush.
     * @param {import("../extent.js").Extent} extent Extent.
     * @param {T} value Value.
     */
    update(extent, value) {
      const item = this.items_[getUid(value)];
      const bbox = [item.minX, item.minY, item.maxX, item.maxY];
      if (!equals$1(bbox, extent)) {
        this.remove(value);
        this.insert(extent, value);
      }
    }

    /**
     * Return all values in the RBush.
     * @return {Array<T>} All.
     */
    getAll() {
      const items = this.rbush_.all();
      return items.map(function (item) {
        return item.value;
      });
    }

    /**
     * Return all values in the given extent.
     * @param {import("../extent.js").Extent} extent Extent.
     * @return {Array<T>} All in extent.
     */
    getInExtent(extent) {
      /** @type {import("rbush").BBox} */
      const bbox = {
        minX: extent[0],
        minY: extent[1],
        maxX: extent[2],
        maxY: extent[3],
      };
      const items = this.rbush_.search(bbox);
      return items.map(function (item) {
        return item.value;
      });
    }

    /**
     * Calls a callback function with each value in the tree.
     * If the callback returns a truthy value, this value is returned without
     * checking the rest of the tree.
     * @param {function(T): R} callback Callback.
     * @return {R|undefined} Callback return value.
     * @template R
     */
    forEach(callback) {
      return this.forEach_(this.getAll(), callback);
    }

    /**
     * Calls a callback function with each value in the provided extent.
     * @param {import("../extent.js").Extent} extent Extent.
     * @param {function(T): R} callback Callback.
     * @return {R|undefined} Callback return value.
     * @template R
     */
    forEachInExtent(extent, callback) {
      return this.forEach_(this.getInExtent(extent), callback);
    }

    /**
     * @param {Array<T>} values Values.
     * @param {function(T): R} callback Callback.
     * @return {R|undefined} Callback return value.
     * @template R
     * @private
     */
    forEach_(values, callback) {
      let result;
      for (let i = 0, l = values.length; i < l; i++) {
        result = callback(values[i]);
        if (result) {
          return result;
        }
      }
      return result;
    }

    /**
     * @return {boolean} Is empty.
     */
    isEmpty() {
      return isEmpty$1(this.items_);
    }

    /**
     * Remove all values from the RBush.
     */
    clear() {
      this.rbush_.clear();
      this.items_ = {};
    }

    /**
     * @param {import("../extent.js").Extent} [extent] Extent.
     * @return {import("../extent.js").Extent} Extent.
     */
    getExtent(extent) {
      const data = this.rbush_.toJSON();
      return createOrUpdate$2(data.minX, data.minY, data.maxX, data.maxY, extent);
    }

    /**
     * @param {RBush<T>} rbush R-Tree.
     */
    concat(rbush) {
      this.rbush_.load(rbush.rbush_.all());
      for (const i in rbush.items_) {
        this.items_[i] = rbush.items_[i];
      }
    }
  }

  /**
   * @module ol/source/Source
   */

  /**
   * @typedef {'undefined' | 'loading' | 'ready' | 'error'} State
   * State of the source, one of 'undefined', 'loading', 'ready' or 'error'.
   */

  /**
   * A function that takes a {@link import("../View.js").ViewStateLayerStateExtent} and returns a string or
   * an array of strings representing source attributions.
   *
   * @typedef {function(import("../View.js").ViewStateLayerStateExtent): (string|Array<string>)} Attribution
   */

  /**
   * A type that can be used to provide attribution information for data sources.
   *
   * It represents either
   * a simple string (e.g. `'© Acme Inc.'`)
   * an array of simple strings (e.g. `['© Acme Inc.', '© Bacme Inc.']`)
   * a function that returns a string or array of strings ({@link module:ol/source/Source~Attribution})
   *
   * @typedef {string|Array<string>|Attribution} AttributionLike
   */

  /**
   * @typedef {Object} Options
   * @property {AttributionLike} [attributions] Attributions.
   * @property {boolean} [attributionsCollapsible=true] Attributions are collapsible.
   * @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection.
   * @property {import("./Source.js").State} [state='ready'] State.
   * @property {boolean} [wrapX=false] WrapX.
   * @property {boolean} [interpolate=false] Use interpolated values when resampling.  By default,
   * the nearest neighbor is used when resampling.
   */

  /**
   * @classdesc
   * Abstract base class; normally only used for creating subclasses and not
   * instantiated in apps.
   * Base class for {@link module:ol/layer/Layer~Layer} sources.
   *
   * A generic `change` event is triggered when the state of the source changes.
   * @abstract
   * @api
   */
  class Source extends BaseObject {
    /**
     * @param {Options} options Source options.
     */
    constructor(options) {
      super();

      /**
       * @protected
       * @type {import("../proj/Projection.js").default|null}
       */
      this.projection = get$1(options.projection);

      /**
       * @private
       * @type {?Attribution}
       */
      this.attributions_ = adaptAttributions(options.attributions);

      /**
       * @private
       * @type {boolean}
       */
      this.attributionsCollapsible_ = options.attributionsCollapsible ?? true;

      /**
       * This source is currently loading data. Sources that defer loading to the
       * map's tile queue never set this to `true`.
       * @type {boolean}
       */
      this.loading = false;

      /**
       * @private
       * @type {import("./Source.js").State}
       */
      this.state_ = options.state !== undefined ? options.state : 'ready';

      /**
       * @private
       * @type {boolean}
       */
      this.wrapX_ = options.wrapX !== undefined ? options.wrapX : false;

      /**
       * @private
       * @type {boolean}
       */
      this.interpolate_ = !!options.interpolate;

      /**
       * @protected
       * @type {function(import("../View.js").ViewOptions):void}
       */
      this.viewResolver = null;

      /**
       * @protected
       * @type {function(Error):void}
       */
      this.viewRejector = null;

      const self = this;
      /**
       * @private
       * @type {Promise<import("../View.js").ViewOptions>}
       */
      this.viewPromise_ = new Promise(function (resolve, reject) {
        self.viewResolver = resolve;
        self.viewRejector = reject;
      });
    }

    /**
     * Get the attribution function for the source.
     * @return {?Attribution} Attribution function.
     * @api
     */
    getAttributions() {
      return this.attributions_;
    }

    /**
     * @return {boolean} Attributions are collapsible.
     * @api
     */
    getAttributionsCollapsible() {
      return this.attributionsCollapsible_;
    }

    /**
     * Get the projection of the source.
     * @return {import("../proj/Projection.js").default|null} Projection.
     * @api
     */
    getProjection() {
      return this.projection;
    }

    /**
     * @param {import("../proj/Projection").default} [projection] Projection.
     * @return {Array<number>|null} Resolutions.
     */
    getResolutions(projection) {
      return null;
    }

    /**
     * @return {Promise<import("../View.js").ViewOptions>} A promise for view-related properties.
     */
    getView() {
      return this.viewPromise_;
    }

    /**
     * Get the state of the source, see {@link import("./Source.js").State} for possible states.
     * @return {import("./Source.js").State} State.
     * @api
     */
    getState() {
      return this.state_;
    }

    /**
     * @return {boolean|undefined} Wrap X.
     */
    getWrapX() {
      return this.wrapX_;
    }

    /**
     * @return {boolean} Use linear interpolation when resampling.
     */
    getInterpolate() {
      return this.interpolate_;
    }

    /**
     * Refreshes the source. The source will be cleared, and data from the server will be reloaded.
     * @api
     */
    refresh() {
      this.changed();
    }

    /**
     * Set the attributions of the source.
     * @param {AttributionLike|undefined} attributions Attributions.
     *     Can be passed as `string`, `Array<string>`, {@link module:ol/source/Source~Attribution},
     *     or `undefined`.
     * @api
     */
    setAttributions(attributions) {
      this.attributions_ = adaptAttributions(attributions);
      this.changed();
    }

    /**
     * Set the state of the source.
     * @param {import("./Source.js").State} state State.
     */
    setState(state) {
      this.state_ = state;
      this.changed();
    }
  }

  /**
   * Turns the attributions option into an attributions function.
   * @param {AttributionLike|undefined} attributionLike The attribution option.
   * @return {Attribution|null} An attribution function (or null).
   */
  function adaptAttributions(attributionLike) {
    if (!attributionLike) {
      return null;
    }
    if (typeof attributionLike === 'function') {
      return attributionLike;
    }
    if (!Array.isArray(attributionLike)) {
      attributionLike = [attributionLike];
    }
    return (frameState) => attributionLike;
  }

  /**
   * @module ol/source/VectorEventType
   */

  /**
   * @enum {string}
   */
  var VectorEventType = {
    /**
     * Triggered when a feature is added to the source.
     * @event module:ol/source/Vector.VectorSourceEvent#addfeature
     * @api
     */
    ADDFEATURE: 'addfeature',

    /**
     * Triggered when a feature is updated.
     * @event module:ol/source/Vector.VectorSourceEvent#changefeature
     * @api
     */
    CHANGEFEATURE: 'changefeature',

    /**
     * Triggered when the clear method is called on the source.
     * @event module:ol/source/Vector.VectorSourceEvent#clear
     * @api
     */
    CLEAR: 'clear',

    /**
     * Triggered when a feature is removed from the source.
     * See {@link module:ol/source/Vector~VectorSource#clear source.clear()} for exceptions.
     * @event module:ol/source/Vector.VectorSourceEvent#removefeature
     * @api
     */
    REMOVEFEATURE: 'removefeature',

    /**
     * Triggered when features starts loading.
     * @event module:ol/source/Vector.VectorSourceEvent#featuresloadstart
     * @api
     */
    FEATURESLOADSTART: 'featuresloadstart',

    /**
     * Triggered when features finishes loading.
     * @event module:ol/source/Vector.VectorSourceEvent#featuresloadend
     * @api
     */
    FEATURESLOADEND: 'featuresloadend',

    /**
     * Triggered if feature loading results in an error.
     * @event module:ol/source/Vector.VectorSourceEvent#featuresloaderror
     * @api
     */
    FEATURESLOADERROR: 'featuresloaderror',
  };

  /**
   * @typedef {'addfeature'|'changefeature'|'clear'|'removefeature'|'featuresloadstart'|'featuresloadend'|'featuresloaderror'} VectorSourceEventTypes
   */

  /**
   * @module ol/source/Vector
   */


  /**
   * A function that takes an {@link module:ol/extent~Extent} and a resolution as arguments, and
   * returns an array of {@link module:ol/extent~Extent} with the extents to load. Usually this
   * is one of the standard {@link module:ol/loadingstrategy} strategies.
   *
   * @typedef {function(import("../extent.js").Extent, number, import("../proj/Projection.js").default): Array<import("../extent.js").Extent>} LoadingStrategy
   * @api
   */

  /**
   * @classdesc
   * Events emitted by {@link module:ol/source/Vector~VectorSource} instances are instances of this
   * type.
   * @template {import("../Feature.js").FeatureLike} [FeatureType=import("../Feature.js").default]
   */
  class VectorSourceEvent extends BaseEvent {
    /**
     * @param {string} type Type.
     * @param {FeatureType} [feature] Feature.
     * @param {Array<FeatureType>} [features] Features.
     */
    constructor(type, feature, features) {
      super(type);

      /**
       * The added or removed feature for the `ADDFEATURE` and `REMOVEFEATURE` events, `undefined` otherwise.
       * @type {FeatureType|undefined}
       * @api
       */
      this.feature = feature;

      /**
       * The loaded features for the `FEATURESLOADED` event, `undefined` otherwise.
       * @type {Array<FeatureType>|undefined}
       * @api
       */
      this.features = features;
    }
  }

  /***
   * @template {import("../Feature.js").FeatureLike} [T=import("../Feature.js").default]
   * @typedef {T extends RenderFeature ? T|Array<T> : T} FeatureClassOrArrayOfRenderFeatures
   */

  /***
   * @template Return
   * @template {import("../Feature.js").FeatureLike} [FeatureType=import("../Feature.js").default]
   * @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> &
   *   import("../Observable").OnSignature<import("../ObjectEventType").Types, import("../Object").ObjectEvent, Return> &
   *   import("../Observable").OnSignature<import("./VectorEventType").VectorSourceEventTypes, VectorSourceEvent<FeatureType>, Return> &
   *   import("../Observable").CombinedOnSignature<import("../Observable").EventTypes|import("../ObjectEventType").Types|
   *     import("./VectorEventType").VectorSourceEventTypes, Return>} VectorSourceOnSignature
   */

  /**
   * @template {import("../Feature.js").FeatureLike} [FeatureType=import("../Feature.js").default]
   * @typedef {Object} Options
   * @property {import("./Source.js").AttributionLike} [attributions] Attributions.
   * @property {Array<FeatureType>|Collection<FeatureType>} [features]
   * Features. If provided as {@link module:ol/Collection~Collection}, the features in the source
   * and the collection will stay in sync.
   * @property {import("../format/Feature.js").default<FeatureType>} [format] The feature format used by the XHR
   * feature loader when `url` is set. Required if `url` is set, otherwise ignored.
   * @property {import("../featureloader.js").FeatureLoader<FeatureType>} [loader]
   * The loader function used to load features, from a remote source for example.
   * If this is not set and `url` is set, the source will create and use an XHR
   * feature loader. The `'featuresloadend'` and `'featuresloaderror'` events
   * will only fire if the `success` and `failure` callbacks are used.
   *
   * Example:
   *
   * ```js
   * import Vector from 'ol/source/Vector.js';
   * import GeoJSON from 'ol/format/GeoJSON.js';
   * import {bbox} from 'ol/loadingstrategy.js';
   *
   * const vectorSource = new Vector({
   *   format: new GeoJSON(),
   *   loader: function(extent, resolution, projection, success, failure) {
   *      const proj = projection.getCode();
   *      const url = 'https://ahocevar.com/geoserver/wfs?service=WFS&' +
   *          'version=1.1.0&request=GetFeature&typename=osm:water_areas&' +
   *          'outputFormat=application/json&srsname=' + proj + '&' +
   *          'bbox=' + extent.join(',') + ',' + proj;
   *      const xhr = new XMLHttpRequest();
   *      xhr.open('GET', url);
   *      const onError = function() {
   *        vectorSource.removeLoadedExtent(extent);
   *        failure();
   *      }
   *      xhr.onerror = onError;
   *      xhr.onload = function() {
   *        if (xhr.status == 200) {
   *          const features = vectorSource.getFormat().readFeatures(xhr.responseText);
   *          vectorSource.addFeatures(features);
   *          success(features);
   *        } else {
   *          onError();
   *        }
   *      }
   *      xhr.send();
   *    },
   *    strategy: bbox,
   *  });
   * ```
   * @property {boolean} [overlaps=true] This source may have overlapping geometries.
   * Setting this to `false` (e.g. for sources with polygons that represent administrative
   * boundaries or TopoJSON sources) allows the renderer to optimise fill and
   * stroke operations.
   * @property {LoadingStrategy} [strategy] The loading strategy to use.
   * By default an {@link module:ol/loadingstrategy.all}
   * strategy is used, a one-off strategy which loads all features at once.
   * @property {string|import("../featureloader.js").FeatureUrlFunction} [url]
   * Setting this option instructs the source to load features using an XHR loader
   * (see {@link module:ol/featureloader.xhr}). Use a `string` and an
   * {@link module:ol/loadingstrategy.all} for a one-off download of all features from
   * the given URL. Use a {@link module:ol/featureloader~FeatureUrlFunction} to generate the url with
   * other loading strategies.
   * Requires `format` to be set as well.
   * When default XHR feature loader is provided, the features will
   * be transformed from the data projection to the view projection
   * during parsing. If your remote data source does not advertise its projection
   * properly, this transformation will be incorrect. For some formats, the
   * default projection (usually EPSG:4326) can be overridden by setting the
   * dataProjection constructor option on the format.
   * Note that if a source contains non-feature data, such as a GeoJSON geometry
   * or a KML NetworkLink, these will be ignored. Use a custom loader to load these.
   * @property {boolean} [useSpatialIndex=true]
   * By default, an RTree is used as spatial index. When features are removed and
   * added frequently, and the total number of features is low, setting this to
   * `false` may improve performance.
   *
   * Note that
   * {@link module:ol/source/Vector~VectorSource#getFeaturesInExtent},
   * {@link module:ol/source/Vector~VectorSource#getClosestFeatureToCoordinate} and
   * {@link module:ol/source/Vector~VectorSource#getExtent} cannot be used when `useSpatialIndex` is
   * set to `false`, and {@link module:ol/source/Vector~VectorSource#forEachFeatureInExtent} will loop
   * through all features.
   *
   * When set to `false`, the features will be maintained in an
   * {@link module:ol/Collection~Collection}, which can be retrieved through
   * {@link module:ol/source/Vector~VectorSource#getFeaturesCollection}.
   * @property {boolean} [wrapX=true] Wrap the world horizontally. For vector editing across the
   * -180° and 180° meridians to work properly, this should be set to `false`. The
   * resulting geometry coordinates will then exceed the world bounds.
   */

  /**
   * @classdesc
   * Provides a source of features for vector layers. Vector features provided
   * by this source are suitable for editing. See {@link module:ol/source/VectorTile~VectorTile} for
   * vector data that is optimized for rendering.
   *
   * @fires VectorSourceEvent
   * @api
   * @template {import("../Feature.js").FeatureLike} [FeatureType=import("../Feature.js").default]
   */
  class VectorSource extends Source {
    /**
     * @param {Options<FeatureType>} [options] Vector source options.
     */
    constructor(options) {
      options = options || {};

      super({
        attributions: options.attributions,
        interpolate: true,
        projection: undefined,
        state: 'ready',
        wrapX: options.wrapX !== undefined ? options.wrapX : true,
      });

      /***
       * @type {VectorSourceOnSignature<import("../events").EventsKey, FeatureType>}
       */
      this.on;

      /***
       * @type {VectorSourceOnSignature<import("../events").EventsKey, FeatureType>}
       */
      this.once;

      /***
       * @type {VectorSourceOnSignature<void>}
       */
      this.un;

      /**
       * @private
       * @type {import("../featureloader.js").FeatureLoader<import("../Feature.js").FeatureLike>}
       */
      this.loader_ = VOID;

      /**
       * @private
       * @type {import("../format/Feature.js").default<FeatureType>|null}
       */
      this.format_ = options.format || null;

      /**
       * @private
       * @type {boolean}
       */
      this.overlaps_ = options.overlaps === undefined ? true : options.overlaps;

      /**
       * @private
       * @type {string|import("../featureloader.js").FeatureUrlFunction|undefined}
       */
      this.url_ = options.url;

      if (options.loader !== undefined) {
        this.loader_ = options.loader;
      } else if (this.url_ !== undefined) {
        assert(this.format_, '`format` must be set when `url` is set');
        // create a XHR feature loader for "url" and "format"
        this.loader_ = xhr(this.url_, this.format_);
      }

      /**
       * @private
       * @type {LoadingStrategy}
       */
      this.strategy_ =
        options.strategy !== undefined ? options.strategy : all$1;

      const useSpatialIndex =
        options.useSpatialIndex !== undefined ? options.useSpatialIndex : true;

      /**
       * @private
       * @type {RBush<FeatureType>}
       */
      this.featuresRtree_ = useSpatialIndex ? new RBush() : null;

      /**
       * @private
       * @type {RBush<{extent: import("../extent.js").Extent}>}
       */
      this.loadedExtentsRtree_ = new RBush();

      /**
       * @type {number}
       * @private
       */
      this.loadingExtentsCount_ = 0;

      /**
       * @private
       * @type {!Object<string, FeatureType>}
       */
      this.nullGeometryFeatures_ = {};

      /**
       * A lookup of features by id (the return from feature.getId()).
       * @private
       * @type {!Object<string, import('../Feature.js').FeatureLike|Array<import('../Feature.js').FeatureLike>>}
       */
      this.idIndex_ = {};

      /**
       * A lookup of features by uid (using getUid(feature)).
       * @private
       * @type {!Object<string, FeatureType>}
       */
      this.uidIndex_ = {};

      /**
       * @private
       * @type {Object<string, Array<import("../events.js").EventsKey>>}
       */
      this.featureChangeKeys_ = {};

      /**
       * @private
       * @type {Collection<FeatureType>|null}
       */
      this.featuresCollection_ = null;

      /** @type {Collection<FeatureType>} */
      let collection;
      /** @type {Array<FeatureType>} */
      let features;
      if (Array.isArray(options.features)) {
        features = options.features;
      } else if (options.features) {
        collection = options.features;
        features = collection.getArray();
      }
      if (!useSpatialIndex && collection === undefined) {
        collection = new Collection(features);
      }
      if (features !== undefined) {
        this.addFeaturesInternal(features);
      }
      if (collection !== undefined) {
        this.bindFeaturesCollection_(collection);
      }
    }

    /**
     * Add a single feature to the source.  If you want to add a batch of features
     * at once, call {@link module:ol/source/Vector~VectorSource#addFeatures #addFeatures()}
     * instead. A feature will not be added to the source if feature with
     * the same id is already there. The reason for this behavior is to avoid
     * feature duplication when using bbox or tile loading strategies.
     * Note: this also applies if a {@link module:ol/Collection~Collection} is used for features,
     * meaning that if a feature with a duplicate id is added in the collection, it will
     * be removed from it right away.
     * @param {FeatureType} feature Feature to add.
     * @api
     */
    addFeature(feature) {
      this.addFeatureInternal(feature);
      this.changed();
    }

    /**
     * Add a feature without firing a `change` event.
     * @param {FeatureType} feature Feature.
     * @protected
     */
    addFeatureInternal(feature) {
      const featureKey = getUid(feature);

      if (!this.addToIndex_(featureKey, feature)) {
        if (this.featuresCollection_) {
          this.featuresCollection_.remove(feature);
        }
        return;
      }

      this.setupChangeEvents_(featureKey, feature);

      const geometry = feature.getGeometry();
      if (geometry) {
        const extent = geometry.getExtent();
        if (this.featuresRtree_) {
          this.featuresRtree_.insert(extent, feature);
        }
      } else {
        this.nullGeometryFeatures_[featureKey] = feature;
      }

      this.dispatchEvent(
        new VectorSourceEvent(VectorEventType.ADDFEATURE, feature),
      );
    }

    /**
     * @param {string} featureKey Unique identifier for the feature.
     * @param {FeatureType} feature The feature.
     * @private
     */
    setupChangeEvents_(featureKey, feature) {
      if (feature instanceof RenderFeature) {
        return;
      }
      this.featureChangeKeys_[featureKey] = [
        listen(feature, EventType.CHANGE, this.handleFeatureChange_, this),
        listen(
          feature,
          ObjectEventType.PROPERTYCHANGE,
          this.handleFeatureChange_,
          this,
        ),
      ];
    }

    /**
     * @param {string} featureKey Unique identifier for the feature.
     * @param {FeatureType} feature The feature.
     * @return {boolean} The feature is "valid", in the sense that it is also a
     *     candidate for insertion into the Rtree.
     * @private
     */
    addToIndex_(featureKey, feature) {
      let valid = true;
      if (feature.getId() !== undefined) {
        const id = String(feature.getId());
        if (!(id in this.idIndex_)) {
          this.idIndex_[id] = feature;
        } else if (feature instanceof RenderFeature) {
          const indexedFeature = this.idIndex_[id];
          if (!(indexedFeature instanceof RenderFeature)) {
            valid = false;
          } else if (!Array.isArray(indexedFeature)) {
            this.idIndex_[id] = [indexedFeature, feature];
          } else {
            indexedFeature.push(feature);
          }
        } else {
          valid = false;
        }
      }
      if (valid) {
        assert(
          !(featureKey in this.uidIndex_),
          'The passed `feature` was already added to the source',
        );
        this.uidIndex_[featureKey] = feature;
      }
      return valid;
    }

    /**
     * Add a batch of features to the source.
     * @param {Array<FeatureType>} features Features to add.
     * @api
     */
    addFeatures(features) {
      this.addFeaturesInternal(features);
      this.changed();
    }

    /**
     * Add features without firing a `change` event.
     * @param {Array<FeatureType>} features Features.
     * @protected
     */
    addFeaturesInternal(features) {
      const extents = [];
      /** @type {Array<FeatureType>} */
      const newFeatures = [];
      /** @type {Array<FeatureType>} */
      const geometryFeatures = [];

      for (let i = 0, length = features.length; i < length; i++) {
        const feature = features[i];
        const featureKey = getUid(feature);
        if (this.addToIndex_(featureKey, feature)) {
          newFeatures.push(feature);
        }
      }

      for (let i = 0, length = newFeatures.length; i < length; i++) {
        const feature = newFeatures[i];
        const featureKey = getUid(feature);
        this.setupChangeEvents_(featureKey, feature);

        const geometry = feature.getGeometry();
        if (geometry) {
          const extent = geometry.getExtent();
          extents.push(extent);
          geometryFeatures.push(feature);
        } else {
          this.nullGeometryFeatures_[featureKey] = feature;
        }
      }
      if (this.featuresRtree_) {
        this.featuresRtree_.load(extents, geometryFeatures);
      }

      if (this.hasListener(VectorEventType.ADDFEATURE)) {
        for (let i = 0, length = newFeatures.length; i < length; i++) {
          this.dispatchEvent(
            new VectorSourceEvent(VectorEventType.ADDFEATURE, newFeatures[i]),
          );
        }
      }
    }

    /**
     * @param {!Collection<FeatureType>} collection Collection.
     * @private
     */
    bindFeaturesCollection_(collection) {
      let modifyingCollection = false;
      this.addEventListener(
        VectorEventType.ADDFEATURE,
        /**
         * @param {VectorSourceEvent<FeatureType>} evt The vector source event
         */
        function (evt) {
          if (!modifyingCollection) {
            modifyingCollection = true;
            collection.push(evt.feature);
            modifyingCollection = false;
          }
        },
      );
      this.addEventListener(
        VectorEventType.REMOVEFEATURE,
        /**
         * @param {VectorSourceEvent<FeatureType>} evt The vector source event
         */
        function (evt) {
          if (!modifyingCollection) {
            modifyingCollection = true;
            collection.remove(evt.feature);
            modifyingCollection = false;
          }
        },
      );
      collection.addEventListener(
        CollectionEventType.ADD,
        /**
         * @param {import("../Collection.js").CollectionEvent<FeatureType>} evt The collection event
         */
        (evt) => {
          if (!modifyingCollection) {
            modifyingCollection = true;
            this.addFeature(evt.element);
            modifyingCollection = false;
          }
        },
      );
      collection.addEventListener(
        CollectionEventType.REMOVE,
        /**
         * @param {import("../Collection.js").CollectionEvent<FeatureType>} evt The collection event
         */
        (evt) => {
          if (!modifyingCollection) {
            modifyingCollection = true;
            this.removeFeature(evt.element);
            modifyingCollection = false;
          }
        },
      );
      this.featuresCollection_ = collection;
    }

    /**
     * Remove all features from the source.
     * @param {boolean} [fast] Skip dispatching of {@link module:ol/source/Vector.VectorSourceEvent#event:removefeature} events.
     * @api
     */
    clear(fast) {
      if (fast) {
        for (const featureId in this.featureChangeKeys_) {
          const keys = this.featureChangeKeys_[featureId];
          keys.forEach(unlistenByKey);
        }
        if (!this.featuresCollection_) {
          this.featureChangeKeys_ = {};
          this.idIndex_ = {};
          this.uidIndex_ = {};
        }
      } else {
        if (this.featuresRtree_) {
          this.featuresRtree_.forEach((feature) => {
            this.removeFeatureInternal(feature);
          });
          for (const id in this.nullGeometryFeatures_) {
            this.removeFeatureInternal(this.nullGeometryFeatures_[id]);
          }
        }
      }
      if (this.featuresCollection_) {
        this.featuresCollection_.clear();
      }

      if (this.featuresRtree_) {
        this.featuresRtree_.clear();
      }
      this.nullGeometryFeatures_ = {};

      const clearEvent = new VectorSourceEvent(VectorEventType.CLEAR);
      this.dispatchEvent(clearEvent);
      this.changed();
    }

    /**
     * Iterate through all features on the source, calling the provided callback
     * with each one.  If the callback returns any "truthy" value, iteration will
     * stop and the function will return the same value.
     * Note: this function only iterate through the feature that have a defined geometry.
     *
     * @param {function(FeatureType): T} callback Called with each feature
     *     on the source.  Return a truthy value to stop iteration.
     * @return {T|undefined} The return value from the last call to the callback.
     * @template T
     * @api
     */
    forEachFeature(callback) {
      if (this.featuresRtree_) {
        return this.featuresRtree_.forEach(callback);
      }
      if (this.featuresCollection_) {
        this.featuresCollection_.forEach(callback);
      }
    }

    /**
     * Iterate through all features whose geometries contain the provided
     * coordinate, calling the callback with each feature.  If the callback returns
     * a "truthy" value, iteration will stop and the function will return the same
     * value.
     *
     * For {@link module:ol/render/Feature~RenderFeature} features, the callback will be
     * called for all features.
     *
     * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
     * @param {function(FeatureType): T} callback Called with each feature
     *     whose goemetry contains the provided coordinate.
     * @return {T|undefined} The return value from the last call to the callback.
     * @template T
     */
    forEachFeatureAtCoordinateDirect(coordinate, callback) {
      const extent = [coordinate[0], coordinate[1], coordinate[0], coordinate[1]];
      return this.forEachFeatureInExtent(extent, function (feature) {
        const geometry = feature.getGeometry();
        if (
          geometry instanceof RenderFeature ||
          geometry.intersectsCoordinate(coordinate)
        ) {
          return callback(feature);
        }
        return undefined;
      });
    }

    /**
     * Iterate through all features whose bounding box intersects the provided
     * extent (note that the feature's geometry may not intersect the extent),
     * calling the callback with each feature.  If the callback returns a "truthy"
     * value, iteration will stop and the function will return the same value.
     *
     * If you are interested in features whose geometry intersects an extent, call
     * the {@link module:ol/source/Vector~VectorSource#forEachFeatureIntersectingExtent #forEachFeatureIntersectingExtent()} method instead.
     *
     * When `useSpatialIndex` is set to false, this method will loop through all
     * features, equivalent to {@link module:ol/source/Vector~VectorSource#forEachFeature #forEachFeature()}.
     *
     * @param {import("../extent.js").Extent} extent Extent.
     * @param {function(FeatureType): T} callback Called with each feature
     *     whose bounding box intersects the provided extent.
     * @return {T|undefined} The return value from the last call to the callback.
     * @template T
     * @api
     */
    forEachFeatureInExtent(extent, callback) {
      if (this.featuresRtree_) {
        return this.featuresRtree_.forEachInExtent(extent, callback);
      }
      if (this.featuresCollection_) {
        this.featuresCollection_.forEach(callback);
      }
    }

    /**
     * Iterate through all features whose geometry intersects the provided extent,
     * calling the callback with each feature.  If the callback returns a "truthy"
     * value, iteration will stop and the function will return the same value.
     *
     * If you only want to test for bounding box intersection, call the
     * {@link module:ol/source/Vector~VectorSource#forEachFeatureInExtent #forEachFeatureInExtent()} method instead.
     *
     * @param {import("../extent.js").Extent} extent Extent.
     * @param {function(FeatureType): T} callback Called with each feature
     *     whose geometry intersects the provided extent.
     * @return {T|undefined} The return value from the last call to the callback.
     * @template T
     * @api
     */
    forEachFeatureIntersectingExtent(extent, callback) {
      return this.forEachFeatureInExtent(
        extent,
        /**
         * @param {FeatureType} feature Feature.
         * @return {T|undefined} The return value from the last call to the callback.
         */
        function (feature) {
          const geometry = feature.getGeometry();
          if (
            geometry instanceof RenderFeature ||
            geometry.intersectsExtent(extent)
          ) {
            const result = callback(feature);
            if (result) {
              return result;
            }
          }
        },
      );
    }

    /**
     * Get the features collection associated with this source. Will be `null`
     * unless the source was configured with `useSpatialIndex` set to `false`, or
     * with a {@link module:ol/Collection~Collection} as `features`.
     * @return {Collection<FeatureType>|null} The collection of features.
     * @api
     */
    getFeaturesCollection() {
      return this.featuresCollection_;
    }

    /**
     * Get a snapshot of the features currently on the source in random order. The returned array
     * is a copy, the features are references to the features in the source.
     * @return {Array<FeatureType>} Features.
     * @api
     */
    getFeatures() {
      let features;
      if (this.featuresCollection_) {
        features = this.featuresCollection_.getArray().slice(0);
      } else if (this.featuresRtree_) {
        features = this.featuresRtree_.getAll();
        if (!isEmpty$1(this.nullGeometryFeatures_)) {
          extend$2(features, Object.values(this.nullGeometryFeatures_));
        }
      }
      return features;
    }

    /**
     * Get all features whose geometry intersects the provided coordinate.
     * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
     * @return {Array<FeatureType>} Features.
     * @api
     */
    getFeaturesAtCoordinate(coordinate) {
      /** @type {Array<FeatureType>} */
      const features = [];
      this.forEachFeatureAtCoordinateDirect(coordinate, function (feature) {
        features.push(feature);
      });
      return features;
    }

    /**
     * Get all features whose bounding box intersects the provided extent.  Note that this returns an array of
     * all features intersecting the given extent in random order (so it may include
     * features whose geometries do not intersect the extent).
     *
     * When `useSpatialIndex` is set to false, this method will return all
     * features.
     *
     * @param {import("../extent.js").Extent} extent Extent.
     * @param {import("../proj/Projection.js").default} [projection] Include features
     * where `extent` exceeds the x-axis bounds of `projection` and wraps around the world.
     * @return {Array<FeatureType>} Features.
     * @api
     */
    getFeaturesInExtent(extent, projection) {
      if (this.featuresRtree_) {
        const multiWorld = projection && projection.canWrapX() && this.getWrapX();

        if (!multiWorld) {
          return this.featuresRtree_.getInExtent(extent);
        }

        const extents = wrapAndSliceX(extent, projection);

        return [].concat(
          ...extents.map((anExtent) => this.featuresRtree_.getInExtent(anExtent)),
        );
      }
      if (this.featuresCollection_) {
        return this.featuresCollection_.getArray().slice(0);
      }
      return [];
    }

    /**
     * Get the closest feature to the provided coordinate.
     *
     * This method is not available when the source is configured with
     * `useSpatialIndex` set to `false` and the features in this source are of type
     * {@link module:ol/Feature~Feature}.
     * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
     * @param {function(FeatureType):boolean} [filter] Feature filter function.
     *     The filter function will receive one argument, the {@link module:ol/Feature~Feature feature}
     *     and it should return a boolean value. By default, no filtering is made.
     * @return {FeatureType|null} Closest feature (or `null` if none found).
     * @api
     */
    getClosestFeatureToCoordinate(coordinate, filter) {
      // Find the closest feature using branch and bound.  We start searching an
      // infinite extent, and find the distance from the first feature found.  This
      // becomes the closest feature.  We then compute a smaller extent which any
      // closer feature must intersect.  We continue searching with this smaller
      // extent, trying to find a closer feature.  Every time we find a closer
      // feature, we update the extent being searched so that any even closer
      // feature must intersect it.  We continue until we run out of features.
      const x = coordinate[0];
      const y = coordinate[1];
      let closestFeature = null;
      const closestPoint = [NaN, NaN];
      let minSquaredDistance = Infinity;
      const extent = [-Infinity, -Infinity, Infinity, Infinity];
      filter = filter ? filter : TRUE;
      this.featuresRtree_.forEachInExtent(
        extent,
        /**
         * @param {FeatureType} feature Feature.
         */
        function (feature) {
          if (filter(feature)) {
            const geometry = feature.getGeometry();
            const previousMinSquaredDistance = minSquaredDistance;
            minSquaredDistance =
              geometry instanceof RenderFeature
                ? 0
                : geometry.closestPointXY(x, y, closestPoint, minSquaredDistance);
            if (minSquaredDistance < previousMinSquaredDistance) {
              closestFeature = feature;
              // This is sneaky.  Reduce the extent that it is currently being
              // searched while the R-Tree traversal using this same extent object
              // is still in progress.  This is safe because the new extent is
              // strictly contained by the old extent.
              const minDistance = Math.sqrt(minSquaredDistance);
              extent[0] = x - minDistance;
              extent[1] = y - minDistance;
              extent[2] = x + minDistance;
              extent[3] = y + minDistance;
            }
          }
        },
      );
      return closestFeature;
    }

    /**
     * Get the extent of the features currently in the source.
     *
     * This will return `null` when the source is configured with
     * `useSpatialIndex` set to `false`.
     * @param {import("../extent.js").Extent} [extent] Destination extent. If provided, no new extent
     *     will be created. Instead, that extent's coordinates will be overwritten.
     * @return {import("../extent.js").Extent | null} Extent.
     * @api
     */
    getExtent(extent) {
      return this.featuresRtree_?.getExtent(extent) ?? null;
    }

    /**
     * Get a feature by its identifier (the value returned by feature.getId()). When `RenderFeature`s
     * are used, `getFeatureById()` can return an array of `RenderFeature`s. This allows for handling
     * of `GeometryCollection` geometries, where format readers create one `RenderFeature` per
     * `GeometryCollection` member.
     * Note that the index treats string and numeric identifiers as the same.  So
     * `source.getFeatureById(2)` will return a feature with id `'2'` or `2`.
     *
     * @param {string|number} id Feature identifier.
     * @return {FeatureClassOrArrayOfRenderFeatures<FeatureType>|null} The feature (or `null` if not found).
     * @api
     */
    getFeatureById(id) {
      const feature = this.idIndex_[id.toString()];
      return feature !== undefined
        ? /** @type {FeatureClassOrArrayOfRenderFeatures<FeatureType>} */ (
            feature
          )
        : null;
    }

    /**
     * Get a feature by its internal unique identifier (using `getUid`).
     *
     * @param {string} uid Feature identifier.
     * @return {FeatureType|null} The feature (or `null` if not found).
     */
    getFeatureByUid(uid) {
      const feature = this.uidIndex_[uid];
      return feature !== undefined ? feature : null;
    }

    /**
     * Get the format associated with this source.
     *
     * @return {import("../format/Feature.js").default<FeatureType>|null}} The feature format.
     * @api
     */
    getFormat() {
      return this.format_;
    }

    /**
     * @return {boolean} The source can have overlapping geometries.
     */
    getOverlaps() {
      return this.overlaps_;
    }

    /**
     * Get the url associated with this source.
     *
     * @return {string|import("../featureloader.js").FeatureUrlFunction|undefined} The url.
     * @api
     */
    getUrl() {
      return this.url_;
    }

    /**
     * @param {Event} event Event.
     * @private
     */
    handleFeatureChange_(event) {
      const feature = /** @type {FeatureType} */ (event.target);
      const featureKey = getUid(feature);
      const geometry = feature.getGeometry();
      if (!geometry) {
        if (!(featureKey in this.nullGeometryFeatures_)) {
          if (this.featuresRtree_) {
            this.featuresRtree_.remove(feature);
          }
          this.nullGeometryFeatures_[featureKey] = feature;
        }
      } else {
        const extent = geometry.getExtent();
        if (featureKey in this.nullGeometryFeatures_) {
          delete this.nullGeometryFeatures_[featureKey];
          if (this.featuresRtree_) {
            this.featuresRtree_.insert(extent, feature);
          }
        } else {
          if (this.featuresRtree_) {
            this.featuresRtree_.update(extent, feature);
          }
        }
      }
      const id = feature.getId();
      if (id !== undefined) {
        const sid = id.toString();
        if (this.idIndex_[sid] !== feature) {
          this.removeFromIdIndex_(feature);
          this.idIndex_[sid] = feature;
        }
      } else {
        this.removeFromIdIndex_(feature);
        this.uidIndex_[featureKey] = feature;
      }
      this.changed();
      this.dispatchEvent(
        new VectorSourceEvent(VectorEventType.CHANGEFEATURE, feature),
      );
    }

    /**
     * Returns true if the feature is contained within the source.
     * @param {FeatureType} feature Feature.
     * @return {boolean} Has feature.
     * @api
     */
    hasFeature(feature) {
      const id = feature.getId();
      if (id !== undefined) {
        return id in this.idIndex_;
      }
      return getUid(feature) in this.uidIndex_;
    }

    /**
     * @return {boolean} Is empty.
     */
    isEmpty() {
      if (this.featuresRtree_) {
        return (
          this.featuresRtree_.isEmpty() && isEmpty$1(this.nullGeometryFeatures_)
        );
      }
      if (this.featuresCollection_) {
        return this.featuresCollection_.getLength() === 0;
      }
      return true;
    }

    /**
     * @param {import("../extent.js").Extent} extent Extent.
     * @param {number} resolution Resolution.
     * @param {import("../proj/Projection.js").default} projection Projection.
     */
    loadFeatures(extent, resolution, projection) {
      const loadedExtentsRtree = this.loadedExtentsRtree_;
      const extentsToLoad = this.strategy_(extent, resolution, projection);
      for (let i = 0, ii = extentsToLoad.length; i < ii; ++i) {
        const extentToLoad = extentsToLoad[i];
        const alreadyLoaded = loadedExtentsRtree.forEachInExtent(
          extentToLoad,
          /**
           * @param {{extent: import("../extent.js").Extent}} object Object.
           * @return {boolean} Contains.
           */
          function (object) {
            return containsExtent(object.extent, extentToLoad);
          },
        );
        if (!alreadyLoaded) {
          ++this.loadingExtentsCount_;
          this.dispatchEvent(
            new VectorSourceEvent(VectorEventType.FEATURESLOADSTART),
          );
          this.loader_.call(
            this,
            extentToLoad,
            resolution,
            projection,
            /**
             * @param {Array<FeatureType>} features Loaded features
             */
            (features) => {
              --this.loadingExtentsCount_;
              this.dispatchEvent(
                new VectorSourceEvent(
                  VectorEventType.FEATURESLOADEND,
                  undefined,
                  features,
                ),
              );
            },
            () => {
              --this.loadingExtentsCount_;
              this.dispatchEvent(
                new VectorSourceEvent(VectorEventType.FEATURESLOADERROR),
              );
            },
          );
          loadedExtentsRtree.insert(extentToLoad, {extent: extentToLoad.slice()});
        }
      }
      this.loading =
        this.loader_.length < 4 ? false : this.loadingExtentsCount_ > 0;
    }

    /**
     * @override
     */
    refresh() {
      this.clear(true);
      this.loadedExtentsRtree_.clear();
      super.refresh();
    }

    /**
     * Remove an extent from the list of loaded extents.
     * @param {import("../extent.js").Extent} extent Extent.
     * @api
     */
    removeLoadedExtent(extent) {
      const loadedExtentsRtree = this.loadedExtentsRtree_;
      const obj = loadedExtentsRtree.forEachInExtent(extent, function (object) {
        if (equals$1(object.extent, extent)) {
          return object;
        }
      });
      if (obj) {
        loadedExtentsRtree.remove(obj);
      }
    }

    /**
     * Batch remove features from the source.  If you want to remove all features
     * at once, use the {@link module:ol/source/Vector~VectorSource#clear #clear()} method
     * instead.
     * @param {Array<FeatureType>} features Features to remove.
     * @api
     */
    removeFeatures(features) {
      let removed = false;
      for (let i = 0, ii = features.length; i < ii; ++i) {
        removed = this.removeFeatureInternal(features[i]) || removed;
      }
      if (removed) {
        this.changed();
      }
    }

    /**
     * Remove a single feature from the source. If you want to batch remove
     * features, use the {@link module:ol/source/Vector~VectorSource#removeFeatures #removeFeatures()} method
     * instead.
     * @param {FeatureType} feature Feature to remove.
     * @api
     */
    removeFeature(feature) {
      if (!feature) {
        return;
      }
      const removed = this.removeFeatureInternal(feature);
      if (removed) {
        this.changed();
      }
    }

    /**
     * Remove feature without firing a `change` event.
     * @param {FeatureType} feature Feature.
     * @return {boolean} True if the feature was removed, false if it was not found.
     * @protected
     */
    removeFeatureInternal(feature) {
      const featureKey = getUid(feature);
      if (!(featureKey in this.uidIndex_)) {
        return false;
      }

      if (featureKey in this.nullGeometryFeatures_) {
        delete this.nullGeometryFeatures_[featureKey];
      } else {
        if (this.featuresRtree_) {
          this.featuresRtree_.remove(feature);
        }
      }

      const featureChangeKeys = this.featureChangeKeys_[featureKey];
      featureChangeKeys?.forEach(unlistenByKey);
      delete this.featureChangeKeys_[featureKey];

      const id = feature.getId();
      if (id !== undefined) {
        const idString = id.toString();
        const indexedFeature = this.idIndex_[idString];
        if (indexedFeature === feature) {
          delete this.idIndex_[idString];
        } else if (Array.isArray(indexedFeature)) {
          indexedFeature.splice(indexedFeature.indexOf(feature), 1);
          if (indexedFeature.length === 1) {
            this.idIndex_[idString] = indexedFeature[0];
          }
        }
      }
      delete this.uidIndex_[featureKey];
      if (this.hasListener(VectorEventType.REMOVEFEATURE)) {
        this.dispatchEvent(
          new VectorSourceEvent(VectorEventType.REMOVEFEATURE, feature),
        );
      }
      return true;
    }

    /**
     * Remove a feature from the id index.  Called internally when the feature id
     * may have changed.
     * @param {FeatureType} feature The feature.
     * @private
     */
    removeFromIdIndex_(feature) {
      for (const id in this.idIndex_) {
        if (this.idIndex_[id] === feature) {
          delete this.idIndex_[id];
          break;
        }
      }
    }

    /**
     * Set the new loader of the source. The next render cycle will use the
     * new loader.
     * @param {import("../featureloader.js").FeatureLoader} loader The loader to set.
     * @api
     */
    setLoader(loader) {
      this.loader_ = loader;
    }

    /**
     * Points the source to a new url. The next render cycle will use the new url.
     * @param {string|import("../featureloader.js").FeatureUrlFunction} url Url.
     * @api
     */
    setUrl(url) {
      assert(this.format_, '`format` must be set when `url` is set');
      this.url_ = url;
      this.setLoader(xhr(url, this.format_));
    }

    /**
     * @param {boolean} overlaps The source can have overlapping geometries.
     */
    setOverlaps(overlaps) {
      this.overlaps_ = overlaps;
      this.changed();
    }
  }

  /**
   * @module ol/style/Fill
   */


  /**
   * @typedef {Object} Options
   * @property {import("../color.js").Color|import("../colorlike.js").ColorLike|import('../colorlike.js').PatternDescriptor|null} [color=null] A color,
   * gradient or pattern.
   * See {@link module:ol/color~Color} and {@link module:ol/colorlike~ColorLike} for possible formats. For polygon fills (not for {@link import("./RegularShape.js").default} fills),
   * a pattern can also be provided as {@link module:ol/colorlike~PatternDescriptor}.
   * Default null; if null, the Canvas/renderer default black will be used.
   */

  /**
   * @classdesc
   * Set fill style for vector features.
   * @api
   */
  class Fill {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      options = options || {};

      /**
       * @private
       * @type {import("./IconImage.js").default|null}
       */
      this.patternImage_ = null;

      /**
       * @private
       * @type {import("../color.js").Color|import("../colorlike.js").ColorLike|import('../colorlike.js').PatternDescriptor|null}
       */
      this.color_ = null;
      if (options.color !== undefined) {
        this.setColor(options.color);
      }
    }

    /**
     * Clones the style. The color is not cloned if it is a {@link module:ol/colorlike~ColorLike}.
     * @return {Fill} The cloned style.
     * @api
     */
    clone() {
      const color = this.getColor();
      return new Fill({
        color: Array.isArray(color) ? color.slice() : color || undefined,
      });
    }

    /**
     * Get the fill color.
     * @return {import("../color.js").Color|import("../colorlike.js").ColorLike|import('../colorlike.js').PatternDescriptor|null} Color.
     * @api
     */
    getColor() {
      return this.color_;
    }

    /**
     * Set the color.
     *
     * @param {import("../color.js").Color|import("../colorlike.js").ColorLike|import('../colorlike.js').PatternDescriptor|null} color Color.
     * @api
     */
    setColor(color) {
      if (color !== null && typeof color === 'object' && 'src' in color) {
        const patternImage = get(
          null,
          color.src,
          {crossOrigin: 'anonymous'},
          undefined,
          color.offset ? null : color.color ? color.color : null,
          !(color.offset && color.size),
        );
        patternImage.ready().then(() => {
          this.patternImage_ = null;
        });
        if (patternImage.getImageState() === ImageState.IDLE) {
          patternImage.load();
        }
        if (patternImage.getImageState() === ImageState.LOADING) {
          this.patternImage_ = patternImage;
        }
      }
      this.color_ = color;
    }

    /**
     * @return {string} Key of the fill for cache lookup.
     */
    getKey() {
      const fill = this.getColor();
      if (!fill) {
        return '';
      }
      return fill instanceof CanvasPattern || fill instanceof CanvasGradient
        ? getUid(fill)
        : typeof fill === 'object' && 'src' in fill
          ? fill.src + ':' + fill.offset
          : asArray(fill).toString();
    }

    /**
     * @return {boolean} The fill style is loading an image pattern.
     */
    loading() {
      return !!this.patternImage_;
    }

    /**
     * @return {Promise<void>} `false` or a promise that resolves when the style is ready to use.
     */
    ready() {
      return this.patternImage_ ? this.patternImage_.ready() : Promise.resolve();
    }
  }

  /**
   * @module ol/style/Stroke
   */

  /**
   * @typedef {Object} Options
   * @property {import("../color.js").Color|import("../colorlike.js").ColorLike} [color] A color, gradient or pattern.
   * See {@link module:ol/color~Color} and {@link module:ol/colorlike~ColorLike} for possible formats.
   * Default null; if null, the Canvas/renderer default black will be used.
   * @property {CanvasLineCap} [lineCap='round'] Line cap style: `butt`, `round`, or `square`.
   * @property {CanvasLineJoin} [lineJoin='round'] Line join style: `bevel`, `round`, or `miter`.
   * @property {Array<number>} [lineDash] Line dash pattern. Default is `null` (no dash).
   * @property {number} [lineDashOffset=0] Line dash offset.
   * @property {number} [miterLimit=10] Miter limit.
   * @property {number} [offset] Line offset in pixels along the normal. A positive value offsets the line to the right,
   * relative to the direction of the line. Default is `null` (no offset).
   * @property {number} [width] Width.
   */

  /**
   * @classdesc
   * Set stroke style for vector features.
   * Note that the defaults given are the Canvas defaults, which will be used if
   * option is not defined. The `get` functions return whatever was entered in
   * the options; they will not return the default.
   * @api
   */
  class Stroke {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      options = options || {};

      /**
       * @private
       * @type {import("../color.js").Color|import("../colorlike.js").ColorLike}
       */
      this.color_ = options.color !== undefined ? options.color : null;

      /**
       * @private
       * @type {CanvasLineCap|undefined}
       */
      this.lineCap_ = options.lineCap;

      /**
       * @private
       * @type {Array<number>|null}
       */
      this.lineDash_ = options.lineDash !== undefined ? options.lineDash : null;

      /**
       * @private
       * @type {number|undefined}
       */
      this.lineDashOffset_ = options.lineDashOffset;

      /**
       * @private
       * @type {CanvasLineJoin|undefined}
       */
      this.lineJoin_ = options.lineJoin;

      /**
       * @private
       * @type {number|undefined}
       */
      this.miterLimit_ = options.miterLimit;

      /**
       * @private
       * @type {number|undefined}
       */
      this.offset_ = options.offset;

      /**
       * @private
       * @type {number|undefined}
       */
      this.width_ = options.width;
    }

    /**
     * Clones the style.
     * @return {Stroke} The cloned style.
     * @api
     */
    clone() {
      const color = this.getColor();
      return new Stroke({
        color: Array.isArray(color) ? color.slice() : color || undefined,
        lineCap: this.getLineCap(),
        lineDash: this.getLineDash() ? this.getLineDash().slice() : undefined,
        lineDashOffset: this.getLineDashOffset(),
        lineJoin: this.getLineJoin(),
        miterLimit: this.getMiterLimit(),
        offset: this.getOffset(),
        width: this.getWidth(),
      });
    }

    /**
     * Get the stroke color.
     * @return {import("../color.js").Color|import("../colorlike.js").ColorLike} Color.
     * @api
     */
    getColor() {
      return this.color_;
    }

    /**
     * Get the line cap type for the stroke.
     * @return {CanvasLineCap|undefined} Line cap.
     * @api
     */
    getLineCap() {
      return this.lineCap_;
    }

    /**
     * Get the line dash style for the stroke.
     * @return {Array<number>|null} Line dash.
     * @api
     */
    getLineDash() {
      return this.lineDash_;
    }

    /**
     * Get the line dash offset for the stroke.
     * @return {number|undefined} Line dash offset.
     * @api
     */
    getLineDashOffset() {
      return this.lineDashOffset_;
    }

    /**
     * Get the line join type for the stroke.
     * @return {CanvasLineJoin|undefined} Line join.
     * @api
     */
    getLineJoin() {
      return this.lineJoin_;
    }

    /**
     * Get the miter limit for the stroke.
     * @return {number|undefined} Miter limit.
     * @api
     */
    getMiterLimit() {
      return this.miterLimit_;
    }

    /**
     * Get the line offset in pixels.
     * @return {number|undefined} Offset.
     * @api
     */
    getOffset() {
      return this.offset_;
    }

    /**
     * Get the stroke width.
     * @return {number|undefined} Width.
     * @api
     */
    getWidth() {
      return this.width_;
    }

    /**
     * Set the color.
     *
     * @param {import("../color.js").Color|import("../colorlike.js").ColorLike} color Color.
     * @api
     */
    setColor(color) {
      this.color_ = color;
    }

    /**
     * Set the line cap.
     *
     * @param {CanvasLineCap|undefined} lineCap Line cap.
     * @api
     */
    setLineCap(lineCap) {
      this.lineCap_ = lineCap;
    }

    /**
     * Set the line dash.
     *
     * @param {Array<number>|null} lineDash Line dash.
     * @api
     */
    setLineDash(lineDash) {
      this.lineDash_ = lineDash;
    }

    /**
     * Set the line dash offset.
     *
     * @param {number|undefined} lineDashOffset Line dash offset.
     * @api
     */
    setLineDashOffset(lineDashOffset) {
      this.lineDashOffset_ = lineDashOffset;
    }

    /**
     * Set the line join.
     *
     * @param {CanvasLineJoin|undefined} lineJoin Line join.
     * @api
     */
    setLineJoin(lineJoin) {
      this.lineJoin_ = lineJoin;
    }

    /**
     * Set the miter limit.
     *
     * @param {number|undefined} miterLimit Miter limit.
     * @api
     */
    setMiterLimit(miterLimit) {
      this.miterLimit_ = miterLimit;
    }

    /**
     * Set the line offset in pixels.
     *
     * @param {number|undefined} offset Offset.
     * @api
     */
    setOffset(offset) {
      this.offset_ = offset;
    }

    /**
     * Set the width.
     *
     * @param {number|undefined} width Width.
     * @api
     */
    setWidth(width) {
      this.width_ = width;
    }
  }

  /**
   * @module ol/size
   */


  /**
   * Determines if a size has a positive area.
   * @param {Size} size The size to test.
   * @return {boolean} The size has a positive area.
   */
  function hasArea(size) {
    return size[0] > 0 && size[1] > 0;
  }

  /**
   * Returns a size scaled by a ratio. The result will be an array of integers.
   * @param {Size} size Size.
   * @param {number} ratio Ratio.
   * @param {Size} [dest] Optional reusable size array.
   * @return {Size} The scaled size.
   */
  function scale(size, ratio, dest) {
    if (dest === undefined) {
      dest = [0, 0];
    }
    dest[0] = (size[0] * ratio + 0.5) | 0;
    dest[1] = (size[1] * ratio + 0.5) | 0;
    return dest;
  }

  /**
   * Returns an `Size` array for the passed in number (meaning: square) or
   * `Size` array.
   * (meaning: non-square),
   * @param {number|Size} size Width and height.
   * @param {Size} [dest] Optional reusable size array.
   * @return {Size} Size.
   * @api
   */
  function toSize(size, dest) {
    if (Array.isArray(size)) {
      return size;
    }
    if (dest === undefined) {
      dest = [size, size];
    } else {
      dest[0] = size;
      dest[1] = size;
    }
    return dest;
  }

  /**
   * @module ol/style/Image
   */

  /**
   * @typedef {Object} Options
   * @property {number} opacity Opacity.
   * @property {boolean} rotateWithView If the image should get rotated with the view.
   * @property {number} rotation Rotation.
   * @property {number|import("../size.js").Size} scale Scale.
   * @property {Array<number>} displacement Displacement.
   * @property {import('../style/Style.js').DeclutterMode} declutterMode Declutter mode: `declutter`, `obstacle`, `none`.
   */

  /**
   * @classdesc
   * A base class used for creating subclasses and not instantiated in
   * apps. Base class for {@link module:ol/style/Icon~Icon}, {@link module:ol/style/Circle~CircleStyle} and
   * {@link module:ol/style/RegularShape~RegularShape}.
   * @abstract
   * @api
   */
  class ImageStyle {
    /**
     * @param {Options} options Options.
     */
    constructor(options) {
      /**
       * @private
       * @type {number}
       */
      this.opacity_ = options.opacity;

      /**
       * @private
       * @type {boolean}
       */
      this.rotateWithView_ = options.rotateWithView;

      /**
       * @private
       * @type {number}
       */
      this.rotation_ = options.rotation;

      /**
       * @private
       * @type {number|import("../size.js").Size}
       */
      this.scale_ = options.scale;

      /**
       * @private
       * @type {import("../size.js").Size}
       */
      this.scaleArray_ = toSize(options.scale);

      /**
       * @private
       * @type {Array<number>}
       */
      this.displacement_ = options.displacement;

      /**
       * @private
       * @type {import('../style/Style.js').DeclutterMode}
       */
      this.declutterMode_ = options.declutterMode;
    }

    /**
     * Clones the style.
     * @return {ImageStyle} The cloned style.
     * @api
     */
    clone() {
      const scale = this.getScale();
      return new ImageStyle({
        opacity: this.getOpacity(),
        scale: Array.isArray(scale) ? scale.slice() : scale,
        rotation: this.getRotation(),
        rotateWithView: this.getRotateWithView(),
        displacement: this.getDisplacement().slice(),
        declutterMode: this.getDeclutterMode(),
      });
    }

    /**
     * Get the symbolizer opacity.
     * @return {number} Opacity.
     * @api
     */
    getOpacity() {
      return this.opacity_;
    }

    /**
     * Determine whether the symbolizer rotates with the map.
     * @return {boolean} Rotate with map.
     * @api
     */
    getRotateWithView() {
      return this.rotateWithView_;
    }

    /**
     * Get the symoblizer rotation.
     * @return {number} Rotation.
     * @api
     */
    getRotation() {
      return this.rotation_;
    }

    /**
     * Get the symbolizer scale.
     * @return {number|import("../size.js").Size} Scale.
     * @api
     */
    getScale() {
      return this.scale_;
    }

    /**
     * Get the symbolizer scale array.
     * @return {import("../size.js").Size} Scale array.
     */
    getScaleArray() {
      return this.scaleArray_;
    }

    /**
     * Get the displacement of the shape
     * @return {Array<number>} Shape's center displacement
     * @api
     */
    getDisplacement() {
      return this.displacement_;
    }

    /**
     * Get the declutter mode of the shape
     * @return {import("./Style.js").DeclutterMode} Shape's declutter mode
     * @api
     */
    getDeclutterMode() {
      return this.declutterMode_;
    }

    /**
     * Get the anchor point in pixels. The anchor determines the center point for the
     * symbolizer.
     * @abstract
     * @return {Array<number>} Anchor.
     */
    getAnchor() {
      return abstract();
    }

    /**
     * Get the image element for the symbolizer.
     * @abstract
     * @param {number} pixelRatio Pixel ratio.
     * @return {import('../DataTile.js').ImageLike} Image element.
     */
    getImage(pixelRatio) {
      return abstract();
    }

    /**
     * @abstract
     * @return {import('../DataTile.js').ImageLike} Image element.
     */
    getHitDetectionImage() {
      return abstract();
    }

    /**
     * Get the image pixel ratio.
     * @param {number} pixelRatio Pixel ratio.
     * @return {number} Pixel ratio.
     */
    getPixelRatio(pixelRatio) {
      return 1;
    }

    /**
     * @abstract
     * @return {import("../ImageState.js").default} Image state.
     */
    getImageState() {
      return abstract();
    }

    /**
     * @abstract
     * @return {import("../size.js").Size} Image size.
     */
    getImageSize() {
      return abstract();
    }

    /**
     * Get the origin of the symbolizer.
     * @abstract
     * @return {Array<number>} Origin.
     */
    getOrigin() {
      return abstract();
    }

    /**
     * Get the size of the symbolizer (in pixels).
     * @abstract
     * @return {import("../size.js").Size} Size.
     */
    getSize() {
      return abstract();
    }

    /**
     * Set the displacement.
     *
     * @param {Array<number>} displacement Displacement.
     * @api
     */
    setDisplacement(displacement) {
      this.displacement_ = displacement;
    }

    /**
     * Set the opacity.
     *
     * @param {number} opacity Opacity.
     * @api
     */
    setOpacity(opacity) {
      this.opacity_ = opacity;
    }

    /**
     * Set whether to rotate the style with the view.
     *
     * @param {boolean} rotateWithView Rotate with map.
     * @api
     */
    setRotateWithView(rotateWithView) {
      this.rotateWithView_ = rotateWithView;
    }

    /**
     * Set the rotation.
     *
     * @param {number} rotation Rotation.
     * @api
     */
    setRotation(rotation) {
      this.rotation_ = rotation;
    }

    /**
     * Set the scale.
     *
     * @param {number|import("../size.js").Size} scale Scale.
     * @api
     */
    setScale(scale) {
      this.scale_ = scale;
      this.scaleArray_ = toSize(scale);
    }

    /**
     * @abstract
     * @param {function(import("../events/Event.js").default): void} listener Listener function.
     */
    listenImageChange(listener) {
      abstract();
    }

    /**
     * Load not yet loaded URI.
     * @abstract
     */
    load() {
      abstract();
    }

    /**
     * @abstract
     * @param {function(import("../events/Event.js").default): void} listener Listener function.
     */
    unlistenImageChange(listener) {
      abstract();
    }

    /**
     * @return {Promise<void>} `false` or Promise that resolves when the style is ready to use.
     */
    ready() {
      return Promise.resolve();
    }
  }

  /**
   * @module ol/style/RegularShape
   */


  /**
   * Specify radius for regular polygons, or both radius and radius2 for stars.
   * @typedef {Object} Options
   * @property {import("./Fill.js").default} [fill] Fill style.
   * @property {number} points Number of points for stars and regular polygons. In case of a polygon, the number of points
   * is the number of sides.
   * @property {number} radius Radius of a regular polygon.
   * @property {number} [radius2] Second radius to make a star instead of a regular polygon.
   * @property {number} [angle=0] Shape's angle in radians. A value of 0 will have one of the shape's points facing up.
   * @property {Array<number>} [displacement=[0, 0]] Displacement of the shape in pixels.
   * Positive values will shift the shape right and up.
   * @property {import("./Stroke.js").default} [stroke] Stroke style.
   * @property {number} [rotation=0] Rotation in radians (positive rotation clockwise).
   * @property {boolean} [rotateWithView=false] Whether to rotate the shape with the view.
   * @property {number|import("../size.js").Size} [scale=1] Scale. Unless two dimensional scaling is required a better
   * result may be obtained with appropriate settings for `radius` and `radius2`.
   * @property {import('./Style.js').DeclutterMode} [declutterMode] Declutter mode.
   */

  /**
   * @typedef {Object} RenderOptions
   * @property {import("../colorlike.js").ColorLike|undefined} strokeStyle StrokeStyle.
   * @property {number} strokeWidth StrokeWidth.
   * @property {number} size Size.
   * @property {CanvasLineCap} lineCap LineCap.
   * @property {Array<number>|null} lineDash LineDash.
   * @property {number} lineDashOffset LineDashOffset.
   * @property {CanvasLineJoin} lineJoin LineJoin.
   * @property {number} miterLimit MiterLimit.
   */

  /**
   * @classdesc
   * Set regular shape style for vector features. The resulting shape will be
   * a regular polygon when `radius` is provided, or a star when both `radius` and
   * `radius2` are provided.
   * @api
   */
  class RegularShape extends ImageStyle {
    /**
     * @param {Options} options Options.
     */
    constructor(options) {
      super({
        opacity: 1,
        rotateWithView:
          options.rotateWithView !== undefined ? options.rotateWithView : false,
        rotation: options.rotation !== undefined ? options.rotation : 0,
        scale: options.scale !== undefined ? options.scale : 1,
        displacement:
          options.displacement !== undefined ? options.displacement : [0, 0],
        declutterMode: options.declutterMode,
      });

      /**
       * @private
       * @type {HTMLCanvasElement|OffscreenCanvas|null}
       */
      this.hitDetectionCanvas_ = null;

      /**
       * @private
       * @type {import("./Fill.js").default|null}
       */
      this.fill_ = options.fill !== undefined ? options.fill : null;

      /**
       * @private
       * @type {Array<number>}
       */
      this.origin_ = [0, 0];

      /**
       * @private
       * @type {number}
       */
      this.points_ = options.points;

      /**
       * @protected
       * @type {number}
       */
      this.radius = options.radius;

      /**
       * @private
       * @type {number|undefined}
       */
      this.radius2_ = options.radius2;

      /**
       * @private
       * @type {number}
       */
      this.angle_ = options.angle !== undefined ? options.angle : 0;

      /**
       * @private
       * @type {import("./Stroke.js").default|null}
       */
      this.stroke_ = options.stroke !== undefined ? options.stroke : null;

      /**
       * @private
       * @type {import("../size.js").Size}
       */
      this.size_;

      /**
       * @private
       * @type {RenderOptions}
       */
      this.renderOptions_;

      /**
       * @private
       */
      this.imageState_ =
        this.fill_ && this.fill_.loading()
          ? ImageState.LOADING
          : ImageState.LOADED;
      if (this.imageState_ === ImageState.LOADING) {
        this.ready().then(() => (this.imageState_ = ImageState.LOADED));
      }
      this.render();
    }

    /**
     * Clones the style.
     * @return {RegularShape} The cloned style.
     * @api
     * @override
     */
    clone() {
      const scale = this.getScale();
      const style = new RegularShape({
        fill: this.getFill() ? this.getFill().clone() : undefined,
        points: this.getPoints(),
        radius: this.getRadius(),
        radius2: this.getRadius2(),
        angle: this.getAngle(),
        stroke: this.getStroke() ? this.getStroke().clone() : undefined,
        rotation: this.getRotation(),
        rotateWithView: this.getRotateWithView(),
        scale: Array.isArray(scale) ? scale.slice() : scale,
        displacement: this.getDisplacement().slice(),
        declutterMode: this.getDeclutterMode(),
      });
      style.setOpacity(this.getOpacity());
      return style;
    }

    /**
     * Get the anchor point in pixels. The anchor determines the center point for the
     * symbolizer.
     * @return {Array<number>} Anchor.
     * @api
     * @override
     */
    getAnchor() {
      const size = this.size_;
      const displacement = this.getDisplacement();
      const scale = this.getScaleArray();
      // anchor is scaled by renderer but displacement should not be scaled
      // so divide by scale here
      return [
        size[0] / 2 - displacement[0] / scale[0],
        size[1] / 2 + displacement[1] / scale[1],
      ];
    }

    /**
     * Get the angle used in generating the shape.
     * @return {number} Shape's rotation in radians.
     * @api
     */
    getAngle() {
      return this.angle_;
    }

    /**
     * Get the fill style for the shape.
     * @return {import("./Fill.js").default|null} Fill style.
     * @api
     */
    getFill() {
      return this.fill_;
    }

    /**
     * Set the fill style.
     * @param {import("./Fill.js").default|null} fill Fill style.
     * @api
     */
    setFill(fill) {
      this.fill_ = fill;
      this.render();
    }

    /**
     * @return {HTMLCanvasElement|OffscreenCanvas} Image element.
     * @override
     */
    getHitDetectionImage() {
      if (!this.hitDetectionCanvas_) {
        this.hitDetectionCanvas_ = this.createHitDetectionCanvas_(
          this.renderOptions_,
        );
      }
      return this.hitDetectionCanvas_;
    }

    /**
     * Get the image icon.
     * @param {number} pixelRatio Pixel ratio.
     * @return {HTMLCanvasElement|OffscreenCanvas} Image or Canvas element.
     * @api
     * @override
     */
    getImage(pixelRatio) {
      const fillKey = this.fill_?.getKey();
      const cacheKey =
        `${pixelRatio},${this.angle_},${this.radius},${this.radius2_},${this.points_},${fillKey}` +
        Object.values(this.renderOptions_).join(',');
      let image = /** @type {HTMLCanvasElement|OffscreenCanvas} */ (
        shared.get(cacheKey, null)?.getImage(1)
      );
      if (!image) {
        const renderOptions = this.renderOptions_;
        const size = Math.ceil(renderOptions.size * pixelRatio);
        const context = createCanvasContext2D(size, size);
        this.draw_(renderOptions, context, pixelRatio);

        image = context.canvas;
        const iconImage = new IconImage(
          image,
          undefined,
          null,
          ImageState.LOADED,
          null,
        );
        shared.set(cacheKey, null, iconImage);
        // Update the image in place to an ImageBitmap for better performance and lower memory usage
        createImageBitmap(image).then((imageBitmap) => {
          iconImage.setImage(imageBitmap);
        });
      }
      return image;
    }

    /**
     * Get the image pixel ratio.
     * @param {number} pixelRatio Pixel ratio.
     * @return {number} Pixel ratio.
     * @override
     */
    getPixelRatio(pixelRatio) {
      return pixelRatio;
    }

    /**
     * @return {import("../size.js").Size} Image size.
     * @override
     */
    getImageSize() {
      return this.size_;
    }

    /**
     * @return {import("../ImageState.js").default} Image state.
     * @override
     */
    getImageState() {
      return this.imageState_;
    }

    /**
     * Get the origin of the symbolizer.
     * @return {Array<number>} Origin.
     * @api
     * @override
     */
    getOrigin() {
      return this.origin_;
    }

    /**
     * Get the number of points for generating the shape.
     * @return {number} Number of points for stars and regular polygons.
     * @api
     */
    getPoints() {
      return this.points_;
    }

    /**
     * Get the (primary) radius for the shape.
     * @return {number} Radius.
     * @api
     */
    getRadius() {
      return this.radius;
    }

    /**
     * Set the (primary) radius for the shape.
     * @param {number} radius Radius.
     * @api
     */
    setRadius(radius) {
      if (this.radius === radius) {
        return;
      }
      this.radius = radius;
      this.render();
    }

    /**
     * Get the secondary radius for the shape.
     * @return {number|undefined} Radius2.
     * @api
     */
    getRadius2() {
      return this.radius2_;
    }

    /**
     * Set the secondary radius for the shape.
     * @param {number|undefined} radius2 Radius2.
     * @api
     */
    setRadius2(radius2) {
      if (this.radius2_ === radius2) {
        return;
      }
      this.radius2_ = radius2;
      this.render();
    }

    /**
     * Get the size of the symbolizer (in pixels).
     * @return {import("../size.js").Size} Size.
     * @api
     * @override
     */
    getSize() {
      return this.size_;
    }

    /**
     * Get the stroke style for the shape.
     * @return {import("./Stroke.js").default|null} Stroke style.
     * @api
     */
    getStroke() {
      return this.stroke_;
    }

    /**
     * Set the stroke style.
     * @param {import("./Stroke.js").default|null} stroke Stroke style.
     * @api
     */
    setStroke(stroke) {
      this.stroke_ = stroke;
      this.render();
    }

    /**
     * @param {function(import("../events/Event.js").default): void} listener Listener function.
     * @override
     */
    listenImageChange(listener) {}

    /**
     * Load not yet loaded URI.
     * @override
     */
    load() {}

    /**
     * @param {function(import("../events/Event.js").default): void} listener Listener function.
     * @override
     */
    unlistenImageChange(listener) {}

    /**
     * Calculate additional canvas size needed for the miter.
     * @param {string} lineJoin Line join
     * @param {number} strokeWidth Stroke width
     * @param {number} miterLimit Miter limit
     * @return {number} Additional canvas size needed
     * @private
     */
    calculateLineJoinSize_(lineJoin, strokeWidth, miterLimit) {
      if (
        strokeWidth === 0 ||
        this.points_ === Infinity ||
        (lineJoin !== 'bevel' && lineJoin !== 'miter')
      ) {
        return strokeWidth;
      }
      // m  | ^
      // i  | |\                  .
      // t >|  #\
      // e  | |\ \              .
      // r      \s\
      //      |  \t\          .                 .
      //          \r\                      .   .
      //      |    \o\      .          .  . . .
      //          e \k\            .  .    . .
      //      |      \e\  .    .  .       . .
      //       d      \ \  .  .          . .
      //      | _ _a_ _\#  .            . .
      //   r1          / `             . .
      //      |                       . .
      //       b     /               . .
      //      |                     . .
      //           / r2            . .
      //      |                        .   .
      //         /                           .   .
      //      |α                                   .   .
      //       /                                         .   .
      //      ° center
      let r1 = this.radius;
      let r2 = this.radius2_ === undefined ? r1 : this.radius2_;
      if (r1 < r2) {
        const tmp = r1;
        r1 = r2;
        r2 = tmp;
      }
      const points =
        this.radius2_ === undefined ? this.points_ : this.points_ * 2;
      const alpha = (2 * Math.PI) / points;
      const a = r2 * Math.sin(alpha);
      const b = Math.sqrt(r2 * r2 - a * a);
      const d = r1 - b;
      const e = Math.sqrt(a * a + d * d);
      const miterRatio = e / a;
      if (lineJoin === 'miter' && miterRatio <= miterLimit) {
        return miterRatio * strokeWidth;
      }
      // Calculate the distance from center to the stroke corner where
      // it was cut short because of the miter limit.
      //              l
      //        ----+---- <= distance from center to here is maxr
      //       /####|k ##\
      //      /#####^#####\
      //     /#### /+\# s #\
      //    /### h/+++\# t #\
      //   /### t/+++++\# r #\
      //  /### a/+++++++\# o #\
      // /### p/++ fill +\# k #\
      ///#### /+++++^+++++\# e #\
      //#####/+++++/+\+++++\#####\
      const k = strokeWidth / 2 / miterRatio;
      const l = (strokeWidth / 2) * (d / e);
      const maxr = Math.sqrt((r1 + k) * (r1 + k) + l * l);
      const bevelAdd = maxr - r1;
      if (this.radius2_ === undefined || lineJoin === 'bevel') {
        return bevelAdd * 2;
      }
      // If outer miter is over the miter limit the inner miter may reach through the
      // center and be longer than the bevel, same calculation as above but swap r1 / r2.
      const aa = r1 * Math.sin(alpha);
      const bb = Math.sqrt(r1 * r1 - aa * aa);
      const dd = r2 - bb;
      const ee = Math.sqrt(aa * aa + dd * dd);
      const innerMiterRatio = ee / aa;
      if (innerMiterRatio <= miterLimit) {
        const innerLength = (innerMiterRatio * strokeWidth) / 2 - r2 - r1;
        return 2 * Math.max(bevelAdd, innerLength);
      }
      return bevelAdd * 2;
    }

    /**
     * @return {RenderOptions}  The render options
     * @protected
     */
    createRenderOptions() {
      let lineCap = defaultLineCap;
      let lineJoin = defaultLineJoin;
      let miterLimit = 0;
      let lineDash = null;
      let lineDashOffset = 0;
      let strokeStyle;
      let strokeWidth = 0;

      if (this.stroke_) {
        strokeStyle = asColorLike(this.stroke_.getColor() ?? defaultStrokeStyle);
        strokeWidth = this.stroke_.getWidth() ?? defaultLineWidth;
        lineDash = this.stroke_.getLineDash();
        lineDashOffset = this.stroke_.getLineDashOffset() ?? 0;
        lineJoin = this.stroke_.getLineJoin() ?? defaultLineJoin;
        lineCap = this.stroke_.getLineCap() ?? defaultLineCap;
        miterLimit = this.stroke_.getMiterLimit() ?? defaultMiterLimit;
      }

      const add = this.calculateLineJoinSize_(lineJoin, strokeWidth, miterLimit);
      const maxRadius = Math.max(this.radius, this.radius2_ || 0);
      const size = Math.ceil(2 * maxRadius + add);

      return {
        strokeStyle: strokeStyle,
        strokeWidth: strokeWidth,
        size: size,
        lineCap: lineCap,
        lineDash: lineDash,
        lineDashOffset: lineDashOffset,
        lineJoin: lineJoin,
        miterLimit: miterLimit,
      };
    }

    /**
     * @protected
     */
    render() {
      this.renderOptions_ = this.createRenderOptions();
      const size = this.renderOptions_.size;
      this.hitDetectionCanvas_ = null;
      this.size_ = [size, size];
    }

    /**
     * @private
     * @param {RenderOptions} renderOptions Render options.
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context The rendering context.
     * @param {number} pixelRatio The pixel ratio.
     */
    draw_(renderOptions, context, pixelRatio) {
      context.scale(pixelRatio, pixelRatio);
      // set origin to canvas center
      context.translate(renderOptions.size / 2, renderOptions.size / 2);

      this.createPath_(context);

      if (this.fill_) {
        let color = this.fill_.getColor();
        if (color === null) {
          color = defaultFillStyle;
        }
        context.fillStyle = asColorLike(color);
        context.fill();
      }
      if (renderOptions.strokeStyle) {
        context.strokeStyle = renderOptions.strokeStyle;
        context.lineWidth = renderOptions.strokeWidth;
        if (renderOptions.lineDash) {
          context.setLineDash(renderOptions.lineDash);
          context.lineDashOffset = renderOptions.lineDashOffset;
        }
        context.lineCap = renderOptions.lineCap;
        context.lineJoin = renderOptions.lineJoin;
        context.miterLimit = renderOptions.miterLimit;
        context.stroke();
      }
    }

    /**
     * @private
     * @param {RenderOptions} renderOptions Render options.
     * @return {HTMLCanvasElement|OffscreenCanvas} Canvas containing the icon
     */
    createHitDetectionCanvas_(renderOptions) {
      let context;
      if (this.fill_) {
        let color = this.fill_.getColor();

        // determine if fill is transparent (or pattern or gradient)
        let opacity = 0;
        if (typeof color === 'string') {
          color = asArray(color);
        }
        if (color === null) {
          opacity = 1;
        } else if (Array.isArray(color)) {
          opacity = color.length === 4 ? color[3] : 1;
        }
        if (opacity === 0) {
          // if a transparent fill style is set, create an extra hit-detection image
          // with a default fill style
          context = createCanvasContext2D(renderOptions.size, renderOptions.size);
          this.drawHitDetectionCanvas_(renderOptions, context);
        }
      }
      return context ? context.canvas : this.getImage(1);
    }

    /**
     * @private
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context The context to draw in.
     */
    createPath_(context) {
      let points = this.points_;
      const radius = this.radius;
      if (points === Infinity) {
        context.arc(0, 0, radius, 0, 2 * Math.PI);
      } else {
        const radius2 = this.radius2_ === undefined ? radius : this.radius2_;
        if (this.radius2_ !== undefined) {
          points *= 2;
        }
        const startAngle = this.angle_ - Math.PI / 2;
        const step = (2 * Math.PI) / points;
        for (let i = 0; i < points; i++) {
          const angle0 = startAngle + i * step;
          const radiusC = i % 2 === 0 ? radius : radius2;
          context.lineTo(radiusC * Math.cos(angle0), radiusC * Math.sin(angle0));
        }
        context.closePath();
      }
    }

    /**
     * @private
     * @param {RenderOptions} renderOptions Render options.
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context The context.
     */
    drawHitDetectionCanvas_(renderOptions, context) {
      // set origin to canvas center
      context.translate(renderOptions.size / 2, renderOptions.size / 2);

      this.createPath_(context);

      context.fillStyle = defaultFillStyle;
      context.fill();
      if (renderOptions.strokeStyle) {
        context.strokeStyle = renderOptions.strokeStyle;
        context.lineWidth = renderOptions.strokeWidth;
        if (renderOptions.lineDash) {
          context.setLineDash(renderOptions.lineDash);
          context.lineDashOffset = renderOptions.lineDashOffset;
        }
        context.lineJoin = renderOptions.lineJoin;
        context.miterLimit = renderOptions.miterLimit;
        context.stroke();
      }
    }

    /**
     * @override
     */
    ready() {
      return this.fill_ ? this.fill_.ready() : Promise.resolve();
    }
  }

  /**
   * @module ol/style/Circle
   */


  /**
   * @typedef {Object} Options
   * @property {import("./Fill.js").default} [fill] Fill style.
   * @property {number} radius Circle radius.
   * @property {import("./Stroke.js").default} [stroke] Stroke style.
   * @property {Array<number>} [displacement=[0,0]] displacement
   * @property {number|import("../size.js").Size} [scale=1] Scale. A two dimensional scale will produce an ellipse.
   * Unless two dimensional scaling is required a better result may be obtained with an appropriate setting for `radius`.
   * @property {number} [rotation=0] Rotation in radians
   * (positive rotation clockwise, meaningful only when used in conjunction with a two dimensional scale).
   * @property {boolean} [rotateWithView=false] Whether to rotate the shape with the view
   * (meaningful only when used in conjunction with a two dimensional scale).
   * @property {import('./Style.js').DeclutterMode} [declutterMode] Declutter mode
   */

  /**
   * @classdesc
   * Set circle style for vector features.
   * @api
   */
  class CircleStyle extends RegularShape {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      options = options ? options : {radius: 5};

      super({
        points: Infinity,
        fill: options.fill,
        radius: options.radius,
        stroke: options.stroke,
        scale: options.scale !== undefined ? options.scale : 1,
        rotation: options.rotation !== undefined ? options.rotation : 0,
        rotateWithView:
          options.rotateWithView !== undefined ? options.rotateWithView : false,
        displacement:
          options.displacement !== undefined ? options.displacement : [0, 0],
        declutterMode: options.declutterMode,
      });
    }

    /**
     * Clones the style.
     * @return {CircleStyle} The cloned style.
     * @api
     * @override
     */
    clone() {
      const scale = this.getScale();
      const style = new CircleStyle({
        fill: this.getFill() ? this.getFill().clone() : undefined,
        stroke: this.getStroke() ? this.getStroke().clone() : undefined,
        radius: this.getRadius(),
        scale: Array.isArray(scale) ? scale.slice() : scale,
        rotation: this.getRotation(),
        rotateWithView: this.getRotateWithView(),
        displacement: this.getDisplacement().slice(),
        declutterMode: this.getDeclutterMode(),
      });
      style.setOpacity(this.getOpacity());
      return style;
    }
  }

  /**
   * @module ol/style/Style
   */


  /**
   * Defines how symbols and text are decluttered on layers ith `declutter` set to `true`
   * **declutter**: Overlapping symbols and text are decluttered.
   * **obstacle**: Symbols and text are rendered, but serve as obstacle for subsequent attempts
   *   to place a symbol or text at the same location.
   * **none**: No decluttering is done.
   *
   * @typedef {"declutter"|"obstacle"|"none"} DeclutterMode
   */

  /**
   * A function that takes a {@link module:ol/Feature~Feature} and a `{number}`
   * representing the view's resolution. The function should return a
   * {@link module:ol/style/Style~Style} or an array of them. This way e.g. a
   * vector layer can be styled. If the function returns `undefined`, the
   * feature will not be rendered.
   *
   * @typedef {function(import("../Feature.js").FeatureLike, number):(Style|Array<Style>|void)} StyleFunction
   */

  /**
   * A {@link Style}, an array of {@link Style}, or a {@link StyleFunction}.
   * @typedef {Style|Array<Style>|StyleFunction} StyleLike
   */

  /**
   * A function that takes a {@link module:ol/Feature~Feature} as argument and returns an
   * {@link module:ol/geom/Geometry~Geometry} that will be rendered and styled for the feature.
   *
   * @typedef {function(import("../Feature.js").FeatureLike):
   *     (import("../geom/Geometry.js").default|import("../render/Feature.js").default|undefined)} GeometryFunction
   */

  /**
   * Custom renderer function. Takes two arguments:
   *
   * 1. The pixel coordinates of the geometry in GeoJSON notation.
   * 2. The {@link module:ol/render~State} of the layer renderer.
   *
   * @typedef {function((import("../coordinate.js").Coordinate|Array<import("../coordinate.js").Coordinate>|Array<Array<import("../coordinate.js").Coordinate>>|Array<Array<Array<import("../coordinate.js").Coordinate>>>),import("../render.js").State): void} RenderFunction
   */

  /**
   * @typedef {Object} Options
   * @property {string|import("../geom/Geometry.js").default|GeometryFunction} [geometry] Feature property or geometry
   * or function returning a geometry to render for this style.
   * @property {import("./Fill.js").default} [fill] Fill style.
   * @property {import("./Image.js").default} [image] Image style.
   * @property {RenderFunction} [renderer] Custom renderer. When configured, `fill`, `stroke` and `image` will be
   * ignored, and the provided function will be called with each render frame for each geometry.
   * @property {RenderFunction} [hitDetectionRenderer] Custom renderer for hit detection. If provided will be used
   * in hit detection rendering.
   * @property {import("./Stroke.js").default} [stroke] Stroke style.
   * @property {import("./Text.js").default} [text] Text style.
   * @property {number} [zIndex] Z index.
   */

  /**
   * @classdesc
   * Container for vector feature rendering styles. Any changes made to the style
   * or its children through `set*()` methods will not take effect until the
   * feature or layer that uses the style is re-rendered.
   *
   * ## Feature styles
   *
   * If no style is defined, the following default style is used:
   * ```js
   *  import {Circle, Fill, Stroke, Style} from 'ol/style.js';
   *
   *  const fill = new Fill({
   *    color: 'rgba(255,255,255,0.4)',
   *  });
   *  const stroke = new Stroke({
   *    color: '#3399CC',
   *    width: 1.25,
   *  });
   *  const styles = [
   *    new Style({
   *      image: new Circle({
   *        fill: fill,
   *        stroke: stroke,
   *        radius: 5,
   *      }),
   *      fill: fill,
   *      stroke: stroke,
   *    }),
   *  ];
   * ```
   *
   * A separate editing style has the following defaults:
   * ```js
   *  import {Circle, Fill, Stroke, Style} from 'ol/style.js';
   *
   *  const styles = {};
   *  const white = [255, 255, 255, 1];
   *  const blue = [0, 153, 255, 1];
   *  const width = 3;
   *  styles['Polygon'] = [
   *    new Style({
   *      fill: new Fill({
   *        color: [255, 255, 255, 0.5],
   *      }),
   *    }),
   *  ];
   *  styles['MultiPolygon'] =
   *      styles['Polygon'];
   *  styles['LineString'] = [
   *    new Style({
   *      stroke: new Stroke({
   *        color: white,
   *        width: width + 2,
   *      }),
   *    }),
   *    new Style({
   *      stroke: new Stroke({
   *        color: blue,
   *        width: width,
   *      }),
   *    }),
   *  ];
   *  styles['MultiLineString'] = styles['LineString'];
   *
   *  styles['Circle'] = styles['Polygon'].concat(
   *    styles['LineString']
   *  );
   *
   *  styles['Point'] = [
   *    new Style({
   *      image: new Circle({
   *        radius: width * 2,
   *        fill: new Fill({
   *          color: blue,
   *        }),
   *        stroke: new Stroke({
   *          color: white,
   *          width: width / 2,
   *        }),
   *      }),
   *      zIndex: Infinity,
   *    }),
   *  ];
   *  styles['MultiPoint'] =
   *      styles['Point'];
   *  styles['GeometryCollection'] =
   *      styles['Polygon'].concat(
   *          styles['LineString'],
   *          styles['Point']
   *      );
   * ```
   *
   * @api
   */
  class Style {
    /**
     * @param {Options} [options] Style options.
     */
    constructor(options) {
      options = options || {};

      /**
       * @private
       * @type {string|import("../geom/Geometry.js").default|GeometryFunction|null}
       */
      this.geometry_ = null;

      /**
       * @private
       * @type {!GeometryFunction}
       */
      this.geometryFunction_ = defaultGeometryFunction;

      if (options.geometry !== undefined) {
        this.setGeometry(options.geometry);
      }

      /**
       * @private
       * @type {import("./Fill.js").default|null}
       */
      this.fill_ = options.fill !== undefined ? options.fill : null;

      /**
       * @private
       * @type {import("./Image.js").default|null}
       */
      this.image_ = options.image !== undefined ? options.image : null;

      /**
       * @private
       * @type {RenderFunction|null}
       */
      this.renderer_ = options.renderer !== undefined ? options.renderer : null;

      /**
       * @private
       * @type {RenderFunction|null}
       */
      this.hitDetectionRenderer_ =
        options.hitDetectionRenderer !== undefined
          ? options.hitDetectionRenderer
          : null;

      /**
       * @private
       * @type {import("./Stroke.js").default|null}
       */
      this.stroke_ = options.stroke !== undefined ? options.stroke : null;

      /**
       * @private
       * @type {import("./Text.js").default|null}
       */
      this.text_ = options.text !== undefined ? options.text : null;

      /**
       * @private
       * @type {number|undefined}
       */
      this.zIndex_ = options.zIndex;
    }

    /**
     * Clones the style.
     * @return {Style} The cloned style.
     * @api
     */
    clone() {
      let geometry = this.getGeometry();
      if (geometry && typeof geometry === 'object') {
        geometry = /** @type {import("../geom/Geometry.js").default} */ (
          geometry
        ).clone();
      }
      return new Style({
        geometry: geometry ?? undefined,
        fill: this.getFill() ? this.getFill().clone() : undefined,
        image: this.getImage() ? this.getImage().clone() : undefined,
        renderer: this.getRenderer() ?? undefined,
        stroke: this.getStroke() ? this.getStroke().clone() : undefined,
        text: this.getText() ? this.getText().clone() : undefined,
        zIndex: this.getZIndex(),
      });
    }

    /**
     * Get the custom renderer function that was configured with
     * {@link #setRenderer} or the `renderer` constructor option.
     * @return {RenderFunction|null} Custom renderer function.
     * @api
     */
    getRenderer() {
      return this.renderer_;
    }

    /**
     * Sets a custom renderer function for this style. When set, `fill`, `stroke`
     * and `image` options of the style will be ignored.
     * @param {RenderFunction|null} renderer Custom renderer function.
     * @api
     */
    setRenderer(renderer) {
      this.renderer_ = renderer;
    }

    /**
     * Sets a custom renderer function for this style used
     * in hit detection.
     * @param {RenderFunction|null} renderer Custom renderer function.
     * @api
     */
    setHitDetectionRenderer(renderer) {
      this.hitDetectionRenderer_ = renderer;
    }

    /**
     * Get the custom renderer function that was configured with
     * {@link #setHitDetectionRenderer} or the `hitDetectionRenderer` constructor option.
     * @return {RenderFunction|null} Custom renderer function.
     * @api
     */
    getHitDetectionRenderer() {
      return this.hitDetectionRenderer_;
    }

    /**
     * Get the geometry to be rendered.
     * @return {string|import("../geom/Geometry.js").default|GeometryFunction|null}
     * Feature property or geometry or function that returns the geometry that will
     * be rendered with this style.
     * @api
     */
    getGeometry() {
      return this.geometry_;
    }

    /**
     * Get the function used to generate a geometry for rendering.
     * @return {!GeometryFunction} Function that is called with a feature
     * and returns the geometry to render instead of the feature's geometry.
     * @api
     */
    getGeometryFunction() {
      return this.geometryFunction_;
    }

    /**
     * Get the fill style.
     * @return {import("./Fill.js").default|null} Fill style.
     * @api
     */
    getFill() {
      return this.fill_;
    }

    /**
     * Set the fill style.
     * @param {import("./Fill.js").default|null} fill Fill style.
     * @api
     */
    setFill(fill) {
      this.fill_ = fill;
    }

    /**
     * Get the image style.
     * @return {import("./Image.js").default|null} Image style.
     * @api
     */
    getImage() {
      return this.image_;
    }

    /**
     * Set the image style.
     * @param {import("./Image.js").default} image Image style.
     * @api
     */
    setImage(image) {
      this.image_ = image;
    }

    /**
     * Get the stroke style.
     * @return {import("./Stroke.js").default|null} Stroke style.
     * @api
     */
    getStroke() {
      return this.stroke_;
    }

    /**
     * Set the stroke style.
     * @param {import("./Stroke.js").default|null} stroke Stroke style.
     * @api
     */
    setStroke(stroke) {
      this.stroke_ = stroke;
    }

    /**
     * Get the text style.
     * @return {import("./Text.js").default|null} Text style.
     * @api
     */
    getText() {
      return this.text_;
    }

    /**
     * Set the text style.
     * @param {import("./Text.js").default} text Text style.
     * @api
     */
    setText(text) {
      this.text_ = text;
    }

    /**
     * Get the z-index for the style.
     * @return {number|undefined} ZIndex.
     * @api
     */
    getZIndex() {
      return this.zIndex_;
    }

    /**
     * Set a geometry that is rendered instead of the feature's geometry.
     *
     * @param {string|import("../geom/Geometry.js").default|GeometryFunction|null} geometry
     *     Feature property or geometry or function returning a geometry to render
     *     for this style.
     * @api
     */
    setGeometry(geometry) {
      if (typeof geometry === 'function') {
        this.geometryFunction_ = geometry;
      } else if (typeof geometry === 'string') {
        this.geometryFunction_ = function (feature) {
          return /** @type {import("../geom/Geometry.js").default} */ (
            feature.get(geometry)
          );
        };
      } else if (!geometry) {
        this.geometryFunction_ = defaultGeometryFunction;
      } else if (geometry !== undefined) {
        this.geometryFunction_ = function () {
          return /** @type {import("../geom/Geometry.js").default} */ (geometry);
        };
      }
      this.geometry_ = geometry;
    }

    /**
     * Set the z-index.
     *
     * @param {number|undefined} zIndex ZIndex.
     * @api
     */
    setZIndex(zIndex) {
      this.zIndex_ = zIndex;
    }
  }

  /**
   * Convert the provided object into a style function.  Functions passed through
   * unchanged.  Arrays of Style or single style objects wrapped in a
   * new style function.
   * @param {StyleFunction|Array<Style>|Style} obj
   *     A style function, a single style, or an array of styles.
   * @return {StyleFunction} A style function.
   */
  function toFunction(obj) {
    let styleFunction;

    if (typeof obj === 'function') {
      styleFunction = obj;
    } else {
      /**
       * @type {Array<Style>}
       */
      let styles;
      if (Array.isArray(obj)) {
        styles = obj;
      } else {
        assert(
          typeof (/** @type {?} */ (obj).getZIndex) === 'function',
          'Expected an `Style` or an array of `Style`',
        );
        const style = /** @type {Style} */ (obj);
        styles = [style];
      }
      styleFunction = function () {
        return styles;
      };
    }
    return styleFunction;
  }

  /**
   * @type {Array<Style>|null}
   */
  let defaultStyles = null;

  /**
   * @param {import("../Feature.js").FeatureLike} feature Feature.
   * @param {number} resolution Resolution.
   * @return {Array<Style>} Style.
   */
  function createDefaultStyle(feature, resolution) {
    // We don't use an immediately-invoked function
    // and a closure so we don't get an error at script evaluation time in
    // browsers that do not support Canvas. (import("./Circle.js").CircleStyle does
    // canvas.getContext('2d') at construction time, which will cause an.error
    // in such browsers.)
    if (!defaultStyles) {
      const fill = new Fill({
        color: 'rgba(255,255,255,0.4)',
      });
      const stroke = new Stroke({
        color: '#3399CC',
        width: 1.25,
      });
      defaultStyles = [
        new Style({
          image: new CircleStyle({
            fill: fill,
            stroke: stroke,
            radius: 5,
          }),
          fill: fill,
          stroke: stroke,
        }),
      ];
    }
    return defaultStyles;
  }

  /**
   * Function that is called with a feature and returns its default geometry.
   * @param {import("../Feature.js").FeatureLike} feature Feature to get the geometry for.
   * @return {import("../geom/Geometry.js").default|import("../render/Feature.js").default|undefined} Geometry to render.
   */
  function defaultGeometryFunction(feature) {
    return feature.getGeometry();
  }

  /**
   * @module ol/style/Text
   */

  /**
   * @typedef {'point' | 'line'} TextPlacement
   * Default text placement is `'point'`. Note that
   * `'line'` requires the underlying geometry to be a {@link module:ol/geom/LineString~LineString},
   * {@link module:ol/geom/Polygon~Polygon}, {@link module:ol/geom/MultiLineString~MultiLineString} or
   * {@link module:ol/geom/MultiPolygon~MultiPolygon}.
   */

  /**
   * @typedef {'left' | 'center' | 'right'} TextJustify
   */

  /**
   * The default fill color to use if no fill was set at construction time; a
   * blackish `#333`.
   *
   * @const {string}
   */
  const DEFAULT_FILL_COLOR = '#333';

  /**
   * @typedef {Object} Options
   * @property {string} [font] Font style as CSS `font` value, see:
   * https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/font. Default is `'10px sans-serif'`
   * @property {number} [maxAngle=Math.PI/4] When `placement` is set to `'line'`, allow a maximum angle between adjacent characters.
   * The expected value is in radians, and the default is 45° (`Math.PI / 4`).
   * @property {number} [offsetX=0] Horizontal text offset in pixels. A positive will shift the text right.
   * @property {number} [offsetY=0] Vertical text offset in pixels. A positive will shift the text down.
   * @property {boolean} [overflow=false] For polygon labels or when `placement` is set to `'line'`, allow text to exceed
   * the width of the polygon at the label position or the length of the path that it follows.
   * @property {TextPlacement} [placement='point'] Text placement.
   * @property {number} [repeat] Repeat interval. When set, the text will be repeated at this interval, which specifies
   * the distance between two text anchors in pixels. Only available when `placement` is set to `'line'`. Overrides 'textAlign'.
   * @property {number|import("../size.js").Size} [scale] Scale.
   * @property {boolean} [rotateWithView=false] Whether to rotate the text with the view.
   * @property {boolean} [keepUpright=true] Whether the text can be rotated 180° to prevent being rendered upside down.
   * @property {number} [rotation=0] Rotation in radians (positive rotation clockwise).
   * @property {string|Array<string>} [text] Text content or rich text content. For plain text provide a string, which can
   * contain line breaks (`\n`). For rich text provide an array of text/font tuples. A tuple consists of the text to
   * render and the font to use (or `''` to use the text style's font). A line break has to be a separate tuple (i.e. `'\n', ''`).
   * **Example:** `['foo', 'bold 10px sans-serif', ' bar', 'italic 10px sans-serif', ' baz', '']` will yield "**foo** *bar* baz".
   * **Note:** Rich text is not supported for `placement: 'line'` or the immediate rendering API.
   * @property {CanvasTextAlign} [textAlign] Text alignment. Possible values: `'left'`, `'right'`, `'center'`, `'end'` or `'start'`.
   * Default is `'center'` for `placement: 'point'`. For `placement: 'line'`, the default is to let the renderer choose a
   * placement where `maxAngle` is not exceeded.
   * @property {TextJustify} [justify] Text justification within the text box.
   * If not set, text is justified towards the `textAlign` anchor.
   * Otherwise, use options `'left'`, `'center'`, or `'right'` to justify the text within the text box.
   * **Note:** `justify` is ignored for immediate rendering and also for `placement: 'line'`.
   * @property {CanvasTextBaseline} [textBaseline='middle'] Text base line. Possible values: `'bottom'`, `'top'`, `'middle'`, `'alphabetic'`,
   * `'hanging'`, `'ideographic'`.
   * @property {import("./Fill.js").default|null} [fill] Fill style. If none is provided, we'll use a dark fill-style (#333). Specify `null` for no fill.
   * @property {import("./Stroke.js").default} [stroke] Stroke style.
   * @property {import("./Fill.js").default} [backgroundFill] Fill style for the text background when `placement` is
   * `'point'`. Default is no fill.
   * @property {import("./Stroke.js").default} [backgroundStroke] Stroke style for the text background  when `placement`
   * is `'point'`. Default is no stroke.
   * @property {Array<number>} [padding=[0, 0, 0, 0]] Padding in pixels around the text for decluttering and background. The order of
   * values in the array is `[top, right, bottom, left]`.
   * @property {import('../style/Style.js').DeclutterMode} [declutterMode] Declutter mode: `declutter`, `obstacle`, `none`
   */

  /**
   * @classdesc
   * Set text style for vector features.
   * @api
   */
  class Text {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      options = options || {};

      /**
       * @private
       * @type {string|undefined}
       */
      this.font_ = options.font;

      /**
       * @private
       * @type {number|undefined}
       */
      this.rotation_ = options.rotation;

      /**
       * @private
       * @type {boolean|undefined}
       */
      this.rotateWithView_ = options.rotateWithView;

      /**
       * @private
       * @type {boolean|undefined}
       */
      this.keepUpright_ = options.keepUpright;

      /**
       * @private
       * @type {number|import("../size.js").Size|undefined}
       */
      this.scale_ = options.scale;

      /**
       * @private
       * @type {import("../size.js").Size}
       */
      this.scaleArray_ = toSize(options.scale !== undefined ? options.scale : 1);

      /**
       * @private
       * @type {string|Array<string>|undefined}
       */
      this.text_ = options.text;

      /**
       * @private
       * @type {CanvasTextAlign|undefined}
       */
      this.textAlign_ = options.textAlign;

      /**
       * @private
       * @type {TextJustify|undefined}
       */
      this.justify_ = options.justify;

      /**
       * @private
       * @type {number|undefined}
       */
      this.repeat_ = options.repeat;

      /**
       * @private
       * @type {CanvasTextBaseline|undefined}
       */
      this.textBaseline_ = options.textBaseline;

      /**
       * @private
       * @type {import("./Fill.js").default|null}
       */
      this.fill_ =
        options.fill !== undefined
          ? options.fill
          : new Fill({color: DEFAULT_FILL_COLOR});

      /**
       * @private
       * @type {number}
       */
      this.maxAngle_ =
        options.maxAngle !== undefined ? options.maxAngle : Math.PI / 4;

      /**
       * @private
       * @type {TextPlacement}
       */
      this.placement_ =
        options.placement !== undefined ? options.placement : 'point';

      /**
       * @private
       * @type {boolean}
       */
      this.overflow_ = !!options.overflow;

      /**
       * @private
       * @type {import("./Stroke.js").default|null}
       */
      this.stroke_ = options.stroke !== undefined ? options.stroke : null;

      /**
       * @private
       * @type {number}
       */
      this.offsetX_ = options.offsetX !== undefined ? options.offsetX : 0;

      /**
       * @private
       * @type {number}
       */
      this.offsetY_ = options.offsetY !== undefined ? options.offsetY : 0;

      /**
       * @private
       * @type {import("./Fill.js").default|null}
       */
      this.backgroundFill_ = options.backgroundFill
        ? options.backgroundFill
        : null;

      /**
       * @private
       * @type {import("./Stroke.js").default|null}
       */
      this.backgroundStroke_ = options.backgroundStroke
        ? options.backgroundStroke
        : null;

      /**
       * @private
       * @type {Array<number>|null}
       */
      this.padding_ = options.padding === undefined ? null : options.padding;

      /**
       * @private
       * @type {import('../style/Style.js').DeclutterMode}
       */
      this.declutterMode_ = options.declutterMode;
    }

    /**
     * Clones the style.
     * @return {Text} The cloned style.
     * @api
     */
    clone() {
      const scale = this.getScale();
      return new Text({
        font: this.getFont(),
        placement: this.getPlacement(),
        repeat: this.getRepeat(),
        maxAngle: this.getMaxAngle(),
        overflow: this.getOverflow(),
        rotation: this.getRotation(),
        rotateWithView: this.getRotateWithView(),
        keepUpright: this.getKeepUpright(),
        scale: Array.isArray(scale) ? scale.slice() : scale,
        text: this.getText(),
        textAlign: this.getTextAlign(),
        justify: this.getJustify(),
        textBaseline: this.getTextBaseline(),
        fill:
          this.getFill() instanceof Fill
            ? this.getFill().clone()
            : this.getFill(),
        stroke: this.getStroke() ? this.getStroke().clone() : undefined,
        offsetX: this.getOffsetX(),
        offsetY: this.getOffsetY(),
        backgroundFill: this.getBackgroundFill()
          ? this.getBackgroundFill().clone()
          : undefined,
        backgroundStroke: this.getBackgroundStroke()
          ? this.getBackgroundStroke().clone()
          : undefined,
        padding: this.getPadding() || undefined,
        declutterMode: this.getDeclutterMode(),
      });
    }

    /**
     * Get the `overflow` configuration.
     * @return {boolean} Let text overflow the length of the path they follow.
     * @api
     */
    getOverflow() {
      return this.overflow_;
    }

    /**
     * Get the font name.
     * @return {string|undefined} Font.
     * @api
     */
    getFont() {
      return this.font_;
    }

    /**
     * Get the maximum angle between adjacent characters.
     * @return {number} Angle in radians.
     * @api
     */
    getMaxAngle() {
      return this.maxAngle_;
    }

    /**
     * Get the label placement.
     * @return {TextPlacement} Text placement.
     * @api
     */
    getPlacement() {
      return this.placement_;
    }

    /**
     * Get the repeat interval of the text.
     * @return {number|undefined} Repeat interval in pixels.
     * @api
     */
    getRepeat() {
      return this.repeat_;
    }

    /**
     * Get the x-offset for the text.
     * @return {number} Horizontal text offset.
     * @api
     */
    getOffsetX() {
      return this.offsetX_;
    }

    /**
     * Get the y-offset for the text.
     * @return {number} Vertical text offset.
     * @api
     */
    getOffsetY() {
      return this.offsetY_;
    }

    /**
     * Get the fill style for the text.
     * @return {import("./Fill.js").default|null} Fill style.
     * @api
     */
    getFill() {
      return this.fill_;
    }

    /**
     * Determine whether the text rotates with the map.
     * @return {boolean|undefined} Rotate with map.
     * @api
     */
    getRotateWithView() {
      return this.rotateWithView_;
    }

    /**
     * Determine whether the text can be rendered upside down.
     * @return {boolean|undefined} Keep text upright.
     * @api
     */
    getKeepUpright() {
      return this.keepUpright_;
    }

    /**
     * Get the text rotation.
     * @return {number|undefined} Rotation.
     * @api
     */
    getRotation() {
      return this.rotation_;
    }

    /**
     * Get the text scale.
     * @return {number|import("../size.js").Size|undefined} Scale.
     * @api
     */
    getScale() {
      return this.scale_;
    }

    /**
     * Get the symbolizer scale array.
     * @return {import("../size.js").Size} Scale array.
     */
    getScaleArray() {
      return this.scaleArray_;
    }

    /**
     * Get the stroke style for the text.
     * @return {import("./Stroke.js").default|null} Stroke style.
     * @api
     */
    getStroke() {
      return this.stroke_;
    }

    /**
     * Get the text to be rendered.
     * @return {string|Array<string>|undefined} Text.
     * @api
     */
    getText() {
      return this.text_;
    }

    /**
     * Get the text alignment.
     * @return {CanvasTextAlign|undefined} Text align.
     * @api
     */
    getTextAlign() {
      return this.textAlign_;
    }

    /**
     * Get the justification.
     * @return {TextJustify|undefined} Justification.
     * @api
     */
    getJustify() {
      return this.justify_;
    }

    /**
     * Get the text baseline.
     * @return {CanvasTextBaseline|undefined} Text baseline.
     * @api
     */
    getTextBaseline() {
      return this.textBaseline_;
    }

    /**
     * Get the background fill style for the text.
     * @return {import("./Fill.js").default|null} Fill style.
     * @api
     */
    getBackgroundFill() {
      return this.backgroundFill_;
    }

    /**
     * Get the background stroke style for the text.
     * @return {import("./Stroke.js").default|null} Stroke style.
     * @api
     */
    getBackgroundStroke() {
      return this.backgroundStroke_;
    }

    /**
     * Get the padding for the text.
     * @return {Array<number>|null} Padding.
     * @api
     */
    getPadding() {
      return this.padding_;
    }

    /**
     * Get the declutter mode of the shape
     * @return {import("./Style.js").DeclutterMode} Shape's declutter mode
     * @api
     */
    getDeclutterMode() {
      return this.declutterMode_;
    }

    /**
     * Set the `overflow` property.
     *
     * @param {boolean} overflow Let text overflow the path that it follows.
     * @api
     */
    setOverflow(overflow) {
      this.overflow_ = overflow;
    }

    /**
     * Set the font.
     *
     * @param {string|undefined} font Font.
     * @api
     */
    setFont(font) {
      this.font_ = font;
    }

    /**
     * Set the maximum angle between adjacent characters.
     *
     * @param {number} maxAngle Angle in radians.
     * @api
     */
    setMaxAngle(maxAngle) {
      this.maxAngle_ = maxAngle;
    }

    /**
     * Set the x offset.
     *
     * @param {number} offsetX Horizontal text offset.
     * @api
     */
    setOffsetX(offsetX) {
      this.offsetX_ = offsetX;
    }

    /**
     * Set the y offset.
     *
     * @param {number} offsetY Vertical text offset.
     * @api
     */
    setOffsetY(offsetY) {
      this.offsetY_ = offsetY;
    }

    /**
     * Set the text placement.
     *
     * @param {TextPlacement} placement Placement.
     * @api
     */
    setPlacement(placement) {
      this.placement_ = placement;
    }

    /**
     * Set the repeat interval of the text.
     * @param {number|undefined} [repeat] Repeat interval in pixels.
     * @api
     */
    setRepeat(repeat) {
      this.repeat_ = repeat;
    }

    /**
     * Set whether to rotate the text with the view.
     *
     * @param {boolean} rotateWithView Rotate with map.
     * @api
     */
    setRotateWithView(rotateWithView) {
      this.rotateWithView_ = rotateWithView;
    }

    /**
     * Set whether the text can be rendered upside down.
     *
     * @param {boolean} keepUpright Keep text upright.
     * @api
     */
    setKeepUpright(keepUpright) {
      this.keepUpright_ = keepUpright;
    }

    /**
     * Set the fill.
     *
     * @param {import("./Fill.js").default|null} fill Fill style.
     * @api
     */
    setFill(fill) {
      this.fill_ = fill;
    }

    /**
     * Set the rotation.
     *
     * @param {number|undefined} rotation Rotation.
     * @api
     */
    setRotation(rotation) {
      this.rotation_ = rotation;
    }

    /**
     * Set the scale.
     *
     * @param {number|import("../size.js").Size|undefined} scale Scale.
     * @api
     */
    setScale(scale) {
      this.scale_ = scale;
      this.scaleArray_ = toSize(scale !== undefined ? scale : 1);
    }

    /**
     * Set the stroke.
     *
     * @param {import("./Stroke.js").default|null} stroke Stroke style.
     * @api
     */
    setStroke(stroke) {
      this.stroke_ = stroke;
    }

    /**
     * Set the text.
     *
     * @param {string|Array<string>|undefined} text Text.
     * @api
     */
    setText(text) {
      this.text_ = text;
    }

    /**
     * Set the text alignment.
     *
     * @param {CanvasTextAlign|undefined} textAlign Text align.
     * @api
     */
    setTextAlign(textAlign) {
      this.textAlign_ = textAlign;
    }

    /**
     * Set the justification.
     *
     * @param {TextJustify|undefined} justify Justification.
     * @api
     */
    setJustify(justify) {
      this.justify_ = justify;
    }

    /**
     * Set the text baseline.
     *
     * @param {CanvasTextBaseline|undefined} textBaseline Text baseline.
     * @api
     */
    setTextBaseline(textBaseline) {
      this.textBaseline_ = textBaseline;
    }

    /**
     * Set the background fill.
     *
     * @param {import("./Fill.js").default|null} fill Fill style.
     * @api
     */
    setBackgroundFill(fill) {
      this.backgroundFill_ = fill;
    }

    /**
     * Set the background stroke.
     *
     * @param {import("./Stroke.js").default|null} stroke Stroke style.
     * @api
     */
    setBackgroundStroke(stroke) {
      this.backgroundStroke_ = stroke;
    }

    /**
     * Set the padding (`[top, right, bottom, left]`).
     *
     * @param {Array<number>|null} padding Padding.
     * @api
     */
    setPadding(padding) {
      this.padding_ = padding;
    }
  }

  /**
   * @module ol/ViewHint
   */

  /**
   * @enum {number}
   */
  var ViewHint = {
    ANIMATING: 0,
    INTERACTING: 1,
  };

  /**
   * @module ol/render/canvas/Instruction
   */

  /**
   * @enum {number}
   */
  const Instruction = {
    BEGIN_GEOMETRY: 0,
    BEGIN_PATH: 1,
    CIRCLE: 2,
    CLOSE_PATH: 3,
    CUSTOM: 4,
    DRAW_CHARS: 5,
    DRAW_IMAGE: 6,
    END_GEOMETRY: 7,
    FILL: 8,
    MOVE_TO_LINE_TO: 9,
    SET_FILL_STYLE: 10,
    SET_STROKE_STYLE: 11,
    STROKE: 12,
  };

  /**
   * @type {Array<Instruction>}
   */
  const fillInstruction = [Instruction.FILL];

  /**
   * @type {Array<Instruction>}
   */
  const strokeInstruction = [Instruction.STROKE];

  /**
   * @type {Array<Instruction>}
   */
  const beginPathInstruction = [Instruction.BEGIN_PATH];

  /**
   * @type {Array<Instruction>}
   */
  const closePathInstruction = [Instruction.CLOSE_PATH];

  /**
   * @module ol/render/canvas/Builder
   */

  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.
     */
    constructor(tolerance, maxExtent, resolution, pixelRatio) {
      super();

      /**
       * @protected
       * @type {number}
       */
      this.tolerance = tolerance;

      /**
       * @protected
       * @const
       * @type {import("../../extent.js").Extent}
       */
      this.maxExtent = maxExtent;

      /**
       * @protected
       * @type {number}
       */
      this.pixelRatio = pixelRatio;

      /**
       * @protected
       * @type {number}
       */
      this.maxLineWidth = 0;

      /**
       * @protected
       * @const
       * @type {number}
       */
      this.resolution = resolution;

      /**
       * @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<number>}
       */
      this.coordinates = [];

      /**
       * @private
       * @type {import("../../coordinate.js").Coordinate}
       */
      this.tmpCoordinate_ = [];

      /**
       * @protected
       * @type {Array<*>}
       */
      this.hitDetectionInstructions = [];

      /**
       * @protected
       * @type {import("../canvas.js").FillStrokeState}
       */
      this.state = /** @type {import("../canvas.js").FillStrokeState} */ ({});
    }

    /**
     * @protected
     * @param {Array<number>} dashArray Dash array.
     * @return {Array<number>} 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<number>} flatCoordinates Flat coordinates.
     * @param {number} stride Stride.
     * @protected
     * @return {number} My end
     */
    appendFlatPointCoordinates(flatCoordinates, stride) {
      const extent = this.getBufferedMaxExtent();
      const tmpCoord = this.tmpCoordinate_;
      const coordinates = this.coordinates;
      let myEnd = coordinates.length;
      for (let i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
        tmpCoord[0] = flatCoordinates[i];
        tmpCoord[1] = flatCoordinates[i + 1];
        if (containsCoordinate(extent, tmpCoord)) {
          coordinates[myEnd++] = tmpCoord[0];
          coordinates[myEnd++] = tmpCoord[1];
        }
      }
      return myEnd;
    }

    /**
     * @param {Array<number>} 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.
     */
    appendFlatLineCoordinates(
      flatCoordinates,
      offset,
      end,
      stride,
      closed,
      skipFirst,
    ) {
      const coordinates = this.coordinates;
      let myEnd = coordinates.length;
      const extent = this.getBufferedMaxExtent();
      if (skipFirst) {
        offset += stride;
      }
      let lastXCoord = flatCoordinates[offset];
      let lastYCoord = flatCoordinates[offset + 1];
      const nextCoord = this.tmpCoordinate_;
      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) {
            coordinates[myEnd++] = lastXCoord;
            coordinates[myEnd++] = lastYCoord;
            skipped = false;
          }
          coordinates[myEnd++] = nextCoord[0];
          coordinates[myEnd++] = nextCoord[1];
        } else if (nextRel === Relationship.INTERSECTING) {
          coordinates[myEnd++] = nextCoord[0];
          coordinates[myEnd++] = nextCoord[1];
          skipped = false;
        } else {
          skipped = true;
        }
        lastXCoord = nextCoord[0];
        lastYCoord = nextCoord[1];
        lastRel = nextRel;
      }

      // Last coordinate equals first or only one point to append:
      if ((closed && skipped) || i === offset + stride) {
        coordinates[myEnd++] = lastXCoord;
        coordinates[myEnd++] = lastYCoord;
      }
      return myEnd;
    }

    /**
     * @param {Array<number>} flatCoordinates Flat coordinates.
     * @param {number} offset Offset.
     * @param {Array<number>} ends Ends.
     * @param {number} stride Stride.
     * @param {Array<number>} 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.appendFlatLineCoordinates(
          flatCoordinates,
          offset,
          end,
          stride,
          false,
          false,
        );
        builderEnds.push(builderEnd);
        offset = end;
      }
      return offset;
    }

    /**
     * @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     * @param {Function} renderer Renderer.
     * @param {Function} hitDetectionRenderer Renderer.
     * @param {number} [index] Render order index.
     * @override
     */
    drawCustom(geometry, feature, renderer, hitDetectionRenderer, index) {
      this.beginGeometry(geometry, feature, index);

      const type = geometry.getType();
      const stride = geometry.getStride();
      const builderBegin = this.coordinates.length;

      let flatCoordinates, builderEnd, builderEnds, builderEndss;
      let offset;

      switch (type) {
        case 'MultiPolygon':
          flatCoordinates =
            /** @type {import("../../geom/MultiPolygon.js").default} */ (
              geometry
            ).getOrientedFlatCoordinates();
          builderEndss = [];
          const endss =
            /** @type {import("../../geom/MultiPolygon.js").default} */ (
              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([
            Instruction.CUSTOM,
            builderBegin,
            builderEndss,
            geometry,
            renderer,
            inflateMultiCoordinatesArray,
            index,
          ]);
          this.hitDetectionInstructions.push([
            Instruction.CUSTOM,
            builderBegin,
            builderEndss,
            geometry,
            hitDetectionRenderer || renderer,
            inflateMultiCoordinatesArray,
            index,
          ]);
          break;
        case 'Polygon':
        case 'MultiLineString':
          builderEnds = [];
          flatCoordinates =
            type == '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([
            Instruction.CUSTOM,
            builderBegin,
            builderEnds,
            geometry,
            renderer,
            inflateCoordinatesArray,
            index,
          ]);
          this.hitDetectionInstructions.push([
            Instruction.CUSTOM,
            builderBegin,
            builderEnds,
            geometry,
            hitDetectionRenderer || renderer,
            inflateCoordinatesArray,
            index,
          ]);
          break;
        case 'LineString':
        case 'Circle':
          flatCoordinates = geometry.getFlatCoordinates();
          builderEnd = this.appendFlatLineCoordinates(
            flatCoordinates,
            0,
            flatCoordinates.length,
            stride,
            false,
            false,
          );
          this.instructions.push([
            Instruction.CUSTOM,
            builderBegin,
            builderEnd,
            geometry,
            renderer,
            inflateCoordinates,
            index,
          ]);
          this.hitDetectionInstructions.push([
            Instruction.CUSTOM,
            builderBegin,
            builderEnd,
            geometry,
            hitDetectionRenderer || renderer,
            inflateCoordinates,
            index,
          ]);
          break;
        case 'MultiPoint':
          flatCoordinates = geometry.getFlatCoordinates();
          builderEnd = this.appendFlatPointCoordinates(flatCoordinates, stride);

          if (builderEnd > builderBegin) {
            this.instructions.push([
              Instruction.CUSTOM,
              builderBegin,
              builderEnd,
              geometry,
              renderer,
              inflateCoordinates,
              index,
            ]);
            this.hitDetectionInstructions.push([
              Instruction.CUSTOM,
              builderBegin,
              builderEnd,
              geometry,
              hitDetectionRenderer || renderer,
              inflateCoordinates,
              index,
            ]);
          }
          break;
        case 'Point':
          flatCoordinates = geometry.getFlatCoordinates();
          this.coordinates.push(flatCoordinates[0], flatCoordinates[1]);
          builderEnd = this.coordinates.length;

          this.instructions.push([
            Instruction.CUSTOM,
            builderBegin,
            builderEnd,
            geometry,
            renderer,
            undefined,
            index,
          ]);
          this.hitDetectionInstructions.push([
            Instruction.CUSTOM,
            builderBegin,
            builderEnd,
            geometry,
            hitDetectionRenderer || renderer,
            undefined,
            index,
          ]);
          break;
      }
      this.endGeometry(feature);
    }

    /**
     * @protected
     * @param {import("../../geom/Geometry").default|import("../Feature.js").default} geometry The geometry.
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     * @param {number} index Render order index
     */
    beginGeometry(geometry, feature, index) {
      this.beginGeometryInstruction1_ = [
        Instruction.BEGIN_GEOMETRY,
        feature,
        0,
        geometry,
        index,
      ];
      this.instructions.push(this.beginGeometryInstruction1_);
      this.beginGeometryInstruction2_ = [
        Instruction.BEGIN_GEOMETRY,
        feature,
        0,
        geometry,
        index,
      ];
      this.hitDetectionInstructions.push(this.beginGeometryInstruction2_);
    }

    /**
     * @return {import("../canvas.js").SerializableInstructions} the serializable instructions.
     */
    finish() {
      return {
        instructions: this.instructions,
        hitDetectionInstructions: this.hitDetectionInstructions,
        coordinates: this.coordinates,
      };
    }

    /**
     * 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 {import("./Instruction.js").default} */ (instruction[0]);
        if (type == Instruction.END_GEOMETRY) {
          begin = i;
        } else if (type == Instruction.BEGIN_GEOMETRY) {
          instruction[2] = i;
          reverseSubArray(this.hitDetectionInstructions, begin, i);
          begin = -1;
        }
      }
    }

    /**
     * @param {import("../../style/Fill.js").default} fillStyle Fill style.
     * @param {import('../canvas.js').FillStrokeState} [state] State.
     * @return {import('../canvas.js').FillStrokeState} State.
     */
    fillStyleToState(
      fillStyle,
      state = /** @type {import('../canvas.js').FillStrokeState} */ ({}),
    ) {
      if (fillStyle) {
        const fillStyleColor = fillStyle.getColor();
        state.fillPatternScale =
          fillStyleColor &&
          typeof fillStyleColor === 'object' &&
          'src' in fillStyleColor
            ? this.pixelRatio
            : 1;
        state.fillStyle = asColorLike(
          fillStyleColor ? fillStyleColor : defaultFillStyle,
        );
      } else {
        state.fillStyle = undefined;
      }
      return state;
    }

    /**
     * @param {import("../../style/Stroke.js").default} strokeStyle Stroke style.
     * @param {import("../canvas.js").FillStrokeState} state State.
     * @return {import("../canvas.js").FillStrokeState} State.
     */
    strokeStyleToState(
      strokeStyle,
      state = /** @type {import('../canvas.js').FillStrokeState} */ ({}),
    ) {
      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;
        const strokeStyleOffset = strokeStyle.getOffset();
        state.strokeOffset = strokeStyleOffset ?? defaultStrokeOffset;

        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;
        state.strokeOffset = undefined;
      }
      return state;
    }

    /**
     * @param {import("../../style/Fill.js").default} fillStyle Fill style.
     * @param {import("../../style/Stroke.js").default} strokeStyle Stroke style.
     * @override
     */
    setFillStrokeStyle(fillStyle, strokeStyle) {
      const state = this.state;
      this.fillStyleToState(fillStyle, state);
      this.strokeStyleToState(strokeStyle, state);
    }

    /**
     * @param {import("../canvas.js").FillStrokeState} state State.
     * @return {Array<*>} Fill instruction.
     */
    createFill(state) {
      const fillStyle = state.fillStyle;
      /** @type {Array<*>} */
      const fillInstruction = [Instruction.SET_FILL_STYLE, fillStyle];
      if (typeof fillStyle !== 'string') {
        // Fill is a pattern or gradient - align and scale it!
        fillInstruction.push(state.fillPatternScale);
      }
      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 [
        Instruction.SET_STROKE_STYLE,
        state.strokeStyle,
        state.lineWidth * this.pixelRatio,
        state.lineCap,
        state.lineJoin,
        state.miterLimit,
        state.lineDash ? this.applyPixelRatio(state.lineDash) : null,
        state.lineDashOffset * this.pixelRatio,
      ];
    }

    /**
     * @param {import("../canvas.js").FillStrokeState} state State.
     * @param {function(this:CanvasBuilder, import("../canvas.js").FillStrokeState):Array<*>} createFill Create fill.
     */
    updateFillStyle(state, createFill) {
      const fillStyle = state.fillStyle;
      if (typeof fillStyle !== 'string' || state.currentFillStyle != fillStyle) {
        this.instructions.push(createFill.call(this, state));
        state.currentFillStyle = fillStyle;
      }
    }

    /**
     * @param {import("../canvas.js").FillStrokeState} state State.
     * @param {function(this:CanvasBuilder, import("../canvas.js").FillStrokeState): void} 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;
      const strokeOffset = state.strokeOffset;
      if (
        state.currentStrokeStyle != strokeStyle ||
        state.currentLineCap != lineCap ||
        (lineDash != state.currentLineDash &&
          !equals$2(state.currentLineDash, lineDash)) ||
        state.currentLineDashOffset != lineDashOffset ||
        state.currentLineJoin != lineJoin ||
        state.currentLineWidth != lineWidth ||
        state.currentMiterLimit != miterLimit ||
        state.currentStrokeOffset != strokeOffset
      ) {
        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;
        state.currentStrokeOffset = strokeOffset;
      }
    }

    /**
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     */
    endGeometry(feature) {
      this.beginGeometryInstruction1_[2] = this.instructions.length;
      this.beginGeometryInstruction1_ = null;
      this.beginGeometryInstruction2_[2] = this.hitDetectionInstructions.length;
      this.beginGeometryInstruction2_ = null;
      const endGeometryInstruction = [Instruction.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_;
    }
  }

  /**
   * @module ol/render/canvas/ImageBuilder
   */

  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.
     */
    constructor(tolerance, maxExtent, resolution, pixelRatio) {
      super(tolerance, maxExtent, resolution, pixelRatio);

      /**
       * @private
       * @type {import('../../DataTile.js').ImageLike}
       */
      this.hitDetectionImage_ = null;

      /**
       * @private
       * @type {import('../../DataTile.js').ImageLike}
       */
      this.image_ = null;

      /**
       * @private
       * @type {number|undefined}
       */
      this.imagePixelRatio_ = undefined;

      /**
       * @private
       * @type {number|undefined}
       */
      this.anchorX_ = undefined;

      /**
       * @private
       * @type {number|undefined}
       */
      this.anchorY_ = undefined;

      /**
       * @private
       * @type {number|undefined}
       */
      this.height_ = undefined;

      /**
       * @private
       * @type {number|undefined}
       */
      this.opacity_ = undefined;

      /**
       * @private
       * @type {number|undefined}
       */
      this.originX_ = undefined;

      /**
       * @private
       * @type {number|undefined}
       */
      this.originY_ = undefined;

      /**
       * @private
       * @type {boolean|undefined}
       */
      this.rotateWithView_ = undefined;

      /**
       * @private
       * @type {number|undefined}
       */
      this.rotation_ = undefined;

      /**
       * @private
       * @type {import("../../size.js").Size|undefined}
       */
      this.scale_ = undefined;

      /**
       * @private
       * @type {number|undefined}
       */
      this.width_ = undefined;

      /**
       * @private
       * @type {import('../../style/Style.js').DeclutterMode}
       */
      this.declutterMode_ = undefined;

      /**
       * Data shared with a text builder for combined decluttering.
       * @private
       * @type {import("../canvas.js").DeclutterImageWithText}
       */
      this.declutterImageWithText_ = undefined;
    }

    /**
     * @param {import("../../geom/Point.js").default|import("../Feature.js").default} pointGeometry Point geometry.
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     * @override
     */
    drawPoint(pointGeometry, feature, index) {
      if (
        !this.image_ ||
        (this.maxExtent &&
          !containsCoordinate(this.maxExtent, pointGeometry.getFlatCoordinates()))
      ) {
        return;
      }
      this.beginGeometry(pointGeometry, feature, index);
      const flatCoordinates = pointGeometry.getFlatCoordinates();
      const stride = pointGeometry.getStride();
      const myBegin = this.coordinates.length;
      const myEnd = this.appendFlatPointCoordinates(flatCoordinates, stride);
      this.instructions.push([
        Instruction.DRAW_IMAGE,
        myBegin,
        myEnd,
        this.image_,
        // Remaining arguments to DRAW_IMAGE are in alphabetical order
        this.anchorX_ * this.imagePixelRatio_,
        this.anchorY_ * this.imagePixelRatio_,
        Math.ceil(this.height_ * this.imagePixelRatio_),
        this.opacity_,
        this.originX_ * this.imagePixelRatio_,
        this.originY_ * this.imagePixelRatio_,
        this.rotateWithView_,
        this.rotation_,
        [
          (this.scale_[0] * this.pixelRatio) / this.imagePixelRatio_,
          (this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_,
        ],
        Math.ceil(this.width_ * this.imagePixelRatio_),
        this.declutterMode_,
        this.declutterImageWithText_,
      ]);
      this.hitDetectionInstructions.push([
        Instruction.DRAW_IMAGE,
        myBegin,
        myEnd,
        this.hitDetectionImage_,
        // Remaining arguments to DRAW_IMAGE are in alphabetical order
        this.anchorX_,
        this.anchorY_,
        this.height_,
        1,
        this.originX_,
        this.originY_,
        this.rotateWithView_,
        this.rotation_,
        this.scale_,
        this.width_,
        this.declutterMode_,
        this.declutterImageWithText_,
      ]);
      this.endGeometry(feature);
    }

    /**
     * @param {import("../../geom/MultiPoint.js").default|import("../Feature.js").default} multiPointGeometry MultiPoint geometry.
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     * @override
     */
    drawMultiPoint(multiPointGeometry, feature, index) {
      if (!this.image_) {
        return;
      }
      this.beginGeometry(multiPointGeometry, feature, index);
      const flatCoordinates = multiPointGeometry.getFlatCoordinates();
      const filteredFlatCoordinates = [];
      for (
        let i = 0, ii = flatCoordinates.length;
        i < ii;
        i += multiPointGeometry.getStride()
      ) {
        if (
          !this.maxExtent ||
          containsCoordinate(this.maxExtent, flatCoordinates.slice(i, i + 2))
        ) {
          filteredFlatCoordinates.push(
            flatCoordinates[i],
            flatCoordinates[i + 1],
          );
        }
      }
      const myBegin = this.coordinates.length;
      const myEnd = this.appendFlatPointCoordinates(filteredFlatCoordinates, 2);
      this.instructions.push([
        Instruction.DRAW_IMAGE,
        myBegin,
        myEnd,
        this.image_,
        // Remaining arguments to DRAW_IMAGE are in alphabetical order
        this.anchorX_ * this.imagePixelRatio_,
        this.anchorY_ * this.imagePixelRatio_,
        Math.ceil(this.height_ * this.imagePixelRatio_),
        this.opacity_,
        this.originX_ * this.imagePixelRatio_,
        this.originY_ * this.imagePixelRatio_,
        this.rotateWithView_,
        this.rotation_,
        [
          (this.scale_[0] * this.pixelRatio) / this.imagePixelRatio_,
          (this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_,
        ],
        Math.ceil(this.width_ * this.imagePixelRatio_),
        this.declutterMode_,
        this.declutterImageWithText_,
      ]);
      this.hitDetectionInstructions.push([
        Instruction.DRAW_IMAGE,
        myBegin,
        myEnd,
        this.hitDetectionImage_,
        // Remaining arguments to DRAW_IMAGE are in alphabetical order
        this.anchorX_,
        this.anchorY_,
        this.height_,
        1,
        this.originX_,
        this.originY_,
        this.rotateWithView_,
        this.rotation_,
        this.scale_,
        this.width_,
        this.declutterMode_,
        this.declutterImageWithText_,
      ]);
      this.endGeometry(feature);
    }

    /**
     * @return {import("../canvas.js").SerializableInstructions} the serializable instructions.
     * @override
     */
    finish() {
      this.reverseHitDetectionInstructions();
      // FIXME this doesn't really protect us against further calls to draw*Geometry
      this.anchorX_ = undefined;
      this.anchorY_ = undefined;
      this.hitDetectionImage_ = null;
      this.image_ = null;
      this.imagePixelRatio_ = undefined;
      this.height_ = undefined;
      this.scale_ = undefined;
      this.opacity_ = undefined;
      this.originX_ = undefined;
      this.originY_ = undefined;
      this.rotateWithView_ = undefined;
      this.rotation_ = undefined;
      this.width_ = undefined;
      return super.finish();
    }

    /**
     * @param {import("../../style/Image.js").default} imageStyle Image style.
     * @param {Object} [sharedData] Shared data.
     * @override
     */
    setImageStyle(imageStyle, sharedData) {
      const anchor = imageStyle.getAnchor();
      const size = imageStyle.getSize();
      const origin = imageStyle.getOrigin();
      this.imagePixelRatio_ = imageStyle.getPixelRatio(this.pixelRatio);
      this.anchorX_ = anchor[0];
      this.anchorY_ = anchor[1];
      this.hitDetectionImage_ = imageStyle.getHitDetectionImage();
      this.image_ = imageStyle.getImage(this.pixelRatio);
      this.height_ = size[1];
      this.opacity_ = imageStyle.getOpacity();
      this.originX_ = origin[0];
      this.originY_ = origin[1];
      this.rotateWithView_ = imageStyle.getRotateWithView();
      this.rotation_ = imageStyle.getRotation();
      this.scale_ = imageStyle.getScaleArray();
      this.width_ = size[0];
      this.declutterMode_ = imageStyle.getDeclutterMode();
      this.declutterImageWithText_ = sharedData;
    }
  }

  /**
   * @module ol/render/canvas/LineStringBuilder
   */

  class CanvasLineStringBuilder extends CanvasBuilder {
    /**
     * @param {number} tolerance Tolerance.
     * @param {import("../../extent.js").Extent} maxExtent Maximum extent.
     * @param {number} resolution Resolution.
     * @param {number} pixelRatio Pixel ratio.
     */
    constructor(tolerance, maxExtent, resolution, pixelRatio) {
      super(tolerance, maxExtent, resolution, pixelRatio);
    }

    /**
     * @param {Array<number>} flatCoordinates Flat coordinates.
     * @param {number} offset Offset.
     * @param {number} end End.
     * @param {number} stride Stride.
     * @param {number} [strokeOffset] Stroke Offset in pixels.
     * @private
     * @return {number} end.
     */
    drawFlatCoordinates_(flatCoordinates, offset, end, stride, strokeOffset) {
      const myBegin = this.coordinates.length;
      const myEnd = this.appendFlatLineCoordinates(
        flatCoordinates,
        offset,
        end,
        stride,
        false,
        false,
      );
      this.instructions.push([
        Instruction.MOVE_TO_LINE_TO,
        myBegin,
        myEnd,
        strokeOffset * this.pixelRatio,
      ]);
      this.hitDetectionInstructions.push([
        Instruction.MOVE_TO_LINE_TO,
        myBegin,
        myEnd,
        strokeOffset,
      ]);
      return end;
    }

    /**
     * @param {import("../../geom/LineString.js").default|import("../Feature.js").default} lineStringGeometry Line string geometry.
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     * @override
     */
    drawLineString(lineStringGeometry, feature, index) {
      const state = this.state;
      const strokeStyle = state.strokeStyle;
      const lineWidth = state.lineWidth;
      const strokeOffset = state.strokeOffset;
      if (strokeStyle === undefined || lineWidth === undefined) {
        return;
      }
      this.updateStrokeStyle(state, this.applyStroke);
      this.beginGeometry(lineStringGeometry, feature, index);
      this.hitDetectionInstructions.push(
        [
          Instruction.SET_STROKE_STYLE,
          state.strokeStyle,
          state.lineWidth,
          state.lineCap,
          state.lineJoin,
          state.miterLimit,
          defaultLineDash,
          defaultLineDashOffset,
        ],
        beginPathInstruction,
      );
      const flatCoordinates = lineStringGeometry.getFlatCoordinates();
      const stride = lineStringGeometry.getStride();
      this.drawFlatCoordinates_(
        flatCoordinates,
        0,
        flatCoordinates.length,
        stride,
        strokeOffset,
      );
      this.hitDetectionInstructions.push(strokeInstruction);
      this.endGeometry(feature);
    }

    /**
     * @param {import("../../geom/MultiLineString.js").default|import("../Feature.js").default} multiLineStringGeometry MultiLineString geometry.
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     * @override
     */
    drawMultiLineString(multiLineStringGeometry, feature, index) {
      const state = this.state;
      const strokeStyle = state.strokeStyle;
      const lineWidth = state.lineWidth;
      const strokeOffset = state.strokeOffset;
      if (strokeStyle === undefined || lineWidth === undefined) {
        return;
      }
      this.updateStrokeStyle(state, this.applyStroke);
      this.beginGeometry(multiLineStringGeometry, feature, index);
      this.hitDetectionInstructions.push(
        [
          Instruction.SET_STROKE_STYLE,
          state.strokeStyle,
          state.lineWidth,
          state.lineCap,
          state.lineJoin,
          state.miterLimit,
          defaultLineDash,
          defaultLineDashOffset,
        ],
        beginPathInstruction,
      );
      const ends = multiLineStringGeometry.getEnds();
      const flatCoordinates = multiLineStringGeometry.getFlatCoordinates();
      const stride = multiLineStringGeometry.getStride();
      let offset = 0;
      for (let i = 0, ii = ends.length; i < ii; ++i) {
        offset = this.drawFlatCoordinates_(
          flatCoordinates,
          offset,
          /** @type {number} */ (ends[i]),
          stride,
          strokeOffset,
        );
      }
      this.hitDetectionInstructions.push(strokeInstruction);
      this.endGeometry(feature);
    }

    /**
     * @return {import("../canvas.js").SerializableInstructions} the serializable instructions.
     * @override
     */
    finish() {
      const state = this.state;
      if (
        state.lastStroke != undefined &&
        state.lastStroke != this.coordinates.length
      ) {
        this.instructions.push(strokeInstruction);
      }
      this.reverseHitDetectionInstructions();
      this.state = null;
      return super.finish();
    }

    /**
     * @param {import("../canvas.js").FillStrokeState} state State.
     * @override
     */
    applyStroke(state) {
      if (
        state.lastStroke != undefined &&
        state.lastStroke != this.coordinates.length
      ) {
        this.instructions.push(strokeInstruction);
        state.lastStroke = this.coordinates.length;
      }
      state.lastStroke = 0;
      super.applyStroke(state);
      this.instructions.push(beginPathInstruction);
    }
  }

  /**
   * @module ol/render/canvas/PolygonBuilder
   */

  class CanvasPolygonBuilder extends CanvasBuilder {
    /**
     * @param {number} tolerance Tolerance.
     * @param {import("../../extent.js").Extent} maxExtent Maximum extent.
     * @param {number} resolution Resolution.
     * @param {number} pixelRatio Pixel ratio.
     */
    constructor(tolerance, maxExtent, resolution, pixelRatio) {
      super(tolerance, maxExtent, resolution, pixelRatio);
    }

    /**
     * @param {Array<number>} flatCoordinates Flat coordinates.
     * @param {number} offset Offset.
     * @param {Array<number>} ends Ends.
     * @param {number} stride Stride.
     * @param {number} [strokeOffset] Stroke Offset in pixels.
     * @private
     * @return {number} End.
     */
    drawFlatCoordinatess_(flatCoordinates, offset, ends, stride, strokeOffset) {
      const state = this.state;
      const fill = state.fillStyle !== undefined;
      const stroke = state.strokeStyle !== undefined;
      const numEnds = ends.length;
      this.instructions.push(beginPathInstruction);
      this.hitDetectionInstructions.push(beginPathInstruction);
      for (let i = 0; i < numEnds; ++i) {
        const end = ends[i];
        const myBegin = this.coordinates.length;
        const myEnd = this.appendFlatLineCoordinates(
          flatCoordinates,
          offset,
          end,
          stride,
          true,
          !stroke,
        );
        this.instructions.push([
          Instruction.MOVE_TO_LINE_TO,
          myBegin,
          myEnd,
          strokeOffset * this.pixelRatio,
          true,
        ]);
        this.hitDetectionInstructions.push([
          Instruction.MOVE_TO_LINE_TO,
          myBegin,
          myEnd,
          strokeOffset,
          true,
        ]);
        if (stroke) {
          // Performance optimization: only call closePath() when we have a stroke.
          // Otherwise the ring is closed already (see appendFlatLineCoordinates above).
          this.instructions.push(closePathInstruction);
          this.hitDetectionInstructions.push(closePathInstruction);
        }
        offset = end;
      }
      if (fill) {
        this.instructions.push(fillInstruction);
        this.hitDetectionInstructions.push(fillInstruction);
      }
      if (stroke) {
        this.instructions.push(strokeInstruction);
        this.hitDetectionInstructions.push(strokeInstruction);
      }
      return offset;
    }

    /**
     * @param {import("../../geom/Circle.js").default} circleGeometry Circle geometry.
     * @param {import("../../Feature.js").default} feature Feature.
     * @param {number} [index] Render order index.
     * @override
     */
    drawCircle(circleGeometry, feature, index) {
      const state = this.state;
      const fillStyle = state.fillStyle;
      const strokeStyle = state.strokeStyle;
      const strokeOffset = state.strokeOffset;
      if (fillStyle === undefined && strokeStyle === undefined) {
        return;
      }

      if (
        this.handleStrokeOffset_(() =>
          this.drawCircle(circleGeometry, feature, index),
        )
      ) {
        return;
      }
      this.setFillStrokeStyles_();
      this.beginGeometry(circleGeometry, feature, index);
      if (state.fillStyle !== undefined) {
        this.hitDetectionInstructions.push([
          Instruction.SET_FILL_STYLE,
          defaultFillStyle,
        ]);
      }
      if (state.strokeStyle !== undefined) {
        this.hitDetectionInstructions.push([
          Instruction.SET_STROKE_STYLE,
          state.strokeStyle,
          state.lineWidth,
          state.lineCap,
          state.lineJoin,
          state.miterLimit,
          defaultLineDash,
          defaultLineDashOffset,
        ]);
      }
      const flatCoordinates = circleGeometry.getFlatCoordinates();
      const stride = circleGeometry.getStride();
      const myBegin = this.coordinates.length;
      this.appendFlatLineCoordinates(
        flatCoordinates,
        0,
        flatCoordinates.length,
        stride,
        false,
        false,
      );
      const circleInstruction = [Instruction.CIRCLE, myBegin, strokeOffset];
      this.instructions.push(beginPathInstruction, circleInstruction);
      this.hitDetectionInstructions.push(beginPathInstruction, circleInstruction);
      if (state.fillStyle !== undefined) {
        this.instructions.push(fillInstruction);
        this.hitDetectionInstructions.push(fillInstruction);
      }
      if (state.strokeStyle !== undefined) {
        this.instructions.push(strokeInstruction);
        this.hitDetectionInstructions.push(strokeInstruction);
      }
      this.endGeometry(feature);
    }

    /**
     * @param {import("../../geom/Polygon.js").default|import("../Feature.js").default} polygonGeometry Polygon geometry.
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     * @override
     */
    drawPolygon(polygonGeometry, feature, index) {
      const state = this.state;
      const fillStyle = state.fillStyle;
      const strokeStyle = state.strokeStyle;
      const strokeOffset = state.strokeOffset;
      if (fillStyle === undefined && strokeStyle === undefined) {
        return;
      }
      if (
        this.handleStrokeOffset_(() =>
          this.drawPolygon(polygonGeometry, feature, index),
        )
      ) {
        return;
      }

      this.setFillStrokeStyles_();
      this.beginGeometry(polygonGeometry, feature, index);
      if (state.fillStyle !== undefined) {
        this.hitDetectionInstructions.push([
          Instruction.SET_FILL_STYLE,
          defaultFillStyle,
        ]);
      }
      if (state.strokeStyle !== undefined) {
        this.hitDetectionInstructions.push([
          Instruction.SET_STROKE_STYLE,
          state.strokeStyle,
          state.lineWidth,
          state.lineCap,
          state.lineJoin,
          state.miterLimit,
          defaultLineDash,
          defaultLineDashOffset,
        ]);
      }
      const ends = polygonGeometry.getEnds();
      const flatCoordinates = polygonGeometry.getOrientedFlatCoordinates();
      const stride = polygonGeometry.getStride();
      this.drawFlatCoordinatess_(
        flatCoordinates,
        0,
        /** @type {Array<number>} */ (ends),
        stride,
        strokeOffset,
      );
      this.endGeometry(feature);
    }

    /**
     * @param {import("../../geom/MultiPolygon.js").default} multiPolygonGeometry MultiPolygon geometry.
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     * @override
     */
    drawMultiPolygon(multiPolygonGeometry, feature, index) {
      const state = this.state;
      const fillStyle = state.fillStyle;
      const strokeStyle = state.strokeStyle;
      const strokeOffset = state.strokeOffset;
      if (fillStyle === undefined && strokeStyle === undefined) {
        return;
      }
      if (
        this.handleStrokeOffset_(() =>
          this.drawMultiPolygon(multiPolygonGeometry, feature, index),
        )
      ) {
        return;
      }
      this.setFillStrokeStyles_();
      this.beginGeometry(multiPolygonGeometry, feature, index);
      if (state.fillStyle !== undefined) {
        this.hitDetectionInstructions.push([
          Instruction.SET_FILL_STYLE,
          defaultFillStyle,
        ]);
      }
      if (state.strokeStyle !== undefined) {
        this.hitDetectionInstructions.push([
          Instruction.SET_STROKE_STYLE,
          state.strokeStyle,
          state.lineWidth,
          state.lineCap,
          state.lineJoin,
          state.miterLimit,
          defaultLineDash,
          defaultLineDashOffset,
        ]);
      }
      const endss = multiPolygonGeometry.getEndss();
      const flatCoordinates = multiPolygonGeometry.getOrientedFlatCoordinates();
      const stride = multiPolygonGeometry.getStride();
      let offset = 0;
      for (let i = 0, ii = endss.length; i < ii; ++i) {
        offset = this.drawFlatCoordinatess_(
          flatCoordinates,
          offset,
          endss[i],
          stride,
          strokeOffset,
        );
      }
      this.endGeometry(feature);
    }

    /**
     * @return {import("../canvas.js").SerializableInstructions} the serializable instructions.
     * @override
     */
    finish() {
      this.reverseHitDetectionInstructions();
      this.state = null;
      // We want to preserve topology when drawing polygons.  Polygons are
      // simplified using quantization and point elimination. However, we might
      // have received a mix of quantized and non-quantized geometries, so ensure
      // that all are quantized by quantizing all coordinates in the batch.
      const tolerance = this.tolerance;
      if (tolerance !== 0) {
        const coordinates = this.coordinates;
        for (let i = 0, ii = coordinates.length; i < ii; ++i) {
          coordinates[i] = snap(coordinates[i], tolerance);
        }
      }
      return super.finish();
    }

    /**
     * @private
     */
    setFillStrokeStyles_() {
      const state = this.state;
      this.updateFillStyle(state, this.createFill);
      this.updateStrokeStyle(state, this.applyStroke);
    }

    handleStrokeOffset_(drawGeometryCallback) {
      const state = this.state;
      const fillStyle = state.fillStyle;
      const strokeStyle = state.strokeStyle;
      const strokeOffset = state.strokeOffset;

      // In case both fill style and stroke style are defined and the stroke style has an offset,
      // the stroke and fill should be done in separate steps, because offset stroke shape will
      // be different from the original shape used for the fill.
      if (
        Math.abs(strokeOffset) > 0 &&
        fillStyle !== undefined &&
        strokeStyle !== undefined
      ) {
        // First do only fill
        state.strokeStyle = undefined;
        state.strokeOffset = 0;
        drawGeometryCallback();
        // Now separately do the stroke
        state.fillStyle = undefined;
        state.strokeStyle = strokeStyle;
        state.strokeOffset = strokeOffset;
        drawGeometryCallback();
        // Reset the state to the original
        state.fillStyle = fillStyle;
        return true;
      }
      return false;
    }
  }

  /**
   * Creates chunks of equal length from a linestring
   * @param {number} chunkLength Length of each chunk.
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Start offset of the `flatCoordinates`.
   * @param {number} end End offset of the `flatCoordinates`.
   * @param {number} stride Stride.
   * @return {Array<Array<number>>} Chunks of linestrings with stride 2.
   */
  function lineChunk(chunkLength, flatCoordinates, offset, end, stride) {
    const chunks = [];
    let cursor = offset;
    let chunkM = 0;
    let currentChunk = flatCoordinates.slice(offset, 2);
    while (chunkM < chunkLength && cursor + stride < end) {
      const [x1, y1] = currentChunk.slice(-2);
      const x2 = flatCoordinates[cursor + stride];
      const y2 = flatCoordinates[cursor + stride + 1];
      const segmentLength = Math.sqrt(
        (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1),
      );
      chunkM += segmentLength;
      if (chunkM >= chunkLength) {
        const m = (chunkLength - chunkM + segmentLength) / segmentLength;
        const x = lerp(x1, x2, m);
        const y = lerp(y1, y2, m);
        currentChunk.push(x, y);
        chunks.push(currentChunk);
        currentChunk = [x, y];
        if (chunkM == chunkLength) {
          cursor += stride;
        }
        chunkM = 0;
      } else if (chunkM < chunkLength) {
        currentChunk.push(
          flatCoordinates[cursor + stride],
          flatCoordinates[cursor + stride + 1],
        );
        cursor += stride;
      } else {
        const missing = segmentLength - chunkM;
        const x = lerp(x1, x2, missing / segmentLength);
        const y = lerp(y1, y2, missing / segmentLength);
        currentChunk.push(x, y);
        chunks.push(currentChunk);
        currentChunk = [x, y];
        chunkM = 0;
        cursor += stride;
      }
    }
    if (chunkM > 0) {
      chunks.push(currentChunk);
    }
    return chunks;
  }

  /**
   * @module ol/geom/flat/straightchunk
   */

  /**
   * @param {number} maxAngle Maximum acceptable angle delta between segments.
   * @param {Array<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @return {Array<number>} Start and end of the first suitable chunk of the
   * given `flatCoordinates`.
   */
  function matchingChunk(maxAngle, flatCoordinates, offset, end, stride) {
    let chunkStart = offset;
    let chunkEnd = offset;
    let chunkM = 0;
    let m = 0;
    let start = offset;
    let acos, i, m12, m23, x1, y1, x12, y12, x23, y23;
    for (i = offset; i < end; i += stride) {
      const x2 = flatCoordinates[i];
      const y2 = flatCoordinates[i + 1];
      if (x1 !== undefined) {
        x23 = x2 - x1;
        y23 = y2 - y1;
        m23 = Math.sqrt(x23 * x23 + y23 * y23);
        if (x12 !== undefined) {
          m += m12;
          acos = Math.acos((x12 * x23 + y12 * y23) / (m12 * m23));
          if (acos > maxAngle) {
            if (m > chunkM) {
              chunkM = m;
              chunkStart = start;
              chunkEnd = i;
            }
            m = 0;
            start = i - stride;
          }
        }
        m12 = m23;
        x12 = x23;
        y12 = y23;
      }
      x1 = x2;
      y1 = y2;
    }
    m += m23;
    return m > chunkM ? [start, i] : [chunkStart, chunkEnd];
  }

  /**
   * @module ol/render/canvas/TextBuilder
   */
  /**
   * @const
   * @type {{left: 0, center: 0.5, right: 1, top: 0, middle: 0.5, hanging: 0.2, alphabetic: 0.8, ideographic: 0.8, bottom: 1}}
   */
  const TEXT_ALIGN = {
    'left': 0,
    'center': 0.5,
    'right': 1,
    'top': 0,
    'middle': 0.5,
    'hanging': 0.2,
    'alphabetic': 0.8,
    'ideographic': 0.8,
    'bottom': 1,
  };

  class CanvasTextBuilder extends CanvasBuilder {
    /**
     * @param {number} tolerance Tolerance.
     * @param {import("../../extent.js").Extent} maxExtent Maximum extent.
     * @param {number} resolution Resolution.
     * @param {number} pixelRatio Pixel ratio.
     */
    constructor(tolerance, maxExtent, resolution, pixelRatio) {
      super(tolerance, maxExtent, resolution, pixelRatio);

      /**
       * @private
       * @type {Array<HTMLCanvasElement>}
       */
      this.labels_ = null;

      /**
       * @private
       * @type {string|Array<string>}
       */
      this.text_ = '';

      /**
       * @private
       * @type {number}
       */
      this.textOffsetX_ = 0;

      /**
       * @private
       * @type {number}
       */
      this.textOffsetY_ = 0;

      /**
       * @private
       * @type {boolean|undefined}
       */
      this.textRotateWithView_ = undefined;

      /**
       * @private
       * @type {boolean|undefined}
       */
      this.textKeepUpright_ = undefined;

      /**
       * @private
       * @type {number}
       */
      this.textRotation_ = 0;

      /**
       * @private
       * @type {?import("../canvas.js").FillState}
       */
      this.textFillState_ = null;

      /**
       * @type {!Object<string, import("../canvas.js").FillState>}
       */
      this.fillStates = {};
      this.fillStates[defaultFillStyle] = {fillStyle: defaultFillStyle};

      /**
       * @private
       * @type {?import("../canvas.js").StrokeState}
       */
      this.textStrokeState_ = null;

      /**
       * @type {!Object<string, import("../canvas.js").StrokeState>}
       */
      this.strokeStates = {};

      /**
       * @private
       * @type {import("../canvas.js").TextState}
       */
      this.textState_ = /** @type {import("../canvas.js").TextState} */ ({});

      /**
       * @type {!Object<string, import("../canvas.js").TextState>}
       */
      this.textStates = {};

      /**
       * @private
       * @type {string}
       */
      this.textKey_ = '';

      /**
       * @private
       * @type {string}
       */
      this.fillKey_ = '';

      /**
       * @private
       * @type {string}
       */
      this.strokeKey_ = '';

      /**
       * @private
       * @type {import('../../style/Style.js').DeclutterMode}
       */
      this.declutterMode_ = undefined;

      /**
       * Data shared with an image builder for combined decluttering.
       * @private
       * @type {import("../canvas.js").DeclutterImageWithText}
       */
      this.declutterImageWithText_ = undefined;
    }

    /**
     * @return {import("../canvas.js").SerializableInstructions} the serializable instructions.
     * @override
     */
    finish() {
      const instructions = super.finish();
      instructions.textStates = this.textStates;
      instructions.fillStates = this.fillStates;
      instructions.strokeStates = this.strokeStates;
      return instructions;
    }

    /**
     * @param {import("../../geom/SimpleGeometry.js").default|import("../Feature.js").default} geometry Geometry.
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     * @param {number} [index] Render order index.
     * @override
     */
    drawText(geometry, feature, index) {
      const fillState = this.textFillState_;
      const strokeState = this.textStrokeState_;
      const textState = this.textState_;
      if (this.text_ === '' || !textState || (!fillState && !strokeState)) {
        return;
      }

      const coordinates = this.coordinates;
      let begin = coordinates.length;

      const geometryType = geometry.getType();
      let flatCoordinates = null;
      let stride = geometry.getStride();

      if (
        textState.placement === 'line' &&
        (geometryType == 'LineString' ||
          geometryType == 'MultiLineString' ||
          geometryType == 'Polygon' ||
          geometryType == 'MultiPolygon')
      ) {
        if (!intersects$1(this.maxExtent, geometry.getExtent())) {
          return;
        }
        let ends;
        flatCoordinates = geometry.getFlatCoordinates();
        if (geometryType == 'LineString') {
          ends = [flatCoordinates.length];
        } else if (geometryType == 'MultiLineString') {
          ends = /** @type {import("../../geom/MultiLineString.js").default} */ (
            geometry
          ).getEnds();
        } else if (geometryType == 'Polygon') {
          ends = /** @type {import("../../geom/Polygon.js").default} */ (geometry)
            .getEnds()
            .slice(0, 1);
        } else if (geometryType == 'MultiPolygon') {
          const endss =
            /** @type {import("../../geom/MultiPolygon.js").default} */ (
              geometry
            ).getEndss();
          ends = [];
          for (let i = 0, ii = endss.length; i < ii; ++i) {
            ends.push(endss[i][0]);
          }
        }
        this.beginGeometry(geometry, feature, index);
        const repeat = textState.repeat;
        const textAlign = repeat ? undefined : textState.textAlign;
        // No `justify` support for line placement.
        let flatOffset = 0;
        for (let o = 0, oo = ends.length; o < oo; ++o) {
          let chunks;
          if (repeat) {
            chunks = lineChunk(
              repeat * this.resolution,
              flatCoordinates,
              flatOffset,
              ends[o],
              stride,
            );
          } else {
            chunks = [flatCoordinates.slice(flatOffset, ends[o])];
          }
          for (let c = 0, cc = chunks.length; c < cc; ++c) {
            const chunk = chunks[c];
            let chunkBegin = 0;
            let chunkEnd = chunk.length;
            if (textAlign == undefined) {
              const range = matchingChunk(
                textState.maxAngle,
                chunk,
                0,
                chunk.length,
                2,
              );
              chunkBegin = range[0];
              chunkEnd = range[1];
            }
            for (let i = chunkBegin; i < chunkEnd; i += stride) {
              coordinates.push(chunk[i], chunk[i + 1]);
            }
            const end = coordinates.length;
            flatOffset = ends[o];
            this.drawChars_(begin, end);
            begin = end;
          }
        }
        this.endGeometry(feature);
      } else {
        let geometryWidths = textState.overflow ? null : [];
        switch (geometryType) {
          case 'Point':
          case 'MultiPoint':
            flatCoordinates =
              /** @type {import("../../geom/MultiPoint.js").default} */ (
                geometry
              ).getFlatCoordinates();
            break;
          case 'LineString':
            flatCoordinates =
              /** @type {import("../../geom/LineString.js").default} */ (
                geometry
              ).getFlatMidpoint();
            break;
          case 'Circle':
            flatCoordinates =
              /** @type {import("../../geom/Circle.js").default} */ (
                geometry
              ).getCenter();
            break;
          case 'MultiLineString':
            flatCoordinates =
              /** @type {import("../../geom/MultiLineString.js").default} */ (
                geometry
              ).getFlatMidpoints();
            stride = 2;
            break;
          case 'Polygon':
            flatCoordinates =
              /** @type {import("../../geom/Polygon.js").default} */ (
                geometry
              ).getFlatInteriorPoint();
            if (!textState.overflow) {
              geometryWidths.push(flatCoordinates[2] / this.resolution);
            }
            stride = 3;
            break;
          case 'MultiPolygon':
            const interiorPoints =
              /** @type {import("../../geom/MultiPolygon.js").default} */ (
                geometry
              ).getFlatInteriorPoints();
            flatCoordinates = [];
            for (let i = 0, ii = interiorPoints.length; i < ii; i += 3) {
              if (!textState.overflow) {
                geometryWidths.push(interiorPoints[i + 2] / this.resolution);
              }
              flatCoordinates.push(interiorPoints[i], interiorPoints[i + 1]);
            }
            if (flatCoordinates.length === 0) {
              return;
            }
            stride = 2;
            break;
        }
        const end = this.appendFlatPointCoordinates(flatCoordinates, stride);
        if (end === begin) {
          return;
        }
        if (
          geometryWidths &&
          (end - begin) / 2 !== flatCoordinates.length / stride
        ) {
          let beg = begin / 2;
          geometryWidths = geometryWidths.filter((w, i) => {
            const keep =
              coordinates[(beg + i) * 2] === flatCoordinates[i * stride] &&
              coordinates[(beg + i) * 2 + 1] === flatCoordinates[i * stride + 1];
            if (!keep) {
              --beg;
            }
            return keep;
          });
        }

        this.saveTextStates_();

        const backgroundFill = textState.backgroundFill
          ? this.createFill(this.fillStyleToState(textState.backgroundFill))
          : null;
        const backgroundStroke = textState.backgroundStroke
          ? this.createStroke(this.strokeStyleToState(textState.backgroundStroke))
          : null;

        this.beginGeometry(geometry, feature, index);

        // adjust padding for negative scale
        let padding = textState.padding;
        if (
          padding != defaultPadding &&
          (textState.scale[0] < 0 || textState.scale[1] < 0)
        ) {
          let p0 = textState.padding[0];
          let p1 = textState.padding[1];
          let p2 = textState.padding[2];
          let p3 = textState.padding[3];
          if (textState.scale[0] < 0) {
            p1 = -p1;
            p3 = -p3;
          }
          if (textState.scale[1] < 0) {
            p0 = -p0;
            p2 = -p2;
          }
          padding = [p0, p1, p2, p3];
        }

        // 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([
          Instruction.DRAW_IMAGE,
          begin,
          end,
          null,
          NaN,
          NaN,
          NaN,
          1,
          0,
          0,
          this.textRotateWithView_,
          this.textRotation_,
          [1, 1],
          NaN,
          this.declutterMode_,
          this.declutterImageWithText_,
          padding == defaultPadding
            ? defaultPadding
            : padding.map(function (p) {
                return p * pixelRatio;
              }),
          backgroundFill,
          backgroundStroke,
          this.text_,
          this.textKey_,
          this.strokeKey_,
          this.fillKey_,
          this.textOffsetX_,
          this.textOffsetY_,
          geometryWidths,
        ]);
        const scale = 1 / pixelRatio;
        // Set default fill for hit detection background
        const hitDetectionBackgroundFill = backgroundFill
          ? backgroundFill.slice(0)
          : null;
        if (hitDetectionBackgroundFill) {
          hitDetectionBackgroundFill[1] = defaultFillStyle;
        }
        this.hitDetectionInstructions.push([
          Instruction.DRAW_IMAGE,
          begin,
          end,
          null,
          NaN,
          NaN,
          NaN,
          1,
          0,
          0,
          this.textRotateWithView_,
          this.textRotation_,
          [scale, scale],
          NaN,
          this.declutterMode_,
          this.declutterImageWithText_,
          padding,
          hitDetectionBackgroundFill,
          backgroundStroke,
          this.text_,
          this.textKey_,
          this.strokeKey_,
          this.fillKey_ ? defaultFillStyle : this.fillKey_,
          this.textOffsetX_,
          this.textOffsetY_,
          geometryWidths,
        ]);

        this.endGeometry(feature);
      }
    }

    /**
     * @private
     */
    saveTextStates_() {
      const strokeState = this.textStrokeState_;
      const textState = this.textState_;
      const fillState = this.textFillState_;

      const strokeKey = this.strokeKey_;
      if (strokeState) {
        if (!(strokeKey in this.strokeStates)) {
          this.strokeStates[strokeKey] = {
            strokeStyle: strokeState.strokeStyle,
            lineCap: strokeState.lineCap,
            lineDashOffset: strokeState.lineDashOffset,
            lineWidth: strokeState.lineWidth,
            lineJoin: strokeState.lineJoin,
            miterLimit: strokeState.miterLimit,
            lineDash: strokeState.lineDash,
          };
        }
      }
      const textKey = this.textKey_;
      if (!(textKey in this.textStates)) {
        this.textStates[textKey] = {
          font: textState.font,
          textAlign: textState.textAlign || defaultTextAlign,
          justify: textState.justify,
          textBaseline: textState.textBaseline || defaultTextBaseline,
          scale: textState.scale,
        };
      }
      const fillKey = this.fillKey_;
      if (fillState) {
        if (!(fillKey in this.fillStates)) {
          this.fillStates[fillKey] = {
            fillStyle: fillState.fillStyle,
          };
        }
      }
    }

    /**
     * @private
     * @param {number} begin Begin.
     * @param {number} end End.
     */
    drawChars_(begin, end) {
      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 strokeWidth = strokeState
        ? (strokeState.lineWidth * Math.abs(textState.scale[0])) / 2
        : 0;

      this.instructions.push([
        Instruction.DRAW_CHARS,
        begin,
        end,
        baseline,
        textState.overflow,
        fillKey,
        textState.maxAngle,
        pixelRatio,
        offsetY,
        strokeKey,
        strokeWidth * pixelRatio,
        text,
        textKey,
        1,
        this.declutterMode_,
        this.textKeepUpright_,
      ]);
      this.hitDetectionInstructions.push([
        Instruction.DRAW_CHARS,
        begin,
        end,
        baseline,
        textState.overflow,
        fillKey ? defaultFillStyle : fillKey,
        textState.maxAngle,
        pixelRatio,
        offsetY,
        strokeKey,
        strokeWidth * pixelRatio,
        text,
        textKey,
        1 / pixelRatio,
        this.declutterMode_,
        this.textKeepUpright_,
      ]);
    }

    /**
     * @param {import("../../style/Text.js").default} textStyle Text style.
     * @param {Object} [sharedData] Shared data.
     * @override
     */
    setTextStyle(textStyle, sharedData) {
      let textState, fillState, strokeState;
      if (!textStyle) {
        this.text_ = '';
      } else {
        const textFillStyle = textStyle.getFill();
        if (!textFillStyle) {
          fillState = null;
          this.textFillState_ = fillState;
        } else {
          fillState = this.textFillState_;
          if (!fillState) {
            fillState = /** @type {import("../canvas.js").FillState} */ ({});
            this.textFillState_ = fillState;
          }
          fillState.fillStyle = asColorLike(
            textFillStyle.getColor() || defaultFillStyle,
          );
        }

        const textStrokeStyle = textStyle.getStroke();
        if (!textStrokeStyle) {
          strokeState = null;
          this.textStrokeState_ = strokeState;
        } else {
          strokeState = this.textStrokeState_;
          if (!strokeState) {
            strokeState = /** @type {import("../canvas.js").StrokeState} */ ({});
            this.textStrokeState_ = strokeState;
          }
          const lineDash = textStrokeStyle.getLineDash();
          const lineDashOffset = textStrokeStyle.getLineDashOffset();
          const lineWidth = textStrokeStyle.getWidth();
          const miterLimit = textStrokeStyle.getMiterLimit();
          strokeState.lineCap = textStrokeStyle.getLineCap() || defaultLineCap;
          strokeState.lineDash = lineDash ? lineDash.slice() : defaultLineDash;
          strokeState.lineDashOffset =
            lineDashOffset === undefined ? defaultLineDashOffset : lineDashOffset;
          strokeState.lineJoin = textStrokeStyle.getLineJoin() || defaultLineJoin;
          strokeState.lineWidth =
            lineWidth === undefined ? defaultLineWidth : lineWidth;
          strokeState.miterLimit =
            miterLimit === undefined ? defaultMiterLimit : miterLimit;
          strokeState.strokeStyle = asColorLike(
            textStrokeStyle.getColor() || defaultStrokeStyle,
          );
        }

        textState = this.textState_;
        const font = textStyle.getFont() || defaultFont;
        registerFont(font);
        const textScale = textStyle.getScaleArray();
        textState.overflow = textStyle.getOverflow();
        textState.font = font;
        textState.maxAngle = textStyle.getMaxAngle();
        textState.placement = textStyle.getPlacement();
        textState.textAlign = textStyle.getTextAlign();
        textState.repeat = textStyle.getRepeat();
        textState.justify = textStyle.getJustify();
        textState.textBaseline =
          textStyle.getTextBaseline() || defaultTextBaseline;
        textState.backgroundFill = textStyle.getBackgroundFill();
        textState.backgroundStroke = textStyle.getBackgroundStroke();
        textState.padding = textStyle.getPadding() || defaultPadding;
        textState.scale = textScale === undefined ? [1, 1] : textScale;

        const textOffsetX = textStyle.getOffsetX();
        const textOffsetY = textStyle.getOffsetY();
        const textRotateWithView = textStyle.getRotateWithView();
        const textKeepUpright = textStyle.getKeepUpright();
        const textRotation = textStyle.getRotation();
        this.text_ = textStyle.getText() || '';
        this.textOffsetX_ = textOffsetX === undefined ? 0 : textOffsetX;
        this.textOffsetY_ = textOffsetY === undefined ? 0 : textOffsetY;
        this.textRotateWithView_ =
          textRotateWithView === undefined ? false : textRotateWithView;
        this.textKeepUpright_ =
          textKeepUpright === undefined ? true : textKeepUpright;
        this.textRotation_ = textRotation === undefined ? 0 : textRotation;

        this.strokeKey_ = strokeState
          ? (typeof strokeState.strokeStyle == 'string'
              ? strokeState.strokeStyle
              : getUid(strokeState.strokeStyle)) +
            strokeState.lineCap +
            strokeState.lineDashOffset +
            '|' +
            strokeState.lineWidth +
            strokeState.lineJoin +
            strokeState.miterLimit +
            '[' +
            strokeState.lineDash.join() +
            ']'
          : '';
        this.textKey_ =
          textState.font +
          textState.scale +
          (textState.textAlign || '?') +
          (textState.repeat || '?') +
          (textState.justify || '?') +
          (textState.textBaseline || '?');
        this.fillKey_ =
          fillState && fillState.fillStyle
            ? typeof fillState.fillStyle == 'string'
              ? fillState.fillStyle
              : '|' + getUid(fillState.fillStyle)
            : '';
      }
      this.declutterMode_ = textStyle.getDeclutterMode();
      this.declutterImageWithText_ = sharedData;
    }
  }

  /**
   * @module ol/render/canvas/BuilderGroup
   */


  /**
   * @type {Object<import("../canvas.js").BuilderType, typeof Builder>}
   */
  const BATCH_CONSTRUCTORS = {
    'Circle': CanvasPolygonBuilder,
    'Default': CanvasBuilder,
    'Image': CanvasImageBuilder,
    'LineString': CanvasLineStringBuilder,
    'Polygon': CanvasPolygonBuilder,
    'Text': CanvasTextBuilder,
  };

  class BuilderGroup {
    /**
     * @param {number} tolerance Tolerance.
     * @param {import("../../extent.js").Extent} maxExtent Max extent.
     * @param {number} resolution Resolution.
     * @param {number} pixelRatio Pixel ratio.
     */
    constructor(tolerance, maxExtent, resolution, pixelRatio) {
      /**
       * @private
       * @type {number}
       */
      this.tolerance_ = tolerance;

      /**
       * @private
       * @type {import("../../extent.js").Extent}
       */
      this.maxExtent_ = maxExtent;

      /**
       * @private
       * @type {number}
       */
      this.pixelRatio_ = pixelRatio;

      /**
       * @private
       * @type {number}
       */
      this.resolution_ = resolution;

      /**
       * @private
       * @type {!Object<string, !Object<import("../canvas.js").BuilderType, Builder>>}
       */
      this.buildersByZIndex_ = {};
    }

    /**
     * @return {!Object<string, !Object<import("../canvas.js").BuilderType, import("./Builder.js").SerializableInstructions>>} 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 {number|undefined} zIndex Z index.
     * @param {import("../canvas.js").BuilderType} builderType Replay type.
     * @return {import("../VectorContext.js").default} Replay.
     */
    getBuilder(zIndex, builderType) {
      const zIndexKey = zIndex !== undefined ? zIndex.toString() : '0';
      let replays = this.buildersByZIndex_[zIndexKey];
      if (replays === undefined) {
        replays = {};
        this.buildersByZIndex_[zIndexKey] = replays;
      }
      let replay = replays[builderType];
      if (replay === undefined) {
        const Constructor = BATCH_CONSTRUCTORS[builderType];
        replay = new Constructor(
          this.tolerance_,
          this.maxExtent_,
          this.resolution_,
          this.pixelRatio_,
        );
        replays[builderType] = replay;
      }
      return replay;
    }
  }

  /**
   * @module ol/geom/flat/textpath
   */

  /**
   * @param {Array<number>} flatCoordinates Path to put text on.
   * @param {number} offset Start offset of the `flatCoordinates`.
   * @param {number} end End offset of the `flatCoordinates`.
   * @param {number} stride Stride.
   * @param {string} text Text to place on the path.
   * @param {number} startM m along the path where the text starts.
   * @param {number} maxAngle Max angle between adjacent chars in radians.
   * @param {number} scale The product of the text scale and the device pixel ratio.
   * @param {function(string, string, Object<string, number>):number} measureAndCacheTextWidth Measure and cache text width.
   * @param {string} font The font.
   * @param {Object<string, number>} cache A cache of measured widths.
   * @param {number} rotation Rotation to apply to the flatCoordinates to determine whether text needs to be reversed.
   * @param {boolean} keepUpright Whether the text needs to be kept upright
   * @return {Array<Array<*>>|null} The result array (or null if `maxAngle` was
   * exceeded). Entries of the array are x, y, anchorX, angle, chunk.
   */
  function drawTextOnPath(
    flatCoordinates,
    offset,
    end,
    stride,
    text,
    startM,
    maxAngle,
    scale,
    measureAndCacheTextWidth,
    font,
    cache,
    rotation,
    keepUpright = true,
  ) {
    let x2 = flatCoordinates[offset];
    let y2 = flatCoordinates[offset + 1];
    let x1 = 0;
    let y1 = 0;
    let segmentLength = 0;
    let segmentM = 0;

    function advance() {
      x1 = x2;
      y1 = y2;
      offset += stride;
      x2 = flatCoordinates[offset];
      y2 = flatCoordinates[offset + 1];
      segmentM += segmentLength;
      segmentLength = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
    }
    do {
      advance();
    } while (offset < end - stride && segmentM + segmentLength < startM);

    let interpolate =
      segmentLength === 0 ? 0 : (startM - segmentM) / segmentLength;
    const beginX = lerp(x1, x2, interpolate);
    const beginY = lerp(y1, y2, interpolate);

    const startOffset = offset - stride;
    const startLength = segmentM;
    const endM = startM + scale * measureAndCacheTextWidth(font, text, cache);
    while (offset < end - stride && segmentM + segmentLength < endM) {
      advance();
    }
    interpolate = segmentLength === 0 ? 0 : (endM - segmentM) / segmentLength;
    const endX = lerp(x1, x2, interpolate);
    const endY = lerp(y1, y2, interpolate);

    // Keep text upright if the option is selected
    let reverse = false;
    if (keepUpright) {
      if (rotation) {
        const flat = [beginX, beginY, endX, endY];
        rotate(flat, 0, 4, 2, rotation, flat, flat);
        reverse = flat[0] > flat[2];
      } else {
        reverse = beginX > endX;
      }
    }

    const PI = Math.PI;
    const result = [];
    const singleSegment = startOffset + stride === offset;

    offset = startOffset;
    segmentLength = 0;
    segmentM = startLength;
    x2 = flatCoordinates[offset];
    y2 = flatCoordinates[offset + 1];

    let previousAngle;
    // All on the same segment
    if (singleSegment) {
      advance();

      previousAngle = Math.atan2(y2 - y1, x2 - x1);
      if (reverse) {
        previousAngle += previousAngle > 0 ? -PI : PI;
      }
      const x = (endX + beginX) / 2;
      const y = (endY + beginY) / 2;
      result[0] = [x, y, (endM - startM) / 2, previousAngle, text];
      return result;
    }

    // rendering across line segments
    text = text.replace(/\n/g, ' '); // ensure rendering in single-line as all calculations below don't handle multi-lines

    for (let i = 0, ii = text.length; i < ii; ) {
      advance();
      let angle = Math.atan2(y2 - y1, x2 - x1);
      if (reverse) {
        angle += angle > 0 ? -PI : PI;
      }
      if (previousAngle !== undefined) {
        let delta = angle - previousAngle;
        delta += delta > PI ? -2 * PI : delta < -PI ? 2 * PI : 0;
        if (Math.abs(delta) > maxAngle) {
          return null;
        }
      }
      previousAngle = angle;

      const iStart = i;
      let charLength = 0;
      for (; i < ii; ++i) {
        const index = reverse ? ii - i - 1 : i;
        const len = scale * measureAndCacheTextWidth(font, text[index], cache);
        if (
          offset + stride < end &&
          segmentM + segmentLength < startM + charLength + len / 2
        ) {
          break;
        }
        charLength += len;
      }
      if (i === iStart) {
        continue;
      }
      const chars = reverse
        ? text.substring(ii - iStart, ii - i)
        : text.substring(iStart, i);
      interpolate =
        segmentLength === 0
          ? 0
          : (startM + charLength / 2 - segmentM) / segmentLength;
      const x = lerp(x1, x2, interpolate);
      const y = lerp(y1, y2, interpolate);
      result.push([x, y, charLength / 2, angle, chars]);
      startM += charLength;
    }
    return result;
  }

  /**
   * @module ol/render/canvas/ZIndexContext
   */


  /** @typedef {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D & {globalAlpha: any}} ZIndexContextProxy */

  /**
   * @extends {CanvasRenderingContext2D}
   */
  class ZIndexContext {
    constructor() {
      /**
       * @private
       * @type {Array<Array<*>>}
       */
      this.instructions_ = [];
      /**
       * @type {number}
       */
      this.zIndex = 0;
      /**
       * @private
       * @type {number}
       */
      this.offset_ = 0;

      /**
       * @private
       * @type {ZIndexContextProxy}
       */
      this.context_ = /** @type {ZIndexContextProxy} */ (
        new Proxy(getSharedCanvasContext2D(), {
          get: (target, property) => {
            if (
              typeof (/** @type {*} */ (getSharedCanvasContext2D())[property]) !==
              'function'
            ) {
              // we only accept calling functions on the proxy, not accessing properties
              return undefined;
            }
            this.push_(property);
            return this.pushMethodArgs_;
          },
          set: (target, property, value) => {
            this.push_(property, value);
            return true;
          },
        })
      );
    }

    /**
     * @param {...*} args Arguments to push to the instructions array.
     * @private
     */
    push_(...args) {
      const instructions = this.instructions_;
      const index = this.zIndex + this.offset_;
      if (!instructions[index]) {
        instructions[index] = [];
      }
      instructions[index].push(...args);
    }

    /**
     * @private
     * @param {...*} args Args.
     * @return {ZIndexContext} This.
     */
    pushMethodArgs_ = (...args) => {
      this.push_(args);
      return this;
    };

    /**
     * Push a function that renders to the context directly.
     * @param {function(CanvasRenderingContext2D): void} render Function.
     */
    pushFunction(render) {
      this.push_(render);
    }

    /**
     * Get a proxy for CanvasRenderingContext2D which does not support getting state
     * (e.g. `context.globalAlpha`, which will return `undefined`). To set state, if it relies on a
     * previous state (e.g. `context.globalAlpha = context.globalAlpha / 2`), set a function,
     * e.g. `context.globalAlpha = (context) => context.globalAlpha / 2`.
     * @return {ZIndexContextProxy} Context.
     */
    getContext() {
      return this.context_;
    }

    /**
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
     */
    draw(context) {
      this.instructions_.forEach((instructionsAtIndex) => {
        for (let i = 0, ii = instructionsAtIndex.length; i < ii; ++i) {
          const property = instructionsAtIndex[i];
          if (typeof property === 'function') {
            property(context);
            continue;
          }
          const instructionAtIndex = instructionsAtIndex[++i];
          if (typeof (/** @type {*} */ (context)[property]) === 'function') {
            /** @type {*} */ (context)[property](...instructionAtIndex);
          } else {
            if (typeof instructionAtIndex === 'function') {
              /** @type {*} */ (context)[property] = instructionAtIndex(context);
              continue;
            }
            /** @type {*} */ (context)[property] = instructionAtIndex;
          }
        }
      });
    }

    clear() {
      this.instructions_.length = 0;
      this.zIndex = 0;
      this.offset_ = 0;
    }

    /**
     * Offsets the zIndex by the highest current zIndex. Useful for rendering multiple worlds or tiles, to
     * avoid conflicting context.clip() or context.save()/restore() calls.
     */
    offset() {
      this.offset_ = this.instructions_.length;
      this.zIndex = 0;
    }
  }

  /**
   * @module ol/render/canvas/Executor
   */

  /**
   * @typedef {import('../../structs/RBush.js').Entry<import('../../Feature.js').FeatureLike>} DeclutterEntry
   */

  /**
   * @typedef {Object} ImageOrLabelDimensions
   * @property {number} drawImageX DrawImageX.
   * @property {number} drawImageY DrawImageY.
   * @property {number} drawImageW DrawImageW.
   * @property {number} drawImageH DrawImageH.
   * @property {number} originX OriginX.
   * @property {number} originY OriginY.
   * @property {Array<number>} scale Scale.
   * @property {DeclutterEntry} declutterBox DeclutterBox.
   * @property {import("../../transform.js").Transform} canvasTransform CanvasTransform.
   */

  /**
   * @typedef {{0: CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, 1: import('../../size.js').Size, 2: import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement, 3: ImageOrLabelDimensions, 4: number, 5: Array<*>, 6: Array<*>}} ReplayImageOrLabelArgs
   */

  /**
   * @template T
   * @typedef {function(import("../../Feature.js").FeatureLike, import("../../geom/SimpleGeometry.js").default, import("../../style/Style.js").DeclutterMode): T} FeatureCallback
   */

  /**
   * @type {import("../../extent.js").Extent}
   */
  const tmpExtent = createEmpty();

  /** @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 = [];

  /**
   * @param {ReplayImageOrLabelArgs} replayImageOrLabelArgs Arguments to replayImageOrLabel
   * @return {DeclutterEntry} Declutter rbush entry.
   */
  function getDeclutterBox(replayImageOrLabelArgs) {
    return replayImageOrLabelArgs[3].declutterBox;
  }

  const rtlRegEx = new RegExp(
    /* eslint-disable prettier/prettier */
    '[' +
      String.fromCharCode(0x00591) + '-' + String.fromCharCode(0x008ff) +
      String.fromCharCode(0x0fb1d) + '-' + String.fromCharCode(0x0fdff) +
      String.fromCharCode(0x0fe70) + '-' + String.fromCharCode(0x0fefc) +
      String.fromCharCode(0x10800) + '-' + String.fromCharCode(0x10fff) +
      String.fromCharCode(0x1e800) + '-' + String.fromCharCode(0x1efff) +
    ']'
    /* eslint-enable prettier/prettier */
  );

  /**
   * @param {string} text Text.
   * @param {CanvasTextAlign} align Alignment.
   * @return {number} Text alignment.
   */
  function horizontalTextAlign(text, align) {
    if (align === 'start') {
      align = rtlRegEx.test(text) ? 'right' : 'left';
    } else if (align === 'end') {
      align = rtlRegEx.test(text) ? 'left' : 'right';
    }
    return TEXT_ALIGN[align];
  }

  /**
   * @param {Array<string>} acc Accumulator.
   * @param {string} line Line of text.
   * @param {number} i Index
   * @return {Array<string>} Accumulator.
   */
  function createTextChunks(acc, line, i) {
    if (i > 0) {
      acc.push('\n', '');
    }
    acc.push(line, '');
    return acc;
  }

  /**
   * Converts rich text to plain text for text along lines.
   * @param {string} result The resulting plain text.
   * @param {string} part Item of the rich text array.
   * @param {number} index Index of the item in the rich text array.
   * @return {string} The resulting plain text.
   */
  function richTextToPlainText(result, part, index) {
    if (index % 2 === 0) {
      result += part;
    }
    return result;
  }

  class Executor {
    /**
     * @param {number} resolution Resolution.
     * @param {number} pixelRatio Pixel ratio.
     * @param {boolean} overlaps The replay can have overlapping geometries.
     * @param {import("../canvas.js").SerializableInstructions} instructions The serializable instructions.
     * @param {boolean} [deferredRendering] Enable deferred rendering.
     */
    constructor(
      resolution,
      pixelRatio,
      overlaps,
      instructions,
      deferredRendering,
    ) {
      /**
       * @protected
       * @type {boolean}
       */
      this.overlaps = overlaps;

      /**
       * @protected
       * @type {number}
       */
      this.pixelRatio = pixelRatio;

      /**
       * @protected
       * @const
       * @type {number}
       */
      this.resolution = resolution;

      /**
       * @private
       * @type {number}
       */
      this.alignAndScaleFill_;

      /**
       * @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_ = create();

      /**
       * @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_ = {};

      /**
       * @private
       * @type {Object<string, import("../canvas.js").Label>}
       */
      this.labels_ = {};

      /**
       * @private
       * @type {import("../canvas/ZIndexContext.js").default}
       */
      this.zIndexContext_ = deferredRendering ? new ZIndexContext() : null;
    }

    /**
     * @return {ZIndexContext} ZIndex context.
     */
    getZIndexContext() {
      return this.zIndexContext_;
    }

    /**
     * @param {string|Array<string>} text Text.
     * @param {string} textKey Text style key.
     * @param {string} fillKey Fill style key.
     * @param {string} strokeKey Stroke style key.
     * @return {import("../canvas.js").Label} Label.
     */
    createLabel(text, textKey, fillKey, strokeKey) {
      const key = text + textKey + fillKey + strokeKey;
      if (this.labels_[key]) {
        return this.labels_[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[0] * pixelRatio,
        textState.scale[1] * pixelRatio,
      ];
      const align = textState.justify
        ? TEXT_ALIGN[textState.justify]
        : horizontalTextAlign(
            Array.isArray(text) ? text[0] : text,
            textState.textAlign || defaultTextAlign,
          );
      const strokeWidth =
        strokeKey && strokeState.lineWidth ? strokeState.lineWidth : 0;

      const chunks = Array.isArray(text)
        ? text
        : String(text).split('\n').reduce(createTextChunks, []);

      const {width, height, widths, heights, lineWidths} = getTextDimensions(
        textState,
        chunks,
      );
      const renderWidth = width + strokeWidth;
      const contextInstructions = [];
      // make canvas 2 pixels wider to account for italic text width measurement errors
      const w = (renderWidth + 2) * scale[0];
      const h = (height + strokeWidth) * scale[1];
      /** @type {import("../canvas.js").Label} */
      const label = {
        width: w < 0 ? Math.floor(w) : Math.ceil(w),
        height: h < 0 ? Math.floor(h) : Math.ceil(h),
        contextInstructions: contextInstructions,
      };
      if (scale[0] != 1 || scale[1] != 1) {
        contextInstructions.push('scale', scale);
      }
      if (strokeKey) {
        contextInstructions.push('strokeStyle', strokeState.strokeStyle);
        contextInstructions.push('lineWidth', strokeWidth);
        contextInstructions.push('lineCap', strokeState.lineCap);
        contextInstructions.push('lineJoin', strokeState.lineJoin);
        contextInstructions.push('miterLimit', strokeState.miterLimit);
        contextInstructions.push('setLineDash', [strokeState.lineDash]);
        contextInstructions.push('lineDashOffset', strokeState.lineDashOffset);
      }
      if (fillKey) {
        contextInstructions.push('fillStyle', fillState.fillStyle);
      }
      contextInstructions.push('textBaseline', 'middle');
      contextInstructions.push('textAlign', 'center');
      const leftRight = 0.5 - align;
      let x = align * renderWidth + leftRight * strokeWidth;
      const strokeInstructions = [];
      const fillInstructions = [];
      let lineHeight = 0;
      let lineOffset = 0;
      let widthHeightIndex = 0;
      let lineWidthIndex = 0;
      let previousFont;
      for (let i = 0, ii = chunks.length; i < ii; i += 2) {
        const text = chunks[i];
        if (text === '\n') {
          lineOffset += lineHeight;
          lineHeight = 0;
          x = align * renderWidth + leftRight * strokeWidth;
          ++lineWidthIndex;
          continue;
        }
        const font = chunks[i + 1] || textState.font;
        if (font !== previousFont) {
          if (strokeKey) {
            strokeInstructions.push('font', font);
          }
          if (fillKey) {
            fillInstructions.push('font', font);
          }
          previousFont = font;
        }
        lineHeight = Math.max(lineHeight, heights[widthHeightIndex]);
        const fillStrokeArgs = [
          text,
          x +
            leftRight * widths[widthHeightIndex] +
            align * (widths[widthHeightIndex] - lineWidths[lineWidthIndex]),
          0.5 * (strokeWidth + lineHeight) + lineOffset,
        ];
        x += widths[widthHeightIndex];
        if (strokeKey) {
          strokeInstructions.push('strokeText', fillStrokeArgs);
        }
        if (fillKey) {
          fillInstructions.push('fillText', fillStrokeArgs);
        }
        ++widthHeightIndex;
      }
      Array.prototype.push.apply(contextInstructions, strokeInstructions);
      Array.prototype.push.apply(contextInstructions, fillInstructions);
      this.labels_[key] = label;
      return label;
    }

    /**
     * @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.alignAndScaleFill_ = /** @type {number} */ (fillInstruction[2]);
        context.fillStyle = /** @type {string} */ (fillInstruction[1]);
        this.fill_(context);
      }
      if (strokeInstruction) {
        this.setStrokeStyle_(
          context,
          /** @type {Array<*>} */ (strokeInstruction),
        );
        context.stroke();
      }
    }

    /**
     * @private
     * @param {number} sheetWidth Width of the sprite sheet.
     * @param {number} sheetHeight Height of the sprite sheet.
     * @param {number} centerX X.
     * @param {number} centerY Y.
     * @param {number} width Width.
     * @param {number} height Height.
     * @param {number} anchorX Anchor X.
     * @param {number} anchorY Anchor Y.
     * @param {number} originX Origin X.
     * @param {number} originY Origin Y.
     * @param {number} rotation Rotation.
     * @param {import("../../size.js").Size} scale Scale.
     * @param {boolean} snapToPixel Snap to pixel.
     * @param {Array<number>} padding Padding.
     * @param {boolean} fillStroke Background fill or stroke.
     * @param {import("../../Feature.js").FeatureLike} feature Feature.
     * @return {ImageOrLabelDimensions} Dimensions for positioning and decluttering the image or label.
     */
    calculateImageOrLabelDimensions_(
      sheetWidth,
      sheetHeight,
      centerX,
      centerY,
      width,
      height,
      anchorX,
      anchorY,
      originX,
      originY,
      rotation,
      scale,
      snapToPixel,
      padding,
      fillStroke,
      feature,
    ) {
      anchorX *= scale[0];
      anchorY *= scale[1];
      let x = centerX - anchorX;
      let y = centerY - anchorY;

      const w = width + originX > sheetWidth ? sheetWidth - originX : width;
      const h = height + originY > sheetHeight ? sheetHeight - originY : height;
      const boxW = padding[3] + w * scale[0] + padding[1];
      const boxH = padding[0] + h * scale[1] + padding[2];
      const boxX = x - padding[3];
      const boxY = y - padding[0];

      if (fillStroke || rotation !== 0) {
        p1[0] = boxX;
        p4[0] = boxX;
        p1[1] = boxY;
        p2[1] = boxY;
        p2[0] = boxX + boxW;
        p3[0] = p2[0];
        p3[1] = boxY + boxH;
        p4[1] = p3[1];
      }

      let transform;
      if (rotation !== 0) {
        transform = compose(
          create(),
          centerX,
          centerY,
          1,
          1,
          rotation,
          -centerX,
          -centerY,
        );

        apply(transform, p1);
        apply(transform, p2);
        apply(transform, p3);
        apply(transform, p4);
        createOrUpdate$2(
          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$2(
          Math.min(boxX, boxX + boxW),
          Math.min(boxY, boxY + boxH),
          Math.max(boxX, boxX + boxW),
          Math.max(boxY, boxY + boxH),
          tmpExtent,
        );
      }
      if (snapToPixel) {
        x = Math.round(x);
        y = Math.round(y);
      }
      return {
        drawImageX: x,
        drawImageY: y,
        drawImageW: w,
        drawImageH: h,
        originX: originX,
        originY: originY,
        declutterBox: {
          minX: tmpExtent[0],
          minY: tmpExtent[1],
          maxX: tmpExtent[2],
          maxY: tmpExtent[3],
          value: feature,
        },
        canvasTransform: transform,
        scale: scale,
      };
    }

    /**
     * @private
     * @param {CanvasRenderingContext2D} context Context.
     * @param {import('../../size.js').Size} scaledCanvasSize Scaled canvas size.
     * @param {import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} imageOrLabel Image.
     * @param {ImageOrLabelDimensions} dimensions Dimensions.
     * @param {number} opacity Opacity.
     * @param {Array<*>} fillInstruction Fill instruction.
     * @param {Array<*>} strokeInstruction Stroke instruction.
     * @return {boolean} The image or label was rendered.
     */
    replayImageOrLabel_(
      context,
      scaledCanvasSize,
      imageOrLabel,
      dimensions,
      opacity,
      fillInstruction,
      strokeInstruction,
    ) {
      const fillStroke = !!(fillInstruction || strokeInstruction);

      const box = dimensions.declutterBox;
      const strokePadding = strokeInstruction
        ? (strokeInstruction[2] * dimensions.scale[0]) / 2
        : 0;
      const intersects =
        box.minX - strokePadding <= scaledCanvasSize[0] &&
        box.maxX + strokePadding >= 0 &&
        box.minY - strokePadding <= scaledCanvasSize[1] &&
        box.maxY + strokePadding >= 0;

      if (intersects) {
        if (fillStroke) {
          this.replayTextBackground_(
            context,
            p1,
            p2,
            p3,
            p4,
            /** @type {Array<*>} */ (fillInstruction),
            /** @type {Array<*>} */ (strokeInstruction),
          );
        }
        drawImageOrLabel(
          context,
          dimensions.canvasTransform,
          opacity,
          imageOrLabel,
          dimensions.originX,
          dimensions.originY,
          dimensions.drawImageW,
          dimensions.drawImageH,
          dimensions.drawImageX,
          dimensions.drawImageY,
          dimensions.scale,
        );
      }
      return true;
    }

    /**
     * @private
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
     */
    fill_(context) {
      const alignAndScale = this.alignAndScaleFill_;
      if (alignAndScale) {
        const origin = apply(this.renderedTransform_, [0, 0]);
        const repeatSize = 512 * this.pixelRatio;
        context.save();
        context.translate(origin[0] % repeatSize, origin[1] % repeatSize);
        if (alignAndScale !== 1) {
          context.scale(alignAndScale, alignAndScale);
        }
      }
      context.fill();
      if (alignAndScale) {
        context.restore();
      }
    }

    /**
     * @private
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
     * @param {Array<*>} instruction Instruction.
     */
    setStrokeStyle_(context, instruction) {
      context.strokeStyle =
        /** @type {import("../../colorlike.js").ColorLike} */ (instruction[1]);
      if (!instruction[1]) {
        return;
      }
      context.lineWidth = /** @type {number} */ (instruction[2]);
      context.lineCap = /** @type {CanvasLineCap} */ (instruction[3]);
      context.lineJoin = /** @type {CanvasLineJoin} */ (instruction[4]);
      context.miterLimit = /** @type {number} */ (instruction[5]);
      context.lineDashOffset = /** @type {number} */ (instruction[7]);
      context.setLineDash(/** @type {Array<number>} */ (instruction[6]));
    }

    /**
     * @private
     * @param {string|Array<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: import("../canvas.js").Label, anchorX: number, anchorY: number}} The text image and its anchor.
     */
    drawLabelWithPointPlacement_(text, textKey, strokeKey, fillKey) {
      const textState = this.textStates[textKey];

      const label = this.createLabel(text, textKey, fillKey, strokeKey);

      const strokeState = this.strokeStates[strokeKey];
      const pixelRatio = this.pixelRatio;
      const align = horizontalTextAlign(
        Array.isArray(text) ? text[0] : text,
        textState.textAlign || defaultTextAlign,
      );
      const baseline = TEXT_ALIGN[textState.textBaseline || defaultTextBaseline];
      const strokeWidth =
        strokeState && strokeState.lineWidth ? strokeState.lineWidth : 0;

      // Remove the 2 pixels we added in createLabel() for the anchor
      const width = label.width / pixelRatio - 2 * textState.scale[0];
      const anchorX = align * width + 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|OffscreenCanvasRenderingContext2D} context Context.
     * @param {import('../../size.js').Size} scaledCanvasSize Scaled canvas size
     * @param {import("../../transform.js").Transform} transform Transform.
     * @param {Array<*>} instructions Instructions array.
     * @param {boolean} snapToPixel Snap point symbols and text to integer pixels.
     * @param {FeatureCallback<T>} [featureCallback] Feature callback.
     * @param {import("../../extent.js").Extent} [hitExtent] Only check
     *     features that intersect this extent.
     * @param {import("rbush").default<DeclutterEntry>} [declutterTree] Declutter tree.
     * @return {T|undefined} Callback result.
     * @template T
     */
    execute_(
      context,
      scaledCanvasSize,
      transform,
      instructions,
      snapToPixel,
      featureCallback,
      hitExtent,
      declutterTree,
    ) {
      const zIndexContext = this.zIndexContext_;
      /** @type {Array<number>} */
      let pixelCoordinates;
      if (this.pixelCoordinates_ && equals$2(transform, this.renderedTransform_)) {
        pixelCoordinates = this.pixelCoordinates_;
      } else {
        if (!this.pixelCoordinates_) {
          this.pixelCoordinates_ = [];
        }
        pixelCoordinates = transform2D(
          this.coordinates,
          0,
          this.coordinates.length,
          2,
          transform,
          this.pixelCoordinates_,
        );
        setFromArray(this.renderedTransform_, transform);
      }
      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,
        lineOffsetPx,
        /** @type {boolean} */ isClosedRing,
        /** @type {number} */ dStart,
        /** @type {import('../../style/Style.js').DeclutterMode} */
        declutterMode,
        prevX,
        prevY,
        roundX,
        roundY,
        image,
        text,
        textKey,
        strokeKey,
        fillKey;
      let pendingFill = 0;
      let pendingStroke = 0;
      const coordinateCache = this.coordinateCache_;
      const viewRotation = this.viewRotation_;
      const viewRotationFromTransform =
        Math.round(Math.atan2(-transform[1], transform[0]) * 1e12) / 1e12;

      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, currentGeometry;
      while (i < ii) {
        const instruction = instructions[i];
        const type = /** @type {import("./Instruction.js").default} */ (
          instruction[0]
        );
        switch (type) {
          case Instruction.BEGIN_GEOMETRY:
            feature = /** @type {import("../../Feature.js").FeatureLike} */ (
              instruction[1]
            );
            currentGeometry = instruction[3];
            if (!feature.getGeometry()) {
              i = /** @type {number} */ (instruction[2]);
            } else if (
              hitExtent !== undefined &&
              !intersects$1(hitExtent, currentGeometry.getExtent())
            ) {
              i = /** @type {number} */ (instruction[2]) + 1;
            } else {
              ++i;
            }
            if (zIndexContext) {
              zIndexContext.zIndex = instruction[4];
            }
            break;
          case Instruction.BEGIN_PATH:
            if (pendingFill > batchSize) {
              this.fill_(context);
              pendingFill = 0;
            }
            if (pendingStroke > batchSize) {
              context.stroke();
              pendingStroke = 0;
            }
            if (!pendingFill && !pendingStroke) {
              context.beginPath();
              prevX = NaN;
              prevY = NaN;
            }
            ++i;
            break;
          case Instruction.CIRCLE:
            d = /** @type {number} */ (instruction[1]);
            lineOffsetPx = /** @type {number} */ instruction[2] ?? 0;
            const x1 = pixelCoordinates[d];
            const y1 = pixelCoordinates[d + 1];
            const x2 = pixelCoordinates[d + 2] - lineOffsetPx;
            const y2 = pixelCoordinates[d + 3] - lineOffsetPx;
            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 Instruction.CLOSE_PATH:
            context.closePath();
            ++i;
            break;
          case Instruction.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[5];
            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;
            }
            if (zIndexContext) {
              zIndexContext.zIndex = instruction[6];
            }
            renderer(coords, state);
            ++i;
            break;
          case Instruction.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]);
            let height = /** @type {number} */ (instruction[6]);
            const opacity = /** @type {number} */ (instruction[7]);
            const originX = /** @type {number} */ (instruction[8]);
            const originY = /** @type {number} */ (instruction[9]);
            const rotateWithView = /** @type {boolean} */ (instruction[10]);
            let rotation = /** @type {number} */ (instruction[11]);
            const scale = /** @type {import("../../size.js").Size} */ (
              instruction[12]
            );
            let width = /** @type {number} */ (instruction[13]);
            declutterMode = instruction[14] || 'declutter';
            const declutterImageWithText =
              /** @type {{args: import("../canvas.js").DeclutterImageWithText, declutterMode: import('../../style/Style.js').DeclutterMode}} */ (
                instruction[15]
              );

            if (!image && instruction.length >= 20) {
              // create label images
              text = /** @type {string} */ (instruction[19]);
              textKey = /** @type {string} */ (instruction[20]);
              strokeKey = /** @type {string} */ (instruction[21]);
              fillKey = /** @type {string} */ (instruction[22]);
              const labelWithAnchor = this.drawLabelWithPointPlacement_(
                text,
                textKey,
                strokeKey,
                fillKey,
              );
              image = labelWithAnchor.label;
              instruction[3] = image;
              const textOffsetX = /** @type {number} */ (instruction[23]);
              anchorX = (labelWithAnchor.anchorX - textOffsetX) * this.pixelRatio;
              instruction[4] = anchorX;
              const textOffsetY = /** @type {number} */ (instruction[24]);
              anchorY = (labelWithAnchor.anchorY - textOffsetY) * this.pixelRatio;
              instruction[5] = anchorY;
              height = image.height;
              instruction[6] = height;
              width = image.width;
              instruction[13] = width;
            }

            let geometryWidths;
            if (instruction.length > 25) {
              geometryWidths = /** @type {number} */ (instruction[25]);
            }

            let padding, backgroundFillInstruction, backgroundStrokeInstruction;
            if (instruction.length > 17) {
              padding = /** @type {Array<number>} */ (instruction[16]);
              backgroundFillInstruction = /** @type {Array<*>} */ (
                instruction[17]
              );
              backgroundStrokeInstruction = /** @type {Array<*>} */ (
                instruction[18]
              );
            } else {
              padding = defaultPadding;
              backgroundFillInstruction = null;
              backgroundStrokeInstruction = null;
            }

            if (rotateWithView && viewRotationFromTransform) {
              // Canvas is expected to be rotated to reverse view rotation.
              rotation += viewRotation;
            } else if (!rotateWithView && !viewRotationFromTransform) {
              // Canvas is not rotated, images need to be rotated back to be north-up.
              rotation -= viewRotation;
            }
            let widthIndex = 0;
            for (; d < dd; d += 2) {
              if (
                geometryWidths &&
                geometryWidths[widthIndex++] < width / this.pixelRatio
              ) {
                continue;
              }
              const dimensions = this.calculateImageOrLabelDimensions_(
                image.width,
                image.height,
                pixelCoordinates[d],
                pixelCoordinates[d + 1],
                width,
                height,
                anchorX,
                anchorY,
                originX,
                originY,
                rotation,
                scale,
                snapToPixel,
                padding,
                !!backgroundFillInstruction || !!backgroundStrokeInstruction,
                feature,
              );
              /** @type {ReplayImageOrLabelArgs} */
              const args = [
                context,
                scaledCanvasSize,
                image,
                dimensions,
                opacity,
                backgroundFillInstruction,
                backgroundStrokeInstruction,
              ];
              if (declutterTree) {
                let imageArgs, imageDeclutterMode, imageDeclutterBox;
                if (declutterImageWithText) {
                  const index = dd - d;
                  if (!declutterImageWithText[index]) {
                    // We now have the image for an image+text combination.
                    declutterImageWithText[index] = {args, declutterMode};
                    // Don't render anything for now, wait for the text.
                    continue;
                  }
                  const imageDeclutter = declutterImageWithText[index];
                  imageArgs = imageDeclutter.args;
                  imageDeclutterMode = imageDeclutter.declutterMode;
                  delete declutterImageWithText[index];
                  imageDeclutterBox = getDeclutterBox(imageArgs);
                }
                // We now have image and text for an image+text combination.
                let renderImage, renderText;
                if (
                  imageArgs &&
                  (imageDeclutterMode !== 'declutter' ||
                    !declutterTree.collides(imageDeclutterBox))
                ) {
                  renderImage = true;
                }
                if (
                  declutterMode !== 'declutter' ||
                  !declutterTree.collides(dimensions.declutterBox)
                ) {
                  renderText = true;
                }
                if (
                  imageDeclutterMode === 'declutter' &&
                  declutterMode === 'declutter'
                ) {
                  const render = renderImage && renderText;
                  renderImage = render;
                  renderText = render;
                }
                if (renderImage) {
                  if (imageDeclutterMode !== 'none') {
                    declutterTree.insert(imageDeclutterBox);
                  }
                  this.replayImageOrLabel_.apply(this, imageArgs);
                }
                if (renderText) {
                  if (declutterMode !== 'none') {
                    declutterTree.insert(dimensions.declutterBox);
                  }
                  this.replayImageOrLabel_.apply(this, args);
                }
              } else {
                this.replayImageOrLabel_.apply(this, args);
              }
            }
            ++i;
            break;
          case Instruction.DRAW_CHARS:
            const begin = /** @type {number} */ (instruction[1]);
            const end = /** @type {number} */ (instruction[2]);
            const baseline = /** @type {number} */ (instruction[3]);
            const overflow = /** @type {number} */ (instruction[4]);
            fillKey = /** @type {string} */ (instruction[5]);
            const maxAngle = /** @type {number} */ (instruction[6]);
            const measurePixelRatio = /** @type {number} */ (instruction[7]);
            const offsetY = /** @type {number} */ (instruction[8]);
            strokeKey = /** @type {string} */ (instruction[9]);
            const strokeWidth = /** @type {number} */ (instruction[10]);
            text = /** @type {string|Array<string>} */ (instruction[11]);
            if (Array.isArray(text)) {
              //FIXME Add support for rich text along lines
              text = text.reduce(richTextToPlainText, '');
            }
            textKey = /** @type {string} */ (instruction[12]);
            const pixelRatioScale = [
              /** @type {number} */ (instruction[13]),
              /** @type {number} */ (instruction[13]),
            ];
            declutterMode = instruction[14] || 'declutter';

            const textKeepUpright = /** @type {boolean} */ (instruction[15]);
            const textState = this.textStates[textKey];
            const font = textState.font;
            const textScale = [
              textState.scale[0] * measurePixelRatio,
              textState.scale[1] * measurePixelRatio,
            ];

            let cachedWidths;
            if (font in this.widths_) {
              cachedWidths = this.widths_[font];
            } else {
              cachedWidths = {};
              this.widths_[font] = cachedWidths;
            }

            const pathLength = lineStringLength(pixelCoordinates, begin, end, 2);
            const textLength =
              Math.abs(textScale[0]) *
              measureAndCacheTextWidth(font, text, cachedWidths);
            if (overflow || textLength <= pathLength) {
              const textAlign = this.textStates[textKey].textAlign;
              const startM =
                (pathLength - textLength) * horizontalTextAlign(text, textAlign);
              const parts = drawTextOnPath(
                pixelCoordinates,
                begin,
                end,
                2,
                text,
                startM,
                maxAngle,
                Math.abs(textScale[0]),
                measureAndCacheTextWidth,
                font,
                cachedWidths,
                viewRotationFromTransform ? 0 : this.viewRotation_,
                textKeepUpright,
              );
              drawChars: if (parts) {
                /** @type {Array<ReplayImageOrLabelArgs>} */
                const replayImageOrLabelArgs = [];
                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.createLabel(chars, textKey, '', strokeKey);
                    anchorX =
                      /** @type {number} */ (part[2]) +
                      (textScale[0] < 0 ? -strokeWidth : strokeWidth);
                    anchorY =
                      baseline * label.height +
                      ((0.5 - baseline) * 2 * strokeWidth * textScale[1]) /
                        textScale[0] -
                      offsetY;
                    const dimensions = this.calculateImageOrLabelDimensions_(
                      label.width,
                      label.height,
                      part[0],
                      part[1],
                      label.width,
                      label.height,
                      anchorX,
                      anchorY,
                      0,
                      0,
                      part[3],
                      pixelRatioScale,
                      false,
                      defaultPadding,
                      false,
                      feature,
                    );
                    if (
                      declutterTree &&
                      declutterMode === 'declutter' &&
                      declutterTree.collides(dimensions.declutterBox)
                    ) {
                      break drawChars;
                    }
                    replayImageOrLabelArgs.push([
                      context,
                      scaledCanvasSize,
                      label,
                      dimensions,
                      1,
                      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.createLabel(chars, textKey, fillKey, '');
                    anchorX = /** @type {number} */ (part[2]);
                    anchorY = baseline * label.height - offsetY;
                    const dimensions = this.calculateImageOrLabelDimensions_(
                      label.width,
                      label.height,
                      part[0],
                      part[1],
                      label.width,
                      label.height,
                      anchorX,
                      anchorY,
                      0,
                      0,
                      part[3],
                      pixelRatioScale,
                      false,
                      defaultPadding,
                      false,
                      feature,
                    );
                    if (
                      declutterTree &&
                      declutterMode === 'declutter' &&
                      declutterTree.collides(dimensions.declutterBox)
                    ) {
                      break drawChars;
                    }
                    replayImageOrLabelArgs.push([
                      context,
                      scaledCanvasSize,
                      label,
                      dimensions,
                      1,
                      null,
                      null,
                    ]);
                  }
                }
                if (declutterTree && declutterMode !== 'none') {
                  declutterTree.load(replayImageOrLabelArgs.map(getDeclutterBox));
                }
                for (let i = 0, ii = replayImageOrLabelArgs.length; i < ii; ++i) {
                  this.replayImageOrLabel_.apply(this, replayImageOrLabelArgs[i]);
                }
              }
            }
            ++i;
            break;
          case Instruction.END_GEOMETRY:
            if (featureCallback !== undefined) {
              feature = /** @type {import("../../Feature.js").FeatureLike} */ (
                instruction[1]
              );
              const result = featureCallback(
                feature,
                currentGeometry,
                declutterMode,
              );
              if (result) {
                return result;
              }
            }
            ++i;
            break;
          case Instruction.FILL:
            if (batchSize) {
              pendingFill++;
            } else {
              this.fill_(context);
            }
            ++i;
            break;
          case Instruction.MOVE_TO_LINE_TO:
            d = /** @type {number} */ (instruction[1]);
            dd = /** @type {number} */ (instruction[2]);
            lineOffsetPx = /** @type {number|undefined} */ (instruction[3]);
            isClosedRing =
              /** @type {boolean|undefined} */ (instruction[4]) ?? false;
            x = pixelCoordinates[d];
            y = pixelCoordinates[d + 1];

            if (lineOffsetPx) {
              dStart = d;
              [x, y] = offsetLineVertex(
                x,
                y,
                isClosedRing ? pixelCoordinates[dd - 4] : undefined,
                isClosedRing ? pixelCoordinates[dd - 3] : undefined,
                pixelCoordinates[d + 2],
                pixelCoordinates[d + 3],
                lineOffsetPx,
              );
            }

            context.moveTo(x, y);
            prevX = (x + 0.5) | 0;
            prevY = (y + 0.5) | 0;
            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) {
                if (lineOffsetPx) {
                  if (d == dd - 2) {
                    // last coordinate
                    [x, y] = offsetLineVertex(
                      x,
                      y,
                      pixelCoordinates[d - 2],
                      pixelCoordinates[d - 1],
                      isClosedRing ? pixelCoordinates[dStart + 2] : undefined,
                      isClosedRing ? pixelCoordinates[dStart + 3] : undefined,
                      lineOffsetPx,
                    );
                  } else {
                    [x, y] = offsetLineVertex(
                      x,
                      y,
                      pixelCoordinates[d - 2],
                      pixelCoordinates[d - 1],
                      pixelCoordinates[d + 2],
                      pixelCoordinates[d + 3],
                      lineOffsetPx,
                    );
                  }
                }

                context.lineTo(x, y);
                prevX = roundX;
                prevY = roundY;
              }
            }
            ++i;
            break;
          case Instruction.SET_FILL_STYLE:
            this.alignAndScaleFill_ = instruction[2];

            if (pendingFill) {
              this.fill_(context);
              pendingFill = 0;
              if (pendingStroke) {
                context.stroke();
                pendingStroke = 0;
              }
            }

            /** @type {import("../../colorlike.js").ColorLike} */
            context.fillStyle = instruction[1];
            ++i;
            break;
          case Instruction.SET_STROKE_STYLE:
            if (pendingStroke) {
              context.stroke();
              pendingStroke = 0;
            }
            this.setStrokeStyle_(context, /** @type {Array<*>} */ (instruction));
            ++i;
            break;
          case Instruction.STROKE:
            if (batchSize) {
              pendingStroke++;
            } else {
              context.stroke();
            }
            ++i;
            break;
          default: // consume the instruction anyway, to avoid an infinite loop
            ++i;
            break;
        }
      }
      if (pendingFill) {
        this.fill_(context);
      }
      if (pendingStroke) {
        context.stroke();
      }
      return undefined;
    }

    /**
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
     * @param {import('../../size.js').Size} scaledCanvasSize Scaled canvas size.
     * @param {import("../../transform.js").Transform} transform Transform.
     * @param {number} viewRotation View rotation.
     * @param {boolean} snapToPixel Snap point symbols and text to integer pixels.
     * @param {import("rbush").default<DeclutterEntry>} [declutterTree] Declutter tree.
     */
    execute(
      context,
      scaledCanvasSize,
      transform,
      viewRotation,
      snapToPixel,
      declutterTree,
    ) {
      this.viewRotation_ = viewRotation;
      this.execute_(
        context,
        scaledCanvasSize,
        transform,
        this.instructions,
        snapToPixel,
        undefined,
        undefined,
        declutterTree,
      );
    }

    /**
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
     * @param {import("../../transform.js").Transform} transform Transform.
     * @param {number} viewRotation View rotation.
     * @param {FeatureCallback<T>} [featureCallback] Feature callback.
     * @param {import("../../extent.js").Extent} [hitExtent] Only check
     *     features that intersect this extent.
     * @return {T|undefined} Callback result.
     * @template T
     */
    executeHitDetection(
      context,
      transform,
      viewRotation,
      featureCallback,
      hitExtent,
    ) {
      this.viewRotation_ = viewRotation;
      return this.execute_(
        context,
        [context.canvas.width, context.canvas.height],
        transform,
        this.hitDetectionInstructions,
        true,
        featureCallback,
        hitExtent,
      );
    }
  }

  /**
   * @module ol/render/canvas/ExecutorGroup
   */


  /**
   * @const
   * @type {Array<import("../canvas.js").BuilderType>}
   */
  const ALL = [
    'Polygon',
    'Circle',
    'LineString',
    'Image',
    'Text',
    'Default',
  ];

  /**
   * @const
   * @type {Array<import("../canvas.js").BuilderType>}
   */
  const DECLUTTER = ['Image', 'Text'];

  /**
   * @const
   * @type {Array<import("../canvas.js").BuilderType>}
   */
  const NON_DECLUTTER = ALL.filter(
    (builderType) => !DECLUTTER.includes(builderType),
  );

  /** @type {boolean|undefined} */
  let willReadFrequently = false;

  /** @type {boolean|undefined} */
  let canvasReadsBenchmarked = false;

  /** Determine if canvas read operations are faster with willReadFrequently set to true or false */
  function benchmarkCanvasReads() {
    let bestResult = 0;
    /**
     * @param {boolean} willReadFrequently Will read frequently.
     * @return {number} Operation count.
     */
    const measure = (willReadFrequently) => {
      const context = createCanvasContext2D(1, 1, null, {willReadFrequently});
      let count = 0;
      const start = performance.now();
      for (; performance.now() - start < 50; ++count) {
        context.fillStyle = `rgba(255,0,${count % 256},1)`;
        context.fillRect(0, 0, 1, 1);
        context.getImageData(0, 0, 1, 1);
      }
      bestResult = count > bestResult ? count : bestResult;
      return count;
    };

    const measures = {
      [measure(true)]: true,
      [measure(false)]: false,
      [measure(undefined)]: undefined,
    };
    willReadFrequently = measures[bestResult];
    canvasReadsBenchmarked = true;
  }

  class ExecutorGroup {
    /**
     * @param {import("../../extent.js").Extent} maxExtent Max extent for clipping. When a
     * `maxExtent` was set on the Builder for this executor group, the same `maxExtent`
     * should be set here, unless the target context does not exceed that extent (which
     * can be the case when rendering to tiles).
     * @param {number} resolution Resolution.
     * @param {number} pixelRatio Pixel ratio.
     * @param {boolean} overlaps The executor group can have overlapping geometries.
     * @param {!Object<string, !Object<import("../canvas.js").BuilderType, import("../canvas.js").SerializableInstructions>>} allInstructions
     * The serializable instructions.
     * @param {number} [renderBuffer] Optional rendering buffer.
     * @param {boolean} [deferredRendering] Enable deferred rendering with renderDeferred().
     */
    constructor(
      maxExtent,
      resolution,
      pixelRatio,
      overlaps,
      allInstructions,
      renderBuffer,
      deferredRendering,
    ) {
      /**
       * @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_ = renderBuffer;

      /**
       * @private
       * @type {!Object<string, !Object<string, import("./Executor").default>>}
       */
      this.executorsByZIndex_ = {};

      /**
       * @private
       * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
       */
      this.hitDetectionContext_ = null;

      /**
       * @private
       * @type {import("../../transform.js").Transform}
       */
      this.hitDetectionTransform_ = create();

      /**
       * @private
       * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
       */
      this.renderedContext_ = null;

      /**
       * @private
       * @type {Object<number, Array<import("./ZIndexContext.js").default>>}
       */
      this.deferredZIndexContexts_ = {};

      this.createExecutors_(allInstructions, deferredRendering);
    }

    /**
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} 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();
    }

    /**
     * Create executors and populate them using the provided instructions.
     * @private
     * @param {!Object<string, !Object<string, import("../canvas.js").SerializableInstructions>>} allInstructions The serializable instructions
     * @param {boolean} deferredRendering Enable deferred rendering.
     */
    createExecutors_(allInstructions, deferredRendering) {
      for (const zIndex in allInstructions) {
        let executors = this.executorsByZIndex_[zIndex];
        if (executors === undefined) {
          executors = {};
          this.executorsByZIndex_[zIndex] = executors;
        }
        const instructionByZindex = allInstructions[zIndex];
        for (const builderType in instructionByZindex) {
          const instructions = instructionByZindex[builderType];
          executors[builderType] = new Executor(
            this.resolution_,
            this.pixelRatio_,
            this.overlaps_,
            instructions,
            deferredRendering,
          );
        }
      }
    }

    /**
     * @param {Array<import("../canvas.js").BuilderType>} 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;
          }
        }
      }
      return false;
    }

    /**
     * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
     * @param {number} resolution Resolution.
     * @param {number} rotation Rotation.
     * @param {number} hitTolerance Hit tolerance in pixels.
     * @param {function(import("../../Feature.js").FeatureLike, import("../../geom/SimpleGeometry.js").default, number): T} callback Feature callback.
     * @param {Array<import("../../Feature.js").FeatureLike>} declutteredFeatures Decluttered features.
     * @return {T|undefined} Callback result.
     * @template T
     */
    forEachFeatureAtCoordinate(
      coordinate,
      resolution,
      rotation,
      hitTolerance,
      callback,
      declutteredFeatures,
    ) {
      if (canvasReadsBenchmarked === false) {
        benchmarkCanvasReads();
      }

      hitTolerance = Math.round(hitTolerance);
      const contextSize = hitTolerance * 2 + 1;
      const transform = compose(
        this.hitDetectionTransform_,
        hitTolerance + 0.5,
        hitTolerance + 0.5,
        1 / resolution,
        -1 / resolution,
        -rotation,
        -coordinate[0],
        -coordinate[1],
      );

      const newContext = !this.hitDetectionContext_;
      if (newContext) {
        this.hitDetectionContext_ = createCanvasContext2D(
          contextSize,
          contextSize,
          null,
          {willReadFrequently},
        );
      }
      const context = this.hitDetectionContext_;

      if (
        context.canvas.width !== contextSize ||
        context.canvas.height !== contextSize
      ) {
        context.canvas.width = contextSize;
        context.canvas.height = contextSize;
      } else if (!newContext) {
        context.clearRect(0, 0, contextSize, contextSize);
      }

      /** @type {import("../../extent.js").Extent|undefined} */
      let hitExtent;
      if (this.renderBuffer_ !== undefined) {
        hitExtent = createEmpty();
        extendCoordinate(hitExtent, coordinate);
        buffer(
          hitExtent,
          resolution * (this.renderBuffer_ + hitTolerance),
          hitExtent,
        );
      }

      const indexes = getPixelIndexArray(hitTolerance);

      /** @type {import("../canvas.js").BuilderType} */
      let builderType;

      /**
       * @param {import("../../Feature.js").FeatureLike} feature Feature.
       * @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
       * @param {import('../../style/Style.js').DeclutterMode} declutterMode Declutter mode.
       * @return {T|undefined} Callback result.
       */
      function featureCallback(feature, geometry, declutterMode) {
        const imageData = context.getImageData(
          0,
          0,
          contextSize,
          contextSize,
        ).data;
        for (let i = 0, ii = indexes.length; i < ii; i++) {
          if (imageData[indexes[i]] > 0) {
            if (
              !declutteredFeatures ||
              declutterMode === 'none' ||
              (builderType !== 'Image' && builderType !== 'Text') ||
              declutteredFeatures.includes(feature)
            ) {
              const idx = (indexes[i] - 3) / 4;
              const x = hitTolerance - (idx % contextSize);
              const y = hitTolerance - ((idx / contextSize) | 0);
              const result = callback(feature, geometry, x * x + y * y);
              if (result) {
                return result;
              }
            }
            context.clearRect(0, 0, contextSize, contextSize);
            break;
          }
        }
        return undefined;
      }

      /** @type {Array<number>} */
      const zs = Object.keys(this.executorsByZIndex_).map(Number);
      zs.sort(ascending);

      let i, j, executors, executor, result;
      for (i = zs.length - 1; i >= 0; --i) {
        const zIndexKey = zs[i].toString();
        executors = this.executorsByZIndex_[zIndexKey];
        for (j = ALL.length - 1; j >= 0; --j) {
          builderType = ALL[j];
          executor = executors[builderType];
          if (executor !== undefined) {
            result = executor.executeHitDetection(
              context,
              transform,
              rotation,
              featureCallback,
              hitExtent,
            );
            if (result) {
              return result;
            }
          }
        }
      }
      return undefined;
    }

    /**
     * @param {import("../../transform.js").Transform} transform Transform.
     * @return {Array<number>|null} Clip coordinates.
     */
    getClipCoords(transform) {
      const maxExtent = this.maxExtent_;
      if (!maxExtent) {
        return null;
      }
      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;
    }

    /**
     * @return {boolean} Is empty.
     */
    isEmpty() {
      return isEmpty$1(this.executorsByZIndex_);
    }

    /**
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} targetContext Context.
     * @param {import('../../size.js').Size} scaledCanvasSize Scale of the context.
     * @param {import("../../transform.js").Transform} transform Transform.
     * @param {number} viewRotation View rotation.
     * @param {boolean} snapToPixel Snap point symbols and test to integer pixel.
     * @param {Array<import("../canvas.js").BuilderType>} [builderTypes] Ordered replay types to replay.
     *     Default is {@link module:ol/render/replay~ALL}
     * @param {import("rbush").default<import('./Executor.js').DeclutterEntry>|null} [declutterTree] Declutter tree.
     *     When set to null, no decluttering is done, even when the executor group has a `ZIndexContext`.
     */
    execute(
      targetContext,
      scaledCanvasSize,
      transform,
      viewRotation,
      snapToPixel,
      builderTypes,
      declutterTree,
    ) {
      const zs = Object.keys(this.executorsByZIndex_).map(Number);
      zs.sort(declutterTree ? descending : ascending);

      builderTypes = builderTypes ? builderTypes : ALL;
      const maxBuilderTypes = ALL.length;
      for (let i = 0, ii = zs.length; i < ii; ++i) {
        const zIndexKey = zs[i].toString();
        const replays = this.executorsByZIndex_[zIndexKey];
        for (let j = 0, jj = builderTypes.length; j < jj; ++j) {
          const builderType = builderTypes[j];
          const replay = replays[builderType];
          if (replay !== undefined) {
            const zIndexContext =
              declutterTree === null ? undefined : replay.getZIndexContext();
            const context = zIndexContext
              ? zIndexContext.getContext()
              : targetContext;
            const requireClip =
              this.maxExtent_ &&
              builderType !== 'Image' &&
              builderType !== 'Text';
            if (requireClip) {
              context.save();
              // setup clipping so that the parts of over-simplified geometries are not
              // visible outside the current extent when panning
              this.clip(context, transform);
            }
            if (
              !zIndexContext ||
              builderType === 'Text' ||
              builderType === 'Image'
            ) {
              replay.execute(
                context,
                scaledCanvasSize,
                transform,
                viewRotation,
                snapToPixel,
                declutterTree,
              );
            } else {
              zIndexContext.pushFunction((context) =>
                replay.execute(
                  context,
                  scaledCanvasSize,
                  transform,
                  viewRotation,
                  snapToPixel,
                  declutterTree,
                ),
              );
            }
            if (requireClip) {
              context.restore();
            }
            if (zIndexContext) {
              zIndexContext.offset();
              const index = zs[i] * maxBuilderTypes + ALL.indexOf(builderType);
              if (!this.deferredZIndexContexts_[index]) {
                this.deferredZIndexContexts_[index] = [];
              }
              this.deferredZIndexContexts_[index].push(zIndexContext);
            }
          }
        }
      }

      this.renderedContext_ = targetContext;
    }

    getDeferredZIndexContexts() {
      return this.deferredZIndexContexts_;
    }

    getRenderedContext() {
      return this.renderedContext_;
    }

    renderDeferred() {
      const deferredZIndexContexts = this.deferredZIndexContexts_;
      const zs = Object.keys(deferredZIndexContexts).map(Number).sort(ascending);
      for (let i = 0, ii = zs.length; i < ii; ++i) {
        deferredZIndexContexts[zs[i]].forEach((zIndexContext) => {
          zIndexContext.draw(this.renderedContext_); // FIXME Pass clip to replay for temporarily enabling clip
          zIndexContext.clear();
        });
        deferredZIndexContexts[zs[i]].length = 0;
      }
    }
  }

  /**
   * This cache is used to store arrays of indexes for calculated pixel circles
   * to increase performance.
   * It is a static property to allow each Replaygroup to access it.
   * @type {Object<number, Array<number>>}
   */
  const circlePixelIndexArrayCache = {};

  /**
   * This methods creates an array with indexes of all pixels within a circle,
   * ordered by how close they are to the center.
   * A cache is used to increase performance.
   * @param {number} radius Radius.
   * @return {Array<number>} An array with indexes within a circle.
   */
  function getPixelIndexArray(radius) {
    if (circlePixelIndexArrayCache[radius] !== undefined) {
      return circlePixelIndexArrayCache[radius];
    }

    const size = radius * 2 + 1;
    const maxDistanceSq = radius * radius;
    const distances = new Array(maxDistanceSq + 1);
    for (let i = 0; i <= radius; ++i) {
      for (let j = 0; j <= radius; ++j) {
        const distanceSq = i * i + j * j;
        if (distanceSq > maxDistanceSq) {
          break;
        }
        let distance = distances[distanceSq];
        if (!distance) {
          distance = [];
          distances[distanceSq] = distance;
        }
        distance.push(((radius + i) * size + (radius + j)) * 4 + 3);
        if (i > 0) {
          distance.push(((radius - i) * size + (radius + j)) * 4 + 3);
        }
        if (j > 0) {
          distance.push(((radius + i) * size + (radius - j)) * 4 + 3);
          if (i > 0) {
            distance.push(((radius - i) * size + (radius - j)) * 4 + 3);
          }
        }
      }
    }

    const pixelIndex = [];
    for (let i = 0, ii = distances.length; i < ii; ++i) {
      if (distances[i]) {
        pixelIndex.push(...distances[i]);
      }
    }

    circlePixelIndexArrayCache[radius] = pixelIndex;
    return pixelIndex;
  }

  /**
   * @module ol/style/Icon
   */

  /**
   * @typedef {'fraction' | 'pixels'} IconAnchorUnits
   * Anchor unit can be either a fraction of the icon size or in pixels.
   */

  /**
   * @typedef {'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'} IconOrigin
   * Icon origin. One of 'bottom-left', 'bottom-right', 'top-left', 'top-right'.
   */

  /**
   * @typedef {Object} Options
   * @property {Array<number>} [anchor=[0.5, 0.5]] Anchor. Default value is the icon center.
   * @property {IconOrigin} [anchorOrigin='top-left'] Origin of the anchor: `bottom-left`, `bottom-right`,
   * `top-left` or `top-right`.
   * @property {IconAnchorUnits} [anchorXUnits='fraction'] Units in which the anchor x value is
   * specified. A value of `'fraction'` indicates the x value is a fraction of the icon. A value of `'pixels'` indicates
   * the x value in pixels.
   * @property {IconAnchorUnits} [anchorYUnits='fraction'] Units in which the anchor y value is
   * specified. A value of `'fraction'` indicates the y value is a fraction of the icon. A value of `'pixels'` indicates
   * the y value in pixels.
   * @property {import("../color.js").Color|string} [color] Color to tint the icon. If not specified,
   * the icon will be left as is.
   * @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images. Note that you must provide a
   * `crossOrigin` value if you want to access pixel data with the Canvas renderer.
   * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.
   * @property {ReferrerPolicy} [referrerPolicy] The `referrerPolicy` property for loaded images.
   * @property {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} [img] Image object for the icon.
   * @property {Array<number>} [displacement=[0, 0]] Displacement of the icon in pixels.
   * Positive values will shift the icon right and up.
   * @property {number} [opacity=1] Opacity of the icon.
   * @property {number} [width] The width of the icon in pixels. This can't be used together with `scale`.
   * @property {number} [height] The height of the icon in pixels. This can't be used together with `scale`.
   * @property {number|import("../size.js").Size} [scale=1] Scale.
   * @property {boolean} [rotateWithView=false] Whether to rotate the icon with the view.
   * @property {number} [rotation=0] Rotation in radians (positive rotation clockwise).
   * @property {Array<number>} [offset=[0, 0]] Offset which, together with `size` and `offsetOrigin`, defines the
   * sub-rectangle to use from the original (sprite) image.
   * @property {IconOrigin} [offsetOrigin='top-left'] Origin of the offset: `bottom-left`, `bottom-right`,
   * `top-left` or `top-right`.
   * @property {import("../size.js").Size} [size] Icon size in pixels. Used together with `offset` to define the
   * sub-rectangle to use from the original (sprite) image.
   * @property {string} [src] Image source URI.
   * @property {import("./Style.js").DeclutterMode} [declutterMode] Declutter mode.
   */

  /**
   * @param {number} width The width.
   * @param {number} height The height.
   * @param {number|undefined} wantedWidth The wanted width.
   * @param {number|undefined} wantedHeight The wanted height.
   * @return {number|Array<number>} The scale.
   */
  function calculateScale(width, height, wantedWidth, wantedHeight) {
    if (wantedWidth !== undefined && wantedHeight !== undefined) {
      return [wantedWidth / width, wantedHeight / height];
    }
    if (wantedWidth !== undefined) {
      return wantedWidth / width;
    }
    if (wantedHeight !== undefined) {
      return wantedHeight / height;
    }
    return 1;
  }

  /**
   * @classdesc
   * Set icon style for vector features.
   * @api
   */
  class Icon extends ImageStyle {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      options = options || {};

      /**
       * @type {number}
       */
      const opacity = options.opacity !== undefined ? options.opacity : 1;

      /**
       * @type {number}
       */
      const rotation = options.rotation !== undefined ? options.rotation : 0;

      /**
       * @type {number|import("../size.js").Size}
       */
      const scale = options.scale !== undefined ? options.scale : 1;

      /**
       * @type {boolean}
       */
      const rotateWithView =
        options.rotateWithView !== undefined ? options.rotateWithView : false;

      super({
        opacity: opacity,
        rotation: rotation,
        scale: scale,
        displacement:
          options.displacement !== undefined ? options.displacement : [0, 0],
        rotateWithView: rotateWithView,
        declutterMode: options.declutterMode,
      });

      /**
       * @private
       * @type {Array<number>}
       */
      this.anchor_ = options.anchor !== undefined ? options.anchor : [0.5, 0.5];

      /**
       * @private
       * @type {Array<number>}
       */
      this.normalizedAnchor_ = null;

      /**
       * @private
       * @type {IconOrigin}
       */
      this.anchorOrigin_ =
        options.anchorOrigin !== undefined ? options.anchorOrigin : 'top-left';

      /**
       * @private
       * @type {IconAnchorUnits}
       */
      this.anchorXUnits_ =
        options.anchorXUnits !== undefined ? options.anchorXUnits : 'fraction';

      /**
       * @private
       * @type {IconAnchorUnits}
       */
      this.anchorYUnits_ =
        options.anchorYUnits !== undefined ? options.anchorYUnits : 'fraction';

      /**
       * @private
       * @type {?string}
       */
      this.crossOrigin_ =
        options.crossOrigin !== undefined ? options.crossOrigin : null;

      /**
       * @private
       * @type {ReferrerPolicy}
       */
      this.referrerPolicy_ = options.referrerPolicy;

      const image = options.img !== undefined ? options.img : null;

      let cacheKey = options.src;

      assert(
        !(cacheKey !== undefined && image),
        '`image` and `src` cannot be provided at the same time',
      );

      if ((cacheKey === undefined || cacheKey.length === 0) && image) {
        cacheKey = /** @type {HTMLImageElement} */ (image).src || getUid(image);
      }
      assert(
        cacheKey !== undefined && cacheKey.length > 0,
        'A defined and non-empty `src` or `image` must be provided',
      );

      assert(
        !(
          (options.width !== undefined || options.height !== undefined) &&
          options.scale !== undefined
        ),
        '`width` or `height` cannot be provided together with `scale`',
      );

      let imageState;
      if (options.src !== undefined) {
        imageState = ImageState.IDLE;
      } else if (image !== undefined) {
        if ('complete' in image) {
          if (image.complete) {
            imageState = image.src ? ImageState.LOADED : ImageState.IDLE;
          } else {
            imageState = ImageState.LOADING;
          }
        } else {
          imageState = ImageState.LOADED;
        }
      }

      /**
       * @private
       * @type {import("../color.js").Color}
       */
      this.color_ = options.color !== undefined ? asArray(options.color) : null;

      /**
       * @private
       * @type {import("./IconImage.js").default}
       */
      this.iconImage_ = get(
        image,
        /** @type {string} */ (cacheKey),
        {
          crossOrigin: this.crossOrigin_,
          referrerPolicy: this.referrerPolicy_,
        },
        imageState,
        this.color_,
      );

      /**
       * @private
       * @type {Array<number>}
       */
      this.offset_ = options.offset !== undefined ? options.offset : [0, 0];
      /**
       * @private
       * @type {IconOrigin}
       */
      this.offsetOrigin_ =
        options.offsetOrigin !== undefined ? options.offsetOrigin : 'top-left';

      /**
       * @private
       * @type {Array<number>}
       */
      this.origin_ = null;

      /**
       * @private
       * @type {import("../size.js").Size}
       */
      this.size_ = options.size !== undefined ? options.size : null;

      /**
       * @private
       */
      this.initialOptions_;

      /**
       * Calculate the scale if width or height were given.
       */
      if (options.width !== undefined || options.height !== undefined) {
        let width, height;
        if (options.size) {
          [width, height] = options.size;
        } else {
          const image = this.getImage(1);
          if (image.width && image.height) {
            width = image.width;
            height = image.height;
          } else if (image instanceof HTMLImageElement) {
            this.initialOptions_ = options;
            const onload = () => {
              this.unlistenImageChange(onload);
              if (!this.initialOptions_) {
                return;
              }
              const imageSize = this.iconImage_.getSize();
              this.setScale(
                calculateScale(
                  imageSize[0],
                  imageSize[1],
                  options.width,
                  options.height,
                ),
              );
            };
            this.listenImageChange(onload);
            return;
          }
        }
        if (width !== undefined) {
          this.setScale(
            calculateScale(width, height, options.width, options.height),
          );
        }
      }
    }

    /**
     * Clones the style. The underlying Image/HTMLCanvasElement is not cloned.
     * @return {Icon} The cloned style.
     * @api
     * @override
     */
    clone() {
      let scale, width, height;
      if (this.initialOptions_) {
        width = this.initialOptions_.width;
        height = this.initialOptions_.height;
      } else {
        scale = this.getScale();
        scale = Array.isArray(scale) ? scale.slice() : scale;
      }
      return new Icon({
        anchor: this.anchor_.slice(),
        anchorOrigin: this.anchorOrigin_,
        anchorXUnits: this.anchorXUnits_,
        anchorYUnits: this.anchorYUnits_,
        color:
          this.color_ && this.color_.slice
            ? this.color_.slice()
            : this.color_ || undefined,
        crossOrigin: this.crossOrigin_,
        referrerPolicy: this.referrerPolicy_,
        offset: this.offset_.slice(),
        offsetOrigin: this.offsetOrigin_,
        opacity: this.getOpacity(),
        rotateWithView: this.getRotateWithView(),
        rotation: this.getRotation(),
        scale,
        width,
        height,
        size: this.size_ !== null ? this.size_.slice() : undefined,
        src: this.getSrc(),
        displacement: this.getDisplacement().slice(),
        declutterMode: this.getDeclutterMode(),
      });
    }

    /**
     * Get the anchor point in pixels. The anchor determines the center point for the
     * symbolizer.
     * @return {Array<number>} Anchor.
     * @api
     * @override
     */
    getAnchor() {
      let anchor = this.normalizedAnchor_;
      if (!anchor) {
        anchor = this.anchor_;
        const size = this.getSize();
        if (
          this.anchorXUnits_ == 'fraction' ||
          this.anchorYUnits_ == 'fraction'
        ) {
          if (!size) {
            return null;
          }
          anchor = this.anchor_.slice();
          if (this.anchorXUnits_ == 'fraction') {
            anchor[0] *= size[0];
          }
          if (this.anchorYUnits_ == 'fraction') {
            anchor[1] *= size[1];
          }
        }

        if (this.anchorOrigin_ != 'top-left') {
          if (!size) {
            return null;
          }
          if (anchor === this.anchor_) {
            anchor = this.anchor_.slice();
          }
          if (
            this.anchorOrigin_ == 'top-right' ||
            this.anchorOrigin_ == 'bottom-right'
          ) {
            anchor[0] = -anchor[0] + size[0];
          }
          if (
            this.anchorOrigin_ == 'bottom-left' ||
            this.anchorOrigin_ == 'bottom-right'
          ) {
            anchor[1] = -anchor[1] + size[1];
          }
        }
        this.normalizedAnchor_ = anchor;
      }
      const displacement = this.getDisplacement();
      const scale = this.getScaleArray();
      // anchor is scaled by renderer but displacement should not be scaled
      // so divide by scale here
      return [
        anchor[0] - displacement[0] / scale[0],
        anchor[1] + displacement[1] / scale[1],
      ];
    }

    /**
     * Set the anchor point. The anchor determines the center point for the
     * symbolizer.
     *
     * @param {Array<number>} anchor Anchor.
     * @api
     */
    setAnchor(anchor) {
      this.anchor_ = anchor;
      this.normalizedAnchor_ = null;
    }

    /**
     * Get the icon color.
     * @return {import("../color.js").Color} Color.
     * @api
     */
    getColor() {
      return this.color_;
    }

    /**
     * Set the icon color.
     *
     * Warning: Repeatedly setting the color on an icon style
     * causes the icon image to be re-created each time. This can have a
     * severe performance impact.
     *
     * @param {import("../color.js").Color|string|null|undefined} color Color.
     */
    setColor(color) {
      const nextColor = color ? asArray(color) : null;
      if (
        this.color_ === nextColor ||
        (this.color_ &&
          nextColor &&
          this.color_.length === nextColor.length &&
          this.color_.every((value, index) => value === nextColor[index]))
      ) {
        // Discard if the color hasn't changed.
        return;
      }

      this.color_ = nextColor;
      const src = this.getSrc();
      const image = src !== undefined ? null : this.getHitDetectionImage();
      const imageState =
        src !== undefined ? ImageState.IDLE : this.iconImage_.getImageState();
      this.iconImage_ = get(
        image,
        src,
        {
          crossOrigin: this.crossOrigin_,
          referrerPolicy: this.referrerPolicy_,
        },
        imageState,
        this.color_,
      );
    }

    /**
     * Get the image icon.
     * @param {number} pixelRatio Pixel ratio.
     * @return {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} Image or Canvas element. If the Icon
     * style was configured with `src` or with a not let loaded `img`, an `ImageBitmap` will be returned.
     * @api
     * @override
     */
    getImage(pixelRatio) {
      return this.iconImage_.getImage(pixelRatio);
    }

    /**
     * Get the pixel ratio.
     * @param {number} pixelRatio Pixel ratio.
     * @return {number} The pixel ratio of the image.
     * @api
     * @override
     */
    getPixelRatio(pixelRatio) {
      return this.iconImage_.getPixelRatio(pixelRatio);
    }

    /**
     * @return {import("../size.js").Size} Image size.
     * @override
     */
    getImageSize() {
      return this.iconImage_.getSize();
    }

    /**
     * @return {import("../ImageState.js").default} Image state.
     * @override
     */
    getImageState() {
      return this.iconImage_.getImageState();
    }

    /**
     * @return {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} Image element.
     * @override
     */
    getHitDetectionImage() {
      return this.iconImage_.getHitDetectionImage();
    }

    /**
     * Get the origin of the symbolizer.
     * @return {Array<number>} Origin.
     * @api
     * @override
     */
    getOrigin() {
      if (this.origin_) {
        return this.origin_;
      }
      let offset = this.offset_;

      if (this.offsetOrigin_ != 'top-left') {
        const size = this.getSize();
        const iconImageSize = this.iconImage_.getSize();
        if (!size || !iconImageSize) {
          return null;
        }
        offset = offset.slice();
        if (
          this.offsetOrigin_ == 'top-right' ||
          this.offsetOrigin_ == 'bottom-right'
        ) {
          offset[0] = iconImageSize[0] - size[0] - offset[0];
        }
        if (
          this.offsetOrigin_ == 'bottom-left' ||
          this.offsetOrigin_ == 'bottom-right'
        ) {
          offset[1] = iconImageSize[1] - size[1] - offset[1];
        }
      }
      this.origin_ = offset;
      return this.origin_;
    }

    /**
     * Get the image URL.
     * @return {string|undefined} Image src.
     * @api
     */
    getSrc() {
      return this.iconImage_.getSrc();
    }

    /**
     * Set the image URI
     * @param {string} src Image source URI
     * @api
     */
    setSrc(src) {
      this.iconImage_ = get(
        null,
        src,
        {
          crossOrigin: this.crossOrigin_,
          referrerPolicy: this.referrerPolicy_,
        },
        ImageState.IDLE,
        this.color_,
      );
    }

    /**
     * Get the size of the icon (in pixels).
     * @return {import("../size.js").Size} Image size.
     * @api
     * @override
     */
    getSize() {
      return !this.size_ ? this.iconImage_.getSize() : this.size_;
    }

    /**
     * Get the width of the icon (in pixels). Will return undefined when the icon image is not yet loaded.
     * @return {number} Icon width (in pixels).
     * @api
     */
    getWidth() {
      const scale = this.getScaleArray();
      if (this.size_) {
        return this.size_[0] * scale[0];
      }
      if (this.iconImage_.getImageState() == ImageState.LOADED) {
        return this.iconImage_.getSize()[0] * scale[0];
      }
      return undefined;
    }

    /**
     * Get the height of the icon (in pixels). Will return undefined when the icon image is not yet loaded.
     * @return {number} Icon height (in pixels).
     * @api
     */
    getHeight() {
      const scale = this.getScaleArray();
      if (this.size_) {
        return this.size_[1] * scale[1];
      }
      if (this.iconImage_.getImageState() == ImageState.LOADED) {
        return this.iconImage_.getSize()[1] * scale[1];
      }
      return undefined;
    }

    /**
     * Set the scale.
     *
     * @param {number|import("../size.js").Size} scale Scale.
     * @api
     * @override
     */
    setScale(scale) {
      delete this.initialOptions_;
      super.setScale(scale);
    }

    /**
     * @param {function(import("../events/Event.js").default): void} listener Listener function.
     * @override
     */
    listenImageChange(listener) {
      this.iconImage_.addEventListener(EventType.CHANGE, listener);
    }

    /**
     * Load not yet loaded URI.
     * When rendering a feature with an icon style, the vector renderer will
     * automatically call this method. However, you might want to call this
     * method yourself for preloading or other purposes.
     * @api
     * @override
     */
    load() {
      this.iconImage_.load();
    }

    /**
     * @param {function(import("../events/Event.js").default): void} listener Listener function.
     * @override
     */
    unlistenImageChange(listener) {
      this.iconImage_.removeEventListener(EventType.CHANGE, listener);
    }

    /**
     * @override
     */
    ready() {
      return this.iconImage_.ready();
    }
  }

  /**
   * @module ol/render/canvas/hitdetect
   */


  const HIT_DETECT_RESOLUTION = 0.5;

  /**
   * @param {import("../../size.js").Size} size Canvas size in css pixels.
   * @param {Array<import("../../transform.js").Transform>} transforms Transforms
   * for rendering features to all worlds of the viewport, from coordinates to css
   * pixels.
   * @param {Array<import("../../Feature.js").FeatureLike>} features
   * Features to consider for hit detection.
   * @param {import("../../style/Style.js").StyleFunction|undefined} styleFunction
   * Layer style function.
   * @param {import("../../extent.js").Extent} extent Extent in render projection.
   * @param {number} resolution Resolution.
   * @param {number} rotation Rotation.
   * @param {number} [squaredTolerance] Squared tolerance.
   * @param {import("../../proj/Projection.js").default} [projection] Render projection.
   * @return {ImageData} Hit detection image data.
   */
  function createHitDetectionImageData(
    size,
    transforms,
    features,
    styleFunction,
    extent,
    resolution,
    rotation,
    squaredTolerance,
    projection,
  ) {
    const userExtent = extent;
    const width = size[0] * HIT_DETECT_RESOLUTION;
    const height = size[1] * HIT_DETECT_RESOLUTION;
    const context = createCanvasContext2D(width, height);
    context.imageSmoothingEnabled = false;
    const canvas = context.canvas;
    const renderer = new CanvasImmediateRenderer(
      context,
      HIT_DETECT_RESOLUTION,
      extent,
      null,
      rotation,
      squaredTolerance,
      null,
    );
    const featureCount = features.length;
    // Stretch hit detection index to use the whole available color range
    const indexFactor = Math.floor((256 * 256 * 256 - 1) / featureCount);
    const featuresByZIndex = {};
    for (let i = 1; i <= featureCount; ++i) {
      const feature = features[i - 1];
      const featureStyleFunction = feature.getStyleFunction() || styleFunction;
      if (!featureStyleFunction) {
        continue;
      }
      let styles = featureStyleFunction(feature, resolution);
      if (!styles) {
        continue;
      }
      if (!Array.isArray(styles)) {
        styles = [styles];
      }
      const index = i * indexFactor;
      const color = index.toString(16).padStart(7, '#00000');
      for (let j = 0, jj = styles.length; j < jj; ++j) {
        const originalStyle = styles[j];
        const geometry = originalStyle.getGeometryFunction()(feature);
        if (!geometry || !intersects$1(userExtent, geometry.getExtent())) {
          continue;
        }
        const style = originalStyle.clone();
        const fill = style.getFill();
        if (fill) {
          fill.setColor(color);
        }
        const stroke = style.getStroke();
        if (stroke) {
          stroke.setColor(color);
          stroke.setLineDash(null);
        }
        style.setText(undefined);
        const image = originalStyle.getImage();
        if (image) {
          const imgSize = image.getImageSize();
          if (!imgSize) {
            continue;
          }

          const imgContext = createCanvasContext2D(
            imgSize[0],
            imgSize[1],
            undefined,
            {alpha: false},
          );
          const img = imgContext.canvas;
          imgContext.fillStyle = color;
          imgContext.fillRect(0, 0, img.width, img.height);
          style.setImage(
            new Icon({
              img: img,
              anchor: image.getAnchor(),
              anchorXUnits: 'pixels',
              anchorYUnits: 'pixels',
              offset: image.getOrigin(),
              opacity: 1,
              size: image.getSize(),
              scale: image.getScale(),
              rotation: image.getRotation(),
              rotateWithView: image.getRotateWithView(),
            }),
          );
        }
        const zIndex = style.getZIndex() || 0;
        let byGeometryType = featuresByZIndex[zIndex];
        if (!byGeometryType) {
          byGeometryType = {};
          featuresByZIndex[zIndex] = byGeometryType;
          byGeometryType['Polygon'] = [];
          byGeometryType['Circle'] = [];
          byGeometryType['LineString'] = [];
          byGeometryType['Point'] = [];
        }
        const type = geometry.getType();
        if (type === 'GeometryCollection') {
          const geometries =
            /** @type {import("../../geom/GeometryCollection.js").default} */ (
              geometry
            ).getGeometriesArrayRecursive();
          for (let i = 0, ii = geometries.length; i < ii; ++i) {
            const geometry = geometries[i];
            byGeometryType[geometry.getType().replace('Multi', '')].push(
              geometry,
              style,
            );
          }
        } else {
          byGeometryType[type.replace('Multi', '')].push(geometry, style);
        }
      }
    }

    const zIndexKeys = Object.keys(featuresByZIndex).map(Number).sort(ascending);
    for (let i = 0, ii = zIndexKeys.length; i < ii; ++i) {
      const byGeometryType = featuresByZIndex[zIndexKeys[i]];
      for (const type in byGeometryType) {
        const geomAndStyle = byGeometryType[type];
        for (let j = 0, jj = geomAndStyle.length; j < jj; j += 2) {
          renderer.setStyle(geomAndStyle[j + 1]);
          for (let k = 0, kk = transforms.length; k < kk; ++k) {
            renderer.setTransform(transforms[k]);
            renderer.drawGeometry(geomAndStyle[j]);
          }
        }
      }
    }
    return context.getImageData(0, 0, canvas.width, canvas.height);
  }

  /**
   * @param {import("../../pixel").Pixel} pixel Pixel coordinate on the hit
   * detection canvas in css pixels.
   * @param {Array<F>} features Features. Has to
   * match the `features` array that was passed to `createHitDetectionImageData()`.
   * @param {ImageData} imageData Hit detection image data generated by
   * `createHitDetectionImageData()`.
   * @return {Array<F>} Features.
   * @template {import("../../Feature.js").FeatureLike} F
   */
  function hitDetect(pixel, features, imageData) {
    /** @type {Array<F>} */
    const resultFeatures = [];
    if (imageData) {
      const x = Math.floor(Math.round(pixel[0]) * HIT_DETECT_RESOLUTION);
      const y = Math.floor(Math.round(pixel[1]) * HIT_DETECT_RESOLUTION);
      // The pixel coordinate is clamped down to the hit-detect canvas' size to account
      // for browsers returning coordinates slightly larger than the actual canvas size
      // due to a non-integer pixel ratio.
      const index =
        (clamp(x, 0, imageData.width - 1) +
          clamp(y, 0, imageData.height - 1) * imageData.width) *
        4;
      const r = imageData.data[index];
      const g = imageData.data[index + 1];
      const b = imageData.data[index + 2];
      const i = b + 256 * (g + 256 * r);
      const indexFactor = Math.floor((256 * 256 * 256 - 1) / features.length);
      if (i && i % indexFactor === 0) {
        resultFeatures.push(features[i / indexFactor - 1]);
      }
    }
    return resultFeatures;
  }

  /**
   * @module ol/render/Event
   */


  class RenderEvent extends BaseEvent {
    /**
     * @param {import("./EventType.js").default} type Type.
     * @param {import("../transform.js").Transform} [inversePixelTransform] Transform for
     *     CSS pixels to rendered pixels.
     * @param {import("../Map.js").FrameState} [frameState] Frame state.
     * @param {?(CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D|WebGLRenderingContext)} [context] Context.
     */
    constructor(type, inversePixelTransform, frameState, context) {
      super(type);

      /**
       * Transform from CSS pixels (relative to the top-left corner of the map viewport)
       * to rendered pixels on this event's `context`. Only available when a Canvas renderer is used, null otherwise.
       * @type {import("../transform.js").Transform|undefined}
       * @api
       */
      this.inversePixelTransform = inversePixelTransform;

      /**
       * An object representing the current render frame state.
       * @type {import("../Map.js").FrameState|undefined}
       * @api
       */
      this.frameState = frameState;

      /**
       * Canvas context. Not available when the event is dispatched by the map. For Canvas 2D layers,
       * the context will be the 2D rendering context.  For WebGL layers, the context will be the WebGL
       * context.
       * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D|WebGLRenderingContext|undefined}
       * @api
       */
      this.context = context;
    }
  }

  /**
   * @module ol/renderer/Layer
   */

  const maxStaleKeys = 5;

  /**
   * @template {import("../layer/Layer.js").default} LayerType
   */
  class LayerRenderer extends Observable {
    /**
     * @param {LayerType} layer Layer.
     */
    constructor(layer) {
      super();

      /**
       * The renderer is initialized and ready to render.
       * @type {boolean}
       */
      this.ready = true;

      /** @private */
      this.boundHandleImageChange_ = this.handleImageChange_.bind(this);

      /**
       * @private
       * @type {LayerType}
       */
      this.layer_ = layer;

      /**
       * @type {Array<string>}
       * @private
       */
      this.staleKeys_ = new Array();

      /**
       * @type {number}
       * @protected
       */
      this.maxStaleKeys = maxStaleKeys;
    }

    /**
     * @return {Array<string>} Get the list of stale keys.
     */
    getStaleKeys() {
      return this.staleKeys_;
    }

    /**
     * @param {string} key The new stale key.
     */
    prependStaleKey(key) {
      this.staleKeys_.unshift(key);
      if (this.staleKeys_.length > this.maxStaleKeys) {
        this.staleKeys_.length = this.maxStaleKeys;
      }
    }

    /**
     * Asynchronous layer level hit detection.
     * @param {import("../pixel.js").Pixel} pixel Pixel.
     * @return {Promise<Array<import("../Feature").FeatureLike>>} Promise that resolves with
     * an array of features.
     */
    getFeatures(pixel) {
      return abstract();
    }

    /**
     * @param {import("../pixel.js").Pixel} pixel Pixel.
     * @return {Uint8ClampedArray|Uint8Array|Float32Array|DataView|null} Pixel data.
     */
    getData(pixel) {
      return null;
    }

    /**
     * Determine whether render should be called.
     * @abstract
     * @param {import("../Map.js").FrameState} frameState Frame state.
     * @return {boolean} Layer is ready to be rendered.
     */
    prepareFrame(frameState) {
      return abstract();
    }

    /**
     * Render the layer.
     * @abstract
     * @param {import("../Map.js").FrameState} frameState Frame state.
     * @param {HTMLElement|null} target Target that may be used to render content to.
     * @return {HTMLElement} The rendered element.
     */
    renderFrame(frameState, target) {
      return abstract();
    }

    /**
     * @abstract
     * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
     * @param {import("../Map.js").FrameState} frameState Frame state.
     * @param {number} hitTolerance Hit tolerance in pixels.
     * @param {import("./vector.js").FeatureCallback<T>} callback Feature callback.
     * @param {Array<import("./Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
     * @return {T|undefined} Callback result.
     * @template T
     */
    forEachFeatureAtCoordinate(
      coordinate,
      frameState,
      hitTolerance,
      callback,
      matches,
    ) {
      return undefined;
    }

    /**
     * @return {LayerType} Layer.
     */
    getLayer() {
      return this.layer_;
    }

    /**
     * Perform action necessary to get the layer rendered after new fonts have loaded
     * @abstract
     */
    handleFontsChanged() {}

    /**
     * Handle changes in image state.
     * @param {import("../events/Event.js").default} event Image change event.
     * @private
     */
    handleImageChange_(event) {
      const image = /** @type {import("../Image.js").default} */ (event.target);
      if (
        image.getState() === ImageState.LOADED ||
        image.getState() === ImageState.ERROR
      ) {
        this.renderIfReadyAndVisible();
      }
    }

    /**
     * Load the image if not already loaded, and register the image change
     * listener if needed.
     * @param {import("../Image.js").default} image Image.
     * @return {boolean} `true` if the image is already loaded, `false` otherwise.
     * @protected
     */
    loadImage(image) {
      let imageState = image.getState();
      if (imageState != ImageState.LOADED && imageState != ImageState.ERROR) {
        image.addEventListener(EventType.CHANGE, this.boundHandleImageChange_);
      }
      if (imageState == ImageState.IDLE) {
        image.load();
        imageState = image.getState();
      }
      return imageState == ImageState.LOADED;
    }

    /**
     * @protected
     */
    renderIfReadyAndVisible() {
      const layer = this.getLayer();
      if (layer && layer.getVisible() && layer.getSourceState() === 'ready') {
        layer.changed();
      }
    }

    /**
     * @param {import("../Map.js").FrameState} frameState Frame state.
     */
    renderDeferred(frameState) {}

    /**
     * Clean up.
     * @override
     */
    disposeInternal() {
      delete this.layer_;
      super.disposeInternal();
    }
  }

  /**
   * @module ol/renderer/canvas/Layer
   */

  /**
   * @type {Array<HTMLCanvasElement|OffscreenCanvas>}
   */
  const canvasPool$1 = [];

  /**
   * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
   */
  let pixelContext = null;

  function createPixelContext() {
    pixelContext = createCanvasContext2D(1, 1, undefined, {
      willReadFrequently: true,
    });
  }

  /**
   * @abstract
   * @template {import("../../layer/Layer.js").default} LayerType
   * @extends {LayerRenderer<LayerType>}
   */
  class CanvasLayerRenderer extends LayerRenderer {
    /**
     * @param {LayerType} layer Layer.
     */
    constructor(layer) {
      super(layer);

      /**
       * HTMLElement container for the layer to be rendered in.
       * @protected
       * @type {HTMLElement}
       */
      this.container = null;

      /**
       * @protected
       * @type {number}
       */
      this.renderedResolution;

      /**
       * A temporary transform.  The values in this transform should only be used in a
       * function that sets the values.
       * @protected
       * @type {import("../../transform.js").Transform}
       */
      this.tempTransform = create();

      /**
       * The transform for rendered pixels to viewport CSS pixels.  This transform must
       * be set when rendering a frame and may be used by other functions after rendering.
       * @protected
       * @type {import("../../transform.js").Transform}
       */
      this.pixelTransform = create();

      /**
       * The transform for viewport CSS pixels to rendered pixels.  This transform must
       * be set when rendering a frame and may be used by other functions after rendering.
       * @protected
       * @type {import("../../transform.js").Transform}
       */
      this.inversePixelTransform = create();

      /**
       * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
       */
      this.context = null;

      /**
       * @private
       * @type {ZIndexContext}
       */
      this.deferredContext_ = null;

      /**
       * true if the container has been reused from the previous renderer
       * @type {boolean}
       */
      this.containerReused = false;

      /**
       * @protected
       * @type {import("../../Map.js").FrameState|null}
       */
      this.frameState = null;
    }

    /**
     * @param {import('../../DataTile.js').ImageLike} image Image.
     * @param {number} col The column index.
     * @param {number} row The row index.
     * @return {Uint8ClampedArray|null} The image data.
     */
    getImageData(image, col, row) {
      if (!pixelContext) {
        createPixelContext();
      }
      pixelContext.clearRect(0, 0, 1, 1);

      let data;
      try {
        pixelContext.drawImage(image, col, row, 1, 1, 0, 0, 1, 1);
        data = pixelContext.getImageData(0, 0, 1, 1).data;
      } catch {
        pixelContext = null;
        return null;
      }
      return data;
    }

    /**
     * @param {import('../../Map.js').FrameState} frameState Frame state.
     * @return {string} Background color.
     */
    getBackground(frameState) {
      const layer = this.getLayer();
      let background = layer.getBackground();
      if (typeof background === 'function') {
        background = background(frameState.viewState.resolution);
      }
      return background || undefined;
    }

    /**
     * Get a rendering container from an existing target, if compatible.
     * @param {HTMLElement} target Potential render target.
     * @param {string} transform CSS transform matrix.
     * @param {string} [backgroundColor] Background color.
     */
    useContainer(target, transform, backgroundColor) {
      // renderer canvas to target canvas
      const layerClassName = this.getLayer().getClassName();
      let container, context;
      if (
        target &&
        target.className === layerClassName &&
        (!backgroundColor ||
          (target &&
            target.style.backgroundColor &&
            equals$2(
              asArray(target.style.backgroundColor),
              asArray(backgroundColor),
            )))
      ) {
        const canvas = target.firstElementChild;
        if (isCanvas(canvas)) {
          context = canvas.getContext('2d');
        }
      }
      if (context && equivalent(context.canvas.style.transform, transform)) {
        // Container of the previous layer renderer can be used.
        this.container = target;
        this.context = context;
        this.containerReused = true;
      } else if (this.containerReused) {
        // Previously reused container cannot be used any more.
        this.container = null;
        this.context = null;
        this.containerReused = false;
      } else if (this.container) {
        this.container.style.backgroundColor = null;
      }
      if (!this.container) {
        container = WORKER_OFFSCREEN_CANVAS
          ? createMockDiv()
          : document.createElement('div');
        container.className = layerClassName;
        let style = container.style;
        style.position = 'absolute';
        style.width = '100%';
        style.height = '100%';
        context = createCanvasContext2D();
        const canvas = /** @type {HTMLCanvasElement} */ (context.canvas);
        container.appendChild(canvas);
        style = canvas.style;
        style.position = 'absolute';
        style.left = '0';
        style.transformOrigin = 'top left';
        this.container = container;
        this.context = context;
      }
      if (
        !this.containerReused &&
        backgroundColor &&
        !this.container.style.backgroundColor
      ) {
        this.container.style.backgroundColor = backgroundColor;
      }
    }

    /**
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @param {import("../../extent.js").Extent} extent Clip extent.
     * @protected
     */
    clipUnrotated(context, frameState, extent) {
      const topLeft = getTopLeft(extent);
      const topRight = getTopRight(extent);
      const bottomRight = getBottomRight(extent);
      const bottomLeft = getBottomLeft(extent);

      apply(frameState.coordinateToPixelTransform, topLeft);
      apply(frameState.coordinateToPixelTransform, topRight);
      apply(frameState.coordinateToPixelTransform, bottomRight);
      apply(frameState.coordinateToPixelTransform, bottomLeft);

      const inverted = this.inversePixelTransform;
      apply(inverted, topLeft);
      apply(inverted, topRight);
      apply(inverted, bottomRight);
      apply(inverted, bottomLeft);

      context.save();
      context.beginPath();
      context.moveTo(Math.round(topLeft[0]), Math.round(topLeft[1]));
      context.lineTo(Math.round(topRight[0]), Math.round(topRight[1]));
      context.lineTo(Math.round(bottomRight[0]), Math.round(bottomRight[1]));
      context.lineTo(Math.round(bottomLeft[0]), Math.round(bottomLeft[1]));
      context.clip();
    }

    /**
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @param {HTMLElement} target Target that may be used to render content to.
     * @protected
     */
    prepareContainer(frameState, target) {
      const extent = frameState.extent;
      const resolution = frameState.viewState.resolution;
      const rotation = frameState.viewState.rotation;
      const pixelRatio = frameState.pixelRatio;
      const width = Math.round((getWidth(extent) / resolution) * pixelRatio);
      const height = Math.round((getHeight(extent) / resolution) * pixelRatio);
      // set forward and inverse pixel transforms
      compose(
        this.pixelTransform,
        frameState.size[0] / 2,
        frameState.size[1] / 2,
        1 / pixelRatio,
        1 / pixelRatio,
        rotation,
        -width / 2,
        -height / 2,
      );
      makeInverse(this.inversePixelTransform, this.pixelTransform);

      const canvasTransform = toString$1(this.pixelTransform);
      this.useContainer(target, canvasTransform, this.getBackground(frameState));
      if (!this.containerReused) {
        const canvas = this.context.canvas;
        if (canvas.width != width || canvas.height != height) {
          canvas.width = width;
          canvas.height = height;
        } else {
          this.context.clearRect(0, 0, width, height);
        }
        if (
          canvasTransform !==
          /** @type {HTMLCanvasElement} */ (canvas).style.transform
        ) {
          /** @type {HTMLCanvasElement} */ (canvas).style.transform =
            canvasTransform;
        }
      }
    }

    /**
     * @param {import("../../render/EventType.js").default} type Event type.
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @private
     */
    dispatchRenderEvent_(type, context, frameState) {
      const layer = this.getLayer();
      if (layer.hasListener(type)) {
        const event = new RenderEvent(
          type,
          this.inversePixelTransform,
          frameState,
          context,
        );
        layer.dispatchEvent(event);
      }
    }

    /**
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @protected
     */
    preRender(context, frameState) {
      this.frameState = frameState;
      if (frameState.declutter) {
        return;
      }
      this.dispatchRenderEvent_(RenderEventType.PRERENDER, context, frameState);
    }

    /**
     * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context Context.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @protected
     */
    postRender(context, frameState) {
      if (frameState.declutter) {
        return;
      }
      this.dispatchRenderEvent_(RenderEventType.POSTRENDER, context, frameState);
    }

    /**
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     */
    renderDeferredInternal(frameState) {}

    /**
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @return {import('../../render/canvas/ZIndexContext.js').ZIndexContextProxy} Context.
     */
    getRenderContext(frameState) {
      if (frameState.declutter && !this.deferredContext_) {
        this.deferredContext_ = new ZIndexContext();
      }
      return frameState.declutter
        ? this.deferredContext_.getContext()
        : this.context;
    }

    /**
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @override
     */
    renderDeferred(frameState) {
      if (!frameState.declutter) {
        return;
      }
      this.dispatchRenderEvent_(
        RenderEventType.PRERENDER,
        this.context,
        frameState,
      );
      if (frameState.declutter && this.deferredContext_) {
        this.deferredContext_.draw(this.context);
        this.deferredContext_.clear();
      }
      this.renderDeferredInternal(frameState);
      this.dispatchRenderEvent_(
        RenderEventType.POSTRENDER,
        this.context,
        frameState,
      );
    }

    /**
     * Creates a transform for rendering to an element that will be rotated after rendering.
     * @param {import("../../coordinate.js").Coordinate} center Center.
     * @param {number} resolution Resolution.
     * @param {number} rotation Rotation.
     * @param {number} pixelRatio Pixel ratio.
     * @param {number} width Width of the rendered element (in pixels).
     * @param {number} height Height of the rendered element (in pixels).
     * @param {number} offsetX Offset on the x-axis in view coordinates.
     * @protected
     * @return {!import("../../transform.js").Transform} Transform.
     */
    getRenderTransform(
      center,
      resolution,
      rotation,
      pixelRatio,
      width,
      height,
      offsetX,
    ) {
      const dx1 = width / 2;
      const dy1 = height / 2;
      const sx = pixelRatio / resolution;
      const sy = -sx;
      const dx2 = -center[0] + offsetX;
      const dy2 = -center[1];
      return compose(
        this.tempTransform,
        dx1,
        dy1,
        sx,
        sy,
        -rotation,
        dx2,
        dy2,
      );
    }

    /**
     * Clean up.
     * @override
     */
    disposeInternal() {
      delete this.frameState;
      super.disposeInternal();
    }
  }

  /**
   * @module ol/renderer/canvas/VectorLayer
   */

  /**
   * @classdesc
   * Canvas renderer for vector layers.
   * @api
   */
  class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
    /**
     * @param {import("../../layer/BaseVector.js").default} vectorLayer Vector layer.
     */
    constructor(vectorLayer) {
      super(vectorLayer);

      /** @private */
      this.boundHandleStyleImageChange_ = this.handleStyleImageChange_.bind(this);

      /**
       * @private
       * @type {boolean}
       */
      this.animatingOrInteracting_;

      /**
       * @private
       * @type {ImageData|null}
       */
      this.hitDetectionImageData_ = null;

      /**
       * @private
       * @type {import("../../extent.js").Extent}
       */
      this.clipExtent_ = null;

      /**
       * Do we need to extend the rendered area on the x-axis to handle
       * features that cross the antimeridian?
       * @private
       * @type {boolean}
       */
      this.extendX_ = false;

      /**
       * @private
       * @type {Array<import("../../Feature.js").default>}
       */
      this.renderedFeatures_ = null;

      /**
       * @private
       * @type {number}
       */
      this.renderedRevision_ = -1;

      /**
       * @private
       * @type {number}
       */
      this.renderedResolution_ = NaN;

      /**
       * @private
       * @type {import("../../extent.js").Extent}
       */
      this.renderedExtent_ = createEmpty();

      /**
       * @private
       * @type {import("../../extent.js").Extent}
       */
      this.wrappedRenderedExtent_ = createEmpty();

      /**
       * @private
       * @type {number}
       */
      this.renderedRotation_;

      /**
       * @private
       * @type {import("../../coordinate").Coordinate}
       */
      this.renderedCenter_ = null;

      /**
       * @private
       * @type {import("../../proj/Projection").default}
       */
      this.renderedProjection_ = null;

      /**
       * @private
       * @type {number}
       */
      this.renderedPixelRatio_ = 1;

      /**
       * @private
       * @type {import("../../render.js").OrderFunction|null}
       */
      this.renderedRenderOrder_ = null;

      /**
       * @private
       * @type {boolean}
       */
      this.renderedFrameDeclutter_;

      /**
       * @private
       * @type {import("../../render/canvas/ExecutorGroup").default}
       */
      this.replayGroup_ = null;

      /**
       * A new replay group had to be created by `prepareFrame()`
       * @type {boolean}
       */
      this.replayGroupChanged = true;

      /**
       * Clipping to be performed by `renderFrame()`
       * @type {boolean}
       */
      this.clipping = true;

      /**
       * @private
       * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
       */
      this.targetContext_ = null;

      /**
       * @private
       * @type {number}
       */
      this.opacity_ = 1;
    }

    /**
     * @param {ExecutorGroup} executorGroup Executor group.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @param {boolean} [declutterable] `true` to only render declutterable items,
     *     `false` to only render non-declutterable items, `undefined` to render all.
     */
    renderWorlds(executorGroup, frameState, declutterable) {
      const extent = frameState.extent;
      const viewState = frameState.viewState;
      const center = viewState.center;
      const resolution = viewState.resolution;
      const projection = viewState.projection;
      const rotation = viewState.rotation;
      const projectionExtent = projection.getExtent();
      const vectorSource = this.getLayer().getSource();
      const declutter = this.getLayer().getDeclutter();
      const pixelRatio = frameState.pixelRatio;
      const viewHints = frameState.viewHints;
      const snapToPixel = !(
        viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]
      );
      const context = this.context;
      const width = Math.round((getWidth(extent) / resolution) * pixelRatio);
      const height = Math.round((getHeight(extent) / resolution) * pixelRatio);

      const multiWorld = vectorSource.getWrapX() && projection.canWrapX();
      const worldWidth = multiWorld ? getWidth(projectionExtent) : null;
      const endWorld = multiWorld
        ? Math.ceil((extent[2] - projectionExtent[2]) / worldWidth) +
          (this.extendX_ ? 2 : 1)
        : 1;
      let world = multiWorld
        ? Math.floor((extent[0] - projectionExtent[0]) / worldWidth) -
          (this.extendX_ ? 1 : 0)
        : 0;
      do {
        let transform = this.getRenderTransform(
          center,
          resolution,
          0,
          pixelRatio,
          width,
          height,
          world * worldWidth,
        );
        if (frameState.declutter) {
          transform = transform.slice(0);
        }
        executorGroup.execute(
          context,
          [context.canvas.width, context.canvas.height],
          transform,
          rotation,
          snapToPixel,
          declutterable === undefined
            ? ALL
            : declutterable
              ? DECLUTTER
              : NON_DECLUTTER,
          declutterable
            ? declutter && frameState.declutter[declutter]
            : undefined,
        );
      } while (++world < endWorld);
    }

    /**
     * @private
     */
    setDrawContext_() {
      if (this.opacity_ !== 1) {
        this.targetContext_ = this.context;
        this.context = createCanvasContext2D(
          this.context.canvas.width,
          this.context.canvas.height,
          canvasPool$1,
        );
      }
    }

    /**
     * @private
     */
    resetDrawContext_() {
      if (this.opacity_ !== 1 && this.targetContext_) {
        const alpha = this.targetContext_.globalAlpha;
        this.targetContext_.globalAlpha = this.opacity_;
        this.targetContext_.drawImage(this.context.canvas, 0, 0);
        this.targetContext_.globalAlpha = alpha;
        releaseCanvas(this.context);
        canvasPool$1.push(this.context.canvas);
        this.context = this.targetContext_;
        this.targetContext_ = null;
      }
    }

    /**
     * Render declutter items for this layer
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     */
    renderDeclutter(frameState) {
      if (!this.replayGroup_ || !this.getLayer().getDeclutter()) {
        return;
      }
      this.renderWorlds(this.replayGroup_, frameState, true);
    }

    /**
     * Render deferred instructions.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @override
     */
    renderDeferredInternal(frameState) {
      if (!this.replayGroup_) {
        return;
      }
      if (this.clipExtent_) {
        this.clipUnrotated(this.context, frameState, this.clipExtent_);
      }
      this.replayGroup_.renderDeferred();
      if (this.clipExtent_) {
        this.context.restore();
        this.clipExtent_ = null;
      }
      this.resetDrawContext_();
    }

    /**
     * Render the layer.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @param {HTMLElement|null} target Target that may be used to render content to.
     * @return {HTMLElement} The rendered element.
     * @override
     */
    renderFrame(frameState, target) {
      const layerState = frameState.layerStatesArray[frameState.layerIndex];
      this.opacity_ = layerState.opacity;
      const viewState = frameState.viewState;

      this.prepareContainer(frameState, target);
      const context = this.context;

      const replayGroup = this.replayGroup_;
      let render = replayGroup && !replayGroup.isEmpty();
      if (!render) {
        const hasRenderListeners =
          this.getLayer().hasListener(RenderEventType.PRERENDER) ||
          this.getLayer().hasListener(RenderEventType.POSTRENDER);
        if (!hasRenderListeners) {
          return this.container;
        }
      }

      this.setDrawContext_();

      this.preRender(context, frameState);

      viewState.projection;

      // clipped rendering if layer extent is set
      this.clipExtent_ = null;
      let clipped = false;
      if (render && layerState.extent && this.clipping) {
        const layerExtent = fromUserExtent(layerState.extent);
        render = intersects$1(layerExtent, frameState.extent);
        const needsClip =
          render && !containsExtent(layerExtent, frameState.extent);
        if (needsClip) {
          if (frameState.declutter) {
            // Store extent for deferred clipping
            this.clipExtent_ = layerExtent;
          } else {
            // Apply clipping immediately for non-declutter rendering
            this.clipUnrotated(context, frameState, layerExtent);
            clipped = true;
          }
        }
      }

      if (render) {
        this.renderWorlds(
          replayGroup,
          frameState,
          this.getLayer().getDeclutter() ? false : undefined,
        );
      }

      if (clipped) {
        context.restore();
      }

      this.postRender(context, frameState);

      if (this.renderedRotation_ !== viewState.rotation) {
        this.renderedRotation_ = viewState.rotation;
        this.hitDetectionImageData_ = null;
      }
      if (!frameState.declutter) {
        this.resetDrawContext_();
      }
      return this.container;
    }

    /**
     * Asynchronous layer level hit detection.
     * @param {import("../../pixel.js").Pixel} pixel Pixel.
     * @return {Promise<Array<import("../../Feature").default>>} Promise
     * that resolves with an array of features.
     * @override
     */
    getFeatures(pixel) {
      return new Promise((resolve) => {
        if (
          this.frameState &&
          !this.hitDetectionImageData_ &&
          !this.animatingOrInteracting_
        ) {
          const size = this.frameState.size.slice();
          const center = this.renderedCenter_;
          const resolution = this.renderedResolution_;
          const rotation = this.renderedRotation_;
          const projection = this.renderedProjection_;
          const extent = this.wrappedRenderedExtent_;
          const layer = this.getLayer();
          const transforms = [];
          const width = size[0] * HIT_DETECT_RESOLUTION;
          const height = size[1] * HIT_DETECT_RESOLUTION;
          transforms.push(
            this.getRenderTransform(
              center,
              resolution,
              rotation,
              HIT_DETECT_RESOLUTION,
              width,
              height,
              0,
            ).slice(),
          );
          const source = layer.getSource();
          const projectionExtent = projection.getExtent();
          if (
            source.getWrapX() &&
            projection.canWrapX() &&
            !containsExtent(projectionExtent, extent)
          ) {
            let startX = extent[0];
            const worldWidth = getWidth(projectionExtent);
            let world = 0;
            let offsetX;
            while (startX < projectionExtent[0]) {
              --world;
              offsetX = worldWidth * world;
              transforms.push(
                this.getRenderTransform(
                  center,
                  resolution,
                  rotation,
                  HIT_DETECT_RESOLUTION,
                  width,
                  height,
                  offsetX,
                ).slice(),
              );
              startX += worldWidth;
            }
            world = 0;
            startX = extent[2];
            while (startX > projectionExtent[2]) {
              ++world;
              offsetX = worldWidth * world;
              transforms.push(
                this.getRenderTransform(
                  center,
                  resolution,
                  rotation,
                  HIT_DETECT_RESOLUTION,
                  width,
                  height,
                  offsetX,
                ).slice(),
              );
              startX -= worldWidth;
            }
          }
          this.hitDetectionImageData_ = createHitDetectionImageData(
            size,
            transforms,
            this.renderedFeatures_,
            layer.getStyleFunction(),
            extent,
            resolution,
            rotation,
            getSquaredTolerance(resolution, this.renderedPixelRatio_));
        }
        resolve(
          hitDetect(pixel, this.renderedFeatures_, this.hitDetectionImageData_),
        );
      });
    }

    /**
     * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @param {number} hitTolerance Hit tolerance in pixels.
     * @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
     * @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
     * @return {T|undefined} Callback result.
     * @template T
     * @override
     */
    forEachFeatureAtCoordinate(
      coordinate,
      frameState,
      hitTolerance,
      callback,
      matches,
    ) {
      if (!this.replayGroup_) {
        return undefined;
      }
      const resolution = frameState.viewState.resolution;
      const rotation = frameState.viewState.rotation;
      const layer = this.getLayer();

      /** @type {!Object<string, import("../Map.js").HitMatch<T>|true>} */
      const features = {};

      /**
       * @param {import("../../Feature.js").FeatureLike} feature Feature.
       * @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
       * @param {number} distanceSq The squared distance to the click position
       * @return {T|undefined} Callback result.
       */
      const featureCallback = function (feature, geometry, distanceSq) {
        const key = getUid(feature);
        const match = features[key];
        if (!match) {
          if (distanceSq === 0) {
            features[key] = true;
            return callback(feature, layer, geometry);
          }
          matches.push(
            (features[key] = {
              feature: feature,
              layer: layer,
              geometry: geometry,
              distanceSq: distanceSq,
              callback: callback,
            }),
          );
        } else if (match !== true && distanceSq < match.distanceSq) {
          if (distanceSq === 0) {
            features[key] = true;
            matches.splice(matches.lastIndexOf(match), 1);
            return callback(feature, layer, geometry);
          }
          match.geometry = geometry;
          match.distanceSq = distanceSq;
        }
        return undefined;
      };

      const declutter = this.getLayer().getDeclutter();
      return this.replayGroup_.forEachFeatureAtCoordinate(
        coordinate,
        resolution,
        rotation,
        hitTolerance,
        featureCallback,
        declutter
          ? frameState.declutter?.[declutter]?.all().map((item) => item.value)
          : null,
      );
    }

    /**
     * Perform action necessary to get the layer rendered after new fonts have loaded
     * @override
     */
    handleFontsChanged() {
      const layer = this.getLayer();
      if (layer.getVisible() && this.replayGroup_) {
        layer.changed();
      }
    }

    /**
     * Handle changes in image style state.
     * @param {import("../../events/Event.js").default} event Image style change event.
     * @private
     */
    handleStyleImageChange_(event) {
      this.renderIfReadyAndVisible();
    }

    /**
     * Determine whether render should be called.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @return {boolean} Layer is ready to be rendered.
     * @override
     */
    prepareFrame(frameState) {
      const vectorLayer = this.getLayer();
      const vectorSource = vectorLayer.getSource();
      if (!vectorSource) {
        return false;
      }

      const animating = frameState.viewHints[ViewHint.ANIMATING];
      const interacting = frameState.viewHints[ViewHint.INTERACTING];
      const updateWhileAnimating = vectorLayer.getUpdateWhileAnimating();
      const updateWhileInteracting = vectorLayer.getUpdateWhileInteracting();

      if (
        (this.ready && !updateWhileAnimating && animating) ||
        (!updateWhileInteracting && interacting)
      ) {
        this.animatingOrInteracting_ = true;
        return true;
      }
      this.animatingOrInteracting_ = false;

      const frameStateExtent = frameState.extent;
      const viewState = frameState.viewState;
      const projection = viewState.projection;
      const resolution = viewState.resolution;
      const pixelRatio = frameState.pixelRatio;
      const vectorLayerRevision = vectorLayer.getRevision();
      const vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
      let vectorLayerRenderOrder = vectorLayer.getRenderOrder();

      if (vectorLayerRenderOrder === undefined) {
        vectorLayerRenderOrder = defaultOrder;
      }

      const center = viewState.center.slice();
      const extent = buffer(
        frameStateExtent,
        vectorLayerRenderBuffer * resolution,
      );
      const renderedExtent = extent.slice();
      const loadExtents = [extent.slice()];
      const projectionExtent = projection.getExtent();

      const canWrapX = vectorSource.getWrapX() && projection.canWrapX();
      this.extendX_ = false;
      if (canWrapX) {
        const sourceExtent = vectorSource.getExtent();
        if (sourceExtent && !isEmpty(sourceExtent)) {
          this.extendX_ =
            sourceExtent[0] < projectionExtent[0] ||
            sourceExtent[2] > projectionExtent[2];
        }
      }

      if (
        canWrapX &&
        (!containsExtent(projectionExtent, frameState.extent) || this.extendX_)
      ) {
        // For the replay group, we need an extent that intersects the real world
        // (-180° to +180°). To support geometries in a coordinate range from -540°
        // to +540°, we add at least 1 world width on each side of the projection
        // extent. If the viewport is wider than the world, we need to add half of
        // the viewport width to make sure we cover the whole viewport.
        const worldWidth = getWidth(projectionExtent);
        const gutter = Math.max(getWidth(extent) / 2, worldWidth);
        let projMinX = projectionExtent[0];
        let projMaxX = projectionExtent[2];
        if (this.extendX_) {
          projMinX -= worldWidth;
          projMaxX += worldWidth;
        }
        extent[0] = projMinX - gutter;
        extent[2] = projMaxX + gutter;
        wrapX$1(center, projection);
        const loadExtent = wrapX$2(loadExtents[0], projection);
        // If the extent crosses the date line, we load data for both edges of the worlds
        if (
          loadExtent[0] < projectionExtent[0] &&
          loadExtent[2] < projectionExtent[2]
        ) {
          loadExtents.push([
            loadExtent[0] + worldWidth,
            loadExtent[1],
            loadExtent[2] + worldWidth,
            loadExtent[3],
          ]);
        } else if (
          loadExtent[0] > projectionExtent[0] &&
          loadExtent[2] > projectionExtent[2]
        ) {
          loadExtents.push([
            loadExtent[0] - worldWidth,
            loadExtent[1],
            loadExtent[2] - worldWidth,
            loadExtent[3],
          ]);
        }
      }

      if (
        this.ready &&
        this.renderedResolution_ == resolution &&
        this.renderedPixelRatio_ === pixelRatio &&
        this.renderedRevision_ == vectorLayerRevision &&
        this.renderedRenderOrder_ == vectorLayerRenderOrder &&
        this.renderedFrameDeclutter_ === !!frameState.declutter &&
        containsExtent(this.wrappedRenderedExtent_, extent)
      ) {
        if (!equals$2(this.renderedExtent_, renderedExtent)) {
          this.hitDetectionImageData_ = null;
          this.renderedExtent_ = renderedExtent;
        }
        this.renderedCenter_ = center;
        this.replayGroupChanged = false;
        return true;
      }

      this.replayGroup_ = null;

      const replayGroup = new BuilderGroup(
        getTolerance(resolution, pixelRatio),
        extent,
        resolution,
        pixelRatio,
      );
      let userTransform;
      {
        for (let i = 0, ii = loadExtents.length; i < ii; ++i) {
          vectorSource.loadFeatures(loadExtents[i], resolution, projection);
        }
      }

      const squaredTolerance = getSquaredTolerance(resolution, pixelRatio);
      let ready = true;
      const render =
        /**
         * @param {import("../../Feature.js").default} feature Feature.
         * @param {number} index Index.
         */
        (feature, index) => {
          let styles;
          const styleFunction =
            feature.getStyleFunction() || vectorLayer.getStyleFunction();
          if (styleFunction) {
            styles = styleFunction(feature, resolution);
          }
          if (styles) {
            const dirty = this.renderFeature(
              feature,
              squaredTolerance,
              styles,
              replayGroup,
              userTransform,
              this.getLayer().getDeclutter(),
              index,
            );
            ready = ready && !dirty;
          }
        };

      const userExtent = toUserExtent(extent);
      /** @type {Array<import("../../Feature.js").default>} */
      const features = vectorSource.getFeaturesInExtent(userExtent);
      if (vectorLayerRenderOrder) {
        features.sort(vectorLayerRenderOrder);
      }
      for (let i = 0, ii = features.length; i < ii; ++i) {
        render(features[i], i);
      }
      this.renderedFeatures_ = features;
      this.ready = ready;

      const replayGroupInstructions = replayGroup.finish();
      const executorGroup = new ExecutorGroup(
        extent,
        resolution,
        pixelRatio,
        vectorSource.getOverlaps(),
        replayGroupInstructions,
        vectorLayer.getRenderBuffer(),
        !!frameState.declutter,
      );

      this.renderedResolution_ = resolution;
      this.renderedRevision_ = vectorLayerRevision;
      this.renderedRenderOrder_ = vectorLayerRenderOrder;
      this.renderedFrameDeclutter_ = !!frameState.declutter;
      this.renderedExtent_ = renderedExtent;
      this.wrappedRenderedExtent_ = extent;
      this.renderedCenter_ = center;
      this.renderedProjection_ = projection;
      this.renderedPixelRatio_ = pixelRatio;
      this.replayGroup_ = executorGroup;
      this.hitDetectionImageData_ = null;

      this.replayGroupChanged = true;
      return true;
    }

    /**
     * @param {import("../../Feature.js").default} feature Feature.
     * @param {number} squaredTolerance Squared render tolerance.
     * @param {import("../../style/Style.js").default|Array<import("../../style/Style.js").default>} styles The style or array of styles.
     * @param {import("../../render/canvas/BuilderGroup.js").default} builderGroup Builder group.
     * @param {import("../../proj.js").TransformFunction} [transform] Transform from user to view projection.
     * @param {boolean} [declutter] Enable decluttering.
     * @param {number} [index] Render order index.
     * @return {boolean} `true` if an image is loading.
     */
    renderFeature(
      feature,
      squaredTolerance,
      styles,
      builderGroup,
      transform,
      declutter,
      index,
    ) {
      if (!styles) {
        return false;
      }
      let loading = false;
      if (Array.isArray(styles)) {
        for (let i = 0, ii = styles.length; i < ii; ++i) {
          loading =
            renderFeature(
              builderGroup,
              feature,
              styles[i],
              squaredTolerance,
              this.boundHandleStyleImageChange_,
              transform,
              declutter,
              index,
            ) || loading;
        }
      } else {
        loading = renderFeature(
          builderGroup,
          feature,
          styles,
          squaredTolerance,
          this.boundHandleStyleImageChange_,
          transform,
          declutter,
          index,
        );
      }
      return loading;
    }
  }

  /**
   * @module ol/expr/expression
   */

  /**
   * @fileoverview This module includes types and functions for parsing array encoded expressions.
   * The result of parsing an encoded expression is one of the specific expression classes.
   * During parsing, information is added to the parsing context about the data accessed by the
   * expression.
   */

  /**
   * Base type used for literal style parameters; can be a number literal or the output of an operator,
   * which in turns takes {@link import("./expression.js").ExpressionValue} arguments.
   *
   * See below for details on the available operators (with notes for those that are WebGL or Canvas only).
   *
   * Reading operators:
   *   * `['band', bandIndex, xOffset, yOffset]` For tile layers only. Fetches pixel values from band
   *     `bandIndex` of the source's data. The first `bandIndex` of the source data is `1`. Fetched values
   *     are in the 0..1 range. {@link import("../source/TileImage.js").default} sources have 4 bands: red,
   *     green, blue and alpha. {@link import("../source/DataTile.js").default} sources can have any number
   *     of bands, depending on the underlying data source and
   *     {@link import("../source/GeoTIFF.js").Options configuration}. `xOffset` and `yOffset` are optional
   *     and allow specifying pixel offsets for x and y. This is used for sampling data from neighboring pixels (WebGL only).
   *   * `['get', attributeName]` fetches a feature property value, similar to `feature.get('attributeName')`.
   *   * `['get', attributeName, keyOrArrayIndex, ...]` (Canvas only) Access nested properties and array items of a
   *     feature property. The result is `undefined` when there is nothing at the specified key or index.
   *   * `['geometry-type']` returns a feature's geometry type as string, either: 'LineString', 'Point' or 'Polygon'
   *     `Multi*` values are returned as their singular equivalent
   *     `Circle` geometries are returned as 'Polygon'
   *     `GeometryCollection` geometries are returned as the type of the first geometry found in the collection (WebGL only).
   *   * `['resolution']` returns the current resolution
   *   * `['time']` The time in seconds since the creation of the layer (WebGL only).
   *   * `['var', 'varName']` fetches a value from the style variables; will throw an error if that variable is undefined
   *   * `['zoom']` The current zoom level (WebGL only).
   *   * `['line-metric']` returns the M component of the current point on a line (WebGL only); in case where the geometry layout of the line
   *      does not contain an M component (e.g. XY or XYZ), 0 is returned; 0 is also returned for geometries other than lines.
   *      Please note that the M component will be linearly interpolated between the two points composing a segment.
   *
   * Math operators:
   *   * `['*', value1, value2, ...]` multiplies the values (either numbers or colors)
   *   * `['/', value1, value2]` divides `value1` by `value2`
   *   * `['+', value1, value2, ...]` adds the values
   *   * `['-', value1, value2]` subtracts `value2` from `value1`
   *   * `['clamp', value, low, high]` clamps `value` between `low` and `high`
   *   * `['%', value1, value2]` returns the result of `value1 % value2` (modulo)
   *   * `['^', value1, value2]` returns the value of `value1` raised to the `value2` power
   *   * `['abs', value1]` returns the absolute value of `value1`
   *   * `['floor', value1]` returns the nearest integer less than or equal to `value1`
   *   * `['round', value1]` returns the nearest integer to `value1`
   *   * `['ceil', value1]` returns the nearest integer greater than or equal to `value1`
   *   * `['sin', value1]` returns the sine of `value1`
   *   * `['cos', value1]` returns the cosine of `value1`
   *   * `['atan', value1, value2]` returns `atan2(value1, value2)`. If `value2` is not provided, returns `atan(value1)`
   *   * `['sqrt', value1]` returns the square root of `value1`
   *
   * * Transform operators:
   *   * `['case', condition1, output1, ...conditionN, outputN, fallback]` selects the first output whose corresponding
   *     condition evaluates to `true`. If no match is found, returns the `fallback` value.
   *     All conditions should be `boolean`, output and fallback can be any kind.
   *   * `['match', input, match1, output1, ...matchN, outputN, fallback]` compares the `input` value against all
   *     provided `matchX` values, returning the output associated with the first valid match. If no match is found,
   *     returns the `fallback` value.
   *     `input` and `matchX` values must all be of the same type, and can be `number` or `string`. `outputX` and
   *     `fallback` values must be of the same type, and can be of any kind.
   *   * `['interpolate', interpolation, input, stop1, output1, ...stopN, outputN]` returns a value by interpolating between
   *     pairs of inputs and outputs; `interpolation` can either be `['linear']` or `['exponential', base]` where `base` is
   *     the rate of increase from stop A to stop B (i.e. power to which the interpolation ratio is raised); a value
   *     of 1 is equivalent to `['linear']`.
   *     `input` and `stopX` values must all be of type `number`. `outputX` values can be `number` or `color` values.
   *     Note: `input` will be clamped between `stop1` and `stopN`, meaning that all output values will be comprised
   *     between `output1` and `outputN`.
   *   * `['string', value1, value2, ...]` returns the first value in the list that evaluates to a string.
   *     An example would be to provide a default value for get: `['string', ['get', 'propertyname'], 'default value']]`
   *     (Canvas only).
   *   * `['number', value1, value2, ...]` returns the first value in the list that evaluates to a number.
   *     An example would be to provide a default value for get: `['string', ['get', 'propertyname'], 42]]`
   *     (Canvas only).
   *   * `['coalesce', value1, value2, ...]` returns the first value in the list which is not null or undefined.
   *     An example would be to provide a default value for get: `['coalesce', ['get','propertyname'], 'default value']]`
   *     (Canvas only).
   *
   * * Logical operators:
   *   * `['<', value1, value2]` returns `true` if `value1` is strictly lower than `value2`, or `false` otherwise.
   *   * `['<=', value1, value2]` returns `true` if `value1` is lower than or equals `value2`, or `false` otherwise.
   *   * `['>', value1, value2]` returns `true` if `value1` is strictly greater than `value2`, or `false` otherwise.
   *   * `['>=', value1, value2]` returns `true` if `value1` is greater than or equals `value2`, or `false` otherwise.
   *   * `['==', value1, value2]` returns `true` if `value1` equals `value2`, or `false` otherwise.
   *   * `['!=', value1, value2]` returns `true` if `value1` does not equal `value2`, or `false` otherwise.
   *   * `['!', value1]` returns `false` if `value1` is `true` or greater than `0`, or `true` otherwise.
   *   * `['all', value1, value2, ...]` returns `true` if all the inputs are `true`, `false` otherwise.
   *   * `['any', value1, value2, ...]` returns `true` if any of the inputs are `true`, `false` otherwise.
   *   * `['has', attributeName, keyOrArrayIndex, ...]` returns `true` if feature properties include the (nested) key `attributeName`,
   *     `false` otherwise.
   *     Note that for WebGL layers, the hardcoded value `-9999999` is used to distinguish when a property is not defined.
   *   * `['between', value1, value2, value3]` returns `true` if `value1` is contained between `value2` and `value3`
   *     (inclusively), or `false` otherwise.
   *   * `['in', needle, haystack]` returns `true` if `needle` is found in `haystack`, and
   *     `false` otherwise.
   *     This operator has the following limitations:
   *     * `haystack` has to be an array of numbers or strings (searching for a substring in a string is not supported yet)
   *     * Only literal arrays are supported as `haystack` for now; this means that `haystack` cannot be the result of an
   *     expression. If `haystack` is an array of strings, use the `literal` operator to disambiguate from an expression:
   *     `['literal', ['abc', 'def', 'ghi']]`
   *
   * * Conversion operators:
   *   * `['array', value1, ...valueN]` creates a numerical array from `number` values; please note that the amount of
   *     values can currently only be 2, 3 or 4 (WebGL only).
   *   * `['color', red, green, blue, alpha]` or `['color', shade, alpha]` creates a `color` value from `number` values;
   *     the `alpha` parameter is optional; if not specified, it will be set to 1 (WebGL only).
   *     Note: `red`, `green` and `blue` or `shade` components must be values between 0 and 255; `alpha` between 0 and 1.
   *   * `['palette', index, colors]` picks a `color` value from an array of colors using the given index; the `index`
   *     expression must evaluate to a number; the items in the `colors` array must be strings with hex colors
   *     (e.g. `'#86A136'`), colors using the rgba[a] functional notation (e.g. `'rgb(134, 161, 54)'` or `'rgba(134, 161, 54, 1)'`),
   *     named colors (e.g. `'red'`), or array literals with 3 ([r, g, b]) or 4 ([r, g, b, a]) values (with r, g, and b
   *     in the 0-255 range and a in the 0-1 range) (WebGL only).
   *   * `['to-string', value]` converts the input value to a string. If the input is a boolean, the result is "true" or "false".
   *     If the input is a number, it is converted to a string as specified by the "NumberToString" algorithm of the ECMAScript
   *     Language Specification. If the input is a color, it is converted to a string of the form "rgba(r,g,b,a)". (Canvas only)
   *
   * Values can either be literals or another operator, as they will be evaluated recursively.
   * Literal values can be of the following types:
   * * `boolean`
   * * `number`
   * * `number[]` (number arrays can only have a length of 2, 3 or 4)
   * * `string`
   * * {@link module:ol/color~Color}
   *
   * @typedef {Array<*>|import("../color.js").Color|string|number|boolean} ExpressionValue
   * @api
   */

  let numTypes = 0;
  const BooleanType = 1 << numTypes++;
  const NumberType = 1 << numTypes++;
  const StringType = 1 << numTypes++;
  const ColorType = 1 << numTypes++;
  const NumberArrayType = 1 << numTypes++;
  const SizeType = 1 << numTypes++;
  const AnyType = Math.pow(2, numTypes) - 1;

  const typeNames = {
    [BooleanType]: 'boolean',
    [NumberType]: 'number',
    [StringType]: 'string',
    [ColorType]: 'color',
    [NumberArrayType]: 'number[]',
    [SizeType]: 'size',
  };

  const namedTypes = Object.keys(typeNames).map(Number).sort(ascending);

  /**
   * @param {number} type The type.
   * @return {boolean} The type is one of the specific types (not any or a union type).
   */
  function isSpecific(type) {
    return type in typeNames;
  }

  /**
   * Get a string representation for a type.
   * @param {number} type The type.
   * @return {string} The type name.
   */
  function typeName(type) {
    const names = [];
    for (const namedType of namedTypes) {
      if (includesType(type, namedType)) {
        names.push(typeNames[namedType]);
      }
    }
    if (names.length === 0) {
      return 'untyped';
    }
    if (names.length < 3) {
      return names.join(' or ');
    }
    return names.slice(0, -1).join(', ') + ', or ' + names[names.length - 1];
  }

  /**
   * @param {number} broad The broad type.
   * @param {number} specific The specific type.
   * @return {boolean} The broad type includes the specific type.
   */
  function includesType(broad, specific) {
    return (broad & specific) === specific;
  }

  /**
   * @param {number} type The type.
   * @param {number} expected The expected type.
   * @return {boolean} The given type is exactly the expected type.
   */
  function isType(type, expected) {
    return type === expected;
  }

  /**
   * @typedef {boolean|number|string|Array<number>} LiteralValue
   */

  class LiteralExpression {
    /**
     * @param {number} type The value type.
     * @param {LiteralValue} value The literal value.
     */
    constructor(type, value) {
      if (!isSpecific(type)) {
        throw new Error(
          `literal expressions must have a specific type, got ${typeName(type)}`,
        );
      }
      this.type = type;
      this.value = value;
    }
  }

  class CallExpression {
    /**
     * @param {number} type The return type.
     * @param {string} operator The operator.
     * @param {...Expression} args The arguments.
     */
    constructor(type, operator, ...args) {
      this.type = type;
      this.operator = operator;
      this.args = args;
    }
  }

  /**
   * @typedef {LiteralExpression|CallExpression} Expression
   */

  /**
   * @typedef {Object} ParsingContext
   * @property {Set<string>} variables Variables referenced with the 'var' operator.
   * @property {Set<string>} properties Properties referenced with the 'get' operator.
   * @property {boolean} featureId The style uses the feature id.
   * @property {boolean} geometryType The style uses the feature geometry type.
   * @property {boolean} mapState The style uses the map state (view state or time elapsed).
   */

  /**
   * @return {ParsingContext} A new parsing context.
   */
  function newParsingContext() {
    return {
      variables: new Set(),
      properties: new Set(),
      featureId: false,
      geometryType: false,
      mapState: false,
    };
  }

  /**
   * @typedef {LiteralValue|Array} EncodedExpression
   */

  /**
   * @param {EncodedExpression} encoded The encoded expression.
   * @param {number} expectedType The expected type.
   * @param {ParsingContext} context The parsing context.
   * @return {Expression} The parsed expression result.
   */
  function parse(encoded, expectedType, context) {
    switch (typeof encoded) {
      case 'boolean': {
        if (isType(expectedType, StringType)) {
          return new LiteralExpression(StringType, encoded ? 'true' : 'false');
        }
        if (!includesType(expectedType, BooleanType)) {
          throw new Error(
            `got a boolean, but expected ${typeName(expectedType)}`,
          );
        }
        return new LiteralExpression(BooleanType, encoded);
      }
      case 'number': {
        if (isType(expectedType, SizeType)) {
          return new LiteralExpression(SizeType, toSize(encoded));
        }
        if (isType(expectedType, BooleanType)) {
          return new LiteralExpression(BooleanType, !!encoded);
        }
        if (isType(expectedType, StringType)) {
          return new LiteralExpression(StringType, encoded.toString());
        }
        if (!includesType(expectedType, NumberType)) {
          throw new Error(`got a number, but expected ${typeName(expectedType)}`);
        }
        return new LiteralExpression(NumberType, encoded);
      }
      case 'string': {
        if (isType(expectedType, ColorType)) {
          return new LiteralExpression(ColorType, fromString(encoded));
        }
        if (isType(expectedType, BooleanType)) {
          return new LiteralExpression(BooleanType, !!encoded);
        }
        if (!includesType(expectedType, StringType)) {
          throw new Error(`got a string, but expected ${typeName(expectedType)}`);
        }
        return new LiteralExpression(StringType, encoded);
      }
    }

    if (!Array.isArray(encoded)) {
      throw new Error('expression must be an array or a primitive value');
    }

    if (encoded.length === 0) {
      throw new Error('empty expression');
    }

    if (typeof encoded[0] === 'string') {
      return parseCallExpression(encoded, expectedType, context);
    }

    for (const item of encoded) {
      if (typeof item !== 'number') {
        throw new Error('expected an array of numbers');
      }
    }

    if (isType(expectedType, SizeType)) {
      if (encoded.length !== 2) {
        throw new Error(
          `expected an array of two values for a size, got ${encoded.length}`,
        );
      }
      return new LiteralExpression(SizeType, encoded);
    }

    if (isType(expectedType, ColorType)) {
      if (encoded.length === 3) {
        return new LiteralExpression(ColorType, [...encoded, 1]);
      }
      if (encoded.length === 4) {
        return new LiteralExpression(ColorType, encoded);
      }
      throw new Error(
        `expected an array of 3 or 4 values for a color, got ${encoded.length}`,
      );
    }

    if (!includesType(expectedType, NumberArrayType)) {
      throw new Error(
        `got an array of numbers, but expected ${typeName(expectedType)}`,
      );
    }

    return new LiteralExpression(NumberArrayType, encoded);
  }

  /**
   * @type {Object<string, string>}
   */
  const Ops = {
    Get: 'get',
    Var: 'var',
    Concat: 'concat',
    GeometryType: 'geometry-type',
    LineMetric: 'line-metric',
    Any: 'any',
    All: 'all',
    Not: '!',
    Resolution: 'resolution',
    Zoom: 'zoom',
    Time: 'time',
    Equal: '==',
    NotEqual: '!=',
    GreaterThan: '>',
    GreaterThanOrEqualTo: '>=',
    LessThan: '<',
    LessThanOrEqualTo: '<=',
    Multiply: '*',
    Divide: '/',
    Add: '+',
    Subtract: '-',
    Clamp: 'clamp',
    Mod: '%',
    Pow: '^',
    Abs: 'abs',
    Floor: 'floor',
    Ceil: 'ceil',
    Round: 'round',
    Sin: 'sin',
    Cos: 'cos',
    Atan: 'atan',
    Sqrt: 'sqrt',
    Match: 'match',
    Between: 'between',
    Interpolate: 'interpolate',
    Coalesce: 'coalesce',
    Case: 'case',
    In: 'in',
    Number: 'number',
    String: 'string',
    Array: 'array',
    Color: 'color',
    Id: 'id',
    Band: 'band',
    Palette: 'palette',
    ToString: 'to-string',
    Has: 'has',
  };

  /**
   * @typedef {function(Array, number, ParsingContext):Expression} Parser
   *
   * Second argument is the expected type.
   */

  /**
   * @type {Object<string, Parser>}
   */
  const parsers = {
    [Ops.Get]: createCallExpressionParser(hasArgsCount(1, Infinity), withGetArgs),
    [Ops.Var]: createCallExpressionParser(hasArgsCount(1, 1), withVarArgs),
    [Ops.Has]: createCallExpressionParser(hasArgsCount(1, Infinity), withGetArgs),
    [Ops.Id]: createCallExpressionParser(usesFeatureId, withNoArgs),
    [Ops.Concat]: createCallExpressionParser(
      hasArgsCount(2, Infinity),
      withArgsOfType(StringType),
    ),
    [Ops.GeometryType]: createCallExpressionParser(usesGeometryType, withNoArgs),
    [Ops.LineMetric]: createCallExpressionParser(withNoArgs),
    [Ops.Resolution]: createCallExpressionParser(usesMapState, withNoArgs),
    [Ops.Zoom]: createCallExpressionParser(usesMapState, withNoArgs),
    [Ops.Time]: createCallExpressionParser(usesMapState, withNoArgs),
    [Ops.Any]: createCallExpressionParser(
      hasArgsCount(2, Infinity),
      withArgsOfType(BooleanType),
    ),
    [Ops.All]: createCallExpressionParser(
      hasArgsCount(2, Infinity),
      withArgsOfType(BooleanType),
    ),
    [Ops.Not]: createCallExpressionParser(
      hasArgsCount(1, 1),
      withArgsOfType(BooleanType),
    ),
    [Ops.Equal]: createCallExpressionParser(
      hasArgsCount(2, 2),
      withArgsOfType(AnyType),
    ),
    [Ops.NotEqual]: createCallExpressionParser(
      hasArgsCount(2, 2),
      withArgsOfType(AnyType),
    ),
    [Ops.GreaterThan]: createCallExpressionParser(
      hasArgsCount(2, 2),
      withArgsOfType(NumberType),
    ),
    [Ops.GreaterThanOrEqualTo]: createCallExpressionParser(
      hasArgsCount(2, 2),
      withArgsOfType(NumberType),
    ),
    [Ops.LessThan]: createCallExpressionParser(
      hasArgsCount(2, 2),
      withArgsOfType(NumberType),
    ),
    [Ops.LessThanOrEqualTo]: createCallExpressionParser(
      hasArgsCount(2, 2),
      withArgsOfType(NumberType),
    ),
    [Ops.Multiply]: createCallExpressionParser(
      hasArgsCount(2, Infinity),
      withArgsOfReturnType,
    ),
    [Ops.Coalesce]: createCallExpressionParser(
      hasArgsCount(2, Infinity),
      withArgsOfReturnType,
    ),
    [Ops.Divide]: createCallExpressionParser(
      hasArgsCount(2, 2),
      withArgsOfType(NumberType),
    ),
    [Ops.Add]: createCallExpressionParser(
      hasArgsCount(2, Infinity),
      withArgsOfType(NumberType),
    ),
    [Ops.Subtract]: createCallExpressionParser(
      hasArgsCount(2, 2),
      withArgsOfType(NumberType),
    ),
    [Ops.Clamp]: createCallExpressionParser(
      hasArgsCount(3, 3),
      withArgsOfType(NumberType),
    ),
    [Ops.Mod]: createCallExpressionParser(
      hasArgsCount(2, 2),
      withArgsOfType(NumberType),
    ),
    [Ops.Pow]: createCallExpressionParser(
      hasArgsCount(2, 2),
      withArgsOfType(NumberType),
    ),
    [Ops.Abs]: createCallExpressionParser(
      hasArgsCount(1, 1),
      withArgsOfType(NumberType),
    ),
    [Ops.Floor]: createCallExpressionParser(
      hasArgsCount(1, 1),
      withArgsOfType(NumberType),
    ),
    [Ops.Ceil]: createCallExpressionParser(
      hasArgsCount(1, 1),
      withArgsOfType(NumberType),
    ),
    [Ops.Round]: createCallExpressionParser(
      hasArgsCount(1, 1),
      withArgsOfType(NumberType),
    ),
    [Ops.Sin]: createCallExpressionParser(
      hasArgsCount(1, 1),
      withArgsOfType(NumberType),
    ),
    [Ops.Cos]: createCallExpressionParser(
      hasArgsCount(1, 1),
      withArgsOfType(NumberType),
    ),
    [Ops.Atan]: createCallExpressionParser(
      hasArgsCount(1, 2),
      withArgsOfType(NumberType),
    ),
    [Ops.Sqrt]: createCallExpressionParser(
      hasArgsCount(1, 1),
      withArgsOfType(NumberType),
    ),
    [Ops.Match]: createCallExpressionParser(
      hasArgsCount(4, Infinity),
      hasEvenArgs,
      withMatchArgs,
    ),
    [Ops.Between]: createCallExpressionParser(
      hasArgsCount(3, 3),
      withArgsOfType(NumberType),
    ),
    [Ops.Interpolate]: createCallExpressionParser(
      hasArgsCount(6, Infinity),
      hasEvenArgs,
      withInterpolateArgs,
    ),
    [Ops.Case]: createCallExpressionParser(
      hasArgsCount(3, Infinity),
      hasOddArgs,
      withCaseArgs,
    ),
    [Ops.In]: createCallExpressionParser(hasArgsCount(2, 2), withInArgs),
    [Ops.Number]: createCallExpressionParser(
      hasArgsCount(1, Infinity),
      withArgsOfType(AnyType),
    ),
    [Ops.String]: createCallExpressionParser(
      hasArgsCount(1, Infinity),
      withArgsOfType(AnyType),
    ),
    [Ops.Array]: createCallExpressionParser(
      hasArgsCount(1, Infinity),
      withArgsOfType(NumberType),
    ),
    [Ops.Color]: createCallExpressionParser(
      hasArgsCount(1, 4),
      withArgsOfType(NumberType),
    ),
    [Ops.Band]: createCallExpressionParser(
      hasArgsCount(1, 3),
      withArgsOfType(NumberType),
    ),
    [Ops.Palette]: createCallExpressionParser(
      hasArgsCount(2, 2),
      withPaletteArgs,
    ),
    [Ops.ToString]: createCallExpressionParser(
      hasArgsCount(1, 1),
      withArgsOfType(BooleanType | NumberType | StringType | ColorType),
    ),
  };

  /**
   * @typedef {function(Array<EncodedExpression>, number, ParsingContext):Array<Expression>|void} ArgValidator
   *
   * An argument validator applies various checks to an encoded expression arguments and
   * returns the parsed arguments if any.  The second argument is the return type of the call expression.
   */

  /**
   * @type {ArgValidator}
   */
  function withGetArgs(encoded, returnType, context) {
    const argsCount = encoded.length - 1;
    const args = new Array(argsCount);
    for (let i = 0; i < argsCount; ++i) {
      const key = encoded[i + 1];
      switch (typeof key) {
        case 'number': {
          args[i] = new LiteralExpression(NumberType, key);
          break;
        }
        case 'string': {
          args[i] = new LiteralExpression(StringType, key);
          break;
        }
        default: {
          throw new Error(
            `expected a string key or numeric array index for a get operation, got ${key}`,
          );
        }
      }
      if (i === 0) {
        context.properties.add(String(key));
      }
    }
    return args;
  }

  /**
   * @type {ArgValidator}
   */
  function withVarArgs(encoded, returnType, context) {
    const name = encoded[1];
    if (typeof name !== 'string') {
      throw new Error('expected a string argument for var operation');
    }
    context.variables.add(name);

    return [new LiteralExpression(StringType, name)];
  }

  /**
   * @type {ArgValidator}
   */
  function usesFeatureId(encoded, returnType, context) {
    context.featureId = true;
  }

  /**
   * @type {ArgValidator}
   */
  function usesGeometryType(encoded, returnType, context) {
    context.geometryType = true;
  }

  /**
   * @type {ArgValidator}
   */
  function usesMapState(encoded, returnType, context) {
    context.mapState = true;
  }

  /**
   * @type {ArgValidator}
   */
  function withNoArgs(encoded, returnType, context) {
    const operation = encoded[0];
    if (encoded.length !== 1) {
      throw new Error(`expected no arguments for ${operation} operation`);
    }
    return [];
  }

  /**
   * @param {number} minArgs The minimum number of arguments.
   * @param {number} maxArgs The maximum number of arguments.
   * @return {ArgValidator} The argument validator
   */
  function hasArgsCount(minArgs, maxArgs) {
    return function (encoded, returnType, context) {
      const operation = encoded[0];
      const argCount = encoded.length - 1;
      if (minArgs === maxArgs) {
        if (argCount !== minArgs) {
          const plural = minArgs === 1 ? '' : 's';
          throw new Error(
            `expected ${minArgs} argument${plural} for ${operation}, got ${argCount}`,
          );
        }
      } else if (argCount < minArgs || argCount > maxArgs) {
        const range =
          maxArgs === Infinity
            ? `${minArgs} or more`
            : `${minArgs} to ${maxArgs}`;
        throw new Error(
          `expected ${range} arguments for ${operation}, got ${argCount}`,
        );
      }
    };
  }

  /**
   * @type {ArgValidator}
   */
  function withArgsOfReturnType(encoded, returnType, context) {
    const argCount = encoded.length - 1;
    /**
     * @type {Array<Expression>}
     */
    const args = new Array(argCount);
    for (let i = 0; i < argCount; ++i) {
      const expression = parse(encoded[i + 1], returnType, context);
      args[i] = expression;
    }
    return args;
  }

  /**
   * @param {number} argType The argument type.
   * @return {ArgValidator} The argument validator
   */
  function withArgsOfType(argType) {
    return function (encoded, returnType, context) {
      const argCount = encoded.length - 1;
      /**
       * @type {Array<Expression>}
       */
      const args = new Array(argCount);
      for (let i = 0; i < argCount; ++i) {
        const expression = parse(encoded[i + 1], argType, context);
        args[i] = expression;
      }
      return args;
    };
  }

  /**
   * @type {ArgValidator}
   */
  function hasOddArgs(encoded, returnType, context) {
    const operation = encoded[0];
    const argCount = encoded.length - 1;
    if (argCount % 2 === 0) {
      throw new Error(
        `expected an odd number of arguments for ${operation}, got ${argCount} instead`,
      );
    }
  }

  /**
   * @type {ArgValidator}
   */
  function hasEvenArgs(encoded, returnType, context) {
    const operation = encoded[0];
    const argCount = encoded.length - 1;
    if (argCount % 2 === 1) {
      throw new Error(
        `expected an even number of arguments for operation ${operation}, got ${argCount} instead`,
      );
    }
  }

  /**
   * @type {ArgValidator}
   */
  function withMatchArgs(encoded, returnType, context) {
    const argsCount = encoded.length - 1;

    const inputType = StringType | NumberType | BooleanType;

    const input = parse(encoded[1], inputType, context);

    const fallback = parse(encoded[encoded.length - 1], returnType, context);

    const args = new Array(argsCount - 2);
    for (let i = 0; i < argsCount - 2; i += 2) {
      try {
        const match = parse(encoded[i + 2], input.type, context);
        args[i] = match;
      } catch (err) {
        throw new Error(
          `failed to parse argument ${i + 1} of match expression: ${err.message}`,
        );
      }
      try {
        const output = parse(encoded[i + 3], fallback.type, context);
        args[i + 1] = output;
      } catch (err) {
        throw new Error(
          `failed to parse argument ${i + 2} of match expression: ${err.message}`,
        );
      }
    }

    return [input, ...args, fallback];
  }

  /**
   * @type {ArgValidator}
   */
  function withInterpolateArgs(encoded, returnType, context) {
    const interpolationType = encoded[1];
    /**
     * @type {number}
     */
    let base;
    switch (interpolationType[0]) {
      case 'linear':
        base = 1;
        break;
      case 'exponential':
        const b = interpolationType[1];
        if (typeof b !== 'number' || b <= 0) {
          throw new Error(
            `expected a number base for exponential interpolation` +
              `, got ${JSON.stringify(b)} instead`,
          );
        }
        base = b;
        break;
      default:
        throw new Error(
          `invalid interpolation type: ${JSON.stringify(interpolationType)}`,
        );
    }

    const interpolation = new LiteralExpression(NumberType, base);

    let input;
    try {
      input = parse(encoded[2], NumberType, context);
    } catch (err) {
      throw new Error(
        `failed to parse argument 1 in interpolate expression: ${err.message}`,
      );
    }

    const args = new Array(encoded.length - 3);
    for (let i = 0; i < args.length; i += 2) {
      try {
        const stop = parse(encoded[i + 3], NumberType, context);
        args[i] = stop;
      } catch (err) {
        throw new Error(
          `failed to parse argument ${i + 2} for interpolate expression: ${err.message}`,
        );
      }
      try {
        const output = parse(encoded[i + 4], returnType, context);
        args[i + 1] = output;
      } catch (err) {
        throw new Error(
          `failed to parse argument ${i + 3} for interpolate expression: ${err.message}`,
        );
      }
    }

    return [interpolation, input, ...args];
  }

  /**
   * @type {ArgValidator}
   */
  function withCaseArgs(encoded, returnType, context) {
    const fallback = parse(encoded[encoded.length - 1], returnType, context);

    const args = new Array(encoded.length - 1);
    for (let i = 0; i < args.length - 1; i += 2) {
      try {
        const condition = parse(encoded[i + 1], BooleanType, context);
        args[i] = condition;
      } catch (err) {
        throw new Error(
          `failed to parse argument ${i} of case expression: ${err.message}`,
        );
      }
      try {
        const output = parse(encoded[i + 2], fallback.type, context);
        args[i + 1] = output;
      } catch (err) {
        throw new Error(
          `failed to parse argument ${i + 1} of case expression: ${err.message}`,
        );
      }
    }

    args[args.length - 1] = fallback;
    return args;
  }

  /**
   * @type {ArgValidator}
   */
  function withInArgs(encoded, returnType, context) {
    let haystack = encoded[2];
    if (!Array.isArray(haystack)) {
      throw new Error(
        `the second argument for the "in" operator must be an array`,
      );
    }
    /**
     * @type {number}
     */
    let needleType;
    if (typeof haystack[0] === 'string') {
      if (haystack[0] !== 'literal') {
        throw new Error(
          `for the "in" operator, a string array should be wrapped in a "literal" operator to disambiguate from expressions`,
        );
      }
      if (!Array.isArray(haystack[1])) {
        throw new Error(
          `failed to parse "in" expression: the literal operator must be followed by an array`,
        );
      }
      haystack = haystack[1];
      needleType = StringType;
    } else {
      needleType = NumberType;
    }

    const args = new Array(haystack.length);
    for (let i = 0; i < args.length; i++) {
      try {
        const arg = parse(haystack[i], needleType, context);
        args[i] = arg;
      } catch (err) {
        throw new Error(
          `failed to parse haystack item ${i} for "in" expression: ${err.message}`,
        );
      }
    }

    const needle = parse(encoded[1], needleType, context);
    return [needle, ...args];
  }

  /**
   * @type {ArgValidator}
   */
  function withPaletteArgs(encoded, returnType, context) {
    let index;
    try {
      index = parse(encoded[1], NumberType, context);
    } catch (err) {
      throw new Error(
        `failed to parse first argument in palette expression: ${err.message}`,
      );
    }
    const colors = encoded[2];
    if (!Array.isArray(colors)) {
      throw new Error('the second argument of palette must be an array');
    }
    const parsedColors = new Array(colors.length);
    for (let i = 0; i < parsedColors.length; i++) {
      let color;
      try {
        color = parse(colors[i], ColorType, context);
      } catch (err) {
        throw new Error(
          `failed to parse color at index ${i} in palette expression: ${err.message}`,
        );
      }
      if (!(color instanceof LiteralExpression)) {
        throw new Error(
          `the palette color at index ${i} must be a literal value`,
        );
      }
      parsedColors[i] = color;
    }
    return [index, ...parsedColors];
  }

  /**
   * @param {Array<ArgValidator>} validators A chain of argument validators.  The last validator is expected
   * to return the parsed arguments.
   * @return {Parser} The parser.
   */
  function createCallExpressionParser(...validators) {
    return function (encoded, returnType, context) {
      const operator = encoded[0];

      /**
       * @type {Array<Expression>}
       */
      let args;
      for (let i = 0; i < validators.length; i++) {
        const parsed = validators[i](encoded, returnType, context);
        if (i == validators.length - 1) {
          if (!parsed) {
            throw new Error(
              'expected last argument validator to return the parsed args',
            );
          }
          args = parsed;
        }
      }
      return new CallExpression(returnType, operator, ...args);
    };
  }

  /**
   * @param {Array} encoded The encoded expression.
   * @param {number} returnType The expected return type of the call expression.
   * @param {ParsingContext} context The parsing context.
   * @return {Expression} The parsed expression.
   */
  function parseCallExpression(encoded, returnType, context) {
    const operator = encoded[0];

    const parser = parsers[operator];
    if (!parser) {
      throw new Error(`unknown operator: ${operator}`);
    }
    return parser(encoded, returnType, context);
  }

  /**
   * Returns a simplified geometry type suited for the `geometry-type` operator
   * @param {import('../geom/Geometry.js').default|import('../render/Feature.js').default} geometry Geometry object
   * @return {'Point'|'LineString'|'Polygon'|''} Simplified geometry type; empty string of no geometry found
   */
  function computeGeometryType(geometry) {
    if (!geometry) {
      return '';
    }
    const type = geometry.getType();
    switch (type) {
      case 'Point':
      case 'LineString':
      case 'Polygon':
        return type;
      case 'MultiPoint':
      case 'MultiLineString':
      case 'MultiPolygon':
        return /** @type {'Point'|'LineString'|'Polygon'} */ (type.substring(5));
      case 'Circle':
        return 'Polygon';
      case 'GeometryCollection':
        return computeGeometryType(
          /** @type {import("../geom/GeometryCollection.js").default} */ (
            geometry
          ).getGeometries()[0],
        );
      default:
        return '';
    }
  }

  /**
   * @module ol/expr/cpu
   */


  /**
   * @fileoverview This module includes functions to build expressions for evaluation on the CPU.
   * Building is composed of two steps: parsing and compiling.  The parsing step takes an encoded
   * expression and returns an instance of one of the expression classes.  The compiling step takes
   * the expression instance and returns a function that can be evaluated in to return a literal
   * value.  The evaluator function should do as little allocation and work as possible.
   */

  /**
   * @typedef {Object} EvaluationContext
   * @property {Object} properties The values for properties used in 'get' expressions.
   * @property {Object} variables The values for variables used in 'var' expressions.
   * @property {number} resolution The map resolution.
   * @property {string|number|null} featureId The feature id.
   * @property {string} geometryType Geometry type of the current object.
   */

  /**
   * @return {EvaluationContext} A new evaluation context.
   */
  function newEvaluationContext() {
    return {
      variables: {},
      properties: {},
      resolution: NaN,
      featureId: null,
      geometryType: '',
    };
  }

  /**
   * @typedef {function(EvaluationContext):import("./expression.js").LiteralValue} ExpressionEvaluator
   */

  /**
   * @typedef {function(EvaluationContext):boolean} BooleanEvaluator
   */

  /**
   * @typedef {function(EvaluationContext):number} NumberEvaluator
   */

  /**
   * @typedef {function(EvaluationContext):string} StringEvaluator
   */

  /**
   * @typedef {function(EvaluationContext):(Array<number>|string)} ColorLikeEvaluator
   */

  /**
   * @typedef {function(EvaluationContext):Array<number>} NumberArrayEvaluator
   */

  /**
   * @typedef {function(EvaluationContext):Array<number>} CoordinateEvaluator
   */

  /**
   * @typedef {function(EvaluationContext):(Array<number>)} SizeEvaluator
   */

  /**
   * @typedef {function(EvaluationContext):(Array<number>|number)} SizeLikeEvaluator
   */

  /**
   * @param {import('./expression.js').EncodedExpression} encoded The encoded expression.
   * @param {number} type The expected type.
   * @param {import('./expression.js').ParsingContext} context The parsing context.
   * @return {ExpressionEvaluator} The expression evaluator.
   */
  function buildExpression(encoded, type, context) {
    const expression = parse(encoded, type, context);
    return compileExpression(expression);
  }

  /**
   * @param {import("./expression.js").Expression} expression The expression.
   * @param {import('./expression.js').ParsingContext} context The parsing context.
   * @return {ExpressionEvaluator} The evaluator function.
   */
  function compileExpression(expression, context) {
    if (expression instanceof LiteralExpression) {
      // convert colors to array if possible
      if (expression.type === ColorType && typeof expression.value === 'string') {
        const colorValue = fromString(expression.value);
        return function () {
          return colorValue;
        };
      }
      return function () {
        return expression.value;
      };
    }
    const operator = expression.operator;
    switch (operator) {
      case Ops.Number:
      case Ops.String:
      case Ops.Coalesce: {
        return compileAssertionExpression(expression);
      }
      case Ops.Get:
      case Ops.Var:
      case Ops.Has: {
        return compileAccessorExpression(expression);
      }
      case Ops.Id: {
        return (context) => context.featureId;
      }
      case Ops.GeometryType: {
        return (context) => context.geometryType;
      }
      case Ops.Concat: {
        const args = expression.args.map((e) => compileExpression(e));
        return (context) =>
          ''.concat(...args.map((arg) => arg(context).toString()));
      }
      case Ops.Resolution: {
        return (context) => context.resolution;
      }
      case Ops.Any:
      case Ops.All:
      case Ops.Between:
      case Ops.In:
      case Ops.Not: {
        return compileLogicalExpression(expression);
      }
      case Ops.Equal:
      case Ops.NotEqual:
      case Ops.LessThan:
      case Ops.LessThanOrEqualTo:
      case Ops.GreaterThan:
      case Ops.GreaterThanOrEqualTo: {
        return compileComparisonExpression(expression);
      }
      case Ops.Multiply:
      case Ops.Divide:
      case Ops.Add:
      case Ops.Subtract:
      case Ops.Clamp:
      case Ops.Mod:
      case Ops.Pow:
      case Ops.Abs:
      case Ops.Floor:
      case Ops.Ceil:
      case Ops.Round:
      case Ops.Sin:
      case Ops.Cos:
      case Ops.Atan:
      case Ops.Sqrt: {
        return compileNumericExpression(expression);
      }
      case Ops.Case: {
        return compileCaseExpression(expression);
      }
      case Ops.Match: {
        return compileMatchExpression(expression);
      }
      case Ops.Interpolate: {
        return compileInterpolateExpression(expression);
      }
      case Ops.ToString: {
        return compileConvertExpression(expression);
      }
      default: {
        throw new Error(`Unsupported operator ${operator}`);
      }
      // TODO: unimplemented
      // Ops.Zoom
      // Ops.Time
      // Ops.Array
      // Ops.Color
      // Ops.Band
      // Ops.Palette
    }
  }

  /**
   * @param {import('./expression.js').CallExpression} expression The call expression.
   * @param {import('./expression.js').ParsingContext} context The parsing context.
   * @return {ExpressionEvaluator} The evaluator function.
   */
  function compileAssertionExpression(expression, context) {
    const type = expression.operator;
    const length = expression.args.length;

    const args = new Array(length);
    for (let i = 0; i < length; ++i) {
      args[i] = compileExpression(expression.args[i]);
    }
    switch (type) {
      case Ops.Coalesce: {
        return (context) => {
          for (let i = 0; i < length; ++i) {
            const value = args[i](context);
            if (typeof value !== 'undefined' && value !== null) {
              return value;
            }
          }
          throw new Error('Expected one of the values to be non-null');
        };
      }
      case Ops.Number:
      case Ops.String: {
        return (context) => {
          for (let i = 0; i < length; ++i) {
            const value = args[i](context);
            if (typeof value === type) {
              return value;
            }
          }
          throw new Error(`Expected one of the values to be a ${type}`);
        };
      }
      default: {
        throw new Error(`Unsupported assertion operator ${type}`);
      }
    }
  }

  /**
   * @param {import('./expression.js').CallExpression} expression The call expression.
   * @param {import('./expression.js').ParsingContext} context The parsing context.
   * @return {ExpressionEvaluator} The evaluator function.
   */
  function compileAccessorExpression(expression, context) {
    const nameExpression = /** @type {LiteralExpression} */ (expression.args[0]);
    const name = /** @type {string} */ (nameExpression.value);
    switch (expression.operator) {
      case Ops.Get: {
        return (context) => {
          const args = expression.args;
          let value = context.properties[name];
          for (let i = 1, ii = args.length; i < ii; ++i) {
            const keyExpression = /** @type {LiteralExpression} */ (args[i]);
            const key = /** @type {string|number} */ (keyExpression.value);
            value = value[key];
          }
          return value;
        };
      }
      case Ops.Var: {
        return (context) => context.variables[name];
      }
      case Ops.Has: {
        return (context) => {
          const args = expression.args;
          if (!(name in context.properties)) {
            return false;
          }
          let value = context.properties[name];
          for (let i = 1, ii = args.length; i < ii; ++i) {
            const keyExpression = /** @type {LiteralExpression} */ (args[i]);
            const key = /** @type {string|number} */ (keyExpression.value);
            if (!value || !Object.hasOwn(value, key)) {
              return false;
            }
            value = value[key];
          }
          return true;
        };
      }
      default: {
        throw new Error(`Unsupported accessor operator ${expression.operator}`);
      }
    }
  }

  /**
   * @param {import('./expression.js').CallExpression} expression The call expression.
   * @param {import('./expression.js').ParsingContext} context The parsing context.
   * @return {BooleanEvaluator} The evaluator function.
   */
  function compileComparisonExpression(expression, context) {
    const op = expression.operator;
    const left = compileExpression(expression.args[0]);
    const right = compileExpression(expression.args[1]);
    switch (op) {
      case Ops.Equal: {
        return (context) => left(context) === right(context);
      }
      case Ops.NotEqual: {
        return (context) => left(context) !== right(context);
      }
      case Ops.LessThan: {
        return (context) => left(context) < right(context);
      }
      case Ops.LessThanOrEqualTo: {
        return (context) => left(context) <= right(context);
      }
      case Ops.GreaterThan: {
        return (context) => left(context) > right(context);
      }
      case Ops.GreaterThanOrEqualTo: {
        return (context) => left(context) >= right(context);
      }
      default: {
        throw new Error(`Unsupported comparison operator ${op}`);
      }
    }
  }

  /**
   * @param {import('./expression.js').CallExpression} expression The call expression.
   * @param {import('./expression.js').ParsingContext} context The parsing context.
   * @return {BooleanEvaluator} The evaluator function.
   */
  function compileLogicalExpression(expression, context) {
    const op = expression.operator;
    const length = expression.args.length;

    const args = new Array(length);
    for (let i = 0; i < length; ++i) {
      args[i] = compileExpression(expression.args[i]);
    }
    switch (op) {
      case Ops.Any: {
        return (context) => {
          for (let i = 0; i < length; ++i) {
            if (args[i](context)) {
              return true;
            }
          }
          return false;
        };
      }
      case Ops.All: {
        return (context) => {
          for (let i = 0; i < length; ++i) {
            if (!args[i](context)) {
              return false;
            }
          }
          return true;
        };
      }
      case Ops.Between: {
        return (context) => {
          const value = args[0](context);
          const min = args[1](context);
          const max = args[2](context);
          return value >= min && value <= max;
        };
      }
      case Ops.In: {
        return (context) => {
          const value = args[0](context);
          for (let i = 1; i < length; ++i) {
            if (value === args[i](context)) {
              return true;
            }
          }
          return false;
        };
      }
      case Ops.Not: {
        return (context) => !args[0](context);
      }
      default: {
        throw new Error(`Unsupported logical operator ${op}`);
      }
    }
  }

  /**
   * @param {import('./expression.js').CallExpression} expression The call expression.
   * @param {import('./expression.js').ParsingContext} context The parsing context.
   * @return {NumberEvaluator} The evaluator function.
   */
  function compileNumericExpression(expression, context) {
    const op = expression.operator;
    const length = expression.args.length;

    const args = new Array(length);
    for (let i = 0; i < length; ++i) {
      args[i] = compileExpression(expression.args[i]);
    }
    switch (op) {
      case Ops.Multiply: {
        return (context) => {
          let value = 1;
          for (let i = 0; i < length; ++i) {
            value *= args[i](context);
          }
          return value;
        };
      }
      case Ops.Divide: {
        return (context) => args[0](context) / args[1](context);
      }
      case Ops.Add: {
        return (context) => {
          let value = 0;
          for (let i = 0; i < length; ++i) {
            value += args[i](context);
          }
          return value;
        };
      }
      case Ops.Subtract: {
        return (context) => args[0](context) - args[1](context);
      }
      case Ops.Clamp: {
        return (context) => {
          const value = args[0](context);
          const min = args[1](context);
          if (value < min) {
            return min;
          }
          const max = args[2](context);
          if (value > max) {
            return max;
          }
          return value;
        };
      }
      case Ops.Mod: {
        return (context) => args[0](context) % args[1](context);
      }
      case Ops.Pow: {
        return (context) => Math.pow(args[0](context), args[1](context));
      }
      case Ops.Abs: {
        return (context) => Math.abs(args[0](context));
      }
      case Ops.Floor: {
        return (context) => Math.floor(args[0](context));
      }
      case Ops.Ceil: {
        return (context) => Math.ceil(args[0](context));
      }
      case Ops.Round: {
        return (context) => Math.round(args[0](context));
      }
      case Ops.Sin: {
        return (context) => Math.sin(args[0](context));
      }
      case Ops.Cos: {
        return (context) => Math.cos(args[0](context));
      }
      case Ops.Atan: {
        if (length === 2) {
          return (context) => Math.atan2(args[0](context), args[1](context));
        }
        return (context) => Math.atan(args[0](context));
      }
      case Ops.Sqrt: {
        return (context) => Math.sqrt(args[0](context));
      }
      default: {
        throw new Error(`Unsupported numeric operator ${op}`);
      }
    }
  }

  /**
   * @param {import('./expression.js').CallExpression} expression The call expression.
   * @param {import('./expression.js').ParsingContext} context The parsing context.
   * @return {ExpressionEvaluator} The evaluator function.
   */
  function compileCaseExpression(expression, context) {
    const length = expression.args.length;
    const args = new Array(length);
    for (let i = 0; i < length; ++i) {
      args[i] = compileExpression(expression.args[i]);
    }
    return (context) => {
      for (let i = 0; i < length - 1; i += 2) {
        const condition = args[i](context);
        if (condition) {
          return args[i + 1](context);
        }
      }
      return args[length - 1](context);
    };
  }

  /**
   * @param {import('./expression.js').CallExpression} expression The call expression.
   * @param {import('./expression.js').ParsingContext} context The parsing context.
   * @return {ExpressionEvaluator} The evaluator function.
   */
  function compileMatchExpression(expression, context) {
    const length = expression.args.length;
    const args = new Array(length);
    for (let i = 0; i < length; ++i) {
      args[i] = compileExpression(expression.args[i]);
    }
    return (context) => {
      const value = args[0](context);
      for (let i = 1; i < length - 1; i += 2) {
        if (value === args[i](context)) {
          return args[i + 1](context);
        }
      }
      return args[length - 1](context);
    };
  }

  /**
   * @param {import('./expression.js').CallExpression} expression The call expression.
   * @param {import('./expression.js').ParsingContext} context The parsing context.
   * @return {ExpressionEvaluator} The evaluator function.
   */
  function compileInterpolateExpression(expression, context) {
    const length = expression.args.length;
    const args = new Array(length);
    for (let i = 0; i < length; ++i) {
      args[i] = compileExpression(expression.args[i]);
    }
    return (context) => {
      const base = args[0](context);
      const value = args[1](context);

      let previousInput;
      let previousOutput;
      for (let i = 2; i < length; i += 2) {
        const input = args[i](context);
        let output = args[i + 1](context);
        const isColor = Array.isArray(output);
        if (isColor) {
          output = withAlpha(output);
        }
        if (input >= value) {
          if (i === 2) {
            return output;
          }
          if (isColor) {
            return interpolateColor(
              base,
              value,
              previousInput,
              previousOutput,
              input,
              output,
            );
          }
          return interpolateNumber(
            base,
            value,
            previousInput,
            previousOutput,
            input,
            output,
          );
        }
        previousInput = input;
        previousOutput = output;
      }
      return previousOutput;
    };
  }

  /**
   * @param {import('./expression.js').CallExpression} expression The call expression.
   * @param {import('./expression.js').ParsingContext} context The parsing context.
   * @return {ExpressionEvaluator} The evaluator function.
   */
  function compileConvertExpression(expression, context) {
    const op = expression.operator;
    const length = expression.args.length;

    const args = new Array(length);
    for (let i = 0; i < length; ++i) {
      args[i] = compileExpression(expression.args[i]);
    }
    switch (op) {
      case Ops.ToString: {
        return (context) => {
          const value = args[0](context);
          if (expression.args[0].type === ColorType) {
            return toString(value);
          }
          return value.toString();
        };
      }
      default: {
        throw new Error(`Unsupported convert operator ${op}`);
      }
    }
  }

  /**
   * @param {number} base The base.
   * @param {number} value The value.
   * @param {number} input1 The first input value.
   * @param {number} output1 The first output value.
   * @param {number} input2 The second input value.
   * @param {number} output2 The second output value.
   * @return {number} The interpolated value.
   */
  function interpolateNumber(base, value, input1, output1, input2, output2) {
    const delta = input2 - input1;
    if (delta === 0) {
      return output1;
    }
    const along = value - input1;
    const factor =
      base === 1
        ? along / delta
        : (Math.pow(base, along) - 1) / (Math.pow(base, delta) - 1);
    return output1 + factor * (output2 - output1);
  }

  /**
   * @param {number} base The base.
   * @param {number} value The value.
   * @param {number} input1 The first input value.
   * @param {import('../color.js').Color} rgba1 The first output value.
   * @param {number} input2 The second input value.
   * @param {import('../color.js').Color} rgba2 The second output value.
   * @return {import('../color.js').Color} The interpolated color.
   */
  function interpolateColor(base, value, input1, rgba1, input2, rgba2) {
    const delta = input2 - input1;
    if (delta === 0) {
      return rgba1;
    }
    const lcha1 = rgbaToLcha(rgba1);
    const lcha2 = rgbaToLcha(rgba2);
    let deltaHue = lcha2[2] - lcha1[2];
    if (deltaHue > 180) {
      deltaHue -= 360;
    } else if (deltaHue < -180) {
      deltaHue += 360;
    }

    const lcha = [
      interpolateNumber(base, value, input1, lcha1[0], input2, lcha2[0]),
      interpolateNumber(base, value, input1, lcha1[1], input2, lcha2[1]),
      lcha1[2] + interpolateNumber(base, value, input1, 0, input2, deltaHue),
      interpolateNumber(base, value, input1, rgba1[3], input2, rgba2[3]),
    ];
    return lchaToRgba(lcha);
  }

  /**
   * @module ol/render/canvas/style
   */


  /**
   * @fileoverview This module includes functions to build styles for the canvas renderer.  Building
   * is composed of two steps: parsing and compiling.  The parsing step takes an encoded expression
   * and returns an instance of one of the expression classes.  The compiling step takes the
   * expression instance and returns a function that can be evaluated to return a literal value.  The
   * evaluator function should do as little allocation and work as possible.
   */

  /**
   * @typedef {import("../../style/flat.js").FlatStyle} FlatStyle
   */

  /**
   * @typedef {import("../../expr/expression.js").EncodedExpression} EncodedExpression
   */

  /**
   * @typedef {import("../../expr/expression.js").ParsingContext} ParsingContext
   */

  /**
   * @typedef {import("../../expr/expression.js").CallExpression} CallExpression
   */

  /**
   * @typedef {import("../../expr/cpu.js").EvaluationContext} EvaluationContext
   */

  /**
   * @typedef {import("../../expr/cpu.js").ExpressionEvaluator} ExpressionEvaluator
   */

  /**
   * @param {EvaluationContext} context The evaluation context.
   * @return {boolean} Always true.
   */
  function always$1(context) {
    return true;
  }

  /**
   * This function adapts a rule evaluator to the existing style function interface.
   * After we have deprecated the style function, we can use the compiled rules directly
   * and pass a more complete evaluation context (variables, zoom, time, etc.).
   *
   * @param {Array<import('../../style/flat.js').Rule>} rules The rules.
   * @return {import('../../style/Style.js').StyleFunction} A style function.
   */
  function rulesToStyleFunction(rules) {
    const parsingContext = newParsingContext();
    const evaluator = buildRuleSet(rules, parsingContext);
    const evaluationContext = newEvaluationContext();
    return function (feature, resolution) {
      evaluationContext.properties = feature.getPropertiesInternal();
      evaluationContext.resolution = resolution;
      if (parsingContext.featureId) {
        const id = feature.getId();
        if (id !== undefined) {
          evaluationContext.featureId = id;
        } else {
          evaluationContext.featureId = null;
        }
      }
      if (parsingContext.geometryType) {
        evaluationContext.geometryType = computeGeometryType(
          feature.getGeometry(),
        );
      }
      return evaluator(evaluationContext);
    };
  }

  /**
   * This function adapts a style evaluator to the existing style function interface.
   * After we have deprecated the style function, we can use the compiled rules directly
   * and pass a more complete evaluation context (variables, zoom, time, etc.).
   *
   * @param {Array<import('../../style/flat.js').FlatStyle>} flatStyles The flat styles.
   * @return {import('../../style/Style.js').StyleFunction} A style function.
   */
  function flatStylesToStyleFunction(flatStyles) {
    const parsingContext = newParsingContext();
    const length = flatStyles.length;

    /**
     * @type {Array<StyleEvaluator>}
     */
    const evaluators = new Array(length);
    for (let i = 0; i < length; ++i) {
      evaluators[i] = buildStyle(flatStyles[i], parsingContext);
    }
    const evaluationContext = newEvaluationContext();

    /**
     * @type {Array<Style>}
     */
    const styles = new Array(length);

    return function (feature, resolution) {
      evaluationContext.properties = feature.getPropertiesInternal();
      evaluationContext.resolution = resolution;
      if (parsingContext.featureId) {
        const id = feature.getId();
        if (id !== undefined) {
          evaluationContext.featureId = id;
        } else {
          evaluationContext.featureId = null;
        }
      }
      let nonNullCount = 0;
      for (let i = 0; i < length; ++i) {
        const style = evaluators[i](evaluationContext);
        if (style) {
          styles[nonNullCount] = style;
          nonNullCount += 1;
        }
      }
      styles.length = nonNullCount;
      return styles;
    };
  }

  /**
   * @typedef {function(EvaluationContext):Array<Style>} RuleSetEvaluator
   */

  /**
   * @typedef {Object} CompiledRule
   * @property {ExpressionEvaluator} filter The compiled filter evaluator.
   * @property {Array<StyleEvaluator>} styles The list of compiled style evaluators.
   */

  /**
   * @param {Array<import('../../style/flat.js').Rule>} rules The rules.
   * @param {ParsingContext} context The parsing context.
   * @return {RuleSetEvaluator} The evaluator function.
   */
  function buildRuleSet(rules, context) {
    const length = rules.length;

    /**
     * @type {Array<CompiledRule>}
     */
    const compiledRules = new Array(length);

    for (let i = 0; i < length; ++i) {
      const rule = rules[i];
      const filter =
        'filter' in rule
          ? buildExpression(rule.filter, BooleanType, context)
          : always$1;

      /**
       * @type {Array<StyleEvaluator>}
       */
      let styles;
      if (Array.isArray(rule.style)) {
        const styleLength = rule.style.length;
        styles = new Array(styleLength);
        for (let j = 0; j < styleLength; ++j) {
          styles[j] = buildStyle(rule.style[j], context);
        }
      } else {
        styles = [buildStyle(rule.style, context)];
      }

      compiledRules[i] = {filter, styles};
    }

    return function (context) {
      /**
       * @type {Array<Style>}
       */
      const styles = [];

      let someMatched = false;
      for (let i = 0; i < length; ++i) {
        const filterEvaluator = compiledRules[i].filter;
        if (!filterEvaluator(context)) {
          continue;
        }
        if (rules[i].else && someMatched) {
          continue;
        }
        someMatched = true;
        for (const styleEvaluator of compiledRules[i].styles) {
          const style = styleEvaluator(context);
          if (!style) {
            continue;
          }
          styles.push(style);
        }
      }

      return styles;
    };
  }

  /**
   * @typedef {function(EvaluationContext):Style|null} StyleEvaluator
   */

  /**
   * @param {FlatStyle} flatStyle A flat style literal.
   * @param {ParsingContext} context The parsing context.
   * @return {StyleEvaluator} A function that evaluates to a style.  The style returned by
   * this function will be reused between invocations.
   */
  function buildStyle(flatStyle, context) {
    const evaluateFill = buildFill(flatStyle, '', context);
    const evaluateStroke = buildStroke(flatStyle, '', context);
    const evaluateText = buildText(flatStyle, context);
    const evaluateImage = buildImage(flatStyle, context);
    const evaluateZIndex = numberEvaluator(flatStyle, 'z-index', context);

    if (
      !evaluateFill &&
      !evaluateStroke &&
      !evaluateText &&
      !evaluateImage &&
      !isEmpty$1(flatStyle)
    ) {
      // assume this is a user error
      // would be nice to check the properties and suggest "did you mean..."
      throw new Error(
        'No fill, stroke, point, or text symbolizer properties in style: ' +
          JSON.stringify(flatStyle),
      );
    }

    const style = new Style();
    return function (context) {
      let empty = true;
      if (evaluateFill) {
        const fill = evaluateFill(context);
        if (fill) {
          empty = false;
        }
        style.setFill(fill);
      }
      if (evaluateStroke) {
        const stroke = evaluateStroke(context);
        if (stroke) {
          empty = false;
        }
        style.setStroke(stroke);
      }
      if (evaluateText) {
        const text = evaluateText(context);
        if (text) {
          empty = false;
        }
        style.setText(text);
      }
      if (evaluateImage) {
        const image = evaluateImage(context);
        if (image) {
          empty = false;
        }
        style.setImage(image);
      }
      if (evaluateZIndex) {
        style.setZIndex(evaluateZIndex(context));
      }
      if (empty) {
        return null;
      }
      return style;
    };
  }

  /**
   * @typedef {function(EvaluationContext):Fill|null} FillEvaluator
   */

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} prefix The property prefix.
   * @param {ParsingContext} context The parsing context.
   * @return {FillEvaluator?} A function that evaluates to a fill.
   */
  function buildFill(flatStyle, prefix, context) {
    let evaluateColor;
    if (prefix + 'fill-pattern-src' in flatStyle) {
      evaluateColor = patternEvaluator(flatStyle, prefix + 'fill-', context);
    } else {
      if (flatStyle[prefix + 'fill-color'] === 'none') {
        // avoids hit detection
        return (context) => null;
      }

      evaluateColor = colorLikeEvaluator(
        flatStyle,
        prefix + 'fill-color',
        context,
      );
    }
    if (!evaluateColor) {
      return null;
    }

    const fill = new Fill();
    return function (context) {
      const color = evaluateColor(context);
      if (color === NO_COLOR) {
        return null;
      }
      fill.setColor(color);
      return fill;
    };
  }

  /**
   * @typedef {function(EvaluationContext):Stroke|null} StrokeEvaluator
   */

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} prefix The property prefix.
   * @param {ParsingContext} context The parsing context.
   * @return {StrokeEvaluator?} A function the evaluates to a stroke.
   */
  function buildStroke(flatStyle, prefix, context) {
    const evaluateWidth = numberEvaluator(
      flatStyle,
      prefix + 'stroke-width',
      context,
    );

    const evaluateColor = colorLikeEvaluator(
      flatStyle,
      prefix + 'stroke-color',
      context,
    );

    if (!evaluateWidth && !evaluateColor) {
      return null;
    }

    const evaluateLineCap = stringEvaluator(
      flatStyle,
      prefix + 'stroke-line-cap',
      context,
    );

    const evaluateLineJoin = stringEvaluator(
      flatStyle,
      prefix + 'stroke-line-join',
      context,
    );

    const evaluateLineDash = numberArrayEvaluator(
      flatStyle,
      prefix + 'stroke-line-dash',
      context,
    );

    const evaluateLineDashOffset = numberEvaluator(
      flatStyle,
      prefix + 'stroke-line-dash-offset',
      context,
    );

    const evaluateMiterLimit = numberEvaluator(
      flatStyle,
      prefix + 'stroke-miter-limit',
      context,
    );

    const evaluateOffset = numberEvaluator(
      flatStyle,
      prefix + 'stroke-offset',
      context,
    );

    const stroke = new Stroke();
    return function (context) {
      if (evaluateColor) {
        const color = evaluateColor(context);
        if (color === NO_COLOR) {
          return null;
        }
        stroke.setColor(color);
      }

      if (evaluateWidth) {
        stroke.setWidth(evaluateWidth(context));
      }

      if (evaluateLineCap) {
        const lineCap = evaluateLineCap(context);
        if (lineCap !== 'butt' && lineCap !== 'round' && lineCap !== 'square') {
          throw new Error('Expected butt, round, or square line cap');
        }
        stroke.setLineCap(lineCap);
      }

      if (evaluateLineJoin) {
        const lineJoin = evaluateLineJoin(context);
        if (
          lineJoin !== 'bevel' &&
          lineJoin !== 'round' &&
          lineJoin !== 'miter'
        ) {
          throw new Error('Expected bevel, round, or miter line join');
        }
        stroke.setLineJoin(lineJoin);
      }

      if (evaluateLineDash) {
        stroke.setLineDash(evaluateLineDash(context));
      }

      if (evaluateLineDashOffset) {
        stroke.setLineDashOffset(evaluateLineDashOffset(context));
      }

      if (evaluateMiterLimit) {
        stroke.setMiterLimit(evaluateMiterLimit(context));
      }

      if (evaluateOffset) {
        stroke.setOffset(evaluateOffset(context));
      }

      return stroke;
    };
  }

  /**
   * @typedef {function(EvaluationContext):Text} TextEvaluator
   */

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {ParsingContext} context The parsing context.
   * @return {TextEvaluator?} A function that evaluates to a text symbolizer.
   */
  function buildText(flatStyle, context) {
    const prefix = 'text-';

    // Currently, an Array<string> may be used for rich text support.  This doesn't
    // work with our expression syntax where arrays of strings are interpreted as
    // call expressions.  To support rich text, we could add a 'strings' operator
    // where all the following arguments would be string values.
    const evaluateValue = stringEvaluator(flatStyle, prefix + 'value', context);
    if (!evaluateValue) {
      return null;
    }

    const evaluateFill = buildFill(flatStyle, prefix, context);

    const evaluateBackgroundFill = buildFill(
      flatStyle,
      prefix + 'background-',
      context,
    );

    const evaluateStroke = buildStroke(flatStyle, prefix, context);

    const evaluateBackgroundStroke = buildStroke(
      flatStyle,
      prefix + 'background-',
      context,
    );

    const evaluateFont = stringEvaluator(flatStyle, prefix + 'font', context);

    const evaluateMaxAngle = numberEvaluator(
      flatStyle,
      prefix + 'max-angle',
      context,
    );

    const evaluateOffsetX = numberEvaluator(
      flatStyle,
      prefix + 'offset-x',
      context,
    );

    const evaluateOffsetY = numberEvaluator(
      flatStyle,
      prefix + 'offset-y',
      context,
    );

    const evaluateOverflow = booleanEvaluator(
      flatStyle,
      prefix + 'overflow',
      context,
    );

    const evaluatePlacement = stringEvaluator(
      flatStyle,
      prefix + 'placement',
      context,
    );

    const evaluateRepeat = numberEvaluator(flatStyle, prefix + 'repeat', context);

    const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);

    const evaluateRotateWithView = booleanEvaluator(
      flatStyle,
      prefix + 'rotate-with-view',
      context,
    );

    const evaluateRotation = numberEvaluator(
      flatStyle,
      prefix + 'rotation',
      context,
    );

    const evaluateAlign = stringEvaluator(flatStyle, prefix + 'align', context);

    const evaluateJustify = stringEvaluator(
      flatStyle,
      prefix + 'justify',
      context,
    );

    const evaluateBaseline = stringEvaluator(
      flatStyle,
      prefix + 'baseline',
      context,
    );

    const evaluateKeepUpright = booleanEvaluator(
      flatStyle,
      prefix + 'keep-upright',
      context,
    );

    const evaluatePadding = numberArrayEvaluator(
      flatStyle,
      prefix + 'padding',
      context,
    );

    // The following properties are not currently settable
    const declutterMode = optionalDeclutterMode(
      flatStyle,
      prefix + 'declutter-mode',
    );

    const text = new Text({declutterMode});

    return function (context) {
      text.setText(evaluateValue(context));

      if (evaluateFill) {
        text.setFill(evaluateFill(context));
      }

      if (evaluateBackgroundFill) {
        text.setBackgroundFill(evaluateBackgroundFill(context));
      }

      if (evaluateStroke) {
        text.setStroke(evaluateStroke(context));
      }

      if (evaluateBackgroundStroke) {
        text.setBackgroundStroke(evaluateBackgroundStroke(context));
      }

      if (evaluateFont) {
        text.setFont(evaluateFont(context));
      }

      if (evaluateMaxAngle) {
        text.setMaxAngle(evaluateMaxAngle(context));
      }

      if (evaluateOffsetX) {
        text.setOffsetX(evaluateOffsetX(context));
      }

      if (evaluateOffsetY) {
        text.setOffsetY(evaluateOffsetY(context));
      }

      if (evaluateOverflow) {
        text.setOverflow(evaluateOverflow(context));
      }

      if (evaluatePlacement) {
        const placement = evaluatePlacement(context);
        if (placement !== 'point' && placement !== 'line') {
          throw new Error('Expected point or line for text-placement');
        }
        text.setPlacement(placement);
      }

      if (evaluateRepeat) {
        text.setRepeat(evaluateRepeat(context));
      }

      if (evaluateScale) {
        text.setScale(evaluateScale(context));
      }

      if (evaluateRotateWithView) {
        text.setRotateWithView(evaluateRotateWithView(context));
      }

      if (evaluateRotation) {
        text.setRotation(evaluateRotation(context));
      }

      if (evaluateAlign) {
        const textAlign = evaluateAlign(context);
        if (
          textAlign !== 'left' &&
          textAlign !== 'center' &&
          textAlign !== 'right' &&
          textAlign !== 'end' &&
          textAlign !== 'start'
        ) {
          throw new Error(
            'Expected left, right, center, start, or end for text-align',
          );
        }
        text.setTextAlign(textAlign);
      }

      if (evaluateJustify) {
        const justify = evaluateJustify(context);
        if (justify !== 'left' && justify !== 'right' && justify !== 'center') {
          throw new Error('Expected left, right, or center for text-justify');
        }
        text.setJustify(justify);
      }

      if (evaluateBaseline) {
        const textBaseline = evaluateBaseline(context);
        if (
          textBaseline !== 'bottom' &&
          textBaseline !== 'top' &&
          textBaseline !== 'middle' &&
          textBaseline !== 'alphabetic' &&
          textBaseline !== 'hanging'
        ) {
          throw new Error(
            'Expected bottom, top, middle, alphabetic, or hanging for text-baseline',
          );
        }
        text.setTextBaseline(textBaseline);
      }

      if (evaluatePadding) {
        text.setPadding(evaluatePadding(context));
      }

      if (evaluateKeepUpright) {
        text.setKeepUpright(evaluateKeepUpright(context));
      }

      return text;
    };
  }

  /**
   * @typedef {function(EvaluationContext):import("../../style/Image.js").default} ImageEvaluator
   */

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {ParsingContext} context The parsing context.
   * @return {ImageEvaluator?} A function that evaluates to an image symbolizer.
   */
  function buildImage(flatStyle, context) {
    if ('icon-src' in flatStyle) {
      return buildIcon(flatStyle, context);
    }

    if ('shape-points' in flatStyle) {
      return buildShape(flatStyle, context);
    }

    if ('circle-radius' in flatStyle) {
      return buildCircle(flatStyle, context);
    }

    return null;
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {ParsingContext} context The parsing context.
   * @return {ImageEvaluator} A function that evaluates to an image symbolizer.
   */
  function buildIcon(flatStyle, context) {
    const prefix = 'icon-';

    // required property
    const srcName = prefix + 'src';
    const src = requireString(flatStyle[srcName], srcName);

    // settable properties
    const evaluateAnchor = coordinateEvaluator(
      flatStyle,
      prefix + 'anchor',
      context,
    );

    const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);

    const evaluateOpacity = numberEvaluator(
      flatStyle,
      prefix + 'opacity',
      context,
    );

    const evaluateDisplacement = coordinateEvaluator(
      flatStyle,
      prefix + 'displacement',
      context,
    );

    const evaluateRotation = numberEvaluator(
      flatStyle,
      prefix + 'rotation',
      context,
    );

    const evaluateRotateWithView = booleanEvaluator(
      flatStyle,
      prefix + 'rotate-with-view',
      context,
    );

    // the remaining symbolizer properties are not currently settable
    const anchorOrigin = optionalIconOrigin(flatStyle, prefix + 'anchor-origin');
    const anchorXUnits = optionalIconAnchorUnits(
      flatStyle,
      prefix + 'anchor-x-units',
    );
    const anchorYUnits = optionalIconAnchorUnits(
      flatStyle,
      prefix + 'anchor-y-units',
    );
    const colorValue = getExpressionValue(flatStyle, prefix + 'color');
    let color;
    let evaluateColor = null;
    if (colorValue !== undefined) {
      const isColorExpression =
        Array.isArray(colorValue) &&
        colorValue.length > 0 &&
        typeof colorValue[0] === 'string';
      if (isColorExpression) {
        evaluateColor = colorLikeEvaluator(flatStyle, prefix + 'color', context);
      } else {
        color = requireColorLike(colorValue, prefix + 'color');
      }
    }
    const crossOrigin = optionalString(flatStyle, prefix + 'cross-origin');
    const offset = optionalNumberArray(flatStyle, prefix + 'offset');
    const offsetOrigin = optionalIconOrigin(flatStyle, prefix + 'offset-origin');
    const width = optionalNumber(flatStyle, prefix + 'width');
    const height = optionalNumber(flatStyle, prefix + 'height');
    const size = optionalSize(flatStyle, prefix + 'size');
    const declutterMode = optionalDeclutterMode(
      flatStyle,
      prefix + 'declutter-mode',
    );

    const iconOptions = {
      src,
      anchorOrigin,
      anchorXUnits,
      anchorYUnits,
      crossOrigin,
      offset,
      offsetOrigin,
      height,
      width,
      size,
      declutterMode,
    };

    let icon = null;

    return function (context) {
      if (!icon) {
        // lazily create the icon to allow for expression evaluation
        const initialColor = evaluateColor ? evaluateColor(context) : color;
        icon = new Icon(
          initialColor !== undefined
            ? Object.assign({}, iconOptions, {color: initialColor})
            : Object.assign({}, iconOptions),
        );
      } else if (evaluateColor) {
        icon.setColor(evaluateColor(context));
      }
      if (evaluateOpacity) {
        icon.setOpacity(evaluateOpacity(context));
      }

      if (evaluateDisplacement) {
        icon.setDisplacement(evaluateDisplacement(context));
      }

      if (evaluateRotation) {
        icon.setRotation(evaluateRotation(context));
      }

      if (evaluateRotateWithView) {
        icon.setRotateWithView(evaluateRotateWithView(context));
      }

      if (evaluateScale) {
        icon.setScale(evaluateScale(context));
      }

      if (evaluateAnchor) {
        icon.setAnchor(evaluateAnchor(context));
      }
      return icon;
    };
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {ParsingContext} context The parsing context.
   * @return {ImageEvaluator} A function that evaluates to an icon symbolizer.
   */
  function buildShape(flatStyle, context) {
    const prefix = 'shape-';

    // required property
    const pointsName = prefix + 'points';
    const radiusName = prefix + 'radius';
    const points = requireNumber(flatStyle[pointsName], pointsName);
    if (!(radiusName in flatStyle)) {
      throw new Error(`Expected a number for ${radiusName}`);
    }
    const evaluateRadius = numberEvaluator(flatStyle, radiusName, context);
    const initialRadius =
      typeof flatStyle[radiusName] === 'number' ? flatStyle[radiusName] : 5;
    const radius2Name = prefix + 'radius2';
    const evaluateRadius2 = numberEvaluator(flatStyle, radius2Name, context);
    const initialRadius2 =
      typeof flatStyle[radius2Name] === 'number'
        ? flatStyle[radius2Name]
        : undefined;

    // settable properties
    const evaluateFill = buildFill(flatStyle, prefix, context);
    const evaluateStroke = buildStroke(flatStyle, prefix, context);
    const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);
    const evaluateDisplacement = coordinateEvaluator(
      flatStyle,
      prefix + 'displacement',
      context,
    );
    const evaluateRotation = numberEvaluator(
      flatStyle,
      prefix + 'rotation',
      context,
    );
    const evaluateRotateWithView = booleanEvaluator(
      flatStyle,
      prefix + 'rotate-with-view',
      context,
    );

    // the remaining properties are not currently settable
    const angle = optionalNumber(flatStyle, prefix + 'angle');
    const declutterMode = optionalDeclutterMode(
      flatStyle,
      prefix + 'declutter-mode',
    );

    const shape = new RegularShape({
      points,
      radius: initialRadius,
      radius2: initialRadius2,
      angle,
      declutterMode,
    });

    return function (context) {
      if (evaluateRadius) {
        shape.setRadius(evaluateRadius(context));
      }
      if (evaluateRadius2) {
        shape.setRadius2(evaluateRadius2(context));
      }
      if (evaluateFill) {
        shape.setFill(evaluateFill(context));
      }
      if (evaluateStroke) {
        shape.setStroke(evaluateStroke(context));
      }
      if (evaluateDisplacement) {
        shape.setDisplacement(evaluateDisplacement(context));
      }
      if (evaluateRotation) {
        shape.setRotation(evaluateRotation(context));
      }
      if (evaluateRotateWithView) {
        shape.setRotateWithView(evaluateRotateWithView(context));
      }
      if (evaluateScale) {
        shape.setScale(evaluateScale(context));
      }

      return shape;
    };
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {ParsingContext} context The parsing context.
   * @return {ImageEvaluator} A function that evaluates to a circle symbolizer.
   */
  function buildCircle(flatStyle, context) {
    const prefix = 'circle-';

    // settable properties
    const evaluateFill = buildFill(flatStyle, prefix, context);
    const evaluateStroke = buildStroke(flatStyle, prefix, context);
    const evaluateRadius = numberEvaluator(flatStyle, prefix + 'radius', context);
    const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);
    const evaluateDisplacement = coordinateEvaluator(
      flatStyle,
      prefix + 'displacement',
      context,
    );
    const evaluateRotation = numberEvaluator(
      flatStyle,
      prefix + 'rotation',
      context,
    );
    const evaluateRotateWithView = booleanEvaluator(
      flatStyle,
      prefix + 'rotate-with-view',
      context,
    );

    // the remaining properties are not currently settable
    const declutterMode = optionalDeclutterMode(
      flatStyle,
      prefix + 'declutter-mode',
    );

    const circle = new CircleStyle({
      radius: 5, // this is arbitrary, but required - the evaluated radius is used below
      declutterMode,
    });

    return function (context) {
      if (evaluateRadius) {
        circle.setRadius(evaluateRadius(context));
      }
      if (evaluateFill) {
        circle.setFill(evaluateFill(context));
      }
      if (evaluateStroke) {
        circle.setStroke(evaluateStroke(context));
      }
      if (evaluateDisplacement) {
        circle.setDisplacement(evaluateDisplacement(context));
      }
      if (evaluateRotation) {
        circle.setRotation(evaluateRotation(context));
      }
      if (evaluateRotateWithView) {
        circle.setRotateWithView(evaluateRotateWithView(context));
      }
      if (evaluateScale) {
        circle.setScale(evaluateScale(context));
      }

      return circle;
    };
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} name The property name.
   * @return {any|undefined} The encoded value, or undefined if not provided.
   */
  function getExpressionValue(flatStyle, name) {
    if (!(name in flatStyle)) {
      return undefined;
    }
    const value = flatStyle[name];
    return value === undefined ? undefined : value;
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} name The property name.
   * @param {ParsingContext} context The parsing context.
   * @return {import('../../expr/cpu.js').NumberEvaluator|undefined} The expression evaluator or undefined.
   */
  function numberEvaluator(flatStyle, name, context) {
    const encoded = getExpressionValue(flatStyle, name);
    if (encoded === undefined) {
      return undefined;
    }
    const evaluator = buildExpression(encoded, NumberType, context);
    return function (context) {
      return requireNumber(evaluator(context), name);
    };
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} name The property name.
   * @param {ParsingContext} context The parsing context.
   * @return {import('../../expr/cpu.js').StringEvaluator?} The expression evaluator.
   */
  function stringEvaluator(flatStyle, name, context) {
    const encoded = getExpressionValue(flatStyle, name);
    if (encoded === undefined) {
      return null;
    }
    const evaluator = buildExpression(encoded, StringType, context);
    return function (context) {
      return requireString(evaluator(context), name);
    };
  }

  function patternEvaluator(flatStyle, prefix, context) {
    const srcEvaluator = stringEvaluator(
      flatStyle,
      prefix + 'pattern-src',
      context,
    );
    const offsetEvaluator = sizeEvaluator(
      flatStyle,
      prefix + 'pattern-offset',
      context,
    );
    const patternSizeEvaluator = sizeEvaluator(
      flatStyle,
      prefix + 'pattern-size',
      context,
    );
    const colorEvaluator = colorLikeEvaluator(
      flatStyle,
      prefix + 'color',
      context,
    );
    return function (context) {
      return {
        src: srcEvaluator(context),
        offset: offsetEvaluator && offsetEvaluator(context),
        size: patternSizeEvaluator && patternSizeEvaluator(context),
        color: colorEvaluator && colorEvaluator(context),
      };
    };
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} name The property name.
   * @param {ParsingContext} context The parsing context.
   * @return {import('../../expr/cpu.js').BooleanEvaluator?} The expression evaluator.
   */
  function booleanEvaluator(flatStyle, name, context) {
    const encoded = getExpressionValue(flatStyle, name);
    if (encoded === undefined) {
      return null;
    }
    const evaluator = buildExpression(encoded, BooleanType, context);
    return function (context) {
      const value = evaluator(context);
      if (typeof value !== 'boolean') {
        throw new Error(`Expected a boolean for ${name}`);
      }
      return value;
    };
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} name The property name.
   * @param {ParsingContext} context The parsing context.
   * @return {import('../../expr/cpu.js').ColorLikeEvaluator?} The expression evaluator.
   */
  function colorLikeEvaluator(flatStyle, name, context) {
    const encoded = getExpressionValue(flatStyle, name);
    if (encoded === undefined) {
      return null;
    }
    const evaluator = buildExpression(encoded, ColorType, context);
    return function (context) {
      return requireColorLike(evaluator(context), name);
    };
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} name The property name.
   * @param {ParsingContext} context The parsing context.
   * @return {import('../../expr/cpu.js').NumberArrayEvaluator?} The expression evaluator.
   */
  function numberArrayEvaluator(flatStyle, name, context) {
    const encoded = getExpressionValue(flatStyle, name);
    if (encoded === undefined) {
      return null;
    }
    if (
      Array.isArray(encoded) &&
      (encoded.length === 0 || typeof encoded[0] !== 'string')
    ) {
      /** @type {Array<import('../../expr/cpu.js').NumberEvaluator>} */
      const evaluators = encoded.map((value, index) => {
        if (typeof value === 'number') {
          return () => value;
        }
        const evaluator = buildExpression(value, NumberType, context);
        return function (context) {
          return requireNumber(evaluator(context), `${name}[${index}]`);
        };
      });
      return function (context) {
        const array = new Array(evaluators.length);
        for (let i = 0; i < evaluators.length; ++i) {
          array[i] = evaluators[i](context);
        }
        return array;
      };
    }
    const evaluator = buildExpression(encoded, NumberArrayType, context);
    return function (context) {
      return requireNumberArray(evaluator(context), name);
    };
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} name The property name.
   * @param {ParsingContext} context The parsing context.
   * @return {import('../../expr/cpu.js').CoordinateEvaluator?} The expression evaluator.
   */
  function coordinateEvaluator(flatStyle, name, context) {
    const encoded = getExpressionValue(flatStyle, name);
    if (encoded === undefined) {
      return null;
    }
    const evaluator = buildExpression(encoded, NumberArrayType, context);
    return function (context) {
      const array = requireNumberArray(evaluator(context), name);
      if (array.length !== 2) {
        throw new Error(`Expected two numbers for ${name}`);
      }
      return array;
    };
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} name The property name.
   * @param {ParsingContext} context The parsing context.
   * @return {import('../../expr/cpu.js').SizeEvaluator?} The expression evaluator.
   */
  function sizeEvaluator(flatStyle, name, context) {
    const encoded = getExpressionValue(flatStyle, name);
    if (encoded === undefined) {
      return null;
    }
    const evaluator = buildExpression(encoded, NumberArrayType, context);
    return function (context) {
      return requireSize(evaluator(context), name);
    };
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} name The property name.
   * @param {ParsingContext} context The parsing context.
   * @return {import('../../expr/cpu.js').SizeLikeEvaluator?} The expression evaluator.
   */
  function sizeLikeEvaluator(flatStyle, name, context) {
    const encoded = getExpressionValue(flatStyle, name);
    if (encoded === undefined) {
      return null;
    }
    const evaluator = buildExpression(
      encoded,
      NumberArrayType | NumberType,
      context,
    );
    return function (context) {
      return requireSizeLike(evaluator(context), name);
    };
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} property The symbolizer property.
   * @return {number|undefined} A number or undefined.
   */
  function optionalNumber(flatStyle, property) {
    const value = flatStyle[property];
    if (value === undefined) {
      return undefined;
    }
    if (typeof value !== 'number') {
      throw new Error(`Expected a number for ${property}`);
    }
    return value;
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} property The symbolizer property.
   * @return {import("../../size.js").Size|undefined} A size or undefined.
   */
  function optionalSize(flatStyle, property) {
    const encoded = flatStyle[property];
    if (encoded === undefined) {
      return undefined;
    }
    if (typeof encoded === 'number') {
      return toSize(encoded);
    }
    if (!Array.isArray(encoded)) {
      throw new Error(`Expected a number or size array for ${property}`);
    }
    if (
      encoded.length !== 2 ||
      typeof encoded[0] !== 'number' ||
      typeof encoded[1] !== 'number'
    ) {
      throw new Error(`Expected a number or size array for ${property}`);
    }
    return encoded;
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} property The symbolizer property.
   * @return {string|undefined} A string or undefined.
   */
  function optionalString(flatStyle, property) {
    const encoded = flatStyle[property];
    if (encoded === undefined) {
      return undefined;
    }
    if (typeof encoded !== 'string') {
      throw new Error(`Expected a string for ${property}`);
    }
    return encoded;
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} property The symbolizer property.
   * @return {import("../../style/Icon.js").IconOrigin|undefined} An icon origin or undefined.
   */
  function optionalIconOrigin(flatStyle, property) {
    const encoded = flatStyle[property];
    if (encoded === undefined) {
      return undefined;
    }
    if (
      encoded !== 'bottom-left' &&
      encoded !== 'bottom-right' &&
      encoded !== 'top-left' &&
      encoded !== 'top-right'
    ) {
      throw new Error(
        `Expected bottom-left, bottom-right, top-left, or top-right for ${property}`,
      );
    }
    return encoded;
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} property The symbolizer property.
   * @return {import("../../style/Icon.js").IconAnchorUnits|undefined} Icon anchor units or undefined.
   */
  function optionalIconAnchorUnits(flatStyle, property) {
    const encoded = flatStyle[property];
    if (encoded === undefined) {
      return undefined;
    }
    if (encoded !== 'pixels' && encoded !== 'fraction') {
      throw new Error(`Expected pixels or fraction for ${property}`);
    }
    return encoded;
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} property The symbolizer property.
   * @return {Array<number>|undefined} An array of numbers or undefined.
   */
  function optionalNumberArray(flatStyle, property) {
    const encoded = flatStyle[property];
    if (encoded === undefined) {
      return undefined;
    }
    return requireNumberArray(encoded, property);
  }

  /**
   * @param {FlatStyle} flatStyle The flat style.
   * @param {string} property The symbolizer property.
   * @return {import('../../style/Style.js').DeclutterMode} Icon declutter mode.
   */
  function optionalDeclutterMode(flatStyle, property) {
    const encoded = flatStyle[property];
    if (encoded === undefined) {
      return undefined;
    }
    if (typeof encoded !== 'string') {
      throw new Error(`Expected a string for ${property}`);
    }
    if (encoded !== 'declutter' && encoded !== 'obstacle' && encoded !== 'none') {
      throw new Error(`Expected declutter, obstacle, or none for ${property}`);
    }
    return encoded;
  }

  /**
   * @param {any} value The value.
   * @param {string} property The property.
   * @return {Array<number>} An array of numbers.
   */
  function requireNumberArray(value, property) {
    if (!Array.isArray(value)) {
      throw new Error(`Expected an array for ${property}`);
    }
    const length = value.length;
    for (let i = 0; i < length; ++i) {
      if (typeof value[i] !== 'number') {
        throw new Error(`Expected an array of numbers for ${property}`);
      }
    }
    return value;
  }

  /**
   * @param {any} value The value.
   * @param {string} property The property.
   * @return {string} A string.
   */
  function requireString(value, property) {
    if (typeof value !== 'string') {
      throw new Error(`Expected a string for ${property}`);
    }
    return value;
  }

  /**
   * @param {any} value The value.
   * @param {string} property The property.
   * @return {number} A number.
   */
  function requireNumber(value, property) {
    if (typeof value !== 'number') {
      throw new Error(`Expected a number for ${property}`);
    }
    return value;
  }

  /**
   * @param {any} value The value.
   * @param {string} property The property.
   * @return {Array<number>|string} A color.
   */
  function requireColorLike(value, property) {
    if (typeof value === 'string') {
      return value;
    }
    const array = requireNumberArray(value, property);
    const length = array.length;
    if (length < 3 || length > 4) {
      throw new Error(`Expected a color with 3 or 4 values for ${property}`);
    }
    return array;
  }

  /**
   * @param {any} value The value.
   * @param {string} property The property.
   * @return {Array<number>} A number or an array of two numbers.
   */
  function requireSize(value, property) {
    const size = requireNumberArray(value, property);
    if (size.length !== 2) {
      throw new Error(`Expected an array of two numbers for ${property}`);
    }
    return size;
  }

  /**
   * @param {any} value The value.
   * @param {string} property The property.
   * @return {number|Array<number>} A number or an array of two numbers.
   */
  function requireSizeLike(value, property) {
    if (typeof value === 'number') {
      return value;
    }
    return requireSize(value, property);
  }

  /**
   * @module ol/ViewProperty
   */

  /**
   * @enum {string}
   */
  var ViewProperty = {
    CENTER: 'center',
    RESOLUTION: 'resolution',
    ROTATION: 'rotation',
  };

  /**
   * @module ol/centerconstraint
   */

  /**
   * @typedef {function((import("./coordinate.js").Coordinate|undefined), number, import("./size.js").Size, boolean=, Array<number>=): (import("./coordinate.js").Coordinate|undefined)} Type
   */

  /**
   * @param {import("./extent.js").Extent} extent Extent.
   * @param {boolean} onlyCenter If true, the constraint will only apply to the view center.
   * @param {boolean} smooth If true, the view will be able to go slightly out of the given extent
   * (only during interaction and animation).
   * @return {Type} The constraint.
   */
  function createExtent(extent, onlyCenter, smooth) {
    return (
      /**
       * @param {import("./coordinate.js").Coordinate|undefined} center Center.
       * @param {number|undefined} resolution Resolution.
       * @param {import("./size.js").Size} size Viewport size; unused if `onlyCenter` was specified.
       * @param {boolean} [isMoving] True if an interaction or animation is in progress.
       * @param {Array<number>} [centerShift] Shift between map center and viewport center.
       * @return {import("./coordinate.js").Coordinate|undefined} Center.
       */
      function (center, resolution, size, isMoving, centerShift) {
        if (!center) {
          return undefined;
        }
        if (!resolution && !onlyCenter) {
          return center;
        }
        const viewWidth = onlyCenter ? 0 : size[0] * resolution;
        const viewHeight = onlyCenter ? 0 : size[1] * resolution;
        const shiftX = centerShift ? centerShift[0] : 0;
        const shiftY = centerShift ? centerShift[1] : 0;
        let minX = extent[0] + viewWidth / 2 + shiftX;
        let maxX = extent[2] - viewWidth / 2 + shiftX;
        let minY = extent[1] + viewHeight / 2 + shiftY;
        let maxY = extent[3] - viewHeight / 2 + shiftY;

        // note: when zooming out of bounds, min and max values for x and y may
        // end up inverted (min > max); this has to be accounted for
        if (minX > maxX) {
          minX = (maxX + minX) / 2;
          maxX = minX;
        }
        if (minY > maxY) {
          minY = (maxY + minY) / 2;
          maxY = minY;
        }

        let x = clamp(center[0], minX, maxX);
        let y = clamp(center[1], minY, maxY);

        // during an interaction, allow some overscroll
        if (isMoving && smooth && resolution) {
          const ratio = 30 * resolution;
          x +=
            -ratio * Math.log(1 + Math.max(0, minX - center[0]) / ratio) +
            ratio * Math.log(1 + Math.max(0, center[0] - maxX) / ratio);
          y +=
            -ratio * Math.log(1 + Math.max(0, minY - center[1]) / ratio) +
            ratio * Math.log(1 + Math.max(0, center[1] - maxY) / ratio);
        }

        return [x, y];
      }
    );
  }

  /**
   * @param {import("./coordinate.js").Coordinate} [center] Center.
   * @return {import("./coordinate.js").Coordinate|undefined} Center.
   */
  function none$1(center) {
    return center;
  }

  /**
   * @module ol/easing
   */

  /**
   * Start slow and speed up.
   * @param {number} t Input between 0 and 1.
   * @return {number} Output between 0 and 1.
   * @api
   */
  function easeIn(t) {
    return Math.pow(t, 3);
  }

  /**
   * Start fast and slow down.
   * @param {number} t Input between 0 and 1.
   * @return {number} Output between 0 and 1.
   * @api
   */
  function easeOut(t) {
    return 1 - easeIn(1 - t);
  }

  /**
   * Start slow, speed up, and then slow down again.
   * @param {number} t Input between 0 and 1.
   * @return {number} Output between 0 and 1.
   * @api
   */
  function inAndOut(t) {
    return 3 * t * t - 2 * t * t * t;
  }

  /**
   * Maintain a constant speed over time.
   * @param {number} t Input between 0 and 1.
   * @return {number} Output between 0 and 1.
   * @api
   */
  function linear(t) {
    return t;
  }

  /**
   * @module ol/resolutionconstraint
   */

  /**
   * @typedef {function((number|undefined), number, import("./size.js").Size, boolean=): (number|undefined)} Type
   */

  /**
   * Returns a modified resolution taking into account the viewport size and maximum
   * allowed extent.
   * @param {number} resolution Resolution
   * @param {import("./extent.js").Extent} maxExtent Maximum allowed extent.
   * @param {import("./size.js").Size} viewportSize Viewport size.
   * @param {boolean} showFullExtent Whether to show the full extent.
   * @return {number} Capped resolution.
   */
  function getViewportClampedResolution(
    resolution,
    maxExtent,
    viewportSize,
    showFullExtent,
  ) {
    const xResolution = getWidth(maxExtent) / viewportSize[0];
    const yResolution = getHeight(maxExtent) / viewportSize[1];

    if (showFullExtent) {
      return Math.min(resolution, Math.max(xResolution, yResolution));
    }
    return Math.min(resolution, Math.min(xResolution, yResolution));
  }

  /**
   * Returns a modified resolution to be between maxResolution and minResolution while
   * still allowing the value to be slightly out of bounds.
   * Note: the computation is based on the logarithm function (ln):
   *  - at 1, ln(x) is 0
   *  - above 1, ln(x) keeps increasing but at a much slower pace than x
   * The final result is clamped to prevent getting too far away from bounds.
   * @param {number} resolution Resolution.
   * @param {number} maxResolution Max resolution.
   * @param {number} minResolution Min resolution.
   * @return {number} Smoothed resolution.
   */
  function getSmoothClampedResolution(resolution, maxResolution, minResolution) {
    let result = Math.min(resolution, maxResolution);
    const ratio = 50;

    result *=
      Math.log(1 + ratio * Math.max(0, resolution / maxResolution - 1)) / ratio +
      1;
    if (minResolution) {
      result = Math.max(result, minResolution);
      result /=
        Math.log(1 + ratio * Math.max(0, minResolution / resolution - 1)) /
          ratio +
        1;
    }
    return clamp(result, minResolution / 2, maxResolution * 2);
  }

  /**
   * @param {Array<number>} resolutions Resolutions.
   * @param {boolean} [smooth] If true, the view will be able to slightly exceed resolution limits. Default: true.
   * @param {import("./extent.js").Extent} [maxExtent] Maximum allowed extent.
   * @param {boolean} [showFullExtent] If true, allows us to show the full extent. Default: false.
   * @return {Type} Zoom function.
   */
  function createSnapToResolutions(
    resolutions,
    smooth,
    maxExtent,
    showFullExtent,
  ) {
    smooth = smooth !== undefined ? smooth : true;
    return (
      /**
       * @param {number|undefined} resolution Resolution.
       * @param {number} direction Direction.
       * @param {import("./size.js").Size} size Viewport size.
       * @param {boolean} [isMoving] True if an interaction or animation is in progress.
       * @return {number|undefined} Resolution.
       */
      function (resolution, direction, size, isMoving) {
        if (resolution !== undefined) {
          const maxResolution = resolutions[0];
          const minResolution = resolutions[resolutions.length - 1];
          const cappedMaxRes = maxExtent
            ? getViewportClampedResolution(
                maxResolution,
                maxExtent,
                size,
                showFullExtent,
              )
            : maxResolution;

          // during interacting or animating, allow intermediary values
          if (isMoving) {
            if (!smooth) {
              return clamp(resolution, minResolution, cappedMaxRes);
            }
            return getSmoothClampedResolution(
              resolution,
              cappedMaxRes,
              minResolution,
            );
          }

          const capped = Math.min(cappedMaxRes, resolution);
          const z = Math.floor(linearFindNearest(resolutions, capped, direction));
          if (resolutions[z] > cappedMaxRes && z < resolutions.length - 1) {
            return resolutions[z + 1];
          }
          return resolutions[z];
        }
        return undefined;
      }
    );
  }

  /**
   * @param {number} power Power.
   * @param {number} maxResolution Maximum resolution.
   * @param {number} [minResolution] Minimum resolution.
   * @param {boolean} [smooth] If true, the view will be able to slightly exceed resolution limits. Default: true.
   * @param {import("./extent.js").Extent} [maxExtent] Maximum allowed extent.
   * @param {boolean} [showFullExtent] If true, allows us to show the full extent. Default: false.
   * @return {Type} Zoom function.
   */
  function createSnapToPower(
    power,
    maxResolution,
    minResolution,
    smooth,
    maxExtent,
    showFullExtent,
  ) {
    smooth = smooth !== undefined ? smooth : true;
    minResolution = minResolution !== undefined ? minResolution : 0;

    return (
      /**
       * @param {number|undefined} resolution Resolution.
       * @param {number} direction Direction.
       * @param {import("./size.js").Size} size Viewport size.
       * @param {boolean} [isMoving] True if an interaction or animation is in progress.
       * @return {number|undefined} Resolution.
       */
      function (resolution, direction, size, isMoving) {
        if (resolution !== undefined) {
          const cappedMaxRes = maxExtent
            ? getViewportClampedResolution(
                maxResolution,
                maxExtent,
                size,
                showFullExtent,
              )
            : maxResolution;

          // during interacting or animating, allow intermediary values
          if (isMoving) {
            if (!smooth) {
              return clamp(resolution, minResolution, cappedMaxRes);
            }
            return getSmoothClampedResolution(
              resolution,
              cappedMaxRes,
              minResolution,
            );
          }

          const tolerance = 1e-9;
          const minZoomLevel = Math.ceil(
            Math.log(maxResolution / cappedMaxRes) / Math.log(power) - tolerance,
          );
          const offset = -direction * (0.5 - tolerance) + 0.5;
          const capped = Math.min(cappedMaxRes, resolution);
          const cappedZoomLevel = Math.floor(
            Math.log(maxResolution / capped) / Math.log(power) + offset,
          );
          const zoomLevel = Math.max(minZoomLevel, cappedZoomLevel);
          const newResolution = maxResolution / Math.pow(power, zoomLevel);
          return clamp(newResolution, minResolution, cappedMaxRes);
        }
        return undefined;
      }
    );
  }

  /**
   * @param {number} maxResolution Max resolution.
   * @param {number} minResolution Min resolution.
   * @param {boolean} [smooth] If true, the view will be able to slightly exceed resolution limits. Default: true.
   * @param {import("./extent.js").Extent} [maxExtent] Maximum allowed extent.
   * @param {boolean} [showFullExtent] If true, allows us to show the full extent. Default: false.
   * @return {Type} Zoom function.
   */
  function createMinMaxResolution(
    maxResolution,
    minResolution,
    smooth,
    maxExtent,
    showFullExtent,
  ) {
    smooth = smooth !== undefined ? smooth : true;

    return (
      /**
       * @param {number|undefined} resolution Resolution.
       * @param {number} direction Direction.
       * @param {import("./size.js").Size} size Viewport size.
       * @param {boolean} [isMoving] True if an interaction or animation is in progress.
       * @return {number|undefined} Resolution.
       */
      function (resolution, direction, size, isMoving) {
        if (resolution !== undefined) {
          const cappedMaxRes = maxExtent
            ? getViewportClampedResolution(
                maxResolution,
                maxExtent,
                size,
                showFullExtent,
              )
            : maxResolution;

          if (!smooth || !isMoving) {
            return clamp(resolution, minResolution, cappedMaxRes);
          }
          return getSmoothClampedResolution(
            resolution,
            cappedMaxRes,
            minResolution,
          );
        }
        return undefined;
      }
    );
  }

  /**
   * @module ol/rotationconstraint
   */

  /**
   * @typedef {function((number|undefined), boolean=): (number|undefined)} Type
   */

  /**
   * @param {number|undefined} rotation Rotation.
   * @return {number|undefined} Rotation.
   */
  function disable(rotation) {
    if (rotation !== undefined) {
      return 0;
    }
    return undefined;
  }

  /**
   * @param {number|undefined} rotation Rotation.
   * @return {number|undefined} Rotation.
   */
  function none(rotation) {
    if (rotation !== undefined) {
      return rotation;
    }
    return undefined;
  }

  /**
   * @param {number} n N.
   * @return {Type} Rotation constraint.
   */
  function createSnapToN(n) {
    const theta = (2 * Math.PI) / n;
    return (
      /**
       * @param {number|undefined} rotation Rotation.
       * @param {boolean} [isMoving] True if an interaction or animation is in progress.
       * @return {number|undefined} Rotation.
       */
      function (rotation, isMoving) {
        if (isMoving) {
          return rotation;
        }

        if (rotation !== undefined) {
          rotation = Math.floor(rotation / theta + 0.5) * theta;
          return rotation;
        }
        return undefined;
      }
    );
  }

  /**
   * @param {number} [tolerance] Tolerance.
   * @return {Type} Rotation constraint.
   */
  function createSnapToZero(tolerance) {
    const t = toRadians(5) ;
    return (
      /**
       * @param {number|undefined} rotation Rotation.
       * @param {boolean} [isMoving] True if an interaction or animation is in progress.
       * @return {number|undefined} Rotation.
       */
      function (rotation, isMoving) {
        if (isMoving || rotation === undefined) {
          return rotation;
        }

        if (Math.abs(rotation) <= t) {
          return 0;
        }
        return rotation;
      }
    );
  }

  /**
   * @module ol/tilegrid/common
   */

  /**
   * Default maximum zoom for default tile grids.
   * @type {number}
   */
  const DEFAULT_MAX_ZOOM = 42;

  /**
   * Default tile size.
   * @type {number}
   */
  const DEFAULT_TILE_SIZE = 256;

  /**
   * @module ol/View
   */

  /**
   * An animation configuration
   *
   * @typedef {Object} Animation
   * @property {import("./coordinate.js").Coordinate} [sourceCenter] Source center.
   * @property {import("./coordinate.js").Coordinate} [targetCenter] Target center.
   * @property {number} [sourceResolution] Source resolution.
   * @property {number} [targetResolution] Target resolution.
   * @property {number} [sourceRotation] Source rotation.
   * @property {number} [targetRotation] Target rotation.
   * @property {import("./coordinate.js").Coordinate} [anchor] Anchor.
   * @property {number} start Start.
   * @property {number} duration Duration.
   * @property {boolean} complete Complete.
   * @property {function(number):number} easing Easing.
   * @property {function(boolean):void} callback Callback.
   */

  /**
   * @typedef {Object} Constraints
   * @property {import("./centerconstraint.js").Type} center Center.
   * @property {import("./resolutionconstraint.js").Type} resolution Resolution.
   * @property {import("./rotationconstraint.js").Type} rotation Rotation.
   */

  /**
   * @typedef {Object} FitOptions
   * @property {import("./size.js").Size} [size] The size in pixels of the box to
   * fit the extent into. Defaults to the size of the map the view is associated with.
   * If no map or multiple maps are connected to the view, provide the desired box size
   * (e.g. `map.getSize()`).
   * @property {!Array<number>} [padding=[0, 0, 0, 0]] Padding (in pixels) to be
   * cleared inside the view. Values in the array are top, right, bottom and left
   * padding.
   * @property {boolean} [nearest=false] If the view `constrainResolution` option is `true`,
   * get the nearest extent instead of the closest that actually fits the view.
   * @property {number} [minResolution=0] Minimum resolution that we zoom to.
   * @property {number} [maxZoom] Maximum zoom level that we zoom to. If
   * `minResolution` is given, this property is ignored.
   * @property {number} [duration] The duration of the animation in milliseconds.
   * By default, there is no animation to the target extent.
   * @property {function(number):number} [easing] The easing function used during
   * the animation (defaults to {@link module:ol/easing.inAndOut}).
   * The function will be called for each frame with a number representing a
   * fraction of the animation's duration.  The function should return a number
   * between 0 and 1 representing the progress toward the destination state.
   * @property {function(boolean):void} [callback] Function called when the view is in
   * its final position. The callback will be called with `true` if the animation
   * series completed on its own or `false` if it was cancelled.
   */

  /**
   * @typedef {Object} ViewOptions
   * @property {import("./coordinate.js").Coordinate} [center] The initial center for
   * the view. If a user projection is not set, the coordinate system for the center is
   * specified with the `projection` option. Layer sources will not be fetched if this
   * is not set, but the center can be set later with {@link #setCenter}.
   * @property {boolean|number} [constrainRotation=true] Rotation constraint.
   * `false` means no constraint. `true` means no constraint, but snap to zero
   * near zero. A number constrains the rotation to that number of values. For
   * example, `4` will constrain the rotation to 0, 90, 180, and 270 degrees.
   * @property {boolean} [enableRotation=true] Enable rotation.
   * If `false`, a rotation constraint that always sets the rotation to zero is
   * used. The `constrainRotation` option has no effect if `enableRotation` is
   * `false`.
   * @property {import("./extent.js").Extent} [extent] The extent that constrains the
   * view, in other words, nothing outside of this extent can be visible on the map.
   * @property {boolean} [constrainOnlyCenter=false] If true, the extent
   * constraint will only apply to the view center and not the whole extent.
   * @property {boolean} [smoothExtentConstraint=true] If true, the extent
   * constraint will be applied smoothly, i.e. allow the view to go slightly outside
   * of the given `extent`.
   * @property {number} [maxResolution] The maximum resolution used to determine
   * the resolution constraint. It is used together with `minResolution` (or
   * `maxZoom`) and `zoomFactor`. If unspecified it is calculated in such a way
   * that the projection's validity extent fits in a 256x256 px tile. If the
   * projection is Spherical Mercator (the default) then `maxResolution` defaults
   * to `40075016.68557849 / 256 = 156543.03392804097`.
   * @property {number} [minResolution] The minimum resolution used to determine
   * the resolution constraint.  It is used together with `maxResolution` (or
   * `minZoom`) and `zoomFactor`.  If unspecified it is calculated assuming 29
   * zoom levels (with a factor of 2). If the projection is Spherical Mercator
   * (the default) then `minResolution` defaults to
   * `40075016.68557849 / 256 / Math.pow(2, 28) = 0.0005831682455839253`.
   * @property {number} [maxZoom=28] The maximum zoom level used to determine the
   * resolution constraint. It is used together with `minZoom` (or
   * `maxResolution`) and `zoomFactor`.  Note that if `minResolution` is also
   * provided, it is given precedence over `maxZoom`.
   * @property {number} [minZoom=0] The minimum zoom level used to determine the
   * resolution constraint. It is used together with `maxZoom` (or
   * `minResolution`) and `zoomFactor`.  Note that if `maxResolution` is also
   * provided, it is given precedence over `minZoom`.
   * @property {boolean} [multiWorld=false] If `false` the view is constrained so
   * only one world is visible, and you cannot pan off the edge.  If `true` the map
   * may show multiple worlds at low zoom levels.  Only used if the `projection` is
   * global.  Note that if `extent` is also provided it is given precedence.
   * @property {boolean} [constrainResolution=false] If true, the view will always
   * animate to the closest zoom level after an interaction; false means
   * intermediary zoom levels are allowed.
   * @property {boolean} [smoothResolutionConstraint=true] If true, the resolution
   * min/max values will be applied smoothly, i. e. allow the view to exceed slightly
   * the given resolution or zoom bounds.
   * @property {boolean} [showFullExtent=false] Allow the view to be zoomed out to
   * show the full configured extent. By default, when a view is configured with an
   * extent, users will not be able to zoom out so the viewport exceeds the extent in
   * either dimension. This means the full extent may not be visible if the viewport
   * is taller or wider than the aspect ratio of the configured extent. If
   * showFullExtent is true, the user will be able to zoom out so that the viewport
   * exceeds the height or width of the configured extent, but not both, allowing the
   * full extent to be shown.
   * @property {import("./proj.js").ProjectionLike} [projection='EPSG:3857'] The
   * projection. The default is Spherical Mercator.
   * @property {number} [resolution] The initial resolution for the view. The
   * units are `projection` units per pixel (e.g. meters per pixel). An
   * alternative to setting this is to set `zoom`. Layer sources will not be
   * fetched if neither this nor `zoom` are defined, but they can be set later
   * with {@link #setZoom} or {@link #setResolution}.
   * @property {Array<number>} [resolutions] Resolutions that determine the
   * zoom levels if specified. The index in the array corresponds to the zoom level,
   * therefore the resolution values have to be in descending order. It also constrains
   * the resolution by the minimum and maximum value. If set the `maxResolution`,
   * `minResolution`, `minZoom`, `maxZoom`, and `zoomFactor` options are ignored.
   * @property {number} [rotation=0] The initial rotation for the view in radians
   * (positive rotation clockwise, 0 means North).
   * @property {number} [zoom] Only used if `resolution` is not defined. Zoom
   * level used to calculate the initial resolution for the view.
   * @property {number} [zoomFactor=2] The zoom factor used to compute the
   * corresponding resolution.
   * @property {!Array<number>} [padding=[0, 0, 0, 0]] Padding (in css pixels).
   * If the map viewport is partially covered with other content (overlays) along
   * its edges, this setting allows to shift the center of the viewport away from
   * that content. The order of the values is top, right, bottom, left.
   */

  /**
   * @typedef {Object} AnimationOptions
   * @property {import("./coordinate.js").Coordinate} [center] The center of the view at the end of
   * the animation.
   * @property {number} [zoom] The zoom level of the view at the end of the
   * animation. This takes precedence over `resolution`.
   * @property {number} [resolution] The resolution of the view at the end
   * of the animation.  If `zoom` is also provided, this option will be ignored.
   * @property {number} [rotation] The rotation of the view at the end of
   * the animation.
   * @property {import("./coordinate.js").Coordinate} [anchor] Optional anchor to remain fixed
   * during a rotation or resolution animation.
   * @property {number} [duration=1000] The duration of the animation in milliseconds.
   * @property {function(number):number} [easing] The easing function used
   * during the animation (defaults to {@link module:ol/easing.inAndOut}).
   * The function will be called for each frame with a number representing a
   * fraction of the animation's duration.  The function should return a number
   * between 0 and 1 representing the progress toward the destination state.
   */

  /**
   * @typedef {Object} State
   * @property {import("./coordinate.js").Coordinate} center Center (in view projection coordinates).
   * @property {import("./proj/Projection.js").default} projection Projection.
   * @property {number} resolution Resolution.
   * @property {import("./coordinate.js").Coordinate} [nextCenter] The next center during an animation series.
   * @property {number} [nextResolution] The next resolution during an animation series.
   * @property {number} [nextRotation] The next rotation during an animation series.
   * @property {number} rotation Rotation.
   * @property {number} zoom Zoom.
   */

  /**
   * Like {@link import("./Map.js").FrameState}, but just `viewState` and `extent`.
   * @typedef {Object} ViewStateLayerStateExtent
   * @property {State} viewState View state.
   * @property {import("./extent.js").Extent} extent Extent (in user projection coordinates).
   * @property {Array<import("./layer/Layer.js").State>} [layerStatesArray] Layer states.
   */

  /**
   * Default min zoom level for the map view.
   * @type {number}
   */
  const DEFAULT_MIN_ZOOM = 0;

  /**
   * @typedef {import("./ObjectEventType").Types|'change:center'|'change:resolution'|'change:rotation'} ViewObjectEventTypes
   */

  /***
   * @template Return
   * @typedef {import("./Observable").OnSignature<import("./Observable").EventTypes, import("./events/Event.js").default, Return> &
   *   import("./Observable").OnSignature<ViewObjectEventTypes, import("./Object").ObjectEvent, Return> &
   *   import("./Observable").CombinedOnSignature<import("./Observable").EventTypes|ViewObjectEventTypes, Return>} ViewOnSignature
   */

  /**
   * @classdesc
   * A View object represents a simple 2D view of the map.
   *
   * This is the object to act upon to change the center, resolution,
   * and rotation of the map.
   *
   * A View has a `projection`. The projection determines the
   * coordinate system of the center, and its units determine the units of the
   * resolution (projection units per pixel). The default projection is
   * Web Mercator (EPSG:3857).
   *
   * ### The view states
   *
   * A View is determined by three states: `center`, `resolution`,
   * and `rotation`. Each state has a corresponding getter and setter, e.g.
   * `getCenter` and `setCenter` for the `center` state.
   *
   * The `zoom` state is actually not saved on the view: all computations
   * internally use the `resolution` state. Still, the `setZoom` and `getZoom`
   * methods are available, as well as `getResolutionForZoom` and
   * `getZoomForResolution` to switch from one system to the other.
   *
   * ### The constraints
   *
   * `setCenter`, `setResolution` and `setRotation` can be used to change the
   * states of the view, but any constraint defined in the constructor will
   * be applied along the way.
   *
   * A View object can have a *resolution constraint*, a *rotation constraint*
   * and a *center constraint*.
   *
   * The *resolution constraint* typically restricts min/max values and
   * snaps to specific resolutions. It is determined by the following
   * options: `resolutions`, `maxResolution`, `maxZoom` and `zoomFactor`.
   * If `resolutions` is set, the other three options are ignored. See
   * documentation for each option for more information. By default, the view
   * only has a min/max restriction and allow intermediary zoom levels when
   * pinch-zooming for example.
   *
   * The *rotation constraint* snaps to specific angles. It is determined
   * by the following options: `enableRotation` and `constrainRotation`.
   * By default rotation is allowed and its value is snapped to zero when approaching the
   * horizontal.
   *
   * The *center constraint* is determined by the `extent` option. By
   * default the view center is not constrained at all.
   *
   * ### Changing the view state
   *
   * It is important to note that `setZoom`, `setResolution`, `setCenter` and
   * `setRotation` are subject to the above mentioned constraints. As such, it
   * may sometimes not be possible to know in advance the resulting state of the
   * View. For example, calling `setResolution(10)` does not guarantee that
   * `getResolution()` will return `10`.
   *
   * A consequence of this is that, when applying a delta on the view state, one
   * should use `adjustCenter`, `adjustRotation`, `adjustZoom` and `adjustResolution`
   * rather than the corresponding setters. This will let view do its internal
   * computations. Besides, the `adjust*` methods also take an `anchor`
   * argument which allows specifying an origin for the transformation.
   *
   * ### Interacting with the view
   *
   * View constraints are usually only applied when the view is *at rest*, meaning that
   * no interaction or animation is ongoing. As such, if the user puts the view in a
   * state that is not equivalent to a constrained one (e.g. rotating the view when
   * the snap angle is 0), an animation will be triggered at the interaction end to
   * put back the view to a stable state;
   *
   * @api
   * @extends {BaseObject<ViewOptions>}
   */
  class View extends BaseObject {
    /**
     * @param {ViewOptions} [options] View options.
     */
    constructor(options) {
      super();

      /***
       * @type {ViewOnSignature<import("./events").EventsKey>}
       */
      this.on;

      /***
       * @type {ViewOnSignature<import("./events").EventsKey>}
       */
      this.once;

      /***
       * @type {ViewOnSignature<void>}
       */
      this.un;

      options = Object.assign({}, options);

      /**
       * @private
       * @type {Array<number>}
       */
      this.hints_ = [0, 0];

      /**
       * @private
       * @type {Array<Array<Animation>>}
       */
      this.animations_ = [];

      /**
       * @private
       * @type {number|undefined}
       */
      this.updateAnimationKey_;

      /**
       * @private
       * @const
       * @type {import("./proj/Projection.js").default}
       */
      this.projection_ = createProjection(options.projection, 'EPSG:3857');

      /**
       * @private
       * @type {import("./size.js").Size}
       */
      this.viewportSize_ = [100, 100];

      /**
       * @private
       * @type {import("./coordinate.js").Coordinate|undefined}
       */
      this.targetCenter_ = null;

      /**
       * @private
       * @type {number|undefined}
       */
      this.targetResolution_;

      /**
       * @private
       * @type {number|undefined}
       */
      this.targetRotation_;

      /**
       * @private
       * @type {import("./coordinate.js").Coordinate}
       */
      this.nextCenter_ = null;

      /**
       * @private
       * @type {number}
       */
      this.nextResolution_;

      /**
       * @private
       * @type {number}
       */
      this.nextRotation_;

      /**
       * @private
       * @type {import("./coordinate.js").Coordinate|undefined}
       */
      this.cancelAnchor_ = undefined;

      if (options.projection) {
        disableCoordinateWarning();
      }
      if (options.center) {
        options.center = fromUserCoordinate(options.center, this.projection_);
      }
      if (options.extent) {
        options.extent = fromUserExtent(options.extent, this.projection_);
      }

      this.applyOptions_(options);
    }

    /**
     * Set up the view with the given options.
     * @param {ViewOptions} options View options.
     */
    applyOptions_(options) {
      const properties = Object.assign({}, options);
      for (const key in ViewProperty) {
        delete properties[key];
      }
      this.setProperties(properties, true);

      const resolutionConstraintInfo = createResolutionConstraint(options);

      /**
       * @private
       * @type {number}
       */
      this.maxResolution_ = resolutionConstraintInfo.maxResolution;

      /**
       * @private
       * @type {number}
       */
      this.minResolution_ = resolutionConstraintInfo.minResolution;

      /**
       * @private
       * @type {number}
       */
      this.zoomFactor_ = resolutionConstraintInfo.zoomFactor;

      /**
       * @private
       * @type {Array<number>|undefined}
       */
      this.resolutions_ = options.resolutions;

      /**
       * @type {Array<number>|undefined}
       * @private
       */
      this.padding_ = options.padding;

      /**
       * @private
       * @type {number}
       */
      this.minZoom_ = resolutionConstraintInfo.minZoom;

      const centerConstraint = createCenterConstraint(options);
      const resolutionConstraint = resolutionConstraintInfo.constraint;
      const rotationConstraint = createRotationConstraint(options);

      /**
       * @private
       * @type {Constraints}
       */
      this.constraints_ = {
        center: centerConstraint,
        resolution: resolutionConstraint,
        rotation: rotationConstraint,
      };

      this.setRotation(options.rotation !== undefined ? options.rotation : 0);
      this.setCenterInternal(
        options.center !== undefined ? options.center : null,
      );
      if (options.resolution !== undefined) {
        this.setResolution(options.resolution);
      } else if (options.zoom !== undefined) {
        this.setZoom(options.zoom);
      }
    }

    /**
     * Padding (in css pixels).
     * If the map viewport is partially covered with other content (overlays) along
     * its edges, this setting allows to shift the center of the viewport away from that
     * content. The order of the values in the array is top, right, bottom, left.
     * The default is no padding, which is equivalent to `[0, 0, 0, 0]`.
     * @type {Array<number>|undefined}
     * @api
     */
    get padding() {
      return this.padding_;
    }
    set padding(padding) {
      let oldPadding = this.padding_;
      this.padding_ = padding;
      const center = this.getCenterInternal();
      if (center) {
        const newPadding = padding || [0, 0, 0, 0];
        oldPadding = oldPadding || [0, 0, 0, 0];
        const resolution = this.getResolution();
        const offsetX =
          (resolution / 2) *
          (newPadding[3] - oldPadding[3] + oldPadding[1] - newPadding[1]);
        const offsetY =
          (resolution / 2) *
          (newPadding[0] - oldPadding[0] + oldPadding[2] - newPadding[2]);
        this.setCenterInternal([center[0] + offsetX, center[1] - offsetY]);
      }
    }

    /**
     * Get an updated version of the view options used to construct the view.  The
     * current resolution (or zoom), center, and rotation are applied to any stored
     * options.  The provided options can be used to apply new min/max zoom or
     * resolution limits.
     * @param {ViewOptions} newOptions New options to be applied.
     * @return {ViewOptions} New options updated with the current view state.
     */
    getUpdatedOptions_(newOptions) {
      const options = this.getProperties();

      // preserve resolution (or zoom)
      if (options.resolution !== undefined) {
        options.resolution = this.getResolution();
      } else {
        options.zoom = this.getZoom();
      }

      // preserve center
      options.center = this.getCenterInternal();

      // preserve rotation
      options.rotation = this.getRotation();

      return Object.assign({}, options, newOptions);
    }

    /**
     * Animate the view.  The view's center, zoom (or resolution), and rotation
     * can be animated for smooth transitions between view states.  For example,
     * to animate the view to a new zoom level:
     *
     *     view.animate({zoom: view.getZoom() + 1});
     *
     * By default, the animation lasts one second and uses in-and-out easing.  You
     * can customize this behavior by including `duration` (in milliseconds) and
     * `easing` options (see {@link module:ol/easing}).
     *
     * To chain together multiple animations, call the method with multiple
     * animation objects.  For example, to first zoom and then pan:
     *
     *     view.animate({zoom: 10}, {center: [0, 0]});
     *
     * If you provide a function as the last argument to the animate method, it
     * will get called at the end of an animation series.  The callback will be
     * called with `true` if the animation series completed on its own or `false`
     * if it was cancelled.
     *
     * Animations are cancelled by user interactions (e.g. dragging the map) or by
     * calling `view.setCenter()`, `view.setResolution()`, or `view.setRotation()`
     * (or another method that calls one of these).
     *
     * @param {...(AnimationOptions|function(boolean): void)} var_args Animation
     *     options.  Multiple animations can be run in series by passing multiple
     *     options objects.  To run multiple animations in parallel, call the method
     *     multiple times.  An optional callback can be provided as a final
     *     argument.  The callback will be called with a boolean indicating whether
     *     the animation completed without being cancelled.
     * @api
     */
    animate(var_args) {
      if (this.isDef() && !this.getAnimating()) {
        this.resolveConstraints(0);
      }
      const args = new Array(arguments.length);
      for (let i = 0; i < args.length; ++i) {
        let options = arguments[i];
        if (options.center) {
          options = Object.assign({}, options);
          options.center = fromUserCoordinate(
            options.center,
            this.getProjection(),
          );
        }
        if (options.anchor) {
          options = Object.assign({}, options);
          options.anchor = fromUserCoordinate(
            options.anchor,
            this.getProjection(),
          );
        }
        args[i] = options;
      }
      this.animateInternal.apply(this, args);
    }

    /**
     * @param {...(AnimationOptions|function(boolean): void)} var_args Animation options.
     */
    animateInternal(var_args) {
      let animationCount = arguments.length;
      let callback;
      if (
        animationCount > 1 &&
        typeof arguments[animationCount - 1] === 'function'
      ) {
        callback = arguments[animationCount - 1];
        --animationCount;
      }

      let i = 0;
      for (; i < animationCount && !this.isDef(); ++i) {
        // if view properties are not yet set, shortcut to the final state
        const state = arguments[i];
        if (state.center) {
          this.setCenterInternal(state.center);
        }
        if (state.zoom !== undefined) {
          this.setZoom(state.zoom);
        } else if (state.resolution) {
          this.setResolution(state.resolution);
        }
        if (state.rotation !== undefined) {
          this.setRotation(state.rotation);
        }
      }
      if (i === animationCount) {
        if (callback) {
          animationCallback(callback, true);
        }
        return;
      }

      let start = Date.now();
      let center = this.targetCenter_.slice();
      let resolution = this.targetResolution_;
      let rotation = this.targetRotation_;
      const series = [];
      for (; i < animationCount; ++i) {
        const options = /** @type {AnimationOptions} */ (arguments[i]);

        const animation = {
          start: start,
          complete: false,
          anchor: options.anchor,
          duration: options.duration !== undefined ? options.duration : 1000,
          easing: options.easing || inAndOut,
          callback: callback,
        };

        if (options.center) {
          animation.sourceCenter = center;
          animation.targetCenter = options.center.slice();
          center = animation.targetCenter;
        }

        if (options.zoom !== undefined) {
          animation.sourceResolution = resolution;
          animation.targetResolution = this.getResolutionForZoom(options.zoom);
          resolution = animation.targetResolution;
        } else if (options.resolution) {
          animation.sourceResolution = resolution;
          animation.targetResolution = options.resolution;
          resolution = animation.targetResolution;
        }

        if (options.rotation !== undefined) {
          animation.sourceRotation = rotation;
          const delta =
            modulo(options.rotation - rotation + Math.PI, 2 * Math.PI) - Math.PI;
          animation.targetRotation = rotation + delta;
          rotation = animation.targetRotation;
        }

        // check if animation is a no-op
        if (isNoopAnimation(animation)) {
          animation.complete = true;
          // we still push it onto the series for callback handling
        } else {
          start += animation.duration;
        }
        series.push(animation);
      }
      this.animations_.push(series);
      this.setHint(ViewHint.ANIMATING, 1);
      this.updateAnimations_();
    }

    /**
     * Determine if the view is being animated.
     * @return {boolean} The view is being animated.
     * @api
     */
    getAnimating() {
      return this.hints_[ViewHint.ANIMATING] > 0;
    }

    /**
     * Determine if the user is interacting with the view, such as panning or zooming.
     * @return {boolean} The view is being interacted with.
     * @api
     */
    getInteracting() {
      return this.hints_[ViewHint.INTERACTING] > 0;
    }

    /**
     * Cancel any ongoing animations.
     * @api
     */
    cancelAnimations() {
      this.setHint(ViewHint.ANIMATING, -this.hints_[ViewHint.ANIMATING]);
      let anchor;
      for (let i = 0, ii = this.animations_.length; i < ii; ++i) {
        const series = this.animations_[i];
        if (series[0].callback) {
          animationCallback(series[0].callback, false);
        }
        if (!anchor) {
          for (let j = 0, jj = series.length; j < jj; ++j) {
            const animation = series[j];
            if (!animation.complete) {
              anchor = animation.anchor;
              break;
            }
          }
        }
      }
      this.animations_.length = 0;
      this.cancelAnchor_ = anchor;
      this.nextCenter_ = null;
      this.nextResolution_ = NaN;
      this.nextRotation_ = NaN;
    }

    /**
     * Update all animations.
     */
    updateAnimations_() {
      if (this.updateAnimationKey_ !== undefined) {
        cancelAnimationFrame(this.updateAnimationKey_);
        this.updateAnimationKey_ = undefined;
      }
      if (!this.getAnimating()) {
        return;
      }
      const now = Date.now();
      let more = false;
      for (let i = this.animations_.length - 1; i >= 0; --i) {
        const series = this.animations_[i];
        let seriesComplete = true;
        for (let j = 0, jj = series.length; j < jj; ++j) {
          const animation = series[j];
          if (animation.complete) {
            continue;
          }
          const elapsed = now - animation.start;
          let fraction =
            animation.duration > 0 ? elapsed / animation.duration : 1;
          if (fraction >= 1) {
            animation.complete = true;
            fraction = 1;
          } else {
            seriesComplete = false;
          }
          const progress = animation.easing(fraction);
          if (animation.sourceCenter) {
            const x0 = animation.sourceCenter[0];
            const y0 = animation.sourceCenter[1];
            const x1 = animation.targetCenter[0];
            const y1 = animation.targetCenter[1];
            this.nextCenter_ = animation.targetCenter;
            const x = x0 + progress * (x1 - x0);
            const y = y0 + progress * (y1 - y0);
            this.targetCenter_ = [x, y];
          }
          if (animation.sourceResolution && animation.targetResolution) {
            const resolution =
              progress === 1
                ? animation.targetResolution
                : animation.sourceResolution +
                  progress *
                    (animation.targetResolution - animation.sourceResolution);
            if (animation.anchor) {
              const size = this.getViewportSize_(this.getRotation());
              const constrainedResolution = this.constraints_.resolution(
                resolution,
                0,
                size,
                true,
              );
              this.targetCenter_ = this.calculateCenterZoom(
                constrainedResolution,
                animation.anchor,
              );
            }
            this.nextResolution_ = animation.targetResolution;
            this.targetResolution_ = resolution;
            this.applyTargetState_(true);
          }
          if (
            animation.sourceRotation !== undefined &&
            animation.targetRotation !== undefined
          ) {
            const rotation =
              progress === 1
                ? modulo(animation.targetRotation + Math.PI, 2 * Math.PI) -
                  Math.PI
                : animation.sourceRotation +
                  progress *
                    (animation.targetRotation - animation.sourceRotation);
            if (animation.anchor) {
              const constrainedRotation = this.constraints_.rotation(
                rotation,
                true,
              );
              this.targetCenter_ = this.calculateCenterRotate(
                constrainedRotation,
                animation.anchor,
              );
            }
            this.nextRotation_ = animation.targetRotation;
            this.targetRotation_ = rotation;
          }
          this.applyTargetState_(true);
          more = true;
          if (!animation.complete) {
            break;
          }
        }
        if (seriesComplete) {
          this.animations_[i] = null;
          this.setHint(ViewHint.ANIMATING, -1);
          this.nextCenter_ = null;
          this.nextResolution_ = NaN;
          this.nextRotation_ = NaN;
          const callback = series[0].callback;
          if (callback) {
            animationCallback(callback, true);
          }
        }
      }
      // prune completed series
      this.animations_ = this.animations_.filter(Boolean);
      if (more && this.updateAnimationKey_ === undefined) {
        this.updateAnimationKey_ = requestAnimationFrame(
          this.updateAnimations_.bind(this),
        );
      }
    }

    /**
     * @param {number} rotation Target rotation.
     * @param {import("./coordinate.js").Coordinate} anchor Rotation anchor.
     * @return {import("./coordinate.js").Coordinate|undefined} Center for rotation and anchor.
     */
    calculateCenterRotate(rotation, anchor) {
      let center;
      const currentCenter = this.getCenterInternal();
      if (currentCenter !== undefined) {
        center = [currentCenter[0] - anchor[0], currentCenter[1] - anchor[1]];
        rotate$1(center, rotation - this.getRotation());
        add$2(center, anchor);
      }
      return center;
    }

    /**
     * @param {number} resolution Target resolution.
     * @param {import("./coordinate.js").Coordinate} anchor Zoom anchor.
     * @return {import("./coordinate.js").Coordinate|undefined} Center for resolution and anchor.
     */
    calculateCenterZoom(resolution, anchor) {
      let center;
      const currentCenter = this.getCenterInternal();
      const currentResolution = this.getResolution();
      if (currentCenter !== undefined && currentResolution !== undefined) {
        const x =
          anchor[0] -
          (resolution * (anchor[0] - currentCenter[0])) / currentResolution;
        const y =
          anchor[1] -
          (resolution * (anchor[1] - currentCenter[1])) / currentResolution;
        center = [x, y];
      }
      return center;
    }

    /**
     * Returns the current viewport size.
     * @private
     * @param {number} [rotation] Take into account the rotation of the viewport when giving the size
     * @return {import("./size.js").Size} Viewport size or `[100, 100]` when no viewport is found.
     */
    getViewportSize_(rotation) {
      const size = this.viewportSize_;
      if (rotation) {
        const w = size[0];
        const h = size[1];
        return [
          Math.abs(w * Math.cos(rotation)) + Math.abs(h * Math.sin(rotation)),
          Math.abs(w * Math.sin(rotation)) + Math.abs(h * Math.cos(rotation)),
        ];
      }
      return size;
    }

    /**
     * Stores the viewport size on the view. The viewport size is not read every time from the DOM
     * to avoid performance hit and layout reflow.
     * This should be done on map size change.
     * Note: the constraints are not resolved during an animation to avoid stopping it
     * @param {import("./size.js").Size} [size] Viewport size; if undefined, [100, 100] is assumed
     */
    setViewportSize(size) {
      this.viewportSize_ = Array.isArray(size) ? size.slice() : [100, 100];
      if (!this.getAnimating()) {
        this.resolveConstraints(0);
      }
    }

    /**
     * Get the view center.
     * @return {import("./coordinate.js").Coordinate|undefined} The center of the view.
     * @observable
     * @api
     */
    getCenter() {
      const center = this.getCenterInternal();
      if (!center) {
        return center;
      }
      return toUserCoordinate(center, this.getProjection());
    }

    /**
     * Get the view center without transforming to user projection.
     * @return {import("./coordinate.js").Coordinate|undefined} The center of the view.
     */
    getCenterInternal() {
      return /** @type {import("./coordinate.js").Coordinate|undefined} */ (
        this.get(ViewProperty.CENTER)
      );
    }

    /**
     * @return {Constraints} Constraints.
     */
    getConstraints() {
      return this.constraints_;
    }

    /**
     * @return {boolean} Resolution constraint is set
     */
    getConstrainResolution() {
      return this.get('constrainResolution');
    }

    /**
     * @param {Array<number>} [hints] Destination array.
     * @return {Array<number>} Hint.
     */
    getHints(hints) {
      if (hints !== undefined) {
        hints[0] = this.hints_[0];
        hints[1] = this.hints_[1];
        return hints;
      }
      return this.hints_.slice();
    }

    /**
     * Calculate the extent for the current view state and the passed box size.
     * @param {import("./size.js").Size} [size] The pixel dimensions of the box
     * into which the calculated extent should fit. Defaults to the size of the
     * map the view is associated with.
     * If no map or multiple maps are connected to the view, provide the desired
     * box size (e.g. `map.getSize()`).
     * @return {import("./extent.js").Extent} Extent.
     * @api
     */
    calculateExtent(size) {
      const extent = this.calculateExtentInternal(size);
      return toUserExtent(extent, this.getProjection());
    }

    /**
     * @param {import("./size.js").Size} [size] Box pixel size. If not provided,
     * the map's last known viewport size will be used.
     * @return {import("./extent.js").Extent} Extent.
     */
    calculateExtentInternal(size) {
      size = size || this.getViewportSizeMinusPadding_();
      const center = /** @type {!import("./coordinate.js").Coordinate} */ (
        this.getCenterInternal()
      );
      assert(center, 'The view center is not defined');
      const resolution = /** @type {!number} */ (this.getResolution());
      assert(resolution !== undefined, 'The view resolution is not defined');
      const rotation = /** @type {!number} */ (this.getRotation());
      assert(rotation !== undefined, 'The view rotation is not defined');

      return getForViewAndSize(center, resolution, rotation, size);
    }

    /**
     * Get the maximum resolution of the view.
     * @return {number} The maximum resolution of the view.
     * @api
     */
    getMaxResolution() {
      return this.maxResolution_;
    }

    /**
     * Get the minimum resolution of the view.
     * @return {number} The minimum resolution of the view.
     * @api
     */
    getMinResolution() {
      return this.minResolution_;
    }

    /**
     * Get the maximum zoom level for the view.
     * @return {number} The maximum zoom level.
     * @api
     */
    getMaxZoom() {
      return /** @type {number} */ (
        this.getZoomForResolution(this.minResolution_)
      );
    }

    /**
     * Set a new maximum zoom level for the view.
     * @param {number} zoom The maximum zoom level.
     * @api
     */
    setMaxZoom(zoom) {
      this.applyOptions_(this.getUpdatedOptions_({maxZoom: zoom}));
    }

    /**
     * Get the minimum zoom level for the view.
     * @return {number} The minimum zoom level.
     * @api
     */
    getMinZoom() {
      return /** @type {number} */ (
        this.getZoomForResolution(this.maxResolution_)
      );
    }

    /**
     * Set a new minimum zoom level for the view.
     * @param {number} zoom The minimum zoom level.
     * @api
     */
    setMinZoom(zoom) {
      this.applyOptions_(this.getUpdatedOptions_({minZoom: zoom}));
    }

    /**
     * Set whether the view should allow intermediary zoom levels.
     * @param {boolean} enabled Whether the resolution is constrained.
     * @api
     */
    setConstrainResolution(enabled) {
      this.applyOptions_(this.getUpdatedOptions_({constrainResolution: enabled}));
    }

    /**
     * Get the view projection.
     * @return {import("./proj/Projection.js").default} The projection of the view.
     * @api
     */
    getProjection() {
      return this.projection_;
    }

    /**
     * Get the view resolution.
     * @return {number|undefined} The resolution of the view.
     * @observable
     * @api
     */
    getResolution() {
      return /** @type {number|undefined} */ (this.get(ViewProperty.RESOLUTION));
    }

    /**
     * Get the resolutions for the view. This returns the array of resolutions
     * passed to the constructor of the View, or undefined if none were given.
     * @return {Array<number>|undefined} The resolutions of the view.
     * @api
     */
    getResolutions() {
      return this.resolutions_;
    }

    /**
     * Get the resolution for a provided extent (in map units) and size (in pixels).
     * @param {import("./extent.js").Extent} extent Extent.
     * @param {import("./size.js").Size} [size] Box pixel size.
     * @return {number} The resolution at which the provided extent will render at
     *     the given size.
     * @api
     */
    getResolutionForExtent(extent, size) {
      return this.getResolutionForExtentInternal(
        fromUserExtent(extent, this.getProjection()),
        size,
      );
    }

    /**
     * Get the resolution for a provided extent (in map units) and size (in pixels).
     * @param {import("./extent.js").Extent} extent Extent.
     * @param {import("./size.js").Size} [size] Box pixel size.
     * @return {number} The resolution at which the provided extent will render at
     *     the given size.
     */
    getResolutionForExtentInternal(extent, size) {
      size = size || this.getViewportSizeMinusPadding_();
      const xResolution = getWidth(extent) / size[0];
      const yResolution = getHeight(extent) / size[1];
      return Math.max(xResolution, yResolution);
    }

    /**
     * Return a function that returns a value between 0 and 1 for a
     * resolution. Exponential scaling is assumed.
     * @param {number} [power] Power.
     * @return {function(number): number} Resolution for value function.
     */
    getResolutionForValueFunction(power) {
      power = power || 2;
      const maxResolution = this.getConstrainedResolution(this.maxResolution_);
      const minResolution = this.minResolution_;
      const max = Math.log(maxResolution / minResolution) / Math.log(power);
      return (
        /**
         * @param {number} value Value.
         * @return {number} Resolution.
         */
        function (value) {
          const resolution = maxResolution / Math.pow(power, value * max);
          return resolution;
        }
      );
    }

    /**
     * Get the view rotation.
     * @return {number} The rotation of the view in radians.
     * @observable
     * @api
     */
    getRotation() {
      return /** @type {number} */ (this.get(ViewProperty.ROTATION));
    }

    /**
     * Return a function that returns a resolution for a value between
     * 0 and 1. Exponential scaling is assumed.
     * @param {number} [power] Power.
     * @return {function(number): number} Value for resolution function.
     */
    getValueForResolutionFunction(power) {
      const logPower = Math.log(power || 2);
      const maxResolution = this.getConstrainedResolution(this.maxResolution_);
      const minResolution = this.minResolution_;
      const max = Math.log(maxResolution / minResolution) / logPower;
      return (
        /**
         * @param {number} resolution Resolution.
         * @return {number} Value.
         */
        function (resolution) {
          const value = Math.log(maxResolution / resolution) / logPower / max;
          return value;
        }
      );
    }

    /**
     * Returns the size of the viewport minus padding.
     * @private
     * @param {number} [rotation] Take into account the rotation of the viewport when giving the size
     * @return {import("./size.js").Size} Viewport size reduced by the padding.
     */
    getViewportSizeMinusPadding_(rotation) {
      let size = this.getViewportSize_(rotation);
      const padding = this.padding_;
      if (padding) {
        size = [
          size[0] - padding[1] - padding[3],
          size[1] - padding[0] - padding[2],
        ];
      }
      return size;
    }

    /**
     * @return {State} View state.
     */
    getState() {
      const projection = this.getProjection();
      const resolution = this.getResolution();
      const rotation = this.getRotation();
      let center = /** @type {import("./coordinate.js").Coordinate} */ (
        this.getCenterInternal()
      );
      const padding = this.padding_;
      if (padding) {
        const reducedSize = this.getViewportSizeMinusPadding_();
        center = calculateCenterOn(
          center,
          this.getViewportSize_(),
          [reducedSize[0] / 2 + padding[3], reducedSize[1] / 2 + padding[0]],
          resolution,
          rotation,
        );
      }
      return {
        center: center.slice(0),
        projection: projection !== undefined ? projection : null,
        resolution: resolution,
        nextCenter: this.nextCenter_,
        nextResolution: this.nextResolution_,
        nextRotation: this.nextRotation_,
        rotation: rotation,
        zoom: this.getZoom(),
      };
    }

    /**
     * @return {ViewStateLayerStateExtent} Like `FrameState`, but just `viewState` and `extent`.
     */
    getViewStateAndExtent() {
      return {
        viewState: this.getState(),
        extent: this.calculateExtent(),
      };
    }

    /**
     * Get the current zoom level. This method may return non-integer zoom levels
     * if the view does not constrain the resolution, or if an interaction or
     * animation is underway.
     * @return {number|undefined} Zoom.
     * @api
     */
    getZoom() {
      let zoom;
      const resolution = this.getResolution();
      if (resolution !== undefined) {
        zoom = this.getZoomForResolution(resolution);
      }
      return zoom;
    }

    /**
     * Get the zoom level for a resolution.
     * @param {number} resolution The resolution.
     * @return {number|undefined} The zoom level for the provided resolution.
     * @api
     */
    getZoomForResolution(resolution) {
      let offset = this.minZoom_ || 0;
      let max, zoomFactor;
      if (this.resolutions_) {
        const nearest = linearFindNearest(this.resolutions_, resolution, 1);
        offset = nearest;
        max = this.resolutions_[nearest];
        if (nearest == this.resolutions_.length - 1) {
          zoomFactor = 2;
        } else {
          zoomFactor = max / this.resolutions_[nearest + 1];
        }
      } else {
        max = this.maxResolution_;
        zoomFactor = this.zoomFactor_;
      }
      return offset + Math.log(max / resolution) / Math.log(zoomFactor);
    }

    /**
     * Get the resolution for a zoom level.
     * @param {number} zoom Zoom level.
     * @return {number} The view resolution for the provided zoom level.
     * @api
     */
    getResolutionForZoom(zoom) {
      if (this.resolutions_?.length) {
        if (this.resolutions_.length === 1) {
          return this.resolutions_[0];
        }
        const baseLevel = clamp(
          Math.floor(zoom),
          0,
          this.resolutions_.length - 2,
        );
        const zoomFactor =
          this.resolutions_[baseLevel] / this.resolutions_[baseLevel + 1];
        return (
          this.resolutions_[baseLevel] /
          Math.pow(zoomFactor, clamp(zoom - baseLevel, 0, 1))
        );
      }
      return (
        this.maxResolution_ / Math.pow(this.zoomFactor_, zoom - this.minZoom_)
      );
    }

    /**
     * Fit the given geometry or extent based on the given map size and border.
     * The size is pixel dimensions of the box to fit the extent into.
     * In most cases you will want to use the map size, that is `map.getSize()`.
     * Takes care of the map angle.
     * @param {import("./geom/SimpleGeometry.js").default|import("./extent.js").Extent} geometryOrExtent The geometry or
     *     extent to fit the view to.
     * @param {FitOptions} [options] Options.
     * @api
     */
    fit(geometryOrExtent, options) {
      /** @type {import("./geom/SimpleGeometry.js").default} */
      let geometry;
      assert(
        Array.isArray(geometryOrExtent) ||
          typeof (/** @type {?} */ (geometryOrExtent).getSimplifiedGeometry) ===
            'function',
        'Invalid extent or geometry provided as `geometry`',
      );
      if (Array.isArray(geometryOrExtent)) {
        assert(
          !isEmpty(geometryOrExtent),
          'Cannot fit empty extent provided as `geometry`',
        );
        const extent = fromUserExtent(geometryOrExtent, this.getProjection());
        geometry = fromExtent(extent);
      } else if (geometryOrExtent.getType() === 'Circle') {
        const extent = fromUserExtent(
          geometryOrExtent.getExtent(),
          this.getProjection(),
        );
        geometry = fromExtent(extent);
        geometry.rotate(this.getRotation(), getCenter(extent));
      } else {
        {
          geometry = geometryOrExtent;
        }
      }

      this.fitInternal(geometry, options);
    }

    /**
     * Calculate rotated extent
     * @param {import("./geom/SimpleGeometry.js").default} geometry The geometry.
     * @return {import("./extent").Extent} The rotated extent for the geometry.
     */
    rotatedExtentForGeometry(geometry) {
      const rotation = this.getRotation();
      const cosAngle = Math.cos(rotation);
      const sinAngle = Math.sin(-rotation);
      const coords = geometry.getFlatCoordinates();
      const stride = geometry.getStride();
      let minRotX = +Infinity;
      let minRotY = +Infinity;
      let maxRotX = -Infinity;
      let maxRotY = -Infinity;
      for (let i = 0, ii = coords.length; i < ii; i += stride) {
        const rotX = coords[i] * cosAngle - coords[i + 1] * sinAngle;
        const rotY = coords[i] * sinAngle + coords[i + 1] * cosAngle;
        minRotX = Math.min(minRotX, rotX);
        minRotY = Math.min(minRotY, rotY);
        maxRotX = Math.max(maxRotX, rotX);
        maxRotY = Math.max(maxRotY, rotY);
      }
      return [minRotX, minRotY, maxRotX, maxRotY];
    }

    /**
     * @param {import("./geom/SimpleGeometry.js").default} geometry The geometry.
     * @param {FitOptions} [options] Options.
     */
    fitInternal(geometry, options) {
      options = options || {};
      let size = options.size;
      if (!size) {
        size = this.getViewportSizeMinusPadding_();
      }
      const padding =
        options.padding !== undefined ? options.padding : [0, 0, 0, 0];
      const nearest = options.nearest !== undefined ? options.nearest : false;
      let minResolution;
      if (options.minResolution !== undefined) {
        minResolution = options.minResolution;
      } else if (options.maxZoom !== undefined) {
        minResolution = this.getResolutionForZoom(options.maxZoom);
      } else {
        minResolution = 0;
      }

      const rotatedExtent = this.rotatedExtentForGeometry(geometry);

      // calculate resolution
      let resolution = this.getResolutionForExtentInternal(rotatedExtent, [
        size[0] - padding[1] - padding[3],
        size[1] - padding[0] - padding[2],
      ]);
      resolution = isNaN(resolution)
        ? minResolution
        : Math.max(resolution, minResolution);
      resolution = this.getConstrainedResolution(resolution, nearest ? 0 : 1);

      // calculate center
      const rotation = this.getRotation();
      const sinAngle = Math.sin(rotation);
      const cosAngle = Math.cos(rotation);
      const centerRot = getCenter(rotatedExtent);
      centerRot[0] += ((padding[1] - padding[3]) / 2) * resolution;
      centerRot[1] += ((padding[0] - padding[2]) / 2) * resolution;
      const centerX = centerRot[0] * cosAngle - centerRot[1] * sinAngle;
      const centerY = centerRot[1] * cosAngle + centerRot[0] * sinAngle;
      const center = this.getConstrainedCenter([centerX, centerY], resolution);
      const callback = options.callback ? options.callback : VOID;

      if (options.duration !== undefined) {
        this.animateInternal(
          {
            resolution: resolution,
            center: center,
            duration: options.duration,
            easing: options.easing,
          },
          callback,
        );
      } else {
        this.targetResolution_ = resolution;
        this.targetCenter_ = center;
        this.applyTargetState_(false, true);
        animationCallback(callback, true);
      }
    }

    /**
     * Center on coordinate and view position.
     * @param {import("./coordinate.js").Coordinate} coordinate Coordinate.
     * @param {import("./size.js").Size} size Box pixel size.
     * @param {import("./pixel.js").Pixel} position Position on the view to center on.
     * @api
     */
    centerOn(coordinate, size, position) {
      this.centerOnInternal(
        fromUserCoordinate(coordinate, this.getProjection()),
        size,
        position,
      );
    }

    /**
     * @param {import("./coordinate.js").Coordinate} coordinate Coordinate.
     * @param {import("./size.js").Size} size Box pixel size.
     * @param {import("./pixel.js").Pixel} position Position on the view to center on.
     */
    centerOnInternal(coordinate, size, position) {
      this.setCenterInternal(
        calculateCenterOn(
          coordinate,
          size,
          position,
          this.getResolution(),
          this.getRotation(),
        ),
      );
    }

    /**
     * Calculates the shift between map and viewport center.
     * @param {import("./coordinate.js").Coordinate} center Center.
     * @param {number} resolution Resolution.
     * @param {number} rotation Rotation.
     * @param {import("./size.js").Size} size Size.
     * @return {Array<number>|undefined} Center shift.
     */
    calculateCenterShift(center, resolution, rotation, size) {
      let centerShift;
      const padding = this.padding_;
      if (padding && center) {
        const reducedSize = this.getViewportSizeMinusPadding_(-rotation);
        const shiftedCenter = calculateCenterOn(
          center,
          size,
          [reducedSize[0] / 2 + padding[3], reducedSize[1] / 2 + padding[0]],
          resolution,
          rotation,
        );
        centerShift = [
          center[0] - shiftedCenter[0],
          center[1] - shiftedCenter[1],
        ];
      }
      return centerShift;
    }

    /**
     * @return {boolean} Is defined.
     */
    isDef() {
      return !!this.getCenterInternal() && this.getResolution() !== undefined;
    }

    /**
     * Adds relative coordinates to the center of the view. Any extent constraint will apply.
     * @param {import("./coordinate.js").Coordinate} deltaCoordinates Relative value to add.
     * @api
     */
    adjustCenter(deltaCoordinates) {
      const center = toUserCoordinate(this.targetCenter_, this.getProjection());
      this.setCenter([
        center[0] + deltaCoordinates[0],
        center[1] + deltaCoordinates[1],
      ]);
    }

    /**
     * Adds relative coordinates to the center of the view. Any extent constraint will apply.
     * @param {import("./coordinate.js").Coordinate} deltaCoordinates Relative value to add.
     */
    adjustCenterInternal(deltaCoordinates) {
      const center = this.targetCenter_;
      this.setCenterInternal([
        center[0] + deltaCoordinates[0],
        center[1] + deltaCoordinates[1],
      ]);
    }

    /**
     * Multiply the view resolution by a ratio, optionally using an anchor. Any resolution
     * constraint will apply.
     * @param {number} ratio The ratio to apply on the view resolution.
     * @param {import("./coordinate.js").Coordinate} [anchor] The origin of the transformation.
     * @api
     */
    adjustResolution(ratio, anchor) {
      anchor = anchor && fromUserCoordinate(anchor, this.getProjection());
      this.adjustResolutionInternal(ratio, anchor);
    }

    /**
     * Multiply the view resolution by a ratio, optionally using an anchor. Any resolution
     * constraint will apply.
     * @param {number} ratio The ratio to apply on the view resolution.
     * @param {import("./coordinate.js").Coordinate} [anchor] The origin of the transformation.
     */
    adjustResolutionInternal(ratio, anchor) {
      const isMoving = this.getAnimating() || this.getInteracting();
      const size = this.getViewportSize_(this.getRotation());
      const newResolution = this.constraints_.resolution(
        this.targetResolution_ * ratio,
        0,
        size,
        isMoving,
      );

      if (anchor) {
        this.targetCenter_ = this.calculateCenterZoom(newResolution, anchor);
      }

      this.targetResolution_ *= ratio;
      this.applyTargetState_();
    }

    /**
     * Adds a value to the view zoom level, optionally using an anchor. Any resolution
     * constraint will apply.
     * @param {number} delta Relative value to add to the zoom level.
     * @param {import("./coordinate.js").Coordinate} [anchor] The origin of the transformation.
     * @api
     */
    adjustZoom(delta, anchor) {
      this.adjustResolution(Math.pow(this.zoomFactor_, -delta), anchor);
    }

    /**
     * Adds a value to the view rotation, optionally using an anchor. Any rotation
     * constraint will apply.
     * @param {number} delta Relative value to add to the zoom rotation, in radians.
     * @param {import("./coordinate.js").Coordinate} [anchor] The rotation center.
     * @api
     */
    adjustRotation(delta, anchor) {
      if (anchor) {
        anchor = fromUserCoordinate(anchor, this.getProjection());
      }
      this.adjustRotationInternal(delta, anchor);
    }

    /**
     * @param {number} delta Relative value to add to the zoom rotation, in radians.
     * @param {import("./coordinate.js").Coordinate} [anchor] The rotation center.
     */
    adjustRotationInternal(delta, anchor) {
      const isMoving = this.getAnimating() || this.getInteracting();
      const newRotation = this.constraints_.rotation(
        this.targetRotation_ + delta,
        isMoving,
      );
      if (anchor) {
        this.targetCenter_ = this.calculateCenterRotate(newRotation, anchor);
      }
      this.targetRotation_ += delta;
      this.applyTargetState_();
    }

    /**
     * Set the center of the current view. Any extent constraint will apply.
     * @param {import("./coordinate.js").Coordinate|undefined} center The center of the view.
     * @observable
     * @api
     */
    setCenter(center) {
      this.setCenterInternal(
        center ? fromUserCoordinate(center, this.getProjection()) : center,
      );
    }

    /**
     * Set the center using the view projection (not the user projection).
     * @param {import("./coordinate.js").Coordinate|undefined} center The center of the view.
     */
    setCenterInternal(center) {
      this.targetCenter_ = center;
      this.applyTargetState_();
    }

    /**
     * @param {import("./ViewHint.js").default} hint Hint.
     * @param {number} delta Delta.
     * @return {number} New value.
     */
    setHint(hint, delta) {
      this.hints_[hint] += delta;
      this.changed();
      return this.hints_[hint];
    }

    /**
     * Set the resolution for this view. Any resolution constraint will apply.
     * @param {number|undefined} resolution The resolution of the view.
     * @observable
     * @api
     */
    setResolution(resolution) {
      this.targetResolution_ = resolution;
      this.applyTargetState_();
    }

    /**
     * Set the rotation for this view. Any rotation constraint will apply.
     * @param {number} rotation The rotation of the view in radians.
     * @observable
     * @api
     */
    setRotation(rotation) {
      this.targetRotation_ = rotation;
      this.applyTargetState_();
    }

    /**
     * Zoom to a specific zoom level. Any resolution constrain will apply.
     * @param {number} zoom Zoom level.
     * @api
     */
    setZoom(zoom) {
      this.setResolution(this.getResolutionForZoom(zoom));
    }

    /**
     * Recompute rotation/resolution/center based on target values.
     * Note: we have to compute rotation first, then resolution and center considering that
     * parameters can influence one another in case a view extent constraint is present.
     * @param {boolean} [doNotCancelAnims] Do not cancel animations.
     * @param {boolean} [forceMoving] Apply constraints as if the view is moving.
     * @private
     */
    applyTargetState_(doNotCancelAnims, forceMoving) {
      const isMoving =
        this.getAnimating() || this.getInteracting() || forceMoving;

      // compute rotation
      const newRotation = this.constraints_.rotation(
        this.targetRotation_,
        isMoving,
      );
      const size = this.getViewportSize_(newRotation);
      const newResolution = this.constraints_.resolution(
        this.targetResolution_,
        0,
        size,
        isMoving,
      );
      const newCenter = this.constraints_.center(
        this.targetCenter_,
        newResolution,
        size,
        isMoving,
        this.calculateCenterShift(
          this.targetCenter_,
          newResolution,
          newRotation,
          size,
        ),
      );

      if (this.get(ViewProperty.ROTATION) !== newRotation) {
        this.set(ViewProperty.ROTATION, newRotation);
      }
      if (this.get(ViewProperty.RESOLUTION) !== newResolution) {
        this.set(ViewProperty.RESOLUTION, newResolution);
        this.set('zoom', this.getZoom(), true);
      }
      if (
        !newCenter ||
        !this.get(ViewProperty.CENTER) ||
        !equals(this.get(ViewProperty.CENTER), newCenter)
      ) {
        this.set(ViewProperty.CENTER, newCenter);
      }

      if (this.getAnimating() && !doNotCancelAnims) {
        this.cancelAnimations();
      }
      this.cancelAnchor_ = undefined;
    }

    /**
     * If any constraints need to be applied, an animation will be triggered.
     * This is typically done on interaction end.
     * Note: calling this with a duration of 0 will apply the constrained values straight away,
     * without animation.
     * @param {number} [duration] The animation duration in ms.
     * @param {number} [resolutionDirection] Which direction to zoom.
     * @param {import("./coordinate.js").Coordinate} [anchor] The origin of the transformation.
     */
    resolveConstraints(duration, resolutionDirection, anchor) {
      duration = duration !== undefined ? duration : 200;
      const direction = resolutionDirection || 0;

      const newRotation = this.constraints_.rotation(this.targetRotation_);
      const size = this.getViewportSize_(newRotation);
      const newResolution = this.constraints_.resolution(
        this.targetResolution_,
        direction,
        size,
      );
      const newCenter = this.constraints_.center(
        this.targetCenter_,
        newResolution,
        size,
        false,
        this.calculateCenterShift(
          this.targetCenter_,
          newResolution,
          newRotation,
          size,
        ),
      );

      if (duration === 0 && !this.cancelAnchor_) {
        this.targetResolution_ = newResolution;
        this.targetRotation_ = newRotation;
        this.targetCenter_ = newCenter;
        this.applyTargetState_();
        return;
      }

      anchor = anchor || (duration === 0 ? this.cancelAnchor_ : undefined);
      this.cancelAnchor_ = undefined;

      if (
        this.getResolution() !== newResolution ||
        this.getRotation() !== newRotation ||
        !this.getCenterInternal() ||
        !equals(this.getCenterInternal(), newCenter)
      ) {
        if (this.getAnimating()) {
          this.cancelAnimations();
        }

        this.animateInternal({
          rotation: newRotation,
          center: newCenter,
          resolution: newResolution,
          duration: duration,
          easing: easeOut,
          anchor: anchor,
        });
      }
    }

    /**
     * Notify the View that an interaction has started.
     * The view state will be resolved to a stable one if needed
     * (depending on its constraints).
     * @api
     */
    beginInteraction() {
      this.resolveConstraints(0);

      this.setHint(ViewHint.INTERACTING, 1);
    }

    /**
     * Notify the View that an interaction has ended. The view state will be resolved
     * to a stable one if needed (depending on its constraints).
     * @param {number} [duration] Animation duration in ms.
     * @param {number} [resolutionDirection] Which direction to zoom.
     * @param {import("./coordinate.js").Coordinate} [anchor] The origin of the transformation.
     * @api
     */
    endInteraction(duration, resolutionDirection, anchor) {
      anchor = anchor && fromUserCoordinate(anchor, this.getProjection());
      this.endInteractionInternal(duration, resolutionDirection, anchor);
    }

    /**
     * Notify the View that an interaction has ended. The view state will be resolved
     * to a stable one if needed (depending on its constraints).
     * @param {number} [duration] Animation duration in ms.
     * @param {number} [resolutionDirection] Which direction to zoom.
     * @param {import("./coordinate.js").Coordinate} [anchor] The origin of the transformation.
     */
    endInteractionInternal(duration, resolutionDirection, anchor) {
      if (!this.getInteracting()) {
        return;
      }
      this.setHint(ViewHint.INTERACTING, -1);
      this.resolveConstraints(duration, resolutionDirection, anchor);
    }

    /**
     * Get a valid position for the view center according to the current constraints.
     * @param {import("./coordinate.js").Coordinate|undefined} targetCenter Target center position.
     * @param {number} [targetResolution] Target resolution. If not supplied, the current one will be used.
     * This is useful to guess a valid center position at a different zoom level.
     * @return {import("./coordinate.js").Coordinate|undefined} Valid center position.
     */
    getConstrainedCenter(targetCenter, targetResolution) {
      const size = this.getViewportSize_(this.getRotation());
      return this.constraints_.center(
        targetCenter,
        targetResolution || this.getResolution(),
        size,
      );
    }

    /**
     * Get a valid zoom level according to the current view constraints.
     * @param {number|undefined} targetZoom Target zoom.
     * @param {number} [direction] Indicate which resolution should be used
     * by a renderer if the view resolution does not match any resolution of the tile source.
     * If 0, the nearest resolution will be used. If 1, the nearest lower resolution
     * will be used. If -1, the nearest higher resolution will be used.
     * @return {number|undefined} Valid zoom level.
     */
    getConstrainedZoom(targetZoom, direction) {
      const targetRes = this.getResolutionForZoom(targetZoom);
      return this.getZoomForResolution(
        this.getConstrainedResolution(targetRes, direction),
      );
    }

    /**
     * Get a valid resolution according to the current view constraints.
     * @param {number|undefined} targetResolution Target resolution.
     * @param {number} [direction] Indicate which resolution should be used
     * by a renderer if the view resolution does not match any resolution of the tile source.
     * If 0, the nearest resolution will be used. If 1, the nearest lower resolution
     * will be used. If -1, the nearest higher resolution will be used.
     * @return {number|undefined} Valid resolution.
     */
    getConstrainedResolution(targetResolution, direction) {
      direction = direction || 0;
      const size = this.getViewportSize_(this.getRotation());

      return this.constraints_.resolution(targetResolution, direction, size);
    }
  }

  /**
   * @param {Function} callback Callback.
   * @param {*} returnValue Return value.
   */
  function animationCallback(callback, returnValue) {
    setTimeout(function () {
      callback(returnValue);
    }, 0);
  }

  /**
   * @param {ViewOptions} options View options.
   * @return {import("./centerconstraint.js").Type} The constraint.
   */
  function createCenterConstraint(options) {
    if (options.extent !== undefined) {
      const smooth =
        options.smoothExtentConstraint !== undefined
          ? options.smoothExtentConstraint
          : true;
      return createExtent(options.extent, options.constrainOnlyCenter, smooth);
    }

    const projection = createProjection(options.projection, 'EPSG:3857');
    if (options.multiWorld !== true && projection.isGlobal()) {
      const extent = projection.getExtent().slice();
      extent[0] = -Infinity;
      extent[2] = Infinity;
      return createExtent(extent, false, false);
    }

    return none$1;
  }

  /**
   * @param {ViewOptions} options View options.
   * @return {{constraint: import("./resolutionconstraint.js").Type, maxResolution: number,
   *     minResolution: number, minZoom: number, zoomFactor: number}} The constraint.
   */
  function createResolutionConstraint(options) {
    let resolutionConstraint;
    let maxResolution;
    let minResolution;

    // TODO: move these to be ol constants
    // see https://github.com/openlayers/openlayers/issues/2076
    const defaultMaxZoom = 28;
    const defaultZoomFactor = 2;

    let minZoom =
      options.minZoom !== undefined ? options.minZoom : DEFAULT_MIN_ZOOM;

    let maxZoom =
      options.maxZoom !== undefined ? options.maxZoom : defaultMaxZoom;

    const zoomFactor =
      options.zoomFactor !== undefined ? options.zoomFactor : defaultZoomFactor;

    const multiWorld =
      options.multiWorld !== undefined ? options.multiWorld : false;

    const smooth =
      options.smoothResolutionConstraint !== undefined
        ? options.smoothResolutionConstraint
        : true;

    const showFullExtent =
      options.showFullExtent !== undefined ? options.showFullExtent : false;

    const projection = createProjection(options.projection, 'EPSG:3857');
    const projExtent = projection.getExtent();
    let constrainOnlyCenter = options.constrainOnlyCenter;
    let extent = options.extent;
    if (!multiWorld && !extent && projection.isGlobal()) {
      constrainOnlyCenter = false;
      extent = projExtent;
    }

    if (options.resolutions !== undefined) {
      const resolutions = options.resolutions;
      maxResolution = resolutions[minZoom];
      minResolution =
        resolutions[maxZoom] !== undefined
          ? resolutions[maxZoom]
          : resolutions[resolutions.length - 1];

      if (options.constrainResolution) {
        resolutionConstraint = createSnapToResolutions(
          resolutions,
          smooth,
          !constrainOnlyCenter && extent,
          showFullExtent,
        );
      } else {
        resolutionConstraint = createMinMaxResolution(
          maxResolution,
          minResolution,
          smooth,
          !constrainOnlyCenter && extent,
          showFullExtent,
        );
      }
    } else {
      // calculate the default min and max resolution
      const size = !projExtent
        ? // use an extent that can fit the whole world if need be
          (360 * METERS_PER_UNIT$1.degrees) / projection.getMetersPerUnit()
        : Math.max(getWidth(projExtent), getHeight(projExtent));

      const defaultMaxResolution =
        size / DEFAULT_TILE_SIZE / Math.pow(defaultZoomFactor, DEFAULT_MIN_ZOOM);

      const defaultMinResolution =
        defaultMaxResolution /
        Math.pow(defaultZoomFactor, defaultMaxZoom - DEFAULT_MIN_ZOOM);

      // user provided maxResolution takes precedence
      maxResolution = options.maxResolution;
      if (maxResolution !== undefined) {
        minZoom = 0;
      } else {
        maxResolution = defaultMaxResolution / Math.pow(zoomFactor, minZoom);
      }

      // user provided minResolution takes precedence
      minResolution = options.minResolution;
      if (minResolution === undefined) {
        if (options.maxZoom !== undefined) {
          if (options.maxResolution !== undefined) {
            minResolution = maxResolution / Math.pow(zoomFactor, maxZoom);
          } else {
            minResolution = defaultMaxResolution / Math.pow(zoomFactor, maxZoom);
          }
        } else {
          minResolution = defaultMinResolution;
        }
      }

      // given discrete zoom levels, minResolution may be different than provided
      maxZoom =
        minZoom +
        Math.floor(
          Math.log(maxResolution / minResolution) / Math.log(zoomFactor),
        );
      minResolution = maxResolution / Math.pow(zoomFactor, maxZoom - minZoom);

      if (options.constrainResolution) {
        resolutionConstraint = createSnapToPower(
          zoomFactor,
          maxResolution,
          minResolution,
          smooth,
          !constrainOnlyCenter && extent,
          showFullExtent,
        );
      } else {
        resolutionConstraint = createMinMaxResolution(
          maxResolution,
          minResolution,
          smooth,
          !constrainOnlyCenter && extent,
          showFullExtent,
        );
      }
    }
    return {
      constraint: resolutionConstraint,
      maxResolution: maxResolution,
      minResolution: minResolution,
      minZoom: minZoom,
      zoomFactor: zoomFactor,
    };
  }

  /**
   * @param {ViewOptions} options View options.
   * @return {import("./rotationconstraint.js").Type} Rotation constraint.
   */
  function createRotationConstraint(options) {
    const enableRotation =
      options.enableRotation !== undefined ? options.enableRotation : true;
    if (enableRotation) {
      const constrainRotation = options.constrainRotation;
      if (constrainRotation === undefined || constrainRotation === true) {
        return createSnapToZero();
      }
      if (constrainRotation === false) {
        return none;
      }
      if (typeof constrainRotation === 'number') {
        return createSnapToN(constrainRotation);
      }
      return none;
    }
    return disable;
  }

  /**
   * Determine if an animation involves no view change.
   * @param {Animation} animation The animation.
   * @return {boolean} The animation involves no view change.
   */
  function isNoopAnimation(animation) {
    if (animation.sourceCenter && animation.targetCenter) {
      if (!equals(animation.sourceCenter, animation.targetCenter)) {
        return false;
      }
    }
    if (animation.sourceResolution !== animation.targetResolution) {
      return false;
    }
    if (animation.sourceRotation !== animation.targetRotation) {
      return false;
    }
    return true;
  }

  /**
   * @param {import("./coordinate.js").Coordinate} coordinate Coordinate.
   * @param {import("./size.js").Size} size Box pixel size.
   * @param {import("./pixel.js").Pixel} position Position on the view to center on.
   * @param {number} resolution Resolution.
   * @param {number} rotation Rotation.
   * @return {import("./coordinate.js").Coordinate} Shifted center.
   */
  function calculateCenterOn(coordinate, size, position, resolution, rotation) {
    // calculate rotated position
    const cosAngle = Math.cos(-rotation);
    let sinAngle = Math.sin(-rotation);
    let rotX = coordinate[0] * cosAngle - coordinate[1] * sinAngle;
    let rotY = coordinate[1] * cosAngle + coordinate[0] * sinAngle;
    rotX += (size[0] / 2 - position[0]) * resolution;
    rotY += (position[1] - size[1] / 2) * resolution;

    // go back to original angle
    sinAngle = -sinAngle; // go back to original rotation
    const centerX = rotX * cosAngle - rotY * sinAngle;
    const centerY = rotY * cosAngle + rotX * sinAngle;

    return [centerX, centerY];
  }

  /**
   * @module ol/layer/Property
   */

  /**
   * @enum {string}
   */
  var LayerProperty = {
    OPACITY: 'opacity',
    VISIBLE: 'visible',
    EXTENT: 'extent',
    Z_INDEX: 'zIndex',
    MAX_RESOLUTION: 'maxResolution',
    MIN_RESOLUTION: 'minResolution',
    MAX_ZOOM: 'maxZoom',
    MIN_ZOOM: 'minZoom',
    SOURCE: 'source',
    MAP: 'map',
  };

  /**
   * @module ol/layer/Base
   */

  /**
   * A css color, or a function called with a view resolution returning a css color.
   *
   * @typedef {string|function(number):string} BackgroundColor
   * @api
   */

  /**
   * @typedef {import("../ObjectEventType").Types|'change:extent'|'change:maxResolution'|'change:maxZoom'|
   *    'change:minResolution'|'change:minZoom'|'change:opacity'|'change:visible'|'change:zIndex'} BaseLayerObjectEventTypes
   */

  /***
   * @template Return
   * @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> &
   *   import("../Observable").OnSignature<BaseLayerObjectEventTypes, import("../Object").ObjectEvent, Return> &
   *   import("../Observable").CombinedOnSignature<import("../Observable").EventTypes|BaseLayerObjectEventTypes, Return>} BaseLayerOnSignature
   */

  /**
   * @template {Object<string, *>} [Properties=Object<string, *>]
   * @typedef {Object} Options
   * @property {string} [className='ol-layer'] A CSS class name to set to the layer element.
   * @property {number} [opacity=1] Opacity (0, 1).
   * @property {boolean} [visible=true] Visibility.
   * @property {import("../extent.js").Extent} [extent] The bounding extent for layer rendering.  The layer will not be
   * rendered outside of this extent.
   * @property {number | undefined} [zIndex] The z-index for layer rendering.  At rendering time, the layers
   * will be ordered, first by Z-index and then by position. When `undefined`, a `zIndex` of 0 is assumed
   * for layers that are added to the map's `layers` collection, or `Infinity` when the layer's `setMap()`
   * method was used.
   * @property {number} [minResolution] The minimum resolution (inclusive) at which this layer will be
   * visible.
   * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
   * be visible.
   * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
   * visible.
   * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
   * be visible.
   * @property {BackgroundColor} [background] Background color for the layer. If not specified, no background
   * will be rendered.
   * @property {Properties} [properties] Arbitrary observable properties. Can be accessed with `#get()` and `#set()`.
   */

  /**
   * @classdesc
   * Abstract base class; normally only used for creating subclasses and not
   * instantiated in apps.
   * Note that with {@link module:ol/layer/Base~BaseLayer} and all its subclasses, any property set in
   * the options is set as a {@link module:ol/Object~BaseObject} property on the layer object, so
   * is observable, and has get/set accessors.
   *
   * @api
   * @template {Object<string, *>} [Properties=Object<string, *>]
   * @extends {BaseObject<NoInfer<Properties> & Object<string, *>>}
   */
  class BaseLayer extends BaseObject {
    /**
     * @param {Options<NoInfer<Properties>>} options Layer options.
     */
    constructor(options) {
      super();

      /***
       * @type {BaseLayerOnSignature<import("../events").EventsKey>}
       */
      this.on;

      /***
       * @type {BaseLayerOnSignature<import("../events").EventsKey>}
       */
      this.once;

      /***
       * @type {BaseLayerOnSignature<void>}
       */
      this.un;

      /**
       * @type {BackgroundColor|false}
       * @private
       */
      this.background_ = options.background;

      /**
       * @type {?}
       */
      const properties = Object.assign({}, options);
      if (typeof options.properties === 'object') {
        delete properties.properties;
        Object.assign(properties, options.properties);
      }

      properties[LayerProperty.OPACITY] =
        options.opacity !== undefined ? options.opacity : 1;
      assert(
        typeof properties[LayerProperty.OPACITY] === 'number',
        'Layer opacity must be a number',
      );

      properties[LayerProperty.VISIBLE] =
        options.visible !== undefined ? options.visible : true;
      properties[LayerProperty.Z_INDEX] = options.zIndex;
      properties[LayerProperty.MAX_RESOLUTION] =
        options.maxResolution !== undefined ? options.maxResolution : Infinity;
      properties[LayerProperty.MIN_RESOLUTION] =
        options.minResolution !== undefined ? options.minResolution : 0;
      properties[LayerProperty.MIN_ZOOM] =
        options.minZoom !== undefined ? options.minZoom : -Infinity;
      properties[LayerProperty.MAX_ZOOM] =
        options.maxZoom !== undefined ? options.maxZoom : Infinity;

      /**
       * @type {string}
       * @private
       */
      this.className_ =
        properties.className !== undefined ? properties.className : 'ol-layer';
      delete properties.className;

      this.setProperties(properties);

      /**
       * @type {import("./Layer.js").State}
       * @private
       */
      this.state_ = null;
    }

    /**
     * Get the background for this layer.
     * @return {BackgroundColor|false} Layer background.
     */
    getBackground() {
      return this.background_;
    }

    /**
     * @return {string} CSS class name.
     */
    getClassName() {
      return this.className_;
    }

    /**
     * This method is not meant to be called by layers or layer renderers because the state
     * is incorrect if the layer is included in a layer group.
     *
     * @param {boolean} [managed] Layer is managed.
     * @return {import("./Layer.js").State} Layer state.
     */
    getLayerState(managed) {
      /** @type {import("./Layer.js").State} */
      const state =
        this.state_ ||
        /** @type {?} */ ({
          layer: this,
          managed: managed === undefined ? true : managed,
        });
      const zIndex = this.getZIndex();
      state.opacity = clamp(Math.round(this.getOpacity() * 100) / 100, 0, 1);
      state.visible = this.getVisible();
      state.extent = this.getExtent();
      state.zIndex = zIndex === undefined && !state.managed ? Infinity : zIndex;
      state.maxResolution = this.getMaxResolution();
      state.minResolution = Math.max(this.getMinResolution(), 0);
      state.minZoom = this.getMinZoom();
      state.maxZoom = this.getMaxZoom();
      this.state_ = state;

      return state;
    }

    /**
     * @abstract
     * @param {Array<import("./Layer.js").default>} [array] Array of layers (to be
     *     modified in place).
     * @return {Array<import("./Layer.js").default>} Array of layers.
     */
    getLayersArray(array) {
      return abstract();
    }

    /**
     * @abstract
     * @param {Array<import("./Layer.js").State>} [states] Optional list of layer
     *     states (to be modified in place).
     * @return {Array<import("./Layer.js").State>} List of layer states.
     */
    getLayerStatesArray(states) {
      return abstract();
    }

    /**
     * Return the {@link module:ol/extent~Extent extent} of the layer or `undefined` if it
     * will be visible regardless of extent.
     * @return {import("../extent.js").Extent|undefined} The layer extent.
     * @observable
     * @api
     */
    getExtent() {
      return /** @type {import("../extent.js").Extent|undefined} */ (
        this.get(LayerProperty.EXTENT)
      );
    }

    /**
     * Return the maximum resolution of the layer. Returns Infinity if
     * the layer has no maximum resolution set.
     * @return {number} The maximum resolution of the layer.
     * @observable
     * @api
     */
    getMaxResolution() {
      return /** @type {number} */ (this.get(LayerProperty.MAX_RESOLUTION));
    }

    /**
     * Return the minimum resolution of the layer. Returns 0 if
     * the layer has no minimum resolution set.
     * @return {number} The minimum resolution of the layer.
     * @observable
     * @api
     */
    getMinResolution() {
      return /** @type {number} */ (this.get(LayerProperty.MIN_RESOLUTION));
    }

    /**
     * Return the minimum zoom level of the layer. Returns -Infinity if
     * the layer has no minimum zoom set.
     * @return {number} The minimum zoom level of the layer.
     * @observable
     * @api
     */
    getMinZoom() {
      return /** @type {number} */ (this.get(LayerProperty.MIN_ZOOM));
    }

    /**
     * Return the maximum zoom level of the layer. Returns Infinity if
     * the layer has no maximum zoom set.
     * @return {number} The maximum zoom level of the layer.
     * @observable
     * @api
     */
    getMaxZoom() {
      return /** @type {number} */ (this.get(LayerProperty.MAX_ZOOM));
    }

    /**
     * Return the opacity of the layer (between 0 and 1).
     * @return {number} The opacity of the layer.
     * @observable
     * @api
     */
    getOpacity() {
      return /** @type {number} */ (this.get(LayerProperty.OPACITY));
    }

    /**
     * @abstract
     * @return {import("../source/Source.js").State} Source state.
     */
    getSourceState() {
      return abstract();
    }

    /**
     * Return the value of this layer's `visible` property. To find out whether the layer
     * is visible on a map, use `isVisible()` instead.
     * @return {boolean} The value of the `visible` property of the layer.
     * @observable
     * @api
     */
    getVisible() {
      return /** @type {boolean} */ (this.get(LayerProperty.VISIBLE));
    }

    /**
     * Return the Z-index of the layer, which is used to order layers before
     * rendering. Returns undefined if the layer is unmanaged.
     * @return {number|undefined} The Z-index of the layer.
     * @observable
     * @api
     */
    getZIndex() {
      return /** @type {number|undefined} */ (this.get(LayerProperty.Z_INDEX));
    }

    /**
     * Sets the background color.
     * @param {BackgroundColor} [background] Background color.
     */
    setBackground(background) {
      this.background_ = background;
      this.changed();
    }

    /**
     * Set the extent at which the layer is visible.  If `undefined`, the layer
     * will be visible at all extents.
     * @param {import("../extent.js").Extent|undefined} extent The extent of the layer.
     * @observable
     * @api
     */
    setExtent(extent) {
      this.set(LayerProperty.EXTENT, extent);
    }

    /**
     * Set the maximum resolution at which the layer is visible.
     * @param {number} maxResolution The maximum resolution of the layer.
     * @observable
     * @api
     */
    setMaxResolution(maxResolution) {
      this.set(LayerProperty.MAX_RESOLUTION, maxResolution);
    }

    /**
     * Set the minimum resolution at which the layer is visible.
     * @param {number} minResolution The minimum resolution of the layer.
     * @observable
     * @api
     */
    setMinResolution(minResolution) {
      this.set(LayerProperty.MIN_RESOLUTION, minResolution);
    }

    /**
     * Set the maximum zoom (exclusive) at which the layer is visible.
     * Note that the zoom levels for layer visibility are based on the
     * view zoom level, which may be different from a tile source zoom level.
     * @param {number} maxZoom The maximum zoom of the layer.
     * @observable
     * @api
     */
    setMaxZoom(maxZoom) {
      this.set(LayerProperty.MAX_ZOOM, maxZoom);
    }

    /**
     * Set the minimum zoom (inclusive) at which the layer is visible.
     * Note that the zoom levels for layer visibility are based on the
     * view zoom level, which may be different from a tile source zoom level.
     * @param {number} minZoom The minimum zoom of the layer.
     * @observable
     * @api
     */
    setMinZoom(minZoom) {
      this.set(LayerProperty.MIN_ZOOM, minZoom);
    }

    /**
     * Set the opacity of the layer, allowed values range from 0 to 1.
     * @param {number} opacity The opacity of the layer.
     * @observable
     * @api
     */
    setOpacity(opacity) {
      assert(typeof opacity === 'number', 'Layer opacity must be a number');
      this.set(LayerProperty.OPACITY, opacity);
    }

    /**
     * Set the visibility of the layer (`true` or `false`).
     * @param {boolean} visible The visibility of the layer.
     * @observable
     * @api
     */
    setVisible(visible) {
      this.set(LayerProperty.VISIBLE, visible);
    }

    /**
     * Set Z-index of the layer, which is used to order layers before rendering.
     * The default Z-index is 0.
     * @param {number} zindex The z-index of the layer.
     * @observable
     * @api
     */
    setZIndex(zindex) {
      this.set(LayerProperty.Z_INDEX, zindex);
    }

    /**
     * Clean up.
     * @override
     */
    disposeInternal() {
      if (this.state_) {
        this.state_.layer = null;
        this.state_ = null;
      }
      super.disposeInternal();
    }
  }

  /**
   * @module ol/layer/Layer
   */

  /**
   * @typedef {function(import("../Map.js").FrameState):HTMLElement} RenderFunction
   */

  /**
   * @typedef {'sourceready'|'change:source'} LayerEventType
   */

  /***
   * @template Return
   * @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> &
   *   import("../Observable").OnSignature<import("./Base").BaseLayerObjectEventTypes|
   *     LayerEventType, import("../Object").ObjectEvent, Return> &
   *   import("../Observable").OnSignature<import("../render/EventType").LayerRenderEventTypes, import("../render/Event").default, Return> &
   *   import("../Observable").CombinedOnSignature<import("../Observable").EventTypes|import("./Base").BaseLayerObjectEventTypes|LayerEventType|
   *     import("../render/EventType").LayerRenderEventTypes, Return>} LayerOnSignature
   */

  /**
   * @template {import("../source/Source.js").default} [SourceType=import("../source/Source.js").default]
   * @template {Object<string, *>} [Properties=Object<string, *>]
   * @typedef {Object} Options
   * @property {string} [className='ol-layer'] A CSS class name to set to the layer element.
   * @property {number} [opacity=1] Opacity (0, 1).
   * @property {boolean} [visible=true] Visibility.
   * @property {import("../extent.js").Extent} [extent] The bounding extent for layer rendering.  The layer will not be
   * rendered outside of this extent.
   * @property {number} [zIndex] The z-index for layer rendering.  At rendering time, the layers
   * will be ordered, first by Z-index and then by position. When `undefined`, a `zIndex` of 0 is assumed
   * for layers that are added to the map's `layers` collection, or `Infinity` when the layer's `setMap()`
   * method was used.
   * @property {number} [minResolution] The minimum resolution (inclusive) at which this layer will be
   * visible.
   * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
   * be visible.
   * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
   * visible.
   * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
   * be visible.
   * @property {SourceType} [source] Source for this layer.  If not provided to the constructor,
   * the source can be set by calling {@link module:ol/layer/Layer~Layer#setSource layer.setSource(source)} after
   * construction.
   * @property {import("../Map.js").default|null} [map] Map.
   * @property {RenderFunction} [render] Render function. Takes the frame state as input and is expected to return an
   * HTML element. Will overwrite the default rendering for the layer.
   * @property {Properties} [properties] Arbitrary observable properties. Can be accessed with `#get()` and `#set()`.
   */

  /**
   * @typedef {Object} State
   * @property {import("./Layer.js").default} layer Layer.
   * @property {number} opacity Opacity, the value is rounded to two digits to appear after the decimal point.
   * @property {boolean} visible Visible.
   * @property {boolean} managed Managed.
   * @property {import("../extent.js").Extent} [extent] Extent.
   * @property {number} zIndex ZIndex.
   * @property {number} maxResolution Maximum resolution.
   * @property {number} minResolution Minimum resolution.
   * @property {number} minZoom Minimum zoom.
   * @property {number} maxZoom Maximum zoom.
   */

  /**
   * @classdesc
   * Base class from which all layer types are derived. This should only be instantiated
   * in the case where a custom layer is added to the map with a custom `render` function.
   * Such a function can be specified in the `options` object, and is expected to return an HTML element.
   *
   * A visual representation of raster or vector map data.
   * Layers group together those properties that pertain to how the data is to be
   * displayed, irrespective of the source of that data.
   *
   * Layers are usually added to a map with [map.addLayer()]{@link import("../Map.js").default#addLayer}.
   * Components like {@link module:ol/interaction/Draw~Draw} use unmanaged layers
   * internally. These unmanaged layers are associated with the map using
   * [layer.setMap()]{@link module:ol/layer/Layer~Layer#setMap} instead.
   *
   * A generic `change` event is fired when the state of the source changes.
   * A `sourceready` event is fired when the layer's source is ready.
   *
   * @fires import("../render/Event.js").RenderEvent#prerender
   * @fires import("../render/Event.js").RenderEvent#postrender
   * @fires import("../events/Event.js").BaseEvent#sourceready
   *
   * @template {import("../source/Source.js").default} [SourceType=import("../source/Source.js").default]
   * @template {import("../renderer/Layer.js").default} [RendererType=import("../renderer/Layer.js").default]
   * @template {Object<string, *>} [Properties=Object<string, *>]
   * @extends {BaseLayer<NoInfer<Properties>>}
   * @api
   */
  class Layer extends BaseLayer {
    /**
     * @param {Options<SourceType, NoInfer<Properties>>} options Layer options.
     */
    constructor(options) {
      const baseOptions = Object.assign({}, options);
      delete baseOptions.source;

      super(baseOptions);

      /***
       * @type {LayerOnSignature<import("../events").EventsKey>}
       */
      this.on;

      /***
       * @type {LayerOnSignature<import("../events").EventsKey>}
       */
      this.once;

      /***
       * @type {LayerOnSignature<void>}
       */
      this.un;

      /**
       * @private
       * @type {?import("../events.js").EventsKey}
       */
      this.mapPrecomposeKey_ = null;

      /**
       * @private
       * @type {?import("../events.js").EventsKey}
       */
      this.mapRenderKey_ = null;

      /**
       * @private
       * @type {?import("../events.js").EventsKey}
       */
      this.sourceChangeKey_ = null;

      /**
       * @private
       * @type {RendererType}
       */
      this.renderer_ = null;

      /**
       * @private
       * @type {boolean}
       */
      this.sourceReady_ = false;

      /**
       * @protected
       * @type {boolean}
       */
      this.rendered = false;

      // Overwrite default render method with a custom one
      if (options.render) {
        this.render = options.render;
      }

      if (options.map) {
        this.setMap(options.map);
      }

      this.addChangeListener(
        LayerProperty.SOURCE,
        this.handleSourcePropertyChange_,
      );

      const source = options.source
        ? /** @type {SourceType} */ (options.source)
        : null;
      this.setSource(source);
    }

    /**
     * @param {Array<import("./Layer.js").default>} [array] Array of layers (to be modified in place).
     * @return {Array<import("./Layer.js").default>} Array of layers.
     * @override
     */
    getLayersArray(array) {
      array = array ? array : [];
      array.push(this);
      return array;
    }

    /**
     * @param {Array<import("./Layer.js").State>} [states] Optional list of layer states (to be modified in place).
     * @return {Array<import("./Layer.js").State>} List of layer states.
     * @override
     */
    getLayerStatesArray(states) {
      states = states ? states : [];
      states.push(this.getLayerState());
      return states;
    }

    /**
     * Get the layer source.
     * @return {SourceType|null} The layer source (or `null` if not yet set).
     * @observable
     * @api
     */
    getSource() {
      return /** @type {SourceType} */ (this.get(LayerProperty.SOURCE)) || null;
    }

    /**
     * @return {SourceType|null} The source being rendered.
     */
    getRenderSource() {
      return this.getSource();
    }

    /**
     * @return {import("../source/Source.js").State} Source state.
     * @override
     */
    getSourceState() {
      const source = this.getSource();
      return !source ? 'undefined' : source.getState();
    }

    /**
     * @private
     */
    handleSourceChange_() {
      this.changed();
      if (this.sourceReady_ || this.getSource().getState() !== 'ready') {
        return;
      }
      this.sourceReady_ = true;
      this.dispatchEvent('sourceready');
    }

    /**
     * @private
     */
    handleSourcePropertyChange_() {
      if (this.sourceChangeKey_) {
        unlistenByKey(this.sourceChangeKey_);
        this.sourceChangeKey_ = null;
      }
      this.sourceReady_ = false;
      const source = this.getSource();
      if (source) {
        this.sourceChangeKey_ = listen(
          source,
          EventType.CHANGE,
          this.handleSourceChange_,
          this,
        );
        if (source.getState() === 'ready') {
          this.sourceReady_ = true;
          setTimeout(() => {
            this.dispatchEvent('sourceready');
          }, 0);
        }
      }
      this.changed();
    }

    /**
     * @param {import("../pixel").Pixel} pixel Pixel.
     * @return {Promise<Array<import("../Feature").FeatureLike>>} Promise that resolves with
     * an array of features.
     */
    getFeatures(pixel) {
      if (!this.renderer_) {
        return Promise.resolve([]);
      }
      return this.renderer_.getFeatures(pixel);
    }

    /**
     * @param {import("../pixel").Pixel} pixel Pixel.
     * @return {Uint8ClampedArray|Uint8Array|Float32Array|DataView|null} Pixel data.
     */
    getData(pixel) {
      if (!this.renderer_ || !this.rendered) {
        return null;
      }
      return this.renderer_.getData(pixel);
    }

    /**
     * The layer is visible on the map view, i.e. within its min/max resolution or zoom and
     * extent, not set to `visible: false`, and not inside a layer group that is set
     * to `visible: false`.
     * @param {View|import("../View.js").ViewStateLayerStateExtent} [view] View or {@link import("../Map.js").FrameState}.
     * Only required when the layer is not added to a map.
     * @return {boolean} The layer is visible in the map view.
     * @api
     */
    isVisible(view) {
      let frameState;
      const map = this.getMapInternal();
      if (!view && map) {
        view = map.getView();
      }
      if (view instanceof View) {
        frameState = {
          viewState: view.getState(),
          extent: view.calculateExtent(),
        };
      } else {
        frameState = view;
      }
      if (!frameState.layerStatesArray && map) {
        frameState.layerStatesArray = map.getLayerGroup().getLayerStatesArray();
      }
      let layerState;
      if (frameState.layerStatesArray) {
        layerState = frameState.layerStatesArray.find(
          (layerState) => layerState.layer === this,
        );
        if (!layerState) {
          return false;
        }
      } else {
        layerState = this.getLayerState();
      }

      const layerExtent = this.getExtent();

      return (
        inView(layerState, frameState.viewState) &&
        (!layerExtent || intersects$1(layerExtent, frameState.extent))
      );
    }

    /**
     * Get the attributions of the source of this layer for the given view.
     * @param {View|import("../View.js").ViewStateLayerStateExtent} [view] View or {@link import("../Map.js").FrameState}.
     * Only required when the layer is not added to a map.
     * @return {Array<string>} Attributions for this layer at the given view.
     * @api
     */
    getAttributions(view) {
      if (!this.isVisible(view)) {
        return [];
      }
      const getAttributions = this.getSource()?.getAttributions();
      if (!getAttributions) {
        return [];
      }
      const frameState =
        view instanceof View ? view.getViewStateAndExtent() : view;
      let attributions = getAttributions(frameState);
      if (!Array.isArray(attributions)) {
        attributions = [attributions];
      }
      return attributions;
    }

    /**
     * In charge to manage the rendering of the layer. One layer type is
     * bounded with one layer renderer.
     * @param {?import("../Map.js").FrameState} frameState Frame state.
     * @param {HTMLElement} target Target which the renderer may (but need not) use
     * for rendering its content.
     * @return {HTMLElement|null} The rendered element.
     */
    render(frameState, target) {
      const layerRenderer = this.getRenderer();

      if (layerRenderer.prepareFrame(frameState)) {
        this.rendered = true;
        return layerRenderer.renderFrame(frameState, target);
      }
      return null;
    }

    /**
     * Called when a layer is not visible during a map render.
     */
    unrender() {
      this.rendered = false;
    }

    /** @return {string} Declutter */
    getDeclutter() {
      return undefined;
    }

    /**
     * @param {import("../Map.js").FrameState} frameState Frame state.
     * @param {import("../layer/Layer.js").State} layerState Layer state.
     */
    renderDeclutter(frameState, layerState) {}

    /**
     * When the renderer follows a layout -> render approach, do the final rendering here.
     * @param {import('../Map.js').FrameState} frameState Frame state
     */
    renderDeferred(frameState) {
      const layerRenderer = this.getRenderer();
      if (!layerRenderer) {
        return;
      }
      layerRenderer.renderDeferred(frameState);
    }

    /**
     * For use inside the library only.
     * @param {import("../Map.js").default|null} map Map.
     */
    setMapInternal(map) {
      if (!map) {
        this.unrender();
      }
      this.set(LayerProperty.MAP, map);
    }

    /**
     * For use inside the library only.
     * @return {import("../Map.js").default|null} Map.
     */
    getMapInternal() {
      return this.get(LayerProperty.MAP);
    }

    /**
     * Sets the layer to be rendered on top of other layers on a map. The map will
     * not manage this layer in its layers collection. This
     * is useful for temporary layers. To remove an unmanaged layer from the map,
     * use `#setMap(null)`.
     *
     * To add the layer to a map and have it managed by the map, use
     * {@link module:ol/Map~Map#addLayer} instead.
     * @param {import("../Map.js").default|null} map Map.
     * @api
     */
    setMap(map) {
      if (this.mapPrecomposeKey_) {
        unlistenByKey(this.mapPrecomposeKey_);
        this.mapPrecomposeKey_ = null;
      }
      if (!map) {
        this.changed();
      }
      if (this.mapRenderKey_) {
        unlistenByKey(this.mapRenderKey_);
        this.mapRenderKey_ = null;
      }
      if (map) {
        this.mapPrecomposeKey_ = listen(
          map,
          RenderEventType.PRECOMPOSE,
          this.handlePrecompose_,
          this,
        );
        this.mapRenderKey_ = listen(this, EventType.CHANGE, map.render, map);
        this.changed();
      }
    }

    /**
     * @param {import("../events/Event.js").default} renderEvent Render event
     * @private
     */
    handlePrecompose_(renderEvent) {
      const layerStatesArray =
        /** @type {import("../render/Event.js").default} */ (renderEvent)
          .frameState.layerStatesArray;
      const layerState = this.getLayerState(false);
      assert(
        !layerStatesArray.some(
          (arrayLayerState) => arrayLayerState.layer === layerState.layer,
        ),
        'A layer can only be added to the map once. Use either `layer.setMap()` or `map.addLayer()`, not both.',
      );
      layerStatesArray.push(layerState);
    }

    /**
     * Set the layer source.
     * @param {SourceType|null} source The layer source.
     * @observable
     * @api
     */
    setSource(source) {
      this.set(LayerProperty.SOURCE, source);
    }

    /**
     * Get the renderer for this layer.
     * @return {RendererType|null} The layer renderer.
     */
    getRenderer() {
      if (!this.renderer_) {
        this.renderer_ = this.createRenderer();
      }
      return this.renderer_;
    }

    /**
     * @return {boolean} The layer has a renderer.
     */
    hasRenderer() {
      return !!this.renderer_;
    }

    /**
     * Create a renderer for this layer.
     * @return {RendererType} A layer renderer.
     * @protected
     */
    createRenderer() {
      return null;
    }

    /**
     * This will clear the renderer so that a new one can be created next time it is needed
     */
    clearRenderer() {
      if (this.renderer_) {
        this.renderer_.dispose();
        delete this.renderer_;
      }
    }

    /**
     * Clean up.
     * @override
     */
    disposeInternal() {
      this.clearRenderer();
      this.setSource(null);
      super.disposeInternal();
    }
  }

  /**
   * Return `true` if the layer is visible and if the provided view state
   * has resolution and zoom levels that are in range of the layer's min/max.
   * @param {State} layerState Layer state.
   * @param {import("../View.js").State} viewState View state.
   * @return {boolean} The layer is visible at the given view state.
   */
  function inView(layerState, viewState) {
    if (!layerState.visible) {
      return false;
    }
    const resolution = viewState.resolution;
    if (
      resolution < layerState.minResolution ||
      resolution >= layerState.maxResolution
    ) {
      return false;
    }
    const zoom = viewState.zoom;
    return zoom > layerState.minZoom && zoom <= layerState.maxZoom;
  }

  /**
   * @module ol/layer/BaseVector
   */

  /***
   * @template T
   * @typedef {T extends import("../source/Vector.js").default<infer U extends import("../Feature.js").FeatureLike> ? U : never} ExtractedFeatureType
   */

  /**
   * @template {import('../Feature').FeatureLike} FeatureType
   * @template {import("../source/Vector.js").default<FeatureType>|import("../source/VectorTile.js").default<FeatureType>} VectorSourceType<FeatureType>
   * @typedef {Object} Options
   * @property {string} [className='ol-layer'] A CSS class name to set to the layer element.
   * @property {number} [opacity=1] Opacity (0, 1).
   * @property {boolean} [visible=true] Visibility.
   * @property {import("../extent.js").Extent} [extent] The bounding extent for layer rendering.  The layer will not be
   * rendered outside of this extent.
   * @property {number} [zIndex] The z-index for layer rendering.  At rendering time, the layers
   * will be ordered, first by Z-index and then by position. When `undefined`, a `zIndex` of 0 is assumed
   * for layers that are added to the map's `layers` collection, or `Infinity` when the layer's `setMap()`
   * method was used.
   * @property {number} [minResolution] The minimum resolution (inclusive) at which this layer will be
   * visible.
   * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
   * be visible.
   * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
   * visible.
   * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
   * be visible.
   * @property {import("../render.js").OrderFunction} [renderOrder] Render order. Function to be used when sorting
   * features before rendering. By default features are drawn in the order that they are created. Use
   * `null` to avoid the sort, but get an undefined draw order.
   * @property {number} [renderBuffer=100] The buffer in pixels around the viewport extent used by the
   * renderer when getting features from the vector source for the rendering or hit-detection.
   * Recommended value: the size of the largest symbol, line width or label.
   * @property {VectorSourceType} [source] Source.
   * @property {import("../Map.js").default} [map] Sets the layer as overlay on a map. The map will not manage
   * this layer in its layers collection, and the layer will be rendered on top. This is useful for
   * temporary layers. The standard way to add a layer to a map and have it managed by the map is to
   * use [map.addLayer()]{@link import("../Map.js").default#addLayer}.
   * @property {boolean|string|number} [declutter=false] Declutter images and text. Any truthy value will enable
   * decluttering. Within a layer, a feature rendered before another has higher priority. All layers with the
   * same `declutter` value will be decluttered together. The priority is determined by the drawing order of the
   * layers with the same `declutter` value. Higher in the layer stack means higher priority. To declutter distinct
   * layers or groups of layers separately, use different truthy values for `declutter`.
   * @property {import("../style/Style.js").StyleLike|import("../style/flat.js").FlatStyleLike|null} [style] Layer style. When set to `null`, only
   * features that have their own style will be rendered. See {@link module:ol/style/Style~Style} for the default style
   * which will be used if this is not set.
   * @property {import("./Base.js").BackgroundColor} [background] Background color for the layer. If not specified, no background
   * will be rendered.
   * @property {boolean} [updateWhileAnimating=false] When set to `true`, feature batches will
   * be recreated during animations. This means that no vectors will be shown clipped, but the
   * setting will have a performance impact for large amounts of vector data. When set to `false`,
   * batches will be recreated when no animation is active.
   * @property {boolean} [updateWhileInteracting=false] When set to `true`, feature batches will
   * be recreated during interactions. See also `updateWhileAnimating`.
   * @property {Object<string, *>} [properties] Arbitrary observable properties. Can be accessed with `#get()` and `#set()`.
   */

  /**
   * @enum {string}
   * @private
   */
  const Property$2 = {
    RENDER_ORDER: 'renderOrder',
  };

  /**
   * @classdesc
   * Vector data that is rendered client-side.
   * Note that any property set in the options is set as a {@link module:ol/Object~BaseObject}
   * property on the layer object; for example, setting `title: 'My Title'` in the
   * options means that `title` is observable, and has get/set accessors.
   *
   * @template {import('../Feature').FeatureLike} FeatureType
   * @template {import("../source/Vector.js").default<FeatureType>|import("../source/VectorTile.js").default<FeatureType>} VectorSourceType<FeatureType>
   * @extends {Layer<VectorSourceType, RendererType>}
   * @template {import("../renderer/canvas/VectorLayer.js").default|import("../renderer/canvas/VectorTileLayer.js").default|import("../renderer/canvas/VectorImageLayer.js").default|import("../renderer/webgl/VectorLayer.js").default|import("../renderer/webgl/PointsLayer.js").default} RendererType
   * @api
   */
  class BaseVectorLayer extends Layer {
    /**
     * @param {Options<FeatureType, VectorSourceType>} [options] Options.
     */
    constructor(options) {
      options = options ? options : {};

      const baseOptions = Object.assign({}, options);

      delete baseOptions.style;
      delete baseOptions.renderBuffer;
      delete baseOptions.updateWhileAnimating;
      delete baseOptions.updateWhileInteracting;
      super(baseOptions);

      /**
       * @private
       * @type {string}
       */
      this.declutter_ = options.declutter ? String(options.declutter) : undefined;

      /**
       * @type {number}
       * @private
       */
      this.renderBuffer_ =
        options.renderBuffer !== undefined ? options.renderBuffer : 100;

      /**
       * User provided style.
       * @type {import("../style/Style.js").StyleLike|import("../style/flat.js").FlatStyleLike}
       * @private
       */
      this.style_ = null;

      /**
       * Style function for use within the library.
       * @type {import("../style/Style.js").StyleFunction|undefined}
       * @private
       */
      this.styleFunction_ = undefined;

      this.setStyle(options.style);

      /**
       * @type {boolean}
       * @private
       */
      this.updateWhileAnimating_ =
        options.updateWhileAnimating !== undefined
          ? options.updateWhileAnimating
          : false;

      /**
       * @type {boolean}
       * @private
       */
      this.updateWhileInteracting_ =
        options.updateWhileInteracting !== undefined
          ? options.updateWhileInteracting
          : false;
    }

    /**
     * @return {string} Declutter group.
     * @override
     */
    getDeclutter() {
      return this.declutter_;
    }

    /**
     * Get the topmost feature that intersects the given pixel on the viewport. Returns a promise
     * that resolves with an array of features. The array will either contain the topmost feature
     * when a hit was detected, or it will be empty.
     *
     * The hit detection algorithm used for this method is optimized for performance, but is less
     * accurate than the one used in [map.getFeaturesAtPixel()]{@link import("../Map.js").default#getFeaturesAtPixel}.
     * Text is not considered, and icons are only represented by their bounding box instead of the exact
     * image.
     *
     * @param {import("../pixel.js").Pixel} pixel Pixel.
     * @return {Promise<Array<import("../Feature").FeatureLike>>} Promise that resolves with an array of features.
     * @api
     * @override
     */
    getFeatures(pixel) {
      return super.getFeatures(pixel);
    }

    /**
     * @return {number|undefined} Render buffer.
     */
    getRenderBuffer() {
      return this.renderBuffer_;
    }

    /**
     * @return {import("../render.js").OrderFunction|null|undefined} Render order.
     */
    getRenderOrder() {
      return /** @type {import("../render.js").OrderFunction|null|undefined} */ (
        this.get(Property$2.RENDER_ORDER)
      );
    }

    /**
     * Get the style for features.  This returns whatever was passed to the `style`
     * option at construction or to the `setStyle` method.
     * @return {import("../style/Style.js").StyleLike|import("../style/flat.js").FlatStyleLike|null|undefined} Layer style.
     * @api
     */
    getStyle() {
      return this.style_;
    }

    /**
     * Get the style function.
     * @return {import("../style/Style.js").StyleFunction|undefined} Layer style function.
     * @api
     */
    getStyleFunction() {
      return this.styleFunction_;
    }

    /**
     * @return {boolean} Whether the rendered layer should be updated while
     *     animating.
     */
    getUpdateWhileAnimating() {
      return this.updateWhileAnimating_;
    }

    /**
     * @return {boolean} Whether the rendered layer should be updated while
     *     interacting.
     */
    getUpdateWhileInteracting() {
      return this.updateWhileInteracting_;
    }

    /**
     * Render declutter items for this layer
     * @param {import("../Map.js").FrameState} frameState Frame state.
     * @param {import("../layer/Layer.js").State} layerState Layer state.
     * @override
     */
    renderDeclutter(frameState, layerState) {
      const declutterGroup = this.getDeclutter();
      if (declutterGroup in frameState.declutter === false) {
        frameState.declutter[declutterGroup] = new RBush$1(9);
      }
      this.getRenderer().renderDeclutter(frameState, layerState);
    }

    /**
     * @param {import("../render.js").OrderFunction|null|undefined} renderOrder
     *     Render order.
     */
    setRenderOrder(renderOrder) {
      this.set(Property$2.RENDER_ORDER, renderOrder);
    }

    /**
     * Set the style for features.  This can be a single style object, an array
     * of styles, or a function that takes a feature and resolution and returns
     * an array of styles. If set to `null`, the layer has no style (a `null` style),
     * so only features that have their own styles will be rendered in the layer. Call
     * `setStyle()` without arguments to reset to the default style. See
     * [the ol/style/Style module]{@link module:ol/style/Style~Style} for information on the default style.
     *
     * If your layer has a static style, you can use [flat style]{@link module:ol/style/flat~FlatStyle} object
     * literals instead of using the `Style` and symbolizer constructors (`Fill`, `Stroke`, etc.):
     * ```js
     * vectorLayer.setStyle({
     *   "fill-color": "yellow",
     *   "stroke-color": "black",
     *   "stroke-width": 4
     * })
     * ```
     *
     * @param {import("../style/Style.js").StyleLike|import("../style/flat.js").FlatStyleLike|null} [style] Layer style.
     * @api
     */
    setStyle(style) {
      this.style_ = style === undefined ? createDefaultStyle : style;
      const styleLike = toStyleLike(style);
      this.styleFunction_ =
        style === null ? undefined : toFunction(styleLike);
      this.changed();
    }

    /**
     * @param {boolean|string|number} declutter Declutter images and text.
     * @api
     */
    setDeclutter(declutter) {
      this.declutter_ = declutter ? String(declutter) : undefined;
      this.changed();
    }
  }

  /**
   * Coerce the allowed style types into a shorter list of types.  Flat styles, arrays of flat
   * styles, and arrays of rules are converted into style functions.
   *
   * @param {import("../style/Style.js").StyleLike|import("../style/flat.js").FlatStyleLike|null} [style] Layer style.
   * @return {import("../style/Style.js").StyleLike|null} The style.
   */
  function toStyleLike(style) {
    if (style === undefined) {
      return createDefaultStyle;
    }
    if (!style) {
      return null;
    }
    if (typeof style === 'function') {
      return style;
    }
    if (style instanceof Style) {
      return style;
    }
    if (!Array.isArray(style)) {
      return flatStylesToStyleFunction([style]);
    }
    if (style.length === 0) {
      return [];
    }

    const length = style.length;
    const first = style[0];

    if (first instanceof Style) {
      /**
       * @type {Array<Style>}
       */
      const styles = new Array(length);
      for (let i = 0; i < length; ++i) {
        const candidate = style[i];
        if (!(candidate instanceof Style)) {
          throw new Error('Expected a list of style instances');
        }
        styles[i] = candidate;
      }
      return styles;
    }

    if ('style' in first) {
      /**
       * @type {Array<import("../style/flat.js").Rule>}
       */
      const rules = new Array(length);
      for (let i = 0; i < length; ++i) {
        const candidate = style[i];
        if (!('style' in candidate)) {
          throw new Error('Expected a list of rules with a style property');
        }
        rules[i] = candidate;
      }
      return rulesToStyleFunction(rules);
    }

    const flatStyles =
      /** @type {Array<import("../style/flat.js").FlatStyle>} */ (style);
    return flatStylesToStyleFunction(flatStyles);
  }

  /**
   * @module ol/layer/Vector
   */

  /**
   * @template {import("../source/Vector.js").default<FeatureType>} [VectorSourceType=import("../source/Vector.js").default<*>]
   * @template {import('../Feature.js').FeatureLike} [FeatureType=import("./BaseVector.js").ExtractedFeatureType<VectorSourceType>]
   * @typedef {Object} Options
   * @property {string} [className='ol-layer'] A CSS class name to set to the layer element.
   * @property {number} [opacity=1] Opacity (0, 1).
   * @property {boolean} [visible=true] Visibility.
   * @property {import("../extent.js").Extent} [extent] The bounding extent for layer rendering.  The layer will not be
   * rendered outside of this extent.
   * @property {number} [zIndex] The z-index for layer rendering.  At rendering time, the layers
   * will be ordered, first by Z-index and then by position. When `undefined`, a `zIndex` of 0 is assumed
   * for layers that are added to the map's `layers` collection, or `Infinity` when the layer's `setMap()`
   * method was used.
   * @property {number} [minResolution] The minimum resolution (inclusive) at which this layer will be
   * visible.
   * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
   * be visible.
   * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
   * visible.
   * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
   * be visible.
   * @property {import("../render.js").OrderFunction} [renderOrder] Render order. Function to be used when sorting
   * features before rendering. By default features are drawn in the order that they are created. Use
   * `null` to avoid the sort, but get an undefined draw order.
   * @property {number} [renderBuffer=100] The buffer in pixels around the viewport extent used by the
   * renderer when getting features from the vector source for the rendering or hit-detection.
   * Recommended value: the size of the largest symbol, line width or label.
   * @property {VectorSourceType} [source] Source.
   * @property {import("../Map.js").default} [map] Sets the layer as overlay on a map. The map will not manage
   * this layer in its layers collection, and the layer will be rendered on top. This is useful for
   * temporary layers. The standard way to add a layer to a map and have it managed by the map is to
   * use [map.addLayer()]{@link import("../Map.js").default#addLayer}.
   * @property {boolean|string|number} [declutter=false] Declutter images and text. Any truthy value will enable
   * decluttering. Within a layer, a feature rendered before another has higher priority. All layers with the
   * same `declutter` value will be decluttered together. The priority is determined by the drawing order of the
   * layers with the same `declutter` value. Higher in the layer stack means higher priority. To declutter distinct
   * layers or groups of layers separately, use different truthy values for `declutter`.
   * @property {import("../style/Style.js").StyleLike|import("../style/flat.js").FlatStyleLike|null} [style] Layer style. When set to `null`, only
   * features that have their own style will be rendered. See {@link module:ol/style/Style~Style} for the default style
   * which will be used if this is not set.
   * @property {import("./Base.js").BackgroundColor} [background] Background color for the layer. If not specified, no background
   * will be rendered.
   * @property {boolean} [updateWhileAnimating=false] When set to `true`, feature batches will
   * be recreated during animations. This means that no vectors will be shown clipped, but the
   * setting will have a performance impact for large amounts of vector data. When set to `false`,
   * batches will be recreated when no animation is active.
   * @property {boolean} [updateWhileInteracting=false] When set to `true`, feature batches will
   * be recreated during interactions. See also `updateWhileAnimating`.
   * @property {Object<string, *>} [properties] Arbitrary observable properties. Can be accessed with `#get()` and `#set()`.
   */

  /**
   * @classdesc
   * Vector data is rendered client-side, as vectors. This layer type provides most accurate rendering
   * even during animations. Points and labels stay upright on rotated views. For very large
   * amounts of vector data, performance may suffer during pan and zoom animations. In this case,
   * try {@link module:ol/layer/VectorImage~VectorImageLayer}.
   *
   * Note that any property set in the options is set as a {@link module:ol/Object~BaseObject}
   * property on the layer object; for example, setting `title: 'My Title'` in the
   * options means that `title` is observable, and has get/set accessors.
   *
   * @template {import("../source/Vector.js").default<FeatureType>} [VectorSourceType=import("../source/Vector.js").default<*>]
   * @template {import('../Feature.js').FeatureLike} [FeatureType=import("./BaseVector.js").ExtractedFeatureType<VectorSourceType>]
   * @extends {BaseVectorLayer<FeatureType, VectorSourceType, CanvasVectorLayerRenderer>}
   * @api
   */
  class VectorLayer extends BaseVectorLayer {
    /**
     * @param {Options<VectorSourceType, FeatureType>} [options] Options.
     */
    constructor(options) {
      super(options);
    }

    /**
     * @override
     */
    createRenderer() {
      return new CanvasVectorLayerRenderer(this);
    }
  }

  /**
   * @module ol/TileState
   */

  /**
   * @enum {number}
   */
  var TileState = {
    IDLE: 0,
    LOADING: 1,
    LOADED: 2,
    /**
     * Indicates that tile loading failed
     * @type {number}
     */
    ERROR: 3,
    EMPTY: 4,
  };

  /**
   * @module ol/Tile
   */

  /**
   * A function that takes a {@link module:ol/Tile~Tile} for the tile and a
   * `{string}` for the url as arguments. The default is
   * ```js
   * source.setTileLoadFunction(function(tile, src) {
   *   tile.getImage().src = src;
   * });
   * ```
   * For more fine grained control, the load function can use fetch or XMLHttpRequest and involve
   * error handling:
   *
   * ```js
   * import TileState from 'ol/TileState.js';
   *
   * source.setTileLoadFunction(function(tile, src) {
   *   const xhr = new XMLHttpRequest();
   *   xhr.responseType = 'blob';
   *   xhr.addEventListener('loadend', function (evt) {
   *     const data = this.response;
   *     if (data !== undefined) {
   *       tile.getImage().src = URL.createObjectURL(data);
   *     } else {
   *       tile.setState(TileState.ERROR);
   *     }
   *   });
   *   xhr.addEventListener('error', function () {
   *     tile.setState(TileState.ERROR);
   *   });
   *   xhr.open('GET', src);
   *   xhr.send();
   * });
   * ```
   *
   * @typedef {function(Tile, string): void} LoadFunction
   * @api
   */

  /**
   * {@link module:ol/source/Tile~TileSource} sources use a function of this type to get
   * the url that provides a tile for a given tile coordinate.
   *
   * This function takes a {@link module:ol/tilecoord~TileCoord} for the tile
   * coordinate, a `{number}` representing the pixel ratio and a
   * {@link module:ol/proj/Projection~Projection} for the projection  as arguments
   * and returns a `{string}` representing the tile URL, or undefined if no tile
   * should be requested for the passed tile coordinate.
   *
   * @typedef {function(import("./tilecoord.js").TileCoord, number,
   *           import("./proj/Projection.js").default): (string|undefined)} UrlFunction
   * @api
   */

  /**
   * @typedef {Object} Options
   * @property {number} [transition=250] A duration for tile opacity
   * transitions in milliseconds. A duration of 0 disables the opacity transition.
   * @property {boolean} [interpolate=false] Use interpolated values when resampling.  By default,
   * the nearest neighbor is used when resampling.
   * @api
   */

  /**
   * @classdesc
   * Base class for tiles.
   *
   * @abstract
   */
  class Tile extends Target {
    /**
     * @param {import("./tilecoord.js").TileCoord} tileCoord Tile coordinate.
     * @param {import("./TileState.js").default} state State.
     * @param {Options} [options] Tile options.
     */
    constructor(tileCoord, state, options) {
      super();

      options = options ? options : {};

      /**
       * @type {import("./tilecoord.js").TileCoord}
       */
      this.tileCoord = tileCoord;

      /**
       * @protected
       * @type {import("./TileState.js").default}
       */
      this.state = state;

      /**
       * A key assigned to the tile. This is used in conjunction with a source key
       * to determine if a cached version of this tile may be used by the renderer.
       * @type {string}
       */
      this.key = '';

      /**
       * The duration for the opacity transition.
       * @private
       * @type {number}
       */
      this.transition_ =
        options.transition === undefined ? 250 : options.transition;

      /**
       * Lookup of start times for rendering transitions.  If the start time is
       * equal to -1, the transition is complete.
       * @private
       * @type {Object<string, number>}
       */
      this.transitionStarts_ = {};

      /**
       * @type {boolean}
       */
      this.interpolate = !!options.interpolate;
    }

    /**
     * @protected
     */
    changed() {
      this.dispatchEvent(EventType.CHANGE);
    }

    /**
     * Called by the tile cache when the tile is removed from the cache due to expiry
     */
    release() {
      // to remove the `change` listener on this tile in `ol/TileQueue#handleTileChange`
      this.setState(TileState.EMPTY);
    }

    /**
     * @return {string} Key.
     */
    getKey() {
      return this.key + '/' + this.tileCoord;
    }

    /**
     * Get the tile coordinate for this tile.
     * @return {import("./tilecoord.js").TileCoord} The tile coordinate.
     * @api
     */
    getTileCoord() {
      return this.tileCoord;
    }

    /**
     * @return {import("./TileState.js").default} State.
     */
    getState() {
      return this.state;
    }

    /**
     * Sets the state of this tile. If you write your own {@link module:ol/Tile~LoadFunction tileLoadFunction} ,
     * it is important to set the state correctly to {@link module:ol/TileState~ERROR}
     * when the tile cannot be loaded. Otherwise the tile cannot be removed from
     * the tile queue and will block other requests.
     * @param {import("./TileState.js").default} state State.
     * @api
     */
    setState(state) {
      if (this.state === TileState.EMPTY) {
        // no more state changes
        return;
      }
      if (this.state !== TileState.ERROR && this.state > state) {
        throw new Error('Tile load sequence violation');
      }
      this.state = state;
      this.changed();
    }

    /**
     * Load the image or retry if loading previously failed.
     * Loading is taken care of by the tile queue, and calling this method is
     * only needed for preloading or for reloading in case of an error.
     * @abstract
     * @api
     */
    load() {
      abstract();
    }

    /**
     * Get the alpha value for rendering.
     * @param {string} id An id for the renderer.
     * @param {number} time The render frame time.
     * @return {number} A number between 0 and 1.
     */
    getAlpha(id, time) {
      if (!this.transition_) {
        return 1;
      }

      let start = this.transitionStarts_[id];
      if (!start) {
        start = time;
        this.transitionStarts_[id] = start;
      } else if (start === -1) {
        return 1;
      }

      const delta = time - start + 1000 / 60; // avoid rendering at 0
      if (delta >= this.transition_) {
        return 1;
      }
      return easeIn(delta / this.transition_);
    }

    /**
     * Determine if a tile is in an alpha transition.  A tile is considered in
     * transition if tile.getAlpha() has not yet been called or has been called
     * and returned 1.
     * @param {string} id An id for the renderer.
     * @return {boolean} The tile is in transition.
     */
    inTransition(id) {
      if (!this.transition_) {
        return false;
      }
      return this.transitionStarts_[id] !== -1;
    }

    /**
     * Mark a transition as complete.
     * @param {string} id An id for the renderer.
     */
    endTransition(id) {
      if (this.transition_) {
        this.transitionStarts_[id] = -1;
      }
    }

    /**
     * @override
     */
    disposeInternal() {
      this.release();
      super.disposeInternal();
    }
  }

  /**
   * @module ol/ImageTile
   */

  class ImageTile extends Tile {
    /**
     * @param {import("./tilecoord.js").TileCoord} tileCoord Tile coordinate.
     * @param {import("./TileState.js").default} state State.
     * @param {string} src Image source URI.
     * @param {import('./dom.js').ImageAttributes} imageAttributes Image attributes options.
     * @param {import("./Tile.js").LoadFunction} tileLoadFunction Tile load function.
     * @param {import("./Tile.js").Options} [options] Tile options.
     */
    constructor(
      tileCoord,
      state,
      src,
      imageAttributes,
      tileLoadFunction,
      options,
    ) {
      super(tileCoord, state, options);

      /**
       * @private
       * @type {?string}
       */
      this.crossOrigin_ = imageAttributes?.crossOrigin;

      /**
       * @private
       * @type {ReferrerPolicy}
       */
      this.referrerPolicy_ = imageAttributes?.referrerPolicy;

      /**
       * Image URI
       *
       * @private
       * @type {string}
       */
      this.src_ = src;

      this.key = src;

      /**
       * @private
       * @type {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas}
       */
      this.image_;

      if (WORKER_OFFSCREEN_CANVAS) {
        this.image_ = new OffscreenCanvas(1, 1);
      } else {
        this.image_ = new Image();
        if (this.crossOrigin_ !== null) {
          this.image_.crossOrigin = this.crossOrigin_;
        }
        if (this.referrerPolicy_ !== undefined) {
          this.image_.referrerPolicy = this.referrerPolicy_;
        }
      }

      /**
       * @private
       * @type {?function():void}
       */
      this.unlisten_ = null;

      /**
       * @private
       * @type {import("./Tile.js").LoadFunction}
       */
      this.tileLoadFunction_ = tileLoadFunction;
    }

    /**
     * Get the HTML image element for this tile (may be a Canvas, OffscreenCanvas, Image, or Video).
     * @return {HTMLCanvasElement|OffscreenCanvas|HTMLImageElement|HTMLVideoElement} Image.
     * @api
     */
    getImage() {
      return this.image_;
    }

    /**
     * Sets an HTML image element for this tile (may be a Canvas or preloaded Image).
     * @param {HTMLCanvasElement|OffscreenCanvas|HTMLImageElement} element Element.
     */
    setImage(element) {
      this.image_ = element;
      this.state = TileState.LOADED;
      this.unlistenImage_();
      this.changed();
    }

    /**
     * Get the cross origin of the ImageTile.
     * @return {string} Cross origin.
     */
    getCrossOrigin() {
      return this.crossOrigin_;
    }

    /**
     * Get the referrer policy of the ImageTile.
     * @return {ReferrerPolicy} Referrer policy.
     */
    getReferrerPolicy() {
      return this.referrerPolicy_;
    }

    /**
     * Tracks loading or read errors.
     *
     * @private
     */
    handleImageError_() {
      this.state = TileState.ERROR;
      this.unlistenImage_();
      this.image_ = getBlankImage();
      this.changed();
    }

    /**
     * Tracks successful image load.
     *
     * @private
     */
    handleImageLoad_() {
      if (WORKER_OFFSCREEN_CANVAS) {
        // OffscreenCanvas does not have naturalWidth and naturalHeight
        this.state = TileState.LOADED;
      } else {
        const image = /** @type {HTMLImageElement} */ (this.image_);
        if (image.naturalWidth && image.naturalHeight) {
          this.state = TileState.LOADED;
        } else {
          this.state = TileState.EMPTY;
        }
      }
      this.unlistenImage_();
      this.changed();
    }

    /**
     * Load the image or retry if loading previously failed.
     * Loading is taken care of by the tile queue, and calling this method is
     * only needed for preloading or for reloading in case of an error.
     *
     * To retry loading tiles on failed requests, use a custom `tileLoadFunction`
     * that checks for error status codes and reloads only when the status code is
     * 408, 429, 500, 502, 503 and 504, and only when not too many retries have been
     * made already:
     *
     * ```js
     * const retryCodes = [408, 429, 500, 502, 503, 504];
     * const retries = {};
     * source.setTileLoadFunction((tile, src) => {
     *   const image = tile.getImage();
     *   fetch(src)
     *     .then((response) => {
     *       if (retryCodes.includes(response.status)) {
     *         retries[src] = (retries[src] || 0) + 1;
     *         if (retries[src] <= 3) {
     *           setTimeout(() => tile.load(), retries[src] * 1000);
     *         }
     *         return Promise.reject();
     *       }
     *       return response.blob();
     *     })
     *     .then((blob) => {
     *       const imageUrl = URL.createObjectURL(blob);
     *       image.src = imageUrl;
     *       setTimeout(() => URL.revokeObjectURL(imageUrl), 5000);
     *     })
     *     .catch(() => tile.setState(3)); // error
     * });
     * ```
     * @api
     * @override
     */
    load() {
      if (this.state == TileState.ERROR) {
        this.state = TileState.IDLE;
        this.image_ = new Image();
        if (this.crossOrigin_ !== null) {
          this.image_.crossOrigin = this.crossOrigin_;
        }
        if (this.referrerPolicy_ !== undefined) {
          this.image_.referrerPolicy = this.referrerPolicy_;
        }
      }
      if (this.state == TileState.IDLE) {
        this.state = TileState.LOADING;
        this.changed();
        this.tileLoadFunction_(this, this.src_);
        this.unlisten_ = listenImage(
          this.image_,
          this.handleImageLoad_.bind(this),
          this.handleImageError_.bind(this),
        );
      }
    }

    /**
     * Discards event handlers which listen for load completion or errors.
     *
     * @private
     */
    unlistenImage_() {
      if (this.unlisten_) {
        this.unlisten_();
        this.unlisten_ = null;
      }
    }

    /**
     * @override
     */
    disposeInternal() {
      this.unlistenImage_();
      this.image_ = null;
      super.disposeInternal();
    }
  }

  /**
   * Get a 1-pixel blank image.
   * @return {HTMLCanvasElement|OffscreenCanvas} Blank image.
   */
  function getBlankImage() {
    const ctx = createCanvasContext2D(1, 1);
    ctx.fillStyle = 'rgba(0,0,0,0)';
    ctx.fillRect(0, 0, 1, 1);
    return ctx.canvas;
  }

  /**
   * @module ol/Kinetic
   */

  /**
   * @classdesc
   * Implementation of inertial deceleration for map movement.
   *
   * @api
   */
  class Kinetic {
    /**
     * @param {number} decay Rate of decay (must be negative).
     * @param {number} minVelocity Minimum velocity (pixels/millisecond).
     * @param {number} delay Delay to consider to calculate the kinetic
     *     initial values (milliseconds).
     */
    constructor(decay, minVelocity, delay) {
      /**
       * @private
       * @type {number}
       */
      this.decay_ = decay;

      /**
       * @private
       * @type {number}
       */
      this.minVelocity_ = minVelocity;

      /**
       * @private
       * @type {number}
       */
      this.delay_ = delay;

      /**
       * @private
       * @type {Array<number>}
       */
      this.points_ = [];

      /**
       * @private
       * @type {number}
       */
      this.angle_ = 0;

      /**
       * @private
       * @type {number}
       */
      this.initialVelocity_ = 0;
    }

    /**
     * FIXME empty description for jsdoc
     */
    begin() {
      this.points_.length = 0;
      this.angle_ = 0;
      this.initialVelocity_ = 0;
    }

    /**
     * @param {number} x X.
     * @param {number} y Y.
     */
    update(x, y) {
      this.points_.push(x, y, Date.now());
    }

    /**
     * @return {boolean} Whether we should do kinetic animation.
     */
    end() {
      if (this.points_.length < 6) {
        // at least 2 points are required (i.e. there must be at least 6 elements
        // in the array)
        return false;
      }
      const delay = Date.now() - this.delay_;
      const lastIndex = this.points_.length - 3;
      if (this.points_[lastIndex + 2] < delay) {
        // the last tracked point is too old, which means that the user stopped
        // panning before releasing the map
        return false;
      }

      // get the first point which still falls into the delay time
      let firstIndex = lastIndex - 3;
      while (firstIndex > 0 && this.points_[firstIndex + 2] > delay) {
        firstIndex -= 3;
      }

      const duration = this.points_[lastIndex + 2] - this.points_[firstIndex + 2];
      // we don't want a duration of 0 (divide by zero)
      // we also make sure the user panned for a duration of at least one frame
      // (1/60s) to compute sane displacement values
      if (duration < 1000 / 60) {
        return false;
      }

      const dx = this.points_[lastIndex] - this.points_[firstIndex];
      const dy = this.points_[lastIndex + 1] - this.points_[firstIndex + 1];
      this.angle_ = Math.atan2(dy, dx);
      this.initialVelocity_ = Math.sqrt(dx * dx + dy * dy) / duration;
      return this.initialVelocity_ > this.minVelocity_;
    }

    /**
     * @return {number} Total distance travelled (pixels).
     */
    getDistance() {
      return (this.minVelocity_ - this.initialVelocity_) / this.decay_;
    }

    /**
     * @return {number} Angle of the kinetic panning animation (radians).
     */
    getAngle() {
      return this.angle_;
    }
  }

  /**
   * @module ol/MapEvent
   */

  /**
   * @classdesc
   * Events emitted as map events are instances of this type.
   * See {@link module:ol/Map~Map} for which events trigger a map event.
   */
  class MapEvent extends BaseEvent {
    /**
     * @param {string} type Event type.
     * @param {import("./Map.js").default} map Map.
     * @param {?import("./Map.js").FrameState} [frameState] Frame state.
     */
    constructor(type, map, frameState) {
      super(type);

      /**
       * The map where the event occurred.
       * @type {import("./Map.js").default}
       * @api
       */
      this.map = map;

      /**
       * The frame state at the time of the event.
       * @type {?import("./Map.js").FrameState}
       * @api
       */
      this.frameState = frameState !== undefined ? frameState : null;
    }
  }

  /**
   * @module ol/MapBrowserEvent
   */

  /**
   * @classdesc
   * Events emitted as map browser events are instances of this type.
   * See {@link module:ol/Map~Map} for which events trigger a map browser event.
   * @template {PointerEvent|KeyboardEvent|WheelEvent} [EVENT=PointerEvent|KeyboardEvent|WheelEvent]
   */
  class MapBrowserEvent extends MapEvent {
    /**
     * @param {string} type Event type.
     * @param {import("./Map.js").default} map Map.
     * @param {EVENT} originalEvent Original event.
     * @param {boolean} [dragging] Is the map currently being dragged?
     * @param {import("./Map.js").FrameState} [frameState] Frame state.
     * @param {Array<PointerEvent>} [activePointers] Active pointers.
     */
    constructor(type, map, originalEvent, dragging, frameState, activePointers) {
      super(type, map, frameState);

      /**
       * The original browser event.
       * @const
       * @type {EVENT}
       * @api
       */
      this.originalEvent = originalEvent;

      /**
       * The map pixel relative to the viewport corresponding to the original browser event.
       * @type {?import("./pixel.js").Pixel}
       * @private
       */
      this.pixel_ = null;

      /**
       * The coordinate in the user projection corresponding to the original browser event.
       * @type {?import("./coordinate.js").Coordinate}
       * @private
       */
      this.coordinate_ = null;

      /**
       * Indicates if the map is currently being dragged. Only set for
       * `POINTERDRAG` and `POINTERMOVE` events. Default is `false`.
       *
       * @type {boolean}
       * @api
       */
      this.dragging = dragging !== undefined ? dragging : false;

      /**
       * @type {Array<PointerEvent>|undefined}
       */
      this.activePointers = activePointers;
    }

    /**
     * The map pixel relative to the viewport corresponding to the original event.
     * @type {import("./pixel.js").Pixel}
     * @api
     */
    get pixel() {
      if (!this.pixel_) {
        this.pixel_ = this.map.getEventPixel(this.originalEvent);
      }
      return this.pixel_;
    }
    set pixel(pixel) {
      this.pixel_ = pixel;
    }

    /**
     * The coordinate corresponding to the original browser event.  This will be in the user
     * projection if one is set.  Otherwise it will be in the view projection.
     * @type {import("./coordinate.js").Coordinate}
     * @api
     */
    get coordinate() {
      if (!this.coordinate_) {
        this.coordinate_ = this.map.getCoordinateFromPixel(this.pixel);
      }
      return this.coordinate_;
    }
    set coordinate(coordinate) {
      this.coordinate_ = coordinate;
    }

    /**
     * Prevents the default browser action.
     * See https://developer.mozilla.org/en-US/docs/Web/API/event.preventDefault.
     * @api
     * @override
     */
    preventDefault() {
      super.preventDefault();
      if ('preventDefault' in this.originalEvent) {
        /** @type {UIEvent} */ (this.originalEvent).preventDefault();
      }
    }

    /**
     * Prevents further propagation of the current event.
     * See https://developer.mozilla.org/en-US/docs/Web/API/event.stopPropagation.
     * @api
     * @override
     */
    stopPropagation() {
      super.stopPropagation();
      if ('stopPropagation' in this.originalEvent) {
        /** @type {UIEvent} */ (this.originalEvent).stopPropagation();
      }
    }
  }

  /**
   * @module ol/MapBrowserEventType
   */

  /**
   * Constants for event names.
   * @enum {string}
   */
  var MapBrowserEventType = {
    /**
     * A true single click with no dragging and no double click. Note that this
     * event is delayed by 250 ms to ensure that it is not a double click.
     * @event module:ol/MapBrowserEvent~MapBrowserEvent#singleclick
     * @api
     */
    SINGLECLICK: 'singleclick',

    /**
     * A click with no dragging. A double click will fire two of this.
     * @event module:ol/MapBrowserEvent~MapBrowserEvent#click
     * @api
     */
    CLICK: EventType.CLICK,

    /**
     * A true double click, with no dragging.
     * @event module:ol/MapBrowserEvent~MapBrowserEvent#dblclick
     * @api
     */
    DBLCLICK: EventType.DBLCLICK,

    /**
     * Triggered when a pointer is dragged.
     * @event module:ol/MapBrowserEvent~MapBrowserEvent#pointerdrag
     * @api
     */
    POINTERDRAG: 'pointerdrag',

    /**
     * Triggered when a pointer is moved. Note that on touch devices this is
     * triggered when the map is panned, so is not the same as mousemove.
     * @event module:ol/MapBrowserEvent~MapBrowserEvent#pointermove
     * @api
     */
    POINTERMOVE: 'pointermove',

    POINTERDOWN: 'pointerdown',
    POINTERUP: 'pointerup',
    POINTEROVER: 'pointerover',
    POINTEROUT: 'pointerout',
    POINTERENTER: 'pointerenter',
    POINTERLEAVE: 'pointerleave',
    POINTERCANCEL: 'pointercancel',
  };

  /***
   * @typedef {'singleclick'|'click'|'dblclick'|'pointerdrag'|'pointermove'} Types
   */

  /**
   * @module ol/pointer/EventType
   */

  /**
   * Constants for event names.
   * @enum {string}
   */
  var PointerEventType = {
    POINTERMOVE: 'pointermove',
    POINTERDOWN: 'pointerdown'};

  /**
   * @module ol/MapBrowserEventHandler
   */


  class MapBrowserEventHandler extends Target {
    /**
     * @param {import("./Map.js").default} map The map with the viewport to listen to events on.
     * @param {number} [moveTolerance] The minimal distance the pointer must travel to trigger a move.
     */
    constructor(map, moveTolerance) {
      super(map);

      /**
       * This is the element that we will listen to the real events on.
       * @type {import("./Map.js").default}
       * @private
       */
      this.map_ = map;

      /**
       * @type {ReturnType<typeof setTimeout>}
       * @private
       */
      this.clickTimeoutId_;

      /**
       * Emulate dblclick and singleclick. Will be true when only one pointer is active.
       * @type {boolean}
       */
      this.emulateClicks_ = false;

      /**
       * @type {boolean}
       * @private
       */
      this.dragging_ = false;

      /**
       * @type {!Array<import("./events.js").EventsKey>}
       * @private
       */
      this.dragListenerKeys_ = [];

      /**
       * @type {number}
       * @private
       */
      this.moveTolerance_ = moveTolerance === undefined ? 1 : moveTolerance;

      /**
       * The most recent "down" type event (or null if none have occurred).
       * Set on pointerdown.
       * @type {PointerEvent|null}
       * @private
       */
      this.down_ = null;

      const element = this.map_.getViewport();

      /**
       * @type {Array<PointerEvent>}
       * @private
       */
      this.activePointers_ = [];

      /**
       * @type {!Object<number, Event>}
       * @private
       */
      this.trackedTouches_ = {};

      /**
       * @private
       */
      this.element_ = element;

      /**
       * @type {?import("./events.js").EventsKey}
       * @private
       */
      this.pointerdownListenerKey_ = listen(
        element,
        PointerEventType.POINTERDOWN,
        this.handlePointerDown_,
        this,
      );

      /**
       * @type {PointerEvent}
       * @private
       */
      this.originalPointerMoveEvent_;

      /**
       * @type {?import("./events.js").EventsKey}
       * @private
       */
      this.relayedListenerKey_ = listen(
        element,
        PointerEventType.POINTERMOVE,
        this.relayMoveEvent_,
        this,
      );

      /**
       * @private
       */
      this.boundHandleTouchMove_ = this.handleTouchMove_.bind(this);

      this.element_.addEventListener(
        EventType.TOUCHMOVE,
        this.boundHandleTouchMove_,
        PASSIVE_EVENT_LISTENERS ? {passive: false} : false,
      );
    }

    /**
     * @param {PointerEvent} pointerEvent Pointer
     * event.
     * @private
     */
    emulateClick_(pointerEvent) {
      let newEvent = new MapBrowserEvent(
        MapBrowserEventType.CLICK,
        this.map_,
        pointerEvent,
      );
      this.dispatchEvent(newEvent);
      if (this.clickTimeoutId_ !== undefined) {
        // double-click
        clearTimeout(this.clickTimeoutId_);
        this.clickTimeoutId_ = undefined;
        newEvent = new MapBrowserEvent(
          MapBrowserEventType.DBLCLICK,
          this.map_,
          pointerEvent,
        );
        this.dispatchEvent(newEvent);
      } else {
        // click
        this.clickTimeoutId_ = setTimeout(() => {
          this.clickTimeoutId_ = undefined;
          const newEvent = new MapBrowserEvent(
            MapBrowserEventType.SINGLECLICK,
            this.map_,
            pointerEvent,
          );
          this.dispatchEvent(newEvent);
        }, 250);
      }
    }

    /**
     * Keeps track on how many pointers are currently active.
     *
     * @param {PointerEvent} pointerEvent Pointer
     * event.
     * @private
     */
    updateActivePointers_(pointerEvent) {
      const event = pointerEvent;
      const id = event.pointerId;

      if (
        event.type == MapBrowserEventType.POINTERUP ||
        event.type == MapBrowserEventType.POINTERCANCEL
      ) {
        delete this.trackedTouches_[id];
        for (const pointerId in this.trackedTouches_) {
          if (this.trackedTouches_[pointerId].target !== event.target) {
            // Some platforms assign a new pointerId when the target changes.
            // If this happens, delete one tracked pointer. If there is more
            // than one tracked pointer for the old target, it will be cleared
            // by subsequent POINTERUP events from other pointers.
            delete this.trackedTouches_[pointerId];
            break;
          }
        }
      } else if (
        event.type == MapBrowserEventType.POINTERDOWN ||
        event.type == MapBrowserEventType.POINTERMOVE
      ) {
        this.trackedTouches_[id] = event;
      }
      this.activePointers_ = Object.values(this.trackedTouches_);
    }

    /**
     * @param {PointerEvent} pointerEvent Pointer
     * event.
     * @private
     */
    handlePointerUp_(pointerEvent) {
      this.updateActivePointers_(pointerEvent);
      const newEvent = new MapBrowserEvent(
        MapBrowserEventType.POINTERUP,
        this.map_,
        pointerEvent,
        undefined,
        undefined,
        this.activePointers_,
      );
      this.dispatchEvent(newEvent);

      // We emulate click events on left mouse button click, touch contact, and pen
      // contact. isMouseActionButton returns true in these cases (evt.button is set
      // to 0).
      // See http://www.w3.org/TR/pointerevents/#button-states
      // We only fire click, singleclick, and doubleclick if nobody has called
      // event.preventDefault().
      if (
        this.emulateClicks_ &&
        !newEvent.defaultPrevented &&
        !this.dragging_ &&
        this.isMouseActionButton_(pointerEvent)
      ) {
        this.emulateClick_(this.down_);
      }

      if (this.activePointers_.length === 0) {
        this.dragListenerKeys_.forEach(unlistenByKey);
        this.dragListenerKeys_.length = 0;
        this.dragging_ = false;
        this.down_ = null;
      }
    }

    /**
     * @param {PointerEvent} pointerEvent Pointer
     * event.
     * @return {boolean} If the left mouse button was pressed.
     * @private
     */
    isMouseActionButton_(pointerEvent) {
      return pointerEvent.button === 0;
    }

    /**
     * @param {PointerEvent} pointerEvent Pointer
     * event.
     * @private
     */
    handlePointerDown_(pointerEvent) {
      this.emulateClicks_ = this.activePointers_.length === 0;
      this.updateActivePointers_(pointerEvent);
      const newEvent = new MapBrowserEvent(
        MapBrowserEventType.POINTERDOWN,
        this.map_,
        pointerEvent,
        undefined,
        undefined,
        this.activePointers_,
      );
      this.dispatchEvent(newEvent);

      this.down_ = new PointerEvent(pointerEvent.type, pointerEvent);
      Object.defineProperty(this.down_, 'target', {
        writable: false,
        value: pointerEvent.target,
      });

      if (this.dragListenerKeys_.length === 0) {
        const doc = this.map_.getOwnerDocument();
        this.dragListenerKeys_.push(
          listen(
            doc,
            MapBrowserEventType.POINTERMOVE,
            this.handlePointerMove_,
            this,
          ),
          listen(doc, MapBrowserEventType.POINTERUP, this.handlePointerUp_, this),
          /* Note that the listener for `pointercancel is set up on
           * `pointerEventHandler_` and not `documentPointerEventHandler_` like
           * the `pointerup` and `pointermove` listeners.
           *
           * The reason for this is the following: `TouchSource.vacuumTouches_()`
           * issues `pointercancel` events, when there was no `touchend` for a
           * `touchstart`. Now, let's say a first `touchstart` is registered on
           * `pointerEventHandler_`. The `documentPointerEventHandler_` is set up.
           * But `documentPointerEventHandler_` doesn't know about the first
           * `touchstart`. If there is no `touchend` for the `touchstart`, we can
           * only receive a `touchcancel` from `pointerEventHandler_`, because it is
           * only registered there.
           */
          listen(
            this.element_,
            MapBrowserEventType.POINTERCANCEL,
            this.handlePointerUp_,
            this,
          ),
        );
        if (this.element_.getRootNode && this.element_.getRootNode() !== doc) {
          this.dragListenerKeys_.push(
            listen(
              this.element_.getRootNode(),
              MapBrowserEventType.POINTERUP,
              this.handlePointerUp_,
              this,
            ),
          );
        }
      }
    }

    /**
     * @param {PointerEvent} pointerEvent Pointer
     * event.
     * @private
     */
    handlePointerMove_(pointerEvent) {
      // Between pointerdown and pointerup, pointermove events are triggered.
      // To avoid a 'false' touchmove event to be dispatched, we test if the pointer
      // moved a significant distance.
      if (this.isMoving_(pointerEvent)) {
        this.updateActivePointers_(pointerEvent);
        this.dragging_ = true;
        const newEvent = new MapBrowserEvent(
          MapBrowserEventType.POINTERDRAG,
          this.map_,
          pointerEvent,
          this.dragging_,
          undefined,
          this.activePointers_,
        );
        this.dispatchEvent(newEvent);
      }
    }

    /**
     * Wrap and relay a pointermove event.
     * @param {PointerEvent} pointerEvent Pointer
     * event.
     * @private
     */
    relayMoveEvent_(pointerEvent) {
      this.originalPointerMoveEvent_ = pointerEvent;
      const dragging = !!(this.down_ && this.isMoving_(pointerEvent));
      this.dispatchEvent(
        new MapBrowserEvent(
          MapBrowserEventType.POINTERMOVE,
          this.map_,
          pointerEvent,
          dragging,
        ),
      );
    }

    /**
     * Flexible handling of a `touch-action: none` css equivalent: because calling
     * `preventDefault()` on a `pointermove` event does not stop native page scrolling
     * and zooming, we also listen for `touchmove` and call `preventDefault()` on it
     * when an interaction (currently `DragPan` handles the event.
     * @param {TouchEvent} event Event.
     * @private
     */
    handleTouchMove_(event) {
      // Due to https://github.com/mpizenberg/elm-pep/issues/2, `this.originalPointerMoveEvent_`
      // may not be initialized yet when we get here on a platform without native pointer events,
      // when elm-pep is used as pointer events polyfill.
      const originalEvent = this.originalPointerMoveEvent_;
      if (
        (!originalEvent || originalEvent.defaultPrevented) &&
        (typeof event.cancelable !== 'boolean' || event.cancelable === true)
      ) {
        event.preventDefault();
      }
    }

    /**
     * @param {PointerEvent} pointerEvent Pointer
     * event.
     * @return {boolean} Is moving.
     * @private
     */
    isMoving_(pointerEvent) {
      return (
        this.dragging_ ||
        Math.abs(pointerEvent.clientX - this.down_.clientX) >
          this.moveTolerance_ ||
        Math.abs(pointerEvent.clientY - this.down_.clientY) > this.moveTolerance_
      );
    }

    /**
     * Clean up.
     * @override
     */
    disposeInternal() {
      if (this.relayedListenerKey_) {
        unlistenByKey(this.relayedListenerKey_);
        this.relayedListenerKey_ = null;
      }
      this.element_.removeEventListener(
        EventType.TOUCHMOVE,
        this.boundHandleTouchMove_,
      );

      if (this.pointerdownListenerKey_) {
        unlistenByKey(this.pointerdownListenerKey_);
        this.pointerdownListenerKey_ = null;
      }

      this.dragListenerKeys_.forEach(unlistenByKey);
      this.dragListenerKeys_.length = 0;

      this.element_ = null;
      super.disposeInternal();
    }
  }

  /**
   * @module ol/MapEventType
   */

  /**
   * @enum {string}
   */
  var MapEventType = {
    /**
     * Triggered after a map frame is rendered.
     * @event module:ol/MapEvent~MapEvent#postrender
     * @api
     */
    POSTRENDER: 'postrender',

    /**
     * Triggered when the map starts moving.
     * @event module:ol/MapEvent~MapEvent#movestart
     * @api
     */
    MOVESTART: 'movestart',

    /**
     * Triggered after the map is moved.
     * @event module:ol/MapEvent~MapEvent#moveend
     * @api
     */
    MOVEEND: 'moveend',

    /**
     * Triggered when loading of additional map data (tiles, images, features) starts.
     * @event module:ol/MapEvent~MapEvent#loadstart
     * @api
     */
    LOADSTART: 'loadstart',

    /**
     * Triggered when loading of additional map data has completed.
     * @event module:ol/MapEvent~MapEvent#loadend
     * @api
     */
    LOADEND: 'loadend',
  };

  /***
   * @typedef {'postrender'|'movestart'|'moveend'|'loadstart'|'loadend'} Types
   */

  /**
   * @module ol/MapProperty
   */

  /**
   * @enum {string}
   */
  var MapProperty = {
    LAYERGROUP: 'layergroup',
    SIZE: 'size',
    TARGET: 'target',
    VIEW: 'view',
  };

  /**
   * @module ol/structs/PriorityQueue
   */

  /**
   * @type {number}
   */
  const DROP = Infinity;

  /**
   * @classdesc
   * Priority queue.
   *
   * The implementation is inspired from the Closure Library's Heap class and
   * Python's heapq module.
   *
   * See https://github.com/google/closure-library/blob/master/closure/goog/structs/heap.js
   * and https://hg.python.org/cpython/file/2.7/Lib/heapq.py.
   *
   * @template T
   */
  class PriorityQueue {
    /**
     * @param {function(T): number} priorityFunction Priority function.
     * @param {function(T): string} keyFunction Key function.
     */
    constructor(priorityFunction, keyFunction) {
      /**
       * @type {function(T): number}
       * @private
       */
      this.priorityFunction_ = priorityFunction;

      /**
       * @type {function(T): string}
       * @private
       */
      this.keyFunction_ = keyFunction;

      /**
       * @type {Array<T>}
       * @private
       */
      this.elements_ = [];

      /**
       * @type {Array<number>}
       * @private
       */
      this.priorities_ = [];

      /**
       * @type {!Object<string, boolean>}
       * @private
       */
      this.queuedElements_ = {};
    }

    /**
     * FIXME empty description for jsdoc
     */
    clear() {
      this.elements_.length = 0;
      this.priorities_.length = 0;
      clear(this.queuedElements_);
    }

    /**
     * Remove and return the highest-priority element. O(log N).
     * @return {T} Element.
     */
    dequeue() {
      const elements = this.elements_;
      const priorities = this.priorities_;
      const element = elements[0];
      if (elements.length == 1) {
        elements.length = 0;
        priorities.length = 0;
      } else {
        elements[0] = /** @type {T} */ (elements.pop());
        priorities[0] = /** @type {number} */ (priorities.pop());
        this.siftUp_(0);
      }
      const elementKey = this.keyFunction_(element);
      delete this.queuedElements_[elementKey];
      return element;
    }

    /**
     * Enqueue an element. O(log N).
     * @param {T} element Element.
     * @return {boolean} The element was added to the queue.
     */
    enqueue(element) {
      assert(
        !(this.keyFunction_(element) in this.queuedElements_),
        'Tried to enqueue an `element` that was already added to the queue',
      );
      const priority = this.priorityFunction_(element);
      if (priority != DROP) {
        this.elements_.push(element);
        this.priorities_.push(priority);
        this.queuedElements_[this.keyFunction_(element)] = true;
        this.siftDown_(0, this.elements_.length - 1);
        return true;
      }
      return false;
    }

    /**
     * @return {number} Count.
     */
    getCount() {
      return this.elements_.length;
    }

    /**
     * Gets the index of the left child of the node at the given index.
     * @param {number} index The index of the node to get the left child for.
     * @return {number} The index of the left child.
     * @private
     */
    getLeftChildIndex_(index) {
      return index * 2 + 1;
    }

    /**
     * Gets the index of the right child of the node at the given index.
     * @param {number} index The index of the node to get the right child for.
     * @return {number} The index of the right child.
     * @private
     */
    getRightChildIndex_(index) {
      return index * 2 + 2;
    }

    /**
     * Gets the index of the parent of the node at the given index.
     * @param {number} index The index of the node to get the parent for.
     * @return {number} The index of the parent.
     * @private
     */
    getParentIndex_(index) {
      return (index - 1) >> 1;
    }

    /**
     * Make this a heap. O(N).
     * @private
     */
    heapify_() {
      let i;
      for (i = (this.elements_.length >> 1) - 1; i >= 0; i--) {
        this.siftUp_(i);
      }
    }

    /**
     * @return {boolean} Is empty.
     */
    isEmpty() {
      return this.elements_.length === 0;
    }

    /**
     * @param {string} key Key.
     * @return {boolean} Is key queued.
     */
    isKeyQueued(key) {
      return key in this.queuedElements_;
    }

    /**
     * @param {T} element Element.
     * @return {boolean} Is queued.
     */
    isQueued(element) {
      return this.isKeyQueued(this.keyFunction_(element));
    }

    /**
     * @param {number} index The index of the node to move down.
     * @private
     */
    siftUp_(index) {
      const elements = this.elements_;
      const priorities = this.priorities_;
      const count = elements.length;
      const element = elements[index];
      const priority = priorities[index];
      const startIndex = index;

      while (index < count >> 1) {
        const lIndex = this.getLeftChildIndex_(index);
        const rIndex = this.getRightChildIndex_(index);

        const smallerChildIndex =
          rIndex < count && priorities[rIndex] < priorities[lIndex]
            ? rIndex
            : lIndex;

        elements[index] = elements[smallerChildIndex];
        priorities[index] = priorities[smallerChildIndex];
        index = smallerChildIndex;
      }

      elements[index] = element;
      priorities[index] = priority;
      this.siftDown_(startIndex, index);
    }

    /**
     * @param {number} startIndex The index of the root.
     * @param {number} index The index of the node to move up.
     * @private
     */
    siftDown_(startIndex, index) {
      const elements = this.elements_;
      const priorities = this.priorities_;
      const element = elements[index];
      const priority = priorities[index];

      while (index > startIndex) {
        const parentIndex = this.getParentIndex_(index);
        if (priorities[parentIndex] > priority) {
          elements[index] = elements[parentIndex];
          priorities[index] = priorities[parentIndex];
          index = parentIndex;
        } else {
          break;
        }
      }
      elements[index] = element;
      priorities[index] = priority;
    }

    /**
     * FIXME empty description for jsdoc
     */
    reprioritize() {
      const priorityFunction = this.priorityFunction_;
      const elements = this.elements_;
      const priorities = this.priorities_;
      let index = 0;
      const n = elements.length;
      let element, i, priority;
      for (i = 0; i < n; ++i) {
        element = elements[i];
        priority = priorityFunction(element);
        if (priority == DROP) {
          delete this.queuedElements_[this.keyFunction_(element)];
        } else {
          priorities[index] = priority;
          elements[index++] = element;
        }
      }
      elements.length = index;
      priorities.length = index;
      this.heapify_();
    }
  }

  /**
   * @module ol/TileQueue
   */

  /**
   * @typedef {function(import("./Tile.js").default, string, import('./tilecoord.js').TileCoord, number): number} PriorityFunction
   */

  /**
   * @typedef {[import('./Tile.js').default, string, import('./tilecoord.js').TileCoord, number]} TileQueueElement
   */

  /**
   * @extends PriorityQueue<TileQueueElement>}
   */
  class TileQueue extends PriorityQueue {
    /**
     * @param {PriorityFunction} tilePriorityFunction Tile priority function.
     * @param {function(): ?} tileChangeCallback Function called on each tile change event.
     */
    constructor(tilePriorityFunction, tileChangeCallback) {
      super(
        (element) => tilePriorityFunction.apply(null, element),
        (element) => element[0].getKey(),
      );

      /** @private */
      this.boundHandleTileChange_ = this.handleTileChange.bind(this);

      /**
       * @private
       * @type {function(): ?}
       */
      this.tileChangeCallback_ = tileChangeCallback;

      /**
       * @private
       * @type {number}
       */
      this.tilesLoading_ = 0;

      /**
       * @private
       * @type {!Object<string,boolean>}
       */
      this.tilesLoadingKeys_ = {};
    }

    /**
     * @param {TileQueueElement} element Element.
     * @return {boolean} The element was added to the queue.
     * @override
     */
    enqueue(element) {
      const added = super.enqueue(element);
      if (added) {
        const tile = element[0];
        tile.addEventListener(EventType.CHANGE, this.boundHandleTileChange_);
      }
      return added;
    }

    /**
     * @return {number} Number of tiles loading.
     */
    getTilesLoading() {
      return this.tilesLoading_;
    }

    /**
     * @param {import("./events/Event.js").default} event Event.
     * @protected
     */
    handleTileChange(event) {
      const tile = /** @type {import("./Tile.js").default} */ (event.target);
      const state = tile.getState();
      if (
        state === TileState.LOADED ||
        state === TileState.ERROR ||
        state === TileState.EMPTY
      ) {
        if (state !== TileState.ERROR) {
          tile.removeEventListener(EventType.CHANGE, this.boundHandleTileChange_);
        }
        const tileKey = tile.getKey();
        if (tileKey in this.tilesLoadingKeys_) {
          delete this.tilesLoadingKeys_[tileKey];
          --this.tilesLoading_;
        }
        this.tileChangeCallback_();
      }
    }

    /**
     * @param {number} maxTotalLoading Maximum number tiles to load simultaneously.
     * @param {number} maxNewLoads Maximum number of new tiles to load.
     */
    loadMoreTiles(maxTotalLoading, maxNewLoads) {
      let newLoads = 0;
      while (
        this.tilesLoading_ < maxTotalLoading &&
        newLoads < maxNewLoads &&
        this.getCount() > 0
      ) {
        const tile = this.dequeue()[0];
        const tileKey = tile.getKey();
        const state = tile.getState();
        if (state === TileState.IDLE && !(tileKey in this.tilesLoadingKeys_)) {
          this.tilesLoadingKeys_[tileKey] = true;
          ++this.tilesLoading_;
          ++newLoads;
          tile.load();
        }
      }
    }
  }

  /**
   * @param {import('./Map.js').FrameState} frameState Frame state.
   * @param {import("./Tile.js").default} tile Tile.
   * @param {string} tileSourceKey Tile source key.
   * @param {import("./coordinate.js").Coordinate} tileCenter Tile center.
   * @param {number} tileResolution Tile resolution.
   * @return {number} Tile priority.
   */
  function getTilePriority(
    frameState,
    tile,
    tileSourceKey,
    tileCenter,
    tileResolution,
  ) {
    // Filter out tiles at higher zoom levels than the current zoom level, or that
    // are outside the visible extent.
    if (!frameState || !(tileSourceKey in frameState.wantedTiles)) {
      return DROP;
    }
    if (!frameState.wantedTiles[tileSourceKey][tile.getKey()]) {
      return DROP;
    }
    // Prioritize the highest zoom level tiles closest to the focus.
    // Tiles at higher zoom levels are prioritized using Math.log(tileResolution).
    // Within a zoom level, tiles are prioritized by the distance in pixels between
    // the center of the tile and the center of the viewport.  The factor of 65536
    // means that the prioritization should behave as desired for tiles up to
    // 65536 * Math.log(2) = 45426 pixels from the focus.
    const center = frameState.viewState.center;
    const deltaX = tileCenter[0] - center[0];
    const deltaY = tileCenter[1] - center[1];
    return (
      65536 * Math.log(tileResolution) +
      Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution
    );
  }

  /**
   * @module ol/control/Control
   */

  /**
   * @typedef {Object} Options
   * @property {HTMLElement} [element] The element is the control's
   * container element. This only needs to be specified if you're developing
   * a custom control.
   * @property {function(import("../MapEvent.js").default):void} [render] Function called when
   * the control should be re-rendered. This is called in a `requestAnimationFrame`
   * callback.
   * @property {HTMLElement|string} [target] Specify a target if you want
   * the control to be rendered outside of the map's viewport.
   */

  /**
   * @classdesc
   * A control is a visible widget with a DOM element in a fixed position on the
   * screen. They can involve user input (buttons), or be informational only;
   * the position is determined using CSS. By default these are placed in the
   * container with CSS class name `ol-overlaycontainer-stopevent`, but can use
   * any outside DOM element.
   *
   * This is the base class for controls. You can use it for simple custom
   * controls by creating the element with listeners, creating an instance:
   * ```js
   * const myControl = new Control({element: myElement});
   * ```
   * and then adding this to the map.
   *
   * The main advantage of having this as a control rather than a simple separate
   * DOM element is that preventing propagation is handled for you. Controls
   * will also be objects in a {@link module:ol/Collection~Collection}, so you can use their methods.
   *
   * You can also extend this base for your own control class. See
   * examples/custom-controls for an example of how to do this.
   *
   * @api
   */
  class Control extends BaseObject {
    /**
     * @param {Options} options Control options.
     */
    constructor(options) {
      super();

      const element = options.element;
      if (element && !options.target && !element.style.pointerEvents) {
        element.style.pointerEvents = 'auto';
      }

      /**
       * @protected
       * @type {HTMLElement}
       */
      this.element = element ? element : null;

      /**
       * @private
       * @type {HTMLElement}
       */
      this.target_ = null;

      /**
       * @private
       * @type {import("../Map.js").default|null}
       */
      this.map_ = null;

      /**
       * @protected
       * @type {!Array<import("../events.js").EventsKey>}
       */
      this.listenerKeys = [];

      if (options.render) {
        this.render = options.render;
      }

      if (options.target) {
        this.setTarget(options.target);
      }
    }

    /**
     * Clean up.
     * @override
     */
    disposeInternal() {
      this.element?.remove();
      super.disposeInternal();
    }

    /**
     * Get the map associated with this control.
     * @return {import("../Map.js").default|null} Map.
     * @api
     */
    getMap() {
      return this.map_;
    }

    /**
     * Remove the control from its current map and attach it to the new map.
     * Pass `null` to just remove the control from the current map.
     * Subclasses may set up event handlers to get notified about changes to
     * the map here.
     * @param {import("../Map.js").default|null} map Map.
     * @api
     */
    setMap(map) {
      if (this.map_) {
        this.element?.remove();
      }
      for (let i = 0, ii = this.listenerKeys.length; i < ii; ++i) {
        unlistenByKey(this.listenerKeys[i]);
      }
      this.listenerKeys.length = 0;
      this.map_ = map;
      if (map) {
        const target = this.target_ ?? map.getOverlayContainerStopEvent();
        if (this.element) {
          target.appendChild(this.element);
        }
        if (this.render !== VOID) {
          this.listenerKeys.push(
            listen(map, MapEventType.POSTRENDER, this.render, this),
          );
        }
        map.render();
      }
    }

    /**
     * Renders the control.
     * @param {import("../MapEvent.js").default} mapEvent Map event.
     * @api
     */
    render(mapEvent) {}

    /**
     * This function is used to set a target element for the control. It has no
     * effect if it is called after the control has been added to the map (i.e.
     * after `setMap` is called on the control). If no `target` is set in the
     * options passed to the control constructor and if `setTarget` is not called
     * then the control is added to the map's overlay container.
     * @param {HTMLElement|string} target Target.
     * @api
     */
    setTarget(target) {
      this.target_ =
        typeof target === 'string' ? document.getElementById(target) : target;
    }
  }

  /**
   * @module ol/control/Attribution
   */

  /**
   * @typedef {Object} Options
   * @property {string} [className='ol-attribution'] CSS class name.
   * @property {HTMLElement|string} [target] Specify a target if you
   * want the control to be rendered outside of the map's
   * viewport.
   * @property {boolean} [collapsible] Specify if attributions can
   * be collapsed. If not specified, sources control this behavior with their
   * `attributionsCollapsible` setting.
   * @property {boolean} [collapsed=true] Specify if attributions should
   * be collapsed at startup.
   * @property {string} [tipLabel='Attributions'] Text label to use for the button tip.
   * @property {string|HTMLElement} [label='i'] Text label to use for the
   * collapsed attributions button.
   * Instead of text, also an element (e.g. a `span` element) can be used.
   * @property {string} [expandClassName=className + '-expand'] CSS class name for the
   * collapsed attributions button.
   * @property {string|HTMLElement} [collapseLabel='›'] Text label to use
   * for the expanded attributions button.
   * Instead of text, also an element (e.g. a `span` element) can be used.
   * @property {string} [collapseClassName=className + '-collapse'] CSS class name for the
   * expanded attributions button.
   * @property {function(import("../MapEvent.js").default):void} [render] Function called when
   * the control should be re-rendered. This is called in a `requestAnimationFrame`
   * callback.
   * @property {string|Array<string>|undefined} [attributions] Optional attribution(s) that will always be
   * displayed regardless of the layers rendered.
   * **Caution:** Attributions are rendered dynamically using `innerHTML`, which can lead to potential
   * [**XSS (Cross-Site Scripting)**](https://en.wikipedia.org/wiki/Cross-site_scripting) vulnerabilities.
   * Use this feature only for trusted content
   * or ensure that the content is properly sanitized before inserting it.
   */

  /**
   * @classdesc
   * Control to show all the attributions associated with the layer sources
   * in the map. This control is one of the default controls included in maps.
   * By default it will show in the bottom right portion of the map, but this can
   * be changed by using a css selector for `.ol-attribution`.
   *
   * @api
   */
  class Attribution extends Control {
    /**
     * @param {Options} [options] Attribution options.
     */
    constructor(options) {
      options = options ? options : {};

      super({
        element: document.createElement('div'),
        render: options.render,
        target: options.target,
      });

      /**
       * @private
       * @type {HTMLElement}
       */
      this.ulElement_ = document.createElement('ul');

      /**
       * @private
       * @type {boolean}
       */
      this.collapsed_ =
        options.collapsed !== undefined ? options.collapsed : true;

      /**
       * @private
       * @type {boolean}
       */
      this.userCollapsed_ = this.collapsed_;

      /**
       * @private
       * @type {boolean}
       */
      this.overrideCollapsible_ = options.collapsible !== undefined;

      /**
       * @private
       * @type {boolean}
       */
      this.collapsible_ =
        options.collapsible !== undefined ? options.collapsible : true;

      if (!this.collapsible_) {
        this.collapsed_ = false;
      }

      /**
       * @private
       * @type {string | Array<string> | undefined}
       */
      this.attributions_ = options.attributions;

      const className =
        options.className !== undefined ? options.className : 'ol-attribution';

      const tipLabel =
        options.tipLabel !== undefined ? options.tipLabel : 'Attributions';

      const expandClassName =
        options.expandClassName !== undefined
          ? options.expandClassName
          : className + '-expand';

      const collapseLabel =
        options.collapseLabel !== undefined ? options.collapseLabel : '\u203A';

      const collapseClassName =
        options.collapseClassName !== undefined
          ? options.collapseClassName
          : className + '-collapse';

      if (typeof collapseLabel === 'string') {
        /**
         * @private
         * @type {HTMLElement}
         */
        this.collapseLabel_ = document.createElement('span');
        this.collapseLabel_.textContent = collapseLabel;
        this.collapseLabel_.className = collapseClassName;
      } else {
        this.collapseLabel_ = collapseLabel;
      }

      const label = options.label !== undefined ? options.label : 'i';

      if (typeof label === 'string') {
        /**
         * @private
         * @type {HTMLElement}
         */
        this.label_ = document.createElement('span');
        this.label_.textContent = label;
        this.label_.className = expandClassName;
      } else {
        this.label_ = label;
      }

      const activeLabel =
        this.collapsible_ && !this.collapsed_ ? this.collapseLabel_ : this.label_;

      /**
       * @private
       * @type {HTMLElement}
       */
      this.toggleButton_ = document.createElement('button');
      this.toggleButton_.setAttribute('type', 'button');
      this.toggleButton_.setAttribute('aria-expanded', String(!this.collapsed_));
      this.toggleButton_.title = tipLabel;
      this.toggleButton_.appendChild(activeLabel);

      this.toggleButton_.addEventListener(
        EventType.CLICK,
        this.handleClick_.bind(this),
        false,
      );

      const cssClasses =
        className +
        ' ' +
        CLASS_UNSELECTABLE +
        ' ' +
        CLASS_CONTROL +
        (this.collapsed_ && this.collapsible_ ? ' ' + CLASS_COLLAPSED : '') +
        (this.collapsible_ ? '' : ' ol-uncollapsible');
      const element = this.element;
      element.className = cssClasses;
      element.appendChild(this.toggleButton_);
      element.appendChild(this.ulElement_);

      /**
       * A list of currently rendered resolutions.
       * @type {Array<string>}
       * @private
       */
      this.renderedAttributions_ = [];

      /**
       * @private
       * @type {boolean}
       */
      this.renderedVisible_ = true;
    }

    /**
     * Collect a list of visible attributions and set the collapsible state.
     * @param {import("../Map.js").FrameState} frameState Frame state.
     * @return {Array<string>} Attributions.
     * @private
     */
    collectSourceAttributions_(frameState) {
      const layers = this.getMap().getAllLayers();
      const visibleAttributions = new Set(
        layers.flatMap((layer) => layer.getAttributions(frameState)),
      );
      if (this.attributions_ !== undefined) {
        Array.isArray(this.attributions_)
          ? this.attributions_.forEach((item) => visibleAttributions.add(item))
          : visibleAttributions.add(this.attributions_);
      }

      if (!this.overrideCollapsible_) {
        const collapsible = !layers.some(
          (layer) => layer.getSource()?.getAttributionsCollapsible() === false,
        );
        this.setCollapsible(collapsible);
      }
      return Array.from(visibleAttributions);
    }

    /**
     * @private
     * @param {?import("../Map.js").FrameState} frameState Frame state.
     */
    async updateElement_(frameState) {
      if (!frameState) {
        if (this.renderedVisible_) {
          this.element.style.display = 'none';
          this.renderedVisible_ = false;
        }
        return;
      }

      const attributions = await Promise.all(
        this.collectSourceAttributions_(frameState).map((attribution) =>
          toPromise(() => attribution),
        ),
      );

      const visible = attributions.length > 0;
      if (this.renderedVisible_ != visible) {
        this.element.style.display = visible ? '' : 'none';
        this.renderedVisible_ = visible;
      }

      if (equals$2(attributions, this.renderedAttributions_)) {
        return;
      }

      removeChildren(this.ulElement_);

      // append the attributions
      for (let i = 0, ii = attributions.length; i < ii; ++i) {
        const element = document.createElement('li');
        element.innerHTML = attributions[i];
        this.ulElement_.appendChild(element);
      }

      this.renderedAttributions_ = attributions;
    }

    /**
     * @param {MouseEvent} event The event to handle
     * @private
     */
    handleClick_(event) {
      event.preventDefault();
      this.handleToggle_();
      this.userCollapsed_ = this.collapsed_;
    }

    /**
     * @private
     */
    handleToggle_() {
      this.element.classList.toggle(CLASS_COLLAPSED);
      if (this.collapsed_) {
        replaceNode(this.collapseLabel_, this.label_);
      } else {
        replaceNode(this.label_, this.collapseLabel_);
      }
      this.collapsed_ = !this.collapsed_;
      this.toggleButton_.setAttribute('aria-expanded', String(!this.collapsed_));
    }

    /**
     * Return `true` if the attribution is collapsible, `false` otherwise.
     * @return {boolean} True if the widget is collapsible.
     * @api
     */
    getCollapsible() {
      return this.collapsible_;
    }

    /**
     * Set whether the attribution should be collapsible.
     * @param {boolean} collapsible True if the widget is collapsible.
     * @api
     */
    setCollapsible(collapsible) {
      if (this.collapsible_ === collapsible) {
        return;
      }
      this.collapsible_ = collapsible;
      this.element.classList.toggle('ol-uncollapsible');
      if (this.userCollapsed_) {
        this.handleToggle_();
      }
    }

    /**
     * Collapse or expand the attribution according to the passed parameter. Will
     * not do anything if the attribution isn't collapsible or if the current
     * collapsed state is already the one requested.
     * @param {boolean} collapsed True if the widget is collapsed.
     * @api
     */
    setCollapsed(collapsed) {
      this.userCollapsed_ = collapsed;
      if (!this.collapsible_ || this.collapsed_ === collapsed) {
        return;
      }
      this.handleToggle_();
    }

    /**
     * Return `true` when the attribution is currently collapsed or `false`
     * otherwise.
     * @return {boolean} True if the widget is collapsed.
     * @api
     */
    getCollapsed() {
      return this.collapsed_;
    }

    /**
     * Update the attribution element.
     * @param {import("../MapEvent.js").default} mapEvent Map event.
     * @override
     */
    render(mapEvent) {
      this.updateElement_(mapEvent.frameState);
    }
  }

  /**
   * @module ol/control/Rotate
   */

  /**
   * @typedef {Object} Options
   * @property {string} [className='ol-rotate'] CSS class name.
   * @property {string|HTMLElement} [label='⇧'] Text label to use for the rotate button.
   * Instead of text, also an element (e.g. a `span` element) can be used.
   * @property {string} [tipLabel='Reset rotation'] Text label to use for the rotate tip.
   * @property {string} [compassClassName='ol-compass'] CSS class name for the compass.
   * @property {number} [duration=250] Animation duration in milliseconds.
   * @property {boolean} [autoHide=true] Hide the control when rotation is 0.
   * @property {function(import("../MapEvent.js").default):void} [render] Function called when the control should
   * be re-rendered. This is called in a `requestAnimationFrame` callback.
   * @property {function():void} [resetNorth] Function called when the control is clicked.
   * This will override the default `resetNorth`.
   * @property {HTMLElement|string} [target] Specify a target if you want the control to be
   * rendered outside of the map's viewport.
   */

  /**
   * @classdesc
   * A button control to reset rotation to 0.
   * To style this control use css selector `.ol-rotate`. A `.ol-hidden` css
   * selector is added to the button when the rotation is 0.
   *
   * @api
   */
  class Rotate extends Control {
    /**
     * @param {Options} [options] Rotate options.
     */
    constructor(options) {
      options = options ? options : {};

      super({
        element: document.createElement('div'),
        render: options.render,
        target: options.target,
      });

      const className =
        options.className !== undefined ? options.className : 'ol-rotate';

      const label = options.label !== undefined ? options.label : '\u21E7';

      const compassClassName =
        options.compassClassName !== undefined
          ? options.compassClassName
          : 'ol-compass';

      /**
       * @type {HTMLElement}
       * @private
       */
      this.label_ = null;

      if (typeof label === 'string') {
        this.label_ = document.createElement('span');
        this.label_.className = compassClassName;
        this.label_.textContent = label;
      } else {
        this.label_ = label;
        this.label_.classList.add(compassClassName);
      }

      const tipLabel = options.tipLabel ? options.tipLabel : 'Reset rotation';

      const button = document.createElement('button');
      button.className = className + '-reset';
      button.setAttribute('type', 'button');
      button.title = tipLabel;
      button.appendChild(this.label_);

      button.addEventListener(
        EventType.CLICK,
        this.handleClick_.bind(this),
        false,
      );

      const cssClasses =
        className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL;
      const element = this.element;
      element.className = cssClasses;
      element.appendChild(button);

      /**
       * @private
       */
      this.callResetNorth_ = options.resetNorth ? options.resetNorth : undefined;

      /**
       * @type {number}
       * @private
       */
      this.duration_ = options.duration !== undefined ? options.duration : 250;

      /**
       * @type {boolean}
       * @private
       */
      this.autoHide_ = options.autoHide !== undefined ? options.autoHide : true;

      /**
       * @private
       * @type {number|undefined}
       */
      this.rotation_ = undefined;

      if (this.autoHide_) {
        this.element.classList.add(CLASS_HIDDEN);
      }
    }

    /**
     * @param {MouseEvent} event The event to handle
     * @private
     */
    handleClick_(event) {
      event.preventDefault();
      if (this.callResetNorth_ !== undefined) {
        this.callResetNorth_();
      } else {
        this.resetNorth_();
      }
    }

    /**
     * @private
     */
    resetNorth_() {
      const map = this.getMap();
      const view = map.getView();
      if (!view) {
        // the map does not have a view, so we can't act
        // upon it
        return;
      }
      const rotation = view.getRotation();
      if (rotation !== undefined) {
        if (this.duration_ > 0 && rotation % (2 * Math.PI) !== 0) {
          view.animate({
            rotation: 0,
            duration: this.duration_,
            easing: easeOut,
          });
        } else {
          view.setRotation(0);
        }
      }
    }

    /**
     * Update the rotate control element.
     * @param {import("../MapEvent.js").default} mapEvent Map event.
     * @override
     */
    render(mapEvent) {
      const frameState = mapEvent.frameState;
      if (!frameState) {
        return;
      }
      const rotation = frameState.viewState.rotation;
      if (rotation != this.rotation_) {
        const transform = 'rotate(' + rotation + 'rad)';
        if (this.autoHide_) {
          const contains = this.element.classList.contains(CLASS_HIDDEN);
          if (!contains && rotation === 0) {
            this.element.classList.add(CLASS_HIDDEN);
          } else if (contains && rotation !== 0) {
            this.element.classList.remove(CLASS_HIDDEN);
          }
        }
        this.label_.style.transform = transform;
      }
      this.rotation_ = rotation;
    }
  }

  /**
   * @module ol/control/Zoom
   */

  /**
   * @typedef {Object} Options
   * @property {number} [duration=250] Animation duration in milliseconds.
   * @property {string} [className='ol-zoom'] CSS class name.
   * @property {string} [zoomInClassName=className + '-in'] CSS class name for the zoom-in button.
   * @property {string} [zoomOutClassName=className + '-out'] CSS class name for the zoom-out button.
   * @property {string|HTMLElement} [zoomInLabel='+'] Text label to use for the zoom-in
   * button. Instead of text, also an element (e.g. a `span` element) can be used.
   * @property {string|HTMLElement} [zoomOutLabel='–'] Text label to use for the zoom-out button.
   * Instead of text, also an element (e.g. a `span` element) can be used.
   * @property {string} [zoomInTipLabel='Zoom in'] Text label to use for the button tip.
   * @property {string} [zoomOutTipLabel='Zoom out'] Text label to use for the button tip.
   * @property {number} [delta=1] The zoom delta applied on each click.
   * @property {HTMLElement|string} [target] Specify a target if you want the control to be
   * rendered outside of the map's viewport.
   */

  /**
   * @classdesc
   * A control with 2 buttons, one for zoom in and one for zoom out.
   * This control is one of the default controls of a map. To style this control
   * use css selectors `.ol-zoom-in` and `.ol-zoom-out`.
   *
   * @api
   */
  class Zoom extends Control {
    /**
     * @param {Options} [options] Zoom options.
     */
    constructor(options) {
      options = options ? options : {};

      super({
        element: document.createElement('div'),
        target: options.target,
      });

      const className =
        options.className !== undefined ? options.className : 'ol-zoom';

      const delta = options.delta !== undefined ? options.delta : 1;

      const zoomInClassName =
        options.zoomInClassName !== undefined
          ? options.zoomInClassName
          : className + '-in';

      const zoomOutClassName =
        options.zoomOutClassName !== undefined
          ? options.zoomOutClassName
          : className + '-out';

      const zoomInLabel =
        options.zoomInLabel !== undefined ? options.zoomInLabel : '+';
      const zoomOutLabel =
        options.zoomOutLabel !== undefined ? options.zoomOutLabel : '\u2013';

      const zoomInTipLabel =
        options.zoomInTipLabel !== undefined ? options.zoomInTipLabel : 'Zoom in';
      const zoomOutTipLabel =
        options.zoomOutTipLabel !== undefined
          ? options.zoomOutTipLabel
          : 'Zoom out';

      const inElement = document.createElement('button');
      inElement.className = zoomInClassName;
      inElement.setAttribute('type', 'button');
      inElement.title = zoomInTipLabel;
      inElement.appendChild(
        typeof zoomInLabel === 'string'
          ? document.createTextNode(zoomInLabel)
          : zoomInLabel,
      );

      inElement.addEventListener(
        EventType.CLICK,
        this.handleClick_.bind(this, delta),
        false,
      );

      const outElement = document.createElement('button');
      outElement.className = zoomOutClassName;
      outElement.setAttribute('type', 'button');
      outElement.title = zoomOutTipLabel;
      outElement.appendChild(
        typeof zoomOutLabel === 'string'
          ? document.createTextNode(zoomOutLabel)
          : zoomOutLabel,
      );

      outElement.addEventListener(
        EventType.CLICK,
        this.handleClick_.bind(this, -delta),
        false,
      );

      const cssClasses =
        className + ' ' + CLASS_UNSELECTABLE + ' ' + CLASS_CONTROL;
      const element = this.element;
      element.className = cssClasses;
      element.appendChild(inElement);
      element.appendChild(outElement);

      /**
       * @type {number}
       * @private
       */
      this.duration_ = options.duration !== undefined ? options.duration : 250;
    }

    /**
     * @param {number} delta Zoom delta.
     * @param {MouseEvent} event The event to handle
     * @private
     */
    handleClick_(delta, event) {
      event.preventDefault();
      this.zoomByDelta_(delta);
    }

    /**
     * @param {number} delta Zoom delta.
     * @private
     */
    zoomByDelta_(delta) {
      const map = this.getMap();
      const view = map.getView();
      if (!view) {
        // the map does not have a view, so we can't act
        // upon it
        return;
      }
      const currentZoom = view.getZoom();
      if (currentZoom !== undefined) {
        const newZoom = view.getConstrainedZoom(currentZoom + delta);
        if (this.duration_ > 0) {
          if (view.getAnimating()) {
            view.cancelAnimations();
          }
          view.animate({
            zoom: newZoom,
            duration: this.duration_,
            easing: easeOut,
          });
        } else {
          view.setZoom(newZoom);
        }
      }
    }
  }

  /**
   * @module ol/control/defaults
   */

  /**
   * @typedef {Object} DefaultsOptions
   * @property {boolean} [attribution=true] Include
   * {@link module:ol/control/Attribution~Attribution}.
   * @property {import("./Attribution.js").Options} [attributionOptions]
   * Options for {@link module:ol/control/Attribution~Attribution}.
   * @property {boolean} [rotate=true] Include
   * {@link module:ol/control/Rotate~Rotate}.
   * @property {import("./Rotate.js").Options} [rotateOptions] Options
   * for {@link module:ol/control/Rotate~Rotate}.
   * @property {boolean} [zoom] Include {@link module:ol/control/Zoom~Zoom}.
   * @property {import("./Zoom.js").Options} [zoomOptions] Options for
   * {@link module:ol/control/Zoom~Zoom}.
   */

  /**
   * Set of controls included in maps by default. Unless configured otherwise,
   * this returns a collection containing an instance of each of the following
   * controls:
   * {@link module:ol/control/Zoom~Zoom}
   * {@link module:ol/control/Rotate~Rotate}
   * {@link module:ol/control/Attribution~Attribution}
   *
   * @param {DefaultsOptions} [options] Options for the default controls.
   * @return {Collection<import("./Control.js").default>} A collection of controls
   * to be used with the {@link module:ol/Map~Map} constructor's `controls` option.
   * @api
   */
  function defaults$1(options) {
    options = options ? options : {};

    /** @type {Collection<import("./Control.js").default>} */
    const controls = new Collection();

    const zoomControl = options.zoom !== undefined ? options.zoom : true;
    if (zoomControl) {
      controls.push(new Zoom(options.zoomOptions));
    }

    const rotateControl = options.rotate !== undefined ? options.rotate : true;
    if (rotateControl) {
      controls.push(new Rotate(options.rotateOptions));
    }

    const attributionControl =
      options.attribution !== undefined ? options.attribution : true;
    if (attributionControl) {
      controls.push(new Attribution(options.attributionOptions));
    }

    return controls;
  }

  /**
   * @module ol/interaction/Property
   */

  /**
   * @enum {string}
   */
  var InteractionProperty = {
    ACTIVE: 'active',
  };

  /**
   * @module ol/interaction/Interaction
   */

  /***
   * @template Return
   * @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> &
   *   import("../Observable").OnSignature<import("../ObjectEventType").Types|
   *     'change:active', import("../Object").ObjectEvent, Return> &
   *   import("../Observable").CombinedOnSignature<import("../Observable").EventTypes|import("../ObjectEventType").Types|
   *     'change:active', Return>} InteractionOnSignature
   */

  /**
   * Object literal with config options for interactions.
   * @typedef {Object} InteractionOptions
   * @property {function(import("../MapBrowserEvent.js").default):boolean} [handleEvent]
   * Method called by the map to notify the interaction that a browser event was
   * dispatched to the map. If the function returns a falsy value, propagation of
   * the event to other interactions in the map's interactions chain will be
   * prevented (this includes functions with no explicit return). The interactions
   * are traversed in reverse order of the interactions collection of the map.
   */

  /**
   * @classdesc
   * Abstract base class; normally only used for creating subclasses and not
   * instantiated in apps.
   * User actions that change the state of the map. Some are similar to controls,
   * but are not associated with a DOM element.
   * For example, {@link module:ol/interaction/KeyboardZoom~KeyboardZoom} is
   * functionally the same as {@link module:ol/control/Zoom~Zoom}, but triggered
   * by a keyboard event not a button element event.
   * Although interactions do not have a DOM element, some of them do render
   * vectors and so are visible on the screen.
   * @api
   */
  class Interaction extends BaseObject {
    /**
     * @param {InteractionOptions} [options] Options.
     */
    constructor(options) {
      super();

      /***
       * @type {InteractionOnSignature<import("../events").EventsKey>}
       */
      this.on;

      /***
       * @type {InteractionOnSignature<import("../events").EventsKey>}
       */
      this.once;

      /***
       * @type {InteractionOnSignature<void>}
       */
      this.un;

      if (options && options.handleEvent) {
        this.handleEvent = options.handleEvent;
      }

      /**
       * @private
       * @type {import("../Map.js").default|null}
       */
      this.map_ = null;

      this.setActive(true);
    }

    /**
     * Return whether the interaction is currently active.
     * @return {boolean} `true` if the interaction is active, `false` otherwise.
     * @observable
     * @api
     */
    getActive() {
      return /** @type {boolean} */ (this.get(InteractionProperty.ACTIVE));
    }

    /**
     * Get the map associated with this interaction.
     * @return {import("../Map.js").default|null} Map.
     * @api
     */
    getMap() {
      return this.map_;
    }

    /**
     * Handles the {@link module:ol/MapBrowserEvent~MapBrowserEvent map browser event}.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
     * @return {boolean} `false` to stop event propagation.
     * @api
     */
    handleEvent(mapBrowserEvent) {
      return true;
    }

    /**
     * Activate or deactivate the interaction.
     * @param {boolean} active Active.
     * @observable
     * @api
     */
    setActive(active) {
      this.set(InteractionProperty.ACTIVE, active);
    }

    /**
     * Remove the interaction from its current map and attach it to the new map.
     * Subclasses may set up event handlers to get notified about changes to
     * the map here.
     * @param {import("../Map.js").default|null} map Map.
     */
    setMap(map) {
      this.map_ = map;
    }
  }

  /**
   * @param {import("../View.js").default} view View.
   * @param {import("../coordinate.js").Coordinate} delta Delta.
   * @param {number} [duration] Duration.
   */
  function pan(view, delta, duration) {
    const currentCenter = view.getCenterInternal();
    if (currentCenter) {
      const center = [currentCenter[0] + delta[0], currentCenter[1] + delta[1]];
      view.animateInternal({
        duration: duration !== undefined ? duration : 250,
        easing: linear,
        center: view.getConstrainedCenter(center),
      });
    }
  }

  /**
   * @param {import("../View.js").default} view View.
   * @param {number} delta Delta from previous zoom level.
   * @param {import("../coordinate.js").Coordinate} [anchor] Anchor coordinate in the user projection.
   * @param {number} [duration] Duration.
   */
  function zoomByDelta(view, delta, anchor, duration) {
    const currentZoom = view.getZoom();

    if (currentZoom === undefined) {
      return;
    }

    const newZoom = view.getConstrainedZoom(currentZoom + delta);
    const newResolution = view.getResolutionForZoom(newZoom);

    if (view.getAnimating()) {
      view.cancelAnimations();
    }
    view.animate({
      resolution: newResolution,
      anchor: anchor,
      duration: duration !== undefined ? duration : 250,
      easing: easeOut,
    });
  }

  /**
   * @module ol/interaction/DoubleClickZoom
   */

  /**
   * @typedef {Object} Options
   * @property {number} [duration=250] Animation duration in milliseconds.
   * @property {number} [delta=1] The zoom delta applied on each double click.
   */

  /**
   * @classdesc
   * Allows the user to zoom by double-clicking on the map.
   * @api
   */
  class DoubleClickZoom extends Interaction {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      super();

      options = options ? options : {};

      /**
       * @private
       * @type {number}
       */
      this.delta_ = options.delta ? options.delta : 1;

      /**
       * @private
       * @type {number}
       */
      this.duration_ = options.duration !== undefined ? options.duration : 250;
    }

    /**
     * Handles the {@link module:ol/MapBrowserEvent~MapBrowserEvent map browser event} (if it was a
     * doubleclick) and eventually zooms the map.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
     * @return {boolean} `false` to stop event propagation.
     * @override
     */
    handleEvent(mapBrowserEvent) {
      let stopEvent = false;
      if (mapBrowserEvent.type == MapBrowserEventType.DBLCLICK) {
        const browserEvent = /** @type {MouseEvent} */ (
          mapBrowserEvent.originalEvent
        );
        const map = mapBrowserEvent.map;
        const anchor = mapBrowserEvent.coordinate;
        const delta = browserEvent.shiftKey ? -this.delta_ : this.delta_;
        const view = map.getView();
        zoomByDelta(view, delta, anchor, this.duration_);
        browserEvent.preventDefault();
        stopEvent = true;
      }
      return !stopEvent;
    }
  }

  /**
   * @module ol/events/condition
   */

  /**
   * A function that takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a
   * `{boolean}`. If the condition is met, true should be returned.
   *
   * @typedef {function(this: ?, import("../MapBrowserEvent.js").default): boolean} Condition
   */

  /**
   * Creates a condition function that passes when all provided conditions pass.
   * @param {...Condition} var_args Conditions to check.
   * @return {Condition} Condition function.
   */
  function all(var_args) {
    const conditions = arguments;
    /**
     * @param {import("../MapBrowserEvent.js").default} event Event.
     * @return {boolean} All conditions passed.
     */
    return function (event) {
      let pass = true;
      for (let i = 0, ii = conditions.length; i < ii; ++i) {
        pass = pass && conditions[i](event);
        if (!pass) {
          break;
        }
      }
      return pass;
    };
  }

  /**
   * Return `true` if only the alt-key and shift-key is pressed, `false` otherwise
   * (e.g. when additionally the platform-modifier-key is pressed).
   *
   * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
   * @return {boolean} True if only the alt and shift keys are pressed.
   * @api
   */
  const altShiftKeysOnly = function (mapBrowserEvent) {
    const originalEvent = mapBrowserEvent.originalEvent;
    return (
      originalEvent.altKey &&
      !(originalEvent.metaKey || originalEvent.ctrlKey) &&
      originalEvent.shiftKey
    );
  };

  /**
   * Return `true` if the map has the focus. This condition requires a map target
   * element with a `tabindex` attribute, e.g. `<div id="map" tabindex="1">`.
   *
   * @param {import("../MapBrowserEvent.js").default} event Map browser event.
   * @return {boolean} The map has the focus.
   * @api
   */
  const focus = function (event) {
    const targetElement = event.map.getTargetElement();
    const rootNode = targetElement.getRootNode();
    const activeElement = event.map.getOwnerDocument().activeElement;

    return rootNode instanceof ShadowRoot
      ? rootNode.host.contains(activeElement)
      : targetElement.contains(activeElement);
  };

  /**
   * Return `true` if the map has the focus or no 'tabindex' attribute set.
   *
   * @param {import("../MapBrowserEvent.js").default} event Map browser event.
   * @return {boolean} The map container has the focus or no 'tabindex' attribute.
   */
  const focusWithTabindex = function (event) {
    const targetElement = event.map.getTargetElement();
    const rootNode = targetElement.getRootNode();
    const tabIndexCandidate =
      rootNode instanceof ShadowRoot ? rootNode.host : targetElement;

    return tabIndexCandidate.hasAttribute('tabindex') ? focus(event) : true;
  };

  /**
   * Return always true.
   *
   * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
   * @return {boolean} True.
   * @api
   */
  const always = TRUE;

  /**
   * Return `true` if the event has an "action"-producing mouse button.
   *
   * By definition, this includes left-click on windows/linux, and left-click
   * without the ctrl key on Macs.
   *
   * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
   * @return {boolean} The result.
   */
  const mouseActionButton = function (mapBrowserEvent) {
    const originalEvent = mapBrowserEvent.originalEvent;
    return (
      'pointerId' in originalEvent &&
      originalEvent.button == 0 &&
      !(WEBKIT && MAC && originalEvent.ctrlKey)
    );
  };

  /**
   * Return `true` if no modifier key (alt-, shift- or platform-modifier-key) is
   * pressed.
   *
   * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
   * @return {boolean} True only if there no modifier keys are pressed.
   * @api
   */
  const noModifierKeys = function (mapBrowserEvent) {
    const originalEvent = /** @type {KeyboardEvent|MouseEvent|TouchEvent} */ (
      mapBrowserEvent.originalEvent
    );
    return (
      !originalEvent.altKey &&
      !(originalEvent.metaKey || originalEvent.ctrlKey) &&
      !originalEvent.shiftKey
    );
  };

  /**
   * Return `true` if the platform-modifier-key (the meta-key on Mac,
   * ctrl-key otherwise) is pressed.
   *
   * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
   * @return {boolean} True if the platform modifier key is pressed.
   * @api
   */
  const platformModifierKey = function (mapBrowserEvent) {
    const originalEvent = mapBrowserEvent.originalEvent;
    return MAC ? originalEvent.metaKey : originalEvent.ctrlKey;
  };

  /**
   * Return `true` if only the shift-key is pressed, `false` otherwise (e.g. when
   * additionally the alt-key is pressed).
   *
   * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
   * @return {boolean} True if only the shift key is pressed.
   * @api
   */
  const shiftKeyOnly = function (mapBrowserEvent) {
    const originalEvent = mapBrowserEvent.originalEvent;
    return (
      !originalEvent.altKey &&
      !(originalEvent.metaKey || originalEvent.ctrlKey) &&
      originalEvent.shiftKey
    );
  };

  /**
   * Return `true` if the target element is not editable, i.e. not an `input`,
   * `select`, or `textarea` element and no `contenteditable` attribute is
   * set or inherited, `false` otherwise.
   *
   * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
   * @return {boolean} True only if the target element is not editable.
   * @api
   */
  const targetNotEditable = function (mapBrowserEvent) {
    const originalEvent = mapBrowserEvent.originalEvent;
    const tagName = /** @type {Element} */ (originalEvent.target).tagName;
    return (
      tagName !== 'INPUT' &&
      tagName !== 'SELECT' &&
      tagName !== 'TEXTAREA' &&
      // `isContentEditable` is only available on `HTMLElement`, but it may also be a
      // different type like `SVGElement`.
      // @ts-ignore
      !originalEvent.target.isContentEditable
    );
  };

  /**
   * Return `true` if the event originates from a mouse device.
   *
   * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
   * @return {boolean} True if the event originates from a mouse device.
   * @api
   */
  const mouseOnly = function (mapBrowserEvent) {
    const pointerEvent = mapBrowserEvent.originalEvent;
    // see https://www.w3.org/TR/pointerevents/#widl-PointerEvent-pointerType
    return 'pointerId' in pointerEvent && pointerEvent.pointerType == 'mouse';
  };

  /**
   * Return `true` if the event originates from a primary pointer in
   * contact with the surface or if the left mouse button is pressed.
   * See https://www.w3.org/TR/pointerevents/#button-states.
   *
   * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
   * @return {boolean} True if the event originates from a primary pointer.
   * @api
   */
  const primaryAction = function (mapBrowserEvent) {
    const pointerEvent = mapBrowserEvent.originalEvent;
    return (
      'pointerId' in pointerEvent &&
      pointerEvent.isPrimary &&
      pointerEvent.button === 0
    );
  };

  /**
   * @module ol/interaction/Pointer
   */

  /**
   * @typedef {Object} Options
   * @property {function(import("../MapBrowserEvent.js").default):boolean} [handleDownEvent]
   * Function handling "down" events. If the function returns `true` then a drag
   * sequence is started.
   * @property {function(import("../MapBrowserEvent.js").default):void} [handleDragEvent]
   * Function handling "drag" events. This function is called on "move" events
   * during a drag sequence.
   * @property {function(import("../MapBrowserEvent.js").default):boolean} [handleEvent]
   * Method called by the map to notify the interaction that a browser event was
   * dispatched to the map. The function may return `false` to prevent the
   * propagation of the event to other interactions in the map's interactions
   * chain.
   * @property {function(import("../MapBrowserEvent.js").default):void} [handleMoveEvent]
   * Function handling "move" events. This function is called on "move" events.
   * This functions is also called during a drag sequence, so during a drag
   * sequence both the `handleDragEvent` function and this function are called.
   * If `handleDownEvent` is defined and it returns true this function will not
   * be called during a drag sequence.
   * @property {function(import("../MapBrowserEvent.js").default):boolean} [handleUpEvent]
   *  Function handling "up" events. If the function returns `false` then the
   * current drag sequence is stopped.
   * @property {function(boolean):boolean} [stopDown]
   * Should the down event be propagated to other interactions, or should be
   * stopped?
   */

  /**
   * @classdesc
   * Base class that calls user-defined functions on `down`, `move` and `up`
   * events. This class also manages "drag sequences".
   *
   * When the `handleDownEvent` user function returns `true` a drag sequence is
   * started. During a drag sequence the `handleDragEvent` user function is
   * called on `move` events. The drag sequence ends when the `handleUpEvent`
   * user function is called and returns `false`.
   * @api
   */
  class PointerInteraction extends Interaction {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      options = options ? options : {};

      super(
        /** @type {import("./Interaction.js").InteractionOptions} */ (options),
      );

      if (options.handleDownEvent) {
        this.handleDownEvent = options.handleDownEvent;
      }

      if (options.handleDragEvent) {
        this.handleDragEvent = options.handleDragEvent;
      }

      if (options.handleMoveEvent) {
        this.handleMoveEvent = options.handleMoveEvent;
      }

      if (options.handleUpEvent) {
        this.handleUpEvent = options.handleUpEvent;
      }

      if (options.stopDown) {
        this.stopDown = options.stopDown;
      }

      /**
       * @type {boolean}
       * @protected
       */
      this.handlingDownUpSequence = false;

      /**
       * @type {Array<PointerEvent>}
       * @protected
       */
      this.targetPointers = [];
    }

    /**
     * Returns the current number of pointers involved in the interaction,
     * e.g. `2` when two fingers are used.
     * @return {number} The number of pointers.
     * @api
     */
    getPointerCount() {
      return this.targetPointers.length;
    }

    /**
     * Handle pointer down events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @protected
     */
    handleDownEvent(mapBrowserEvent) {
      return false;
    }

    /**
     * Handle pointer drag events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @protected
     */
    handleDragEvent(mapBrowserEvent) {}

    /**
     * Handles the {@link module:ol/MapBrowserEvent~MapBrowserEvent map browser event} and may call into
     * other functions, if event sequences like e.g. 'drag' or 'down-up' etc. are
     * detected.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
     * @return {boolean} `false` to stop event propagation.
     * @api
     * @override
     */
    handleEvent(mapBrowserEvent) {
      if (!mapBrowserEvent.originalEvent) {
        return true;
      }

      let stopEvent = false;
      this.updateTrackedPointers_(mapBrowserEvent);
      if (this.handlingDownUpSequence) {
        if (mapBrowserEvent.type == MapBrowserEventType.POINTERDRAG) {
          this.handleDragEvent(mapBrowserEvent);
          // prevent page scrolling during dragging
          mapBrowserEvent.originalEvent.preventDefault();
        } else if (mapBrowserEvent.type == MapBrowserEventType.POINTERUP) {
          const handledUp = this.handleUpEvent(mapBrowserEvent);
          this.handlingDownUpSequence =
            handledUp && this.targetPointers.length > 0;
        }
      } else {
        if (mapBrowserEvent.type == MapBrowserEventType.POINTERDOWN) {
          const handled = this.handleDownEvent(mapBrowserEvent);
          this.handlingDownUpSequence = handled;
          stopEvent = this.stopDown(handled);
        } else if (mapBrowserEvent.type == MapBrowserEventType.POINTERMOVE) {
          this.handleMoveEvent(mapBrowserEvent);
        }
      }
      return !stopEvent;
    }

    /**
     * Handle pointer move events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @protected
     */
    handleMoveEvent(mapBrowserEvent) {}

    /**
     * Handle pointer up events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @protected
     */
    handleUpEvent(mapBrowserEvent) {
      return false;
    }

    /**
     * This function is used to determine if "down" events should be propagated
     * to other interactions or should be stopped.
     * @param {boolean} handled Was the event handled by the interaction?
     * @return {boolean} Should the `down` event be stopped?
     */
    stopDown(handled) {
      return handled;
    }

    /**
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @private
     */
    updateTrackedPointers_(mapBrowserEvent) {
      if (mapBrowserEvent.activePointers) {
        this.targetPointers = mapBrowserEvent.activePointers;
      }
    }
  }

  /**
   * @param {Array<PointerEvent>} pointerEvents List of events.
   * @return {{clientX: number, clientY: number}} Centroid pixel.
   */
  function centroid(pointerEvents) {
    const length = pointerEvents.length;
    let clientX = 0;
    let clientY = 0;
    for (let i = 0; i < length; i++) {
      clientX += pointerEvents[i].clientX;
      clientY += pointerEvents[i].clientY;
    }
    return {clientX: clientX / length, clientY: clientY / length};
  }

  /**
   * @module ol/interaction/DragPan
   */

  /**
   * @typedef {Object} Options
   * @property {import("../events/condition.js").Condition} [condition] A function that takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a boolean
   * to indicate whether that event should be handled.
   * Default is {@link module:ol/events/condition.noModifierKeys} and {@link module:ol/events/condition.primaryAction}.
   * @property {boolean} [onFocusOnly=false] When the map's target has a `tabindex` attribute set,
   * the interaction will only handle events when the map has the focus.
   * @property {import("../Kinetic.js").default} [kinetic] Kinetic inertia to apply to the pan.
   */

  /**
   * @classdesc
   * Allows the user to pan the map by dragging the map.
   * @api
   */
  class DragPan extends PointerInteraction {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      super({
        stopDown: FALSE,
      });

      options = options ? options : {};

      /**
       * @private
       * @type {import("../Kinetic.js").default|undefined}
       */
      this.kinetic_ = options.kinetic;

      /**
       * @type {import("../pixel.js").Pixel}
       */
      this.lastCentroid = null;

      /**
       * @type {number}
       * @private
       */
      this.lastPointersCount_;

      /**
       * @type {boolean}
       * @private
       */
      this.panning_ = false;

      const condition = options.condition
        ? options.condition
        : all(noModifierKeys, primaryAction);

      /**
       * @private
       * @type {import("../events/condition.js").Condition}
       */
      this.condition_ = options.onFocusOnly
        ? all(focusWithTabindex, condition)
        : condition;

      /**
       * @private
       * @type {boolean}
       */
      this.noKinetic_ = false;
    }

    /**
     * Handle pointer drag events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @override
     */
    handleDragEvent(mapBrowserEvent) {
      const map = mapBrowserEvent.map;
      if (!this.panning_) {
        this.panning_ = true;
        map.getView().beginInteraction();
      }
      const targetPointers = this.targetPointers;
      const centroid$1 = map.getEventPixel(centroid(targetPointers));
      if (targetPointers.length == this.lastPointersCount_) {
        if (this.kinetic_) {
          this.kinetic_.update(centroid$1[0], centroid$1[1]);
        }
        if (this.lastCentroid) {
          const delta = [
            this.lastCentroid[0] - centroid$1[0],
            centroid$1[1] - this.lastCentroid[1],
          ];
          const map = mapBrowserEvent.map;
          const view = map.getView();
          scale$2(delta, view.getResolution());
          rotate$1(delta, view.getRotation());
          view.adjustCenterInternal(delta);
        }
      } else if (this.kinetic_) {
        // reset so we don't overestimate the kinetic energy after
        // after one finger down, tiny drag, second finger down
        this.kinetic_.begin();
      }
      this.lastCentroid = centroid$1;
      this.lastPointersCount_ = targetPointers.length;
      mapBrowserEvent.originalEvent.preventDefault();
    }

    /**
     * Handle pointer up events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @override
     */
    handleUpEvent(mapBrowserEvent) {
      const map = mapBrowserEvent.map;
      const view = map.getView();
      if (this.targetPointers.length === 0) {
        if (!this.noKinetic_ && this.kinetic_ && this.kinetic_.end()) {
          const distance = this.kinetic_.getDistance();
          const angle = this.kinetic_.getAngle();
          const center = view.getCenterInternal();
          const centerpx = map.getPixelFromCoordinateInternal(center);
          const dest = map.getCoordinateFromPixelInternal([
            centerpx[0] - distance * Math.cos(angle),
            centerpx[1] - distance * Math.sin(angle),
          ]);
          view.animateInternal({
            center: view.getConstrainedCenter(dest),
            duration: 500,
            easing: easeOut,
          });
        }
        if (this.panning_) {
          this.panning_ = false;
          view.endInteraction();
        }
        return false;
      }
      if (this.kinetic_) {
        // reset so we don't overestimate the kinetic energy after
        // after one finger up, tiny drag, second finger up
        this.kinetic_.begin();
      }
      this.lastCentroid = null;
      return true;
    }

    /**
     * Handle pointer down events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @override
     */
    handleDownEvent(mapBrowserEvent) {
      if (this.targetPointers.length > 0 && this.condition_(mapBrowserEvent)) {
        const map = mapBrowserEvent.map;
        const view = map.getView();
        this.lastCentroid = null;
        // stop any current animation
        if (view.getAnimating()) {
          view.cancelAnimations();
        }
        if (this.kinetic_) {
          this.kinetic_.begin();
        }
        // No kinetic as soon as more than one pointer on the screen is
        // detected. This is to prevent nasty pans after pinch.
        this.noKinetic_ = this.targetPointers.length > 1;
        return true;
      }
      return false;
    }
  }

  /**
   * @module ol/interaction/DragRotate
   */

  /**
   * @typedef {Object} Options
   * @property {import("../events/condition.js").Condition} [condition] A function that takes a
   * {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a boolean
   * to indicate whether that event should be handled.
   * Default is {@link module:ol/events/condition.altShiftKeysOnly}.
   * @property {number} [duration=250] Animation duration in milliseconds.
   */

  /**
   * @classdesc
   * Allows the user to rotate the map by clicking and dragging on the map,
   * normally combined with a {@link module:ol/events/condition} that limits
   * it to when the alt and shift keys are held down.
   *
   * This interaction is only supported for mouse devices.
   * @api
   */
  class DragRotate extends PointerInteraction {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      options = options ? options : {};

      super({
        stopDown: FALSE,
      });

      /**
       * @private
       * @type {import("../events/condition.js").Condition}
       */
      this.condition_ = options.condition ? options.condition : altShiftKeysOnly;

      /**
       * @private
       * @type {number|undefined}
       */
      this.lastAngle_ = undefined;

      /**
       * @private
       * @type {number}
       */
      this.duration_ = options.duration !== undefined ? options.duration : 250;
    }

    /**
     * Handle pointer drag events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @override
     */
    handleDragEvent(mapBrowserEvent) {
      if (!mouseOnly(mapBrowserEvent)) {
        return;
      }

      const map = mapBrowserEvent.map;
      const view = map.getView();
      if (view.getConstraints().rotation === disable) {
        return;
      }
      const size = map.getSize();
      const offset = mapBrowserEvent.pixel;
      const theta = Math.atan2(size[1] / 2 - offset[1], offset[0] - size[0] / 2);
      if (this.lastAngle_ !== undefined) {
        const delta = theta - this.lastAngle_;
        view.adjustRotationInternal(-delta);
      }
      this.lastAngle_ = theta;
    }

    /**
     * Handle pointer up events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @override
     */
    handleUpEvent(mapBrowserEvent) {
      if (!mouseOnly(mapBrowserEvent)) {
        return true;
      }

      const map = mapBrowserEvent.map;
      const view = map.getView();
      view.endInteraction(this.duration_);
      return false;
    }

    /**
     * Handle pointer down events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @override
     */
    handleDownEvent(mapBrowserEvent) {
      if (!mouseOnly(mapBrowserEvent)) {
        return false;
      }

      if (
        mouseActionButton(mapBrowserEvent) &&
        this.condition_(mapBrowserEvent)
      ) {
        const map = mapBrowserEvent.map;
        map.getView().beginInteraction();
        this.lastAngle_ = undefined;
        return true;
      }
      return false;
    }
  }

  /**
   * @module ol/render/Box
   */


  class RenderBox extends Disposable {
    /**
     * @param {string} className CSS class name.
     */
    constructor(className) {
      super();

      /**
       * @type {import("../geom/Polygon.js").default}
       * @private
       */
      this.geometry_ = null;

      /**
       * @type {HTMLDivElement}
       * @private
       */
      this.element_ = document.createElement('div');
      this.element_.style.position = 'absolute';
      this.element_.style.pointerEvents = 'auto';
      this.element_.className = 'ol-box ' + className;

      /**
       * @private
       * @type {import("../Map.js").default|null}
       */
      this.map_ = null;

      /**
       * @private
       * @type {import("../pixel.js").Pixel}
       */
      this.startPixel_ = null;

      /**
       * @private
       * @type {import("../pixel.js").Pixel}
       */
      this.endPixel_ = null;
    }

    /**
     * Clean up.
     * @override
     */
    disposeInternal() {
      this.setMap(null);
    }

    /**
     * @private
     */
    render_() {
      const startPixel = this.startPixel_;
      const endPixel = this.endPixel_;
      const px = 'px';
      const style = this.element_.style;
      style.left = Math.min(startPixel[0], endPixel[0]) + px;
      style.top = Math.min(startPixel[1], endPixel[1]) + px;
      style.width = Math.abs(endPixel[0] - startPixel[0]) + px;
      style.height = Math.abs(endPixel[1] - startPixel[1]) + px;
    }

    /**
     * @param {import("../Map.js").default|null} map Map.
     */
    setMap(map) {
      if (this.map_) {
        this.map_.getOverlayContainer().removeChild(this.element_);
        const style = this.element_.style;
        style.left = 'inherit';
        style.top = 'inherit';
        style.width = 'inherit';
        style.height = 'inherit';
      }
      this.map_ = map;
      if (this.map_) {
        this.map_.getOverlayContainer().appendChild(this.element_);
      }
    }

    /**
     * @param {import("../pixel.js").Pixel} startPixel Start pixel.
     * @param {import("../pixel.js").Pixel} endPixel End pixel.
     */
    setPixels(startPixel, endPixel) {
      this.startPixel_ = startPixel;
      this.endPixel_ = endPixel;
      this.createOrUpdateGeometry();
      this.render_();
    }

    /**
     * Creates or updates the cached geometry.
     */
    createOrUpdateGeometry() {
      if (!this.map_) {
        return;
      }

      const startPixel = this.startPixel_;
      const endPixel = this.endPixel_;
      const pixels = [
        startPixel,
        [startPixel[0], endPixel[1]],
        endPixel,
        [endPixel[0], startPixel[1]],
      ];
      const coordinates = pixels.map(
        this.map_.getCoordinateFromPixelInternal,
        this.map_,
      );
      // close the polygon
      coordinates[4] = coordinates[0].slice();
      if (!this.geometry_) {
        this.geometry_ = new Polygon([coordinates]);
      } else {
        this.geometry_.setCoordinates([coordinates]);
      }
    }

    /**
     * @return {import("../geom/Polygon.js").default} Geometry.
     */
    getGeometry() {
      return this.geometry_;
    }
  }

  /**
   * @module ol/interaction/DragBox
   */
  // FIXME draw drag box

  /**
   * A function that takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and two
   * {@link module:ol/pixel~Pixel}s and returns a `{boolean}`. If the condition is met,
   * true should be returned.
   * @typedef {function(this: ?, import("../MapBrowserEvent.js").default, import("../pixel.js").Pixel, import("../pixel.js").Pixel):boolean} EndCondition
   */

  /**
   * @typedef {Object} Options
   * @property {string} [className='ol-dragbox'] CSS class name for styling the box.
   * @property {import("../events/condition.js").Condition} [condition] A function that takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a boolean
   * to indicate whether that event should be handled.
   * Default is {@link ol/events/condition~mouseActionButton}.
   * @property {number} [minArea=64] The minimum area of the box in pixel, this value is used by the default
   * `boxEndCondition` function.
   * @property {EndCondition} [boxEndCondition] A function that takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and two
   * {@link module:ol/pixel~Pixel}s to indicate whether a `boxend` event should be fired.
   * Default is `true` if the area of the box is bigger than the `minArea` option.
   * @property {function(this:DragBox, import("../MapBrowserEvent.js").default):void} [onBoxEnd] Code to execute just
   * before `boxend` is fired.
   */

  /**
   * @enum {string}
   */
  const DragBoxEventType = {
    /**
     * Triggered upon drag box start.
     * @event DragBoxEvent#boxstart
     * @api
     */
    BOXSTART: 'boxstart',

    /**
     * Triggered on drag when box is active.
     * @event DragBoxEvent#boxdrag
     * @api
     */
    BOXDRAG: 'boxdrag',

    /**
     * Triggered upon drag box end.
     * @event DragBoxEvent#boxend
     * @api
     */
    BOXEND: 'boxend',

    /**
     * Triggered upon drag box canceled.
     * @event DragBoxEvent#boxcancel
     * @api
     */
    BOXCANCEL: 'boxcancel',
  };

  /**
   * @classdesc
   * Events emitted by {@link module:ol/interaction/DragBox~DragBox} instances are instances of
   * this type.
   */
  class DragBoxEvent extends BaseEvent {
    /**
     * @param {string} type The event type.
     * @param {import("../coordinate.js").Coordinate} coordinate The event coordinate.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Originating event.
     */
    constructor(type, coordinate, mapBrowserEvent) {
      super(type);

      /**
       * The coordinate of the drag event.
       * @const
       * @type {import("../coordinate.js").Coordinate}
       * @api
       */
      this.coordinate = coordinate;

      /**
       * @const
       * @type {import("../MapBrowserEvent.js").default}
       * @api
       */
      this.mapBrowserEvent = mapBrowserEvent;
    }
  }

  /***
   * @template Return
   * @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> &
   *   import("../Observable").OnSignature<import("../ObjectEventType").Types|
   *     'change:active', import("../Object").ObjectEvent, Return> &
   *   import("../Observable").OnSignature<'boxcancel'|'boxdrag'|'boxend'|'boxstart', DragBoxEvent, Return> &
   *   import("../Observable").CombinedOnSignature<import("../Observable").EventTypes|import("../ObjectEventType").Types|
   *     'change:active'|'boxcancel'|'boxdrag'|'boxend', Return>} DragBoxOnSignature
   */

  /**
   * @classdesc
   * Allows the user to draw a vector box by clicking and dragging on the map,
   * normally combined with a {@link module:ol/events/condition} that limits
   * it to when the shift or other key is held down. This is used, for example,
   * for zooming to a specific area of the map
   * (see {@link module:ol/interaction/DragZoom~DragZoom} and
   * {@link module:ol/interaction/DragRotateAndZoom~DragRotateAndZoom}).
   *
   * @fires DragBoxEvent
   * @api
   */
  class DragBox extends PointerInteraction {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      super();

      /***
       * @type {DragBoxOnSignature<import("../events").EventsKey>}
       */
      this.on;

      /***
       * @type {DragBoxOnSignature<import("../events").EventsKey>}
       */
      this.once;

      /***
       * @type {DragBoxOnSignature<void>}
       */
      this.un;

      options = options ?? {};

      /**
       * @type {import("../render/Box.js").default}
       * @private
       */
      this.box_ = new RenderBox(options.className || 'ol-dragbox');

      /**
       * @type {number}
       * @private
       */
      this.minArea_ = options.minArea ?? 64;

      if (options.onBoxEnd) {
        this.onBoxEnd = options.onBoxEnd;
      }

      /**
       * @type {import("../pixel.js").Pixel}
       * @private
       */
      this.startPixel_ = null;

      /**
       * @private
       * @type {import("../events/condition.js").Condition}
       */
      this.condition_ = options.condition ?? mouseActionButton;

      /**
       * @private
       * @type {EndCondition}
       */
      this.boxEndCondition_ =
        options.boxEndCondition ?? this.defaultBoxEndCondition;
    }

    /**
     * The default condition for determining whether the boxend event
     * should fire.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent The originating MapBrowserEvent
     *     leading to the box end.
     * @param {import("../pixel.js").Pixel} startPixel The starting pixel of the box.
     * @param {import("../pixel.js").Pixel} endPixel The end pixel of the box.
     * @return {boolean} Whether or not the boxend condition should be fired.
     */
    defaultBoxEndCondition(mapBrowserEvent, startPixel, endPixel) {
      const width = endPixel[0] - startPixel[0];
      const height = endPixel[1] - startPixel[1];
      return width * width + height * height >= this.minArea_;
    }

    /**
     * Returns geometry of last drawn box.
     * @return {import("../geom/Polygon.js").default} Geometry.
     * @api
     */
    getGeometry() {
      return this.box_.getGeometry();
    }

    /**
     * Handle pointer drag events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @override
     */
    handleDragEvent(mapBrowserEvent) {
      if (!this.startPixel_) {
        return;
      }

      this.box_.setPixels(this.startPixel_, mapBrowserEvent.pixel);

      this.dispatchEvent(
        new DragBoxEvent(
          DragBoxEventType.BOXDRAG,
          mapBrowserEvent.coordinate,
          mapBrowserEvent,
        ),
      );
    }

    /**
     * Handle pointer up events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @override
     */
    handleUpEvent(mapBrowserEvent) {
      if (!this.startPixel_) {
        return false;
      }

      const completeBox = this.boxEndCondition_(
        mapBrowserEvent,
        this.startPixel_,
        mapBrowserEvent.pixel,
      );
      if (completeBox) {
        this.onBoxEnd(mapBrowserEvent);
      }
      this.dispatchEvent(
        new DragBoxEvent(
          completeBox ? DragBoxEventType.BOXEND : DragBoxEventType.BOXCANCEL,
          mapBrowserEvent.coordinate,
          mapBrowserEvent,
        ),
      );

      this.box_.setMap(null);
      this.startPixel_ = null;

      return false;
    }

    /**
     * Handle pointer down events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @override
     */
    handleDownEvent(mapBrowserEvent) {
      if (this.condition_(mapBrowserEvent)) {
        this.startPixel_ = mapBrowserEvent.pixel;
        this.box_.setMap(mapBrowserEvent.map);
        this.box_.setPixels(this.startPixel_, this.startPixel_);
        this.dispatchEvent(
          new DragBoxEvent(
            DragBoxEventType.BOXSTART,
            mapBrowserEvent.coordinate,
            mapBrowserEvent,
          ),
        );
        return true;
      }
      return false;
    }

    /**
     * Function to execute just before `onboxend` is fired
     * @param {import("../MapBrowserEvent.js").default} event Event.
     */
    onBoxEnd(event) {}

    /**
     * Activate or deactivate the interaction.
     * @param {boolean} active Active.
     * @observable
     * @api
     * @override
     */
    setActive(active) {
      if (!active) {
        this.box_.setMap(null);
        if (this.startPixel_) {
          this.dispatchEvent(
            new DragBoxEvent(DragBoxEventType.BOXCANCEL, this.startPixel_, null),
          );
          this.startPixel_ = null;
        }
      }

      super.setActive(active);
    }

    /**
     * @param {import("../Map.js").default|null} map Map.
     * @override
     */
    setMap(map) {
      const oldMap = this.getMap();

      if (oldMap) {
        this.box_.setMap(null);

        if (this.startPixel_) {
          this.dispatchEvent(
            new DragBoxEvent(DragBoxEventType.BOXCANCEL, this.startPixel_, null),
          );
          this.startPixel_ = null;
        }
      }

      super.setMap(map);
    }
  }

  /**
   * @module ol/interaction/DragZoom
   */

  /**
   * @typedef {Object} Options
   * @property {string} [className='ol-dragzoom'] CSS class name for styling the
   * box.
   * @property {import("../events/condition.js").Condition} [condition] A function that
   * takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a
   * boolean to indicate whether that event should be handled.
   * Default is {@link module:ol/events/condition.shiftKeyOnly}.
   * @property {number} [duration=200] Animation duration in milliseconds.
   * @property {boolean} [out=false] Use interaction for zooming out.
   * @property {number} [minArea=64] The minimum area of the box in pixel, this value is used by the parent default
   * `boxEndCondition` function.
   */

  /**
   * @classdesc
   * Allows the user to zoom the map by clicking and dragging on the map,
   * normally combined with a {@link module:ol/events/condition} that limits
   * it to when a key, shift by default, is held down.
   *
   * To change the style of the box, use CSS and the `.ol-dragzoom` selector, or
   * your custom one configured with `className`.
   * @api
   */
  class DragZoom extends DragBox {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      options = options ? options : {};

      const condition = options.condition ? options.condition : shiftKeyOnly;

      super({
        condition: condition,
        className: options.className || 'ol-dragzoom',
        minArea: options.minArea,
      });

      /**
       * @private
       * @type {number}
       */
      this.duration_ = options.duration !== undefined ? options.duration : 200;

      /**
       * @private
       * @type {boolean}
       */
      this.out_ = options.out !== undefined ? options.out : false;
    }

    /**
     * Function to execute just before `onboxend` is fired
     * @param {import("../MapBrowserEvent.js").default} event Event.
     * @override
     */
    onBoxEnd(event) {
      const map = this.getMap();
      const view = /** @type {!import("../View.js").default} */ (map.getView());
      let geometry = this.getGeometry();

      if (this.out_) {
        const rotatedExtent = view.rotatedExtentForGeometry(geometry);
        const resolution = view.getResolutionForExtentInternal(rotatedExtent);
        const factor = view.getResolution() / resolution;
        geometry = geometry.clone();
        geometry.scale(factor * factor);
      }

      view.fitInternal(geometry, {
        duration: this.duration_,
        easing: easeOut,
      });
    }
  }

  /**
   * @module ol/events/Key
   */

  /**
   * @enum {string}
   * @const
   */
  var Key = {
    LEFT: 'ArrowLeft',
    UP: 'ArrowUp',
    RIGHT: 'ArrowRight',
    DOWN: 'ArrowDown',
  };

  /**
   * @module ol/interaction/KeyboardPan
   */

  /**
   * @typedef {Object} Options
   * @property {import("../events/condition.js").Condition} [condition] A function that
   * takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a
   * boolean to indicate whether that event should be handled. Default is
   * {@link module:ol/events/condition.noModifierKeys} and
   * {@link module:ol/events/condition.targetNotEditable}.
   * @property {number} [duration=100] Animation duration in milliseconds.
   * @property {number} [pixelDelta=128] The amount of pixels to pan on each key
   * press.
   */

  /**
   * @classdesc
   * Allows the user to pan the map using keyboard arrows.
   * Note that, although this interaction is by default included in maps,
   * the keys can only be used when browser focus is on the element to which
   * the keyboard events are attached. By default, this is the map div,
   * though you can change this with the `keyboardEventTarget` in
   * {@link module:ol/Map~Map}. `document` never loses focus but, for any other
   * element, focus will have to be on, and returned to, this element if the keys
   * are to function.
   * See also {@link module:ol/interaction/KeyboardZoom~KeyboardZoom}.
   * @api
   */
  class KeyboardPan extends Interaction {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      super();

      options = options || {};

      /**
       * @private
       * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Browser event.
       * @return {boolean} Combined condition result.
       */
      this.defaultCondition_ = function (mapBrowserEvent) {
        return (
          noModifierKeys(mapBrowserEvent) && targetNotEditable(mapBrowserEvent)
        );
      };

      /**
       * @private
       * @type {import("../events/condition.js").Condition}
       */
      this.condition_ =
        options.condition !== undefined
          ? options.condition
          : this.defaultCondition_;

      /**
       * @private
       * @type {number}
       */
      this.duration_ = options.duration !== undefined ? options.duration : 100;

      /**
       * @private
       * @type {number}
       */
      this.pixelDelta_ =
        options.pixelDelta !== undefined ? options.pixelDelta : 128;
    }

    /**
     * Handles the {@link module:ol/MapBrowserEvent~MapBrowserEvent map browser event} if it was a
     * `KeyEvent`, and decides the direction to pan to (if an arrow key was
     * pressed).
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
     * @return {boolean} `false` to stop event propagation.
     * @override
     */
    handleEvent(mapBrowserEvent) {
      let stopEvent = false;
      if (mapBrowserEvent.type == EventType.KEYDOWN) {
        const keyEvent = /** @type {KeyboardEvent} */ (
          mapBrowserEvent.originalEvent
        );
        const key = keyEvent.key;
        if (
          this.condition_(mapBrowserEvent) &&
          (key == Key.DOWN ||
            key == Key.LEFT ||
            key == Key.RIGHT ||
            key == Key.UP)
        ) {
          const map = mapBrowserEvent.map;
          const view = map.getView();
          const mapUnitsDelta = view.getResolution() * this.pixelDelta_;
          let deltaX = 0,
            deltaY = 0;
          if (key == Key.DOWN) {
            deltaY = -mapUnitsDelta;
          } else if (key == Key.LEFT) {
            deltaX = -mapUnitsDelta;
          } else if (key == Key.RIGHT) {
            deltaX = mapUnitsDelta;
          } else {
            deltaY = mapUnitsDelta;
          }
          const delta = [deltaX, deltaY];
          rotate$1(delta, view.getRotation());
          pan(view, delta, this.duration_);
          keyEvent.preventDefault();
          stopEvent = true;
        }
      }
      return !stopEvent;
    }
  }

  /**
   * @module ol/interaction/KeyboardZoom
   */

  /**
   * @typedef {Object} Options
   * @property {number} [duration=100] Animation duration in milliseconds.
   * @property {import("../events/condition.js").Condition} [condition] A function that
   * takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a
   * boolean to indicate whether that event should be handled. The default condition is
   * that {@link module:ol/events/condition.targetNotEditable} is fulfilled and that
   * the platform modifier key isn't pressed
   * (!{@link module:ol/events/condition.platformModifierKey}).
   * @property {number} [delta=1] The zoom level delta on each key press.
   */

  /**
   * @classdesc
   * Allows the user to zoom the map using keyboard + and -.
   * Note that, although this interaction is by default included in maps,
   * the keys can only be used when browser focus is on the element to which
   * the keyboard events are attached. By default, this is the map div,
   * though you can change this with the `keyboardEventTarget` in
   * {@link module:ol/Map~Map}. `document` never loses focus but, for any other
   * element, focus will have to be on, and returned to, this element if the keys
   * are to function.
   * See also {@link module:ol/interaction/KeyboardPan~KeyboardPan}.
   * @api
   */
  class KeyboardZoom extends Interaction {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      super();

      options = options ? options : {};

      /**
       * @private
       * @type {import("../events/condition.js").Condition}
       */
      this.condition_ = options.condition
        ? options.condition
        : function (mapBrowserEvent) {
            return (
              !platformModifierKey(mapBrowserEvent) &&
              targetNotEditable(mapBrowserEvent)
            );
          };

      /**
       * @private
       * @type {number}
       */
      this.delta_ = options.delta ? options.delta : 1;

      /**
       * @private
       * @type {number}
       */
      this.duration_ = options.duration !== undefined ? options.duration : 100;
    }

    /**
     * Handles the {@link module:ol/MapBrowserEvent~MapBrowserEvent map browser event} if it was a
     * `KeyEvent`, and decides whether to zoom in or out (depending on whether the
     * key pressed was '+' or '-').
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
     * @return {boolean} `false` to stop event propagation.
     * @override
     */
    handleEvent(mapBrowserEvent) {
      let stopEvent = false;
      if (
        mapBrowserEvent.type == EventType.KEYDOWN ||
        mapBrowserEvent.type == EventType.KEYPRESS
      ) {
        const keyEvent = /** @type {KeyboardEvent} */ (
          mapBrowserEvent.originalEvent
        );
        const key = keyEvent.key;
        if (this.condition_(mapBrowserEvent) && (key === '+' || key === '-')) {
          const map = mapBrowserEvent.map;
          const delta = key === '+' ? this.delta_ : -this.delta_;
          const view = map.getView();
          zoomByDelta(view, delta, undefined, this.duration_);
          keyEvent.preventDefault();
          stopEvent = true;
        }
      }
      return !stopEvent;
    }
  }

  /**
   * @module ol/interaction/MouseWheelZoom
   */

  /**
   * @typedef {'trackpad' | 'wheel'} Mode
   */

  /**
   * @typedef {Object} Options
   * @property {import("../events/condition.js").Condition} [condition] A function that
   * takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a
   * boolean to indicate whether that event should be handled. Default is
   * {@link module:ol/events/condition.always}.
   * @property {boolean} [onFocusOnly=false] When the map's target has a `tabindex` attribute set,
   * the interaction will only handle events when the map has the focus.
   * @property {number} [maxDelta=1] Maximum mouse wheel delta.
   * @property {number} [duration=250] Animation duration in milliseconds.
   * @property {number} [timeout=80] Mouse wheel timeout duration in milliseconds.
   * @property {boolean} [useAnchor=true] Enable zooming using the mouse's
   * location as the anchor. When set to `false`, zooming in and out will zoom to
   * the center of the screen instead of zooming on the mouse's location.
   * @property {boolean} [constrainResolution=false] If true, the mouse wheel zoom
   * event will always animate to the closest zoom level after an interaction;
   * false means intermediary zoom levels are allowed.
   */

  /**
   * Mutliplier for the DOM_DELTA_LINE delta value.
   * @type {number}
   */
  const DELTA_LINE_MULTIPLIER = 40;

  /**
   * Mutliplier for the DOM_DELTA_PAGE delta value.
   * @type {number}
   */
  const DELTA_PAGE_MULTIPLIER = 300;

  /**
   * @classdesc
   * Allows the user to zoom the map by scrolling the mouse wheel.
   * @api
   */
  class MouseWheelZoom extends Interaction {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      options = options ? options : {};

      super(
        /** @type {import("./Interaction.js").InteractionOptions} */ (options),
      );

      /**
       * @private
       * @type {number}
       */
      this.totalDelta_ = 0;

      /**
       * @private
       * @type {number}
       */
      this.lastDelta_ = 0;

      /**
       * @private
       * @type {number}
       */
      this.maxDelta_ = options.maxDelta !== undefined ? options.maxDelta : 1;

      /**
       * @private
       * @type {number}
       */
      this.duration_ = options.duration !== undefined ? options.duration : 250;

      /**
       * @private
       * @type {number}
       */
      this.timeout_ = options.timeout !== undefined ? options.timeout : 80;

      /**
       * @private
       * @type {boolean}
       */
      this.useAnchor_ =
        options.useAnchor !== undefined ? options.useAnchor : true;

      /**
       * @private
       * @type {boolean}
       */
      this.constrainResolution_ =
        options.constrainResolution !== undefined
          ? options.constrainResolution
          : false;

      const condition = options.condition ? options.condition : always;

      /**
       * @private
       * @type {import("../events/condition.js").Condition}
       */
      this.condition_ = options.onFocusOnly
        ? all(focusWithTabindex, condition)
        : condition;

      /**
       * @private
       * @type {?import("../pixel.js").Pixel}
       */
      this.lastAnchor_ = null;

      /**
       * @private
       * @type {number|undefined}
       */
      this.startTime_ = undefined;

      /**
       * @private
       * @type {ReturnType<typeof setTimeout>}
       */
      this.timeoutId_;

      /**
       * @private
       * @type {Mode|undefined}
       */
      this.mode_ = undefined;

      /**
       * Trackpad events separated by this delay will be considered separate
       * interactions.
       * @private
       * @type {number}
       */
      this.trackpadEventGap_ = 400;

      /**
       * @private
       * @type {ReturnType<typeof setTimeout>}
       */
      this.trackpadTimeoutId_;

      /**
       * The number of delta values per zoom level
       * @private
       * @type {number}
       */
      this.deltaPerZoom_ = 300;
    }

    /**
     * @private
     */
    endInteraction_() {
      this.trackpadTimeoutId_ = undefined;
      const map = this.getMap();
      if (!map) {
        return;
      }
      const view = map.getView();
      view.endInteraction(
        undefined,
        this.lastDelta_ ? (this.lastDelta_ > 0 ? 1 : -1) : 0,
        this.lastAnchor_ ? map.getCoordinateFromPixel(this.lastAnchor_) : null,
      );
    }

    /**
     * Handles the {@link module:ol/MapBrowserEvent~MapBrowserEvent map browser event} (if it was a mousewheel-event) and eventually
     * zooms the map.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
     * @return {boolean} `false` to stop event propagation.
     * @override
     */
    handleEvent(mapBrowserEvent) {
      if (!this.condition_(mapBrowserEvent)) {
        return true;
      }
      const type = mapBrowserEvent.type;
      if (type !== EventType.WHEEL) {
        return true;
      }

      const map = mapBrowserEvent.map;
      const wheelEvent = /** @type {WheelEvent} */ (
        mapBrowserEvent.originalEvent
      );
      wheelEvent.preventDefault();

      if (this.useAnchor_) {
        this.lastAnchor_ = mapBrowserEvent.pixel;
      }

      // Delta normalisation inspired by
      // https://github.com/mapbox/mapbox-gl-js/blob/001c7b9/js/ui/handler/scroll_zoom.js
      let delta = wheelEvent.deltaY;

      switch (wheelEvent.deltaMode) {
        case WheelEvent.DOM_DELTA_LINE:
          delta *= DELTA_LINE_MULTIPLIER;
          break;
        case WheelEvent.DOM_DELTA_PAGE:
          delta *= DELTA_PAGE_MULTIPLIER;
          break;
        // pass
      }

      if (delta === 0) {
        return false;
      }
      this.lastDelta_ = delta;

      const now = Date.now();

      if (this.startTime_ === undefined) {
        this.startTime_ = now;
      }

      if (!this.mode_ || now - this.startTime_ > this.trackpadEventGap_) {
        this.mode_ = Math.abs(delta) < 4 ? 'trackpad' : 'wheel';
      }

      const view = map.getView();
      if (
        this.mode_ === 'trackpad' &&
        !(view.getConstrainResolution() || this.constrainResolution_)
      ) {
        if (this.trackpadTimeoutId_) {
          clearTimeout(this.trackpadTimeoutId_);
        } else {
          if (view.getAnimating()) {
            view.cancelAnimations();
          }
          view.beginInteraction();
        }
        this.trackpadTimeoutId_ = setTimeout(
          this.endInteraction_.bind(this),
          this.timeout_,
        );
        view.adjustZoom(
          -delta / this.deltaPerZoom_,
          this.lastAnchor_ ? map.getCoordinateFromPixel(this.lastAnchor_) : null,
        );
        this.startTime_ = now;
        return false;
      }

      this.totalDelta_ += delta;

      const timeLeft = Math.max(this.timeout_ - (now - this.startTime_), 0);

      clearTimeout(this.timeoutId_);
      this.timeoutId_ = setTimeout(
        this.handleWheelZoom_.bind(this, map),
        timeLeft,
      );

      return false;
    }

    /**
     * @private
     * @param {import("../Map.js").default} map Map.
     */
    handleWheelZoom_(map) {
      const view = map.getView();
      if (view.getAnimating()) {
        view.cancelAnimations();
      }
      let delta =
        -clamp(
          this.totalDelta_,
          -this.maxDelta_ * this.deltaPerZoom_,
          this.maxDelta_ * this.deltaPerZoom_,
        ) / this.deltaPerZoom_;
      if (view.getConstrainResolution() || this.constrainResolution_) {
        // view has a zoom constraint, zoom by 1
        delta = delta ? (delta > 0 ? 1 : -1) : 0;
      }
      zoomByDelta(
        view,
        delta,
        this.lastAnchor_ ? map.getCoordinateFromPixel(this.lastAnchor_) : null,
        this.duration_,
      );

      this.mode_ = undefined;
      this.totalDelta_ = 0;
      this.lastAnchor_ = null;
      this.startTime_ = undefined;
      this.timeoutId_ = undefined;
    }

    /**
     * Enable or disable using the mouse's location as an anchor when zooming
     * @param {boolean} useAnchor true to zoom to the mouse's location, false
     * to zoom to the center of the map
     * @api
     */
    setMouseAnchor(useAnchor) {
      this.useAnchor_ = useAnchor;
      if (!useAnchor) {
        this.lastAnchor_ = null;
      }
    }
  }

  /**
   * @module ol/interaction/PinchRotate
   */

  /**
   * @typedef {Object} Options
   * @property {number} [duration=250] The duration of the animation in
   * milliseconds.
   * @property {number} [threshold=0.3] Minimal angle in radians to start a rotation.
   */

  /**
   * @classdesc
   * Allows the user to rotate the map by twisting with two fingers
   * on a touch screen.
   * @api
   */
  class PinchRotate extends PointerInteraction {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      options = options ? options : {};

      const pointerOptions = /** @type {import("./Pointer.js").Options} */ (
        options
      );

      if (!pointerOptions.stopDown) {
        pointerOptions.stopDown = FALSE;
      }

      super(pointerOptions);

      /**
       * @private
       * @type {import("../coordinate.js").Coordinate}
       */
      this.anchor_ = null;

      /**
       * @private
       * @type {number|undefined}
       */
      this.lastAngle_ = undefined;

      /**
       * @private
       * @type {boolean}
       */
      this.rotating_ = false;

      /**
       * @private
       * @type {number}
       */
      this.rotationDelta_ = 0.0;

      /**
       * @private
       * @type {number}
       */
      this.threshold_ = options.threshold !== undefined ? options.threshold : 0.3;

      /**
       * @private
       * @type {number}
       */
      this.duration_ = options.duration !== undefined ? options.duration : 250;
    }

    /**
     * Handle pointer drag events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @override
     */
    handleDragEvent(mapBrowserEvent) {
      let rotationDelta = 0.0;

      const touch0 = this.targetPointers[0];
      const touch1 = this.targetPointers[1];

      // angle between touches
      const angle = Math.atan2(
        touch1.clientY - touch0.clientY,
        touch1.clientX - touch0.clientX,
      );

      if (this.lastAngle_ !== undefined) {
        const delta = angle - this.lastAngle_;
        this.rotationDelta_ += delta;
        if (!this.rotating_ && Math.abs(this.rotationDelta_) > this.threshold_) {
          this.rotating_ = true;
        }
        rotationDelta = delta;
      }
      this.lastAngle_ = angle;

      const map = mapBrowserEvent.map;
      const view = map.getView();
      if (view.getConstraints().rotation === disable) {
        return;
      }

      // rotate anchor point.
      // FIXME: should be the intersection point between the lines:
      //     touch0,touch1 and previousTouch0,previousTouch1
      this.anchor_ = map.getCoordinateFromPixelInternal(
        map.getEventPixel(centroid(this.targetPointers)),
      );

      // rotate
      if (this.rotating_) {
        map.render();
        view.adjustRotationInternal(rotationDelta, this.anchor_);
      }
    }

    /**
     * Handle pointer up events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @override
     */
    handleUpEvent(mapBrowserEvent) {
      if (this.targetPointers.length < 2) {
        const map = mapBrowserEvent.map;
        const view = map.getView();
        view.endInteraction(this.duration_);
        return false;
      }
      return true;
    }

    /**
     * Handle pointer down events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @override
     */
    handleDownEvent(mapBrowserEvent) {
      if (this.targetPointers.length >= 2) {
        const map = mapBrowserEvent.map;
        this.anchor_ = null;
        this.lastAngle_ = undefined;
        this.rotating_ = false;
        this.rotationDelta_ = 0.0;
        if (!this.handlingDownUpSequence) {
          map.getView().beginInteraction();
        }
        return true;
      }
      return false;
    }
  }

  /**
   * @module ol/interaction/PinchZoom
   */

  /**
   * @typedef {Object} Options
   * @property {number} [duration=400] Animation duration in milliseconds.
   */

  /**
   * @classdesc
   * Allows the user to zoom the map by pinching with two fingers
   * on a touch screen.
   * @api
   */
  class PinchZoom extends PointerInteraction {
    /**
     * @param {Options} [options] Options.
     */
    constructor(options) {
      options = options ? options : {};

      const pointerOptions = /** @type {import("./Pointer.js").Options} */ (
        options
      );

      if (!pointerOptions.stopDown) {
        pointerOptions.stopDown = FALSE;
      }

      super(pointerOptions);

      /**
       * @private
       * @type {import("../coordinate.js").Coordinate}
       */
      this.anchor_ = null;

      /**
       * @private
       * @type {number}
       */
      this.duration_ = options.duration !== undefined ? options.duration : 400;

      /**
       * @private
       * @type {number|undefined}
       */
      this.lastDistance_ = undefined;

      /**
       * @private
       * @type {number}
       */
      this.lastScaleDelta_ = 1;
    }

    /**
     * Handle pointer drag events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @override
     */
    handleDragEvent(mapBrowserEvent) {
      let scaleDelta = 1.0;

      const touch0 = this.targetPointers[0];
      const touch1 = this.targetPointers[1];
      const dx = touch0.clientX - touch1.clientX;
      const dy = touch0.clientY - touch1.clientY;

      // distance between touches
      const distance = Math.sqrt(dx * dx + dy * dy);

      if (this.lastDistance_ !== undefined) {
        scaleDelta = this.lastDistance_ / distance;
      }
      this.lastDistance_ = distance;

      const map = mapBrowserEvent.map;
      const view = map.getView();

      if (scaleDelta != 1.0) {
        this.lastScaleDelta_ = scaleDelta;
      }

      // scale anchor point.
      this.anchor_ = map.getCoordinateFromPixelInternal(
        map.getEventPixel(centroid(this.targetPointers)),
      );

      // scale, bypass the resolution constraint
      map.render();
      view.adjustResolutionInternal(scaleDelta, this.anchor_);
    }

    /**
     * Handle pointer up events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @override
     */
    handleUpEvent(mapBrowserEvent) {
      if (this.targetPointers.length < 2) {
        const map = mapBrowserEvent.map;
        const view = map.getView();
        const direction = this.lastScaleDelta_ > 1 ? 1 : -1;
        view.endInteraction(this.duration_, direction);
        return false;
      }
      return true;
    }

    /**
     * Handle pointer down events.
     * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Event.
     * @return {boolean} If the event was consumed.
     * @override
     */
    handleDownEvent(mapBrowserEvent) {
      if (this.targetPointers.length >= 2) {
        const map = mapBrowserEvent.map;
        this.anchor_ = null;
        this.lastDistance_ = undefined;
        this.lastScaleDelta_ = 1;
        if (!this.handlingDownUpSequence) {
          map.getView().beginInteraction();
        }
        return true;
      }
      return false;
    }
  }

  /**
   * @module ol/interaction/defaults
   */

  /**
   * @typedef {Object} DefaultsOptions
   * @property {boolean} [altShiftDragRotate=true] Whether Alt-Shift-drag rotate is
   * desired.
   * @property {boolean} [onFocusOnly=false] Interact only when the map has the
   * focus. This affects the `MouseWheelZoom` and `DragPan` interactions and is
   * useful when page scroll is desired for maps that do not have the browser's
   * focus.
   * @property {boolean} [doubleClickZoom=true] Whether double click zoom is
   * desired.
   * @property {boolean} [keyboard=true] Whether keyboard interaction is desired.
   * @property {boolean} [mouseWheelZoom=true] Whether mousewheel zoom is desired.
   * @property {boolean} [shiftDragZoom=true] Whether Shift-drag zoom is desired.
   * @property {boolean} [dragPan=true] Whether drag pan is desired.
   * @property {boolean} [pinchRotate=true] Whether pinch rotate is desired.
   * @property {boolean} [pinchZoom=true] Whether pinch zoom is desired.
   * @property {number} [zoomDelta] Zoom level delta when using keyboard or double click zoom.
   * @property {number} [zoomDuration] Duration of the zoom animation in
   * milliseconds.
   */

  /**
   * Set of interactions included in maps by default. Specific interactions can be
   * excluded by setting the appropriate option to false in the constructor
   * options, but the order of the interactions is fixed.  If you want to specify
   * a different order for interactions, you will need to create your own
   * {@link module:ol/interaction/Interaction~Interaction} instances and insert
   * them into a {@link module:ol/Collection~Collection} in the order you want
   * before creating your {@link module:ol/Map~Map} instance. Changing the order can
   * be of interest if the event propagation needs to be stopped at a point.
   * The default set of interactions, in sequence, is:
   * {@link module:ol/interaction/DragRotate~DragRotate}
   * {@link module:ol/interaction/DoubleClickZoom~DoubleClickZoom}
   * {@link module:ol/interaction/DragPan~DragPan}
   * {@link module:ol/interaction/PinchRotate~PinchRotate}
   * {@link module:ol/interaction/PinchZoom~PinchZoom}
   * {@link module:ol/interaction/KeyboardPan~KeyboardPan}
   * {@link module:ol/interaction/KeyboardZoom~KeyboardZoom}
   * {@link module:ol/interaction/MouseWheelZoom~MouseWheelZoom}
   * {@link module:ol/interaction/DragZoom~DragZoom}
   *
   * @param {DefaultsOptions} [options] Defaults options.
   * @return {Collection<import("./Interaction.js").default>}
   * A collection of interactions to be used with the {@link module:ol/Map~Map}
   * constructor's `interactions` option.
   * @api
   */
  function defaults(options) {
    options = options ? options : {};

    /** @type {Collection<import("./Interaction.js").default>} */
    const interactions = new Collection();

    const kinetic = new Kinetic(-5e-3, 0.05, 100);

    const altShiftDragRotate =
      options.altShiftDragRotate !== undefined
        ? options.altShiftDragRotate
        : true;
    if (altShiftDragRotate) {
      interactions.push(new DragRotate());
    }

    const doubleClickZoom =
      options.doubleClickZoom !== undefined ? options.doubleClickZoom : true;
    if (doubleClickZoom) {
      interactions.push(
        new DoubleClickZoom({
          delta: options.zoomDelta,
          duration: options.zoomDuration,
        }),
      );
    }

    const dragPan = options.dragPan !== undefined ? options.dragPan : true;
    if (dragPan) {
      interactions.push(
        new DragPan({
          onFocusOnly: options.onFocusOnly,
          kinetic: kinetic,
        }),
      );
    }

    const pinchRotate =
      options.pinchRotate !== undefined ? options.pinchRotate : true;
    if (pinchRotate) {
      interactions.push(new PinchRotate());
    }

    const pinchZoom = options.pinchZoom !== undefined ? options.pinchZoom : true;
    if (pinchZoom) {
      interactions.push(
        new PinchZoom({
          duration: options.zoomDuration,
        }),
      );
    }

    const keyboard = options.keyboard !== undefined ? options.keyboard : true;
    if (keyboard) {
      interactions.push(new KeyboardPan());
      interactions.push(
        new KeyboardZoom({
          delta: options.zoomDelta,
          duration: options.zoomDuration,
        }),
      );
    }

    const mouseWheelZoom =
      options.mouseWheelZoom !== undefined ? options.mouseWheelZoom : true;
    if (mouseWheelZoom) {
      interactions.push(
        new MouseWheelZoom({
          onFocusOnly: options.onFocusOnly,
          duration: options.zoomDuration,
        }),
      );
    }

    const shiftDragZoom =
      options.shiftDragZoom !== undefined ? options.shiftDragZoom : true;
    if (shiftDragZoom) {
      interactions.push(
        new DragZoom({
          duration: options.zoomDuration,
        }),
      );
    }

    return interactions;
  }

  /**
   * @module ol/layer/Group
   */

  /**
   * @enum {string}
   */
  const GroupEventType = {
    /**
     * Triggered when a layer is added
     * @event GroupEvent#addlayer
     * @api
     */
    ADDLAYER: 'addlayer',
    /**
     * Triggered when a layer is removed
     * @event GroupEvent#removelayer
     * @api
     */
    REMOVELAYER: 'removelayer',
  };

  /**
   * @classdesc
   * A layer group triggers 'addlayer' and 'removelayer' events when layers are added to or removed from
   * the group or one of its child groups.  When a layer group is added to or removed from another layer group,
   * a single event will be triggered (instead of one per layer in the group added or removed).
   */
  class GroupEvent extends BaseEvent {
    /**
     * @param {GroupEventType} type The event type.
     * @param {BaseLayer} layer The layer.
     */
    constructor(type, layer) {
      super(type);

      /**
       * The added or removed layer.
       * @type {BaseLayer}
       * @api
       */
      this.layer = layer;
    }
  }

  /***
   * @template Return
   * @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> &
   *   import("../Observable").OnSignature<import("./Base").BaseLayerObjectEventTypes|
   *     'change:layers', import("../Object").ObjectEvent, Return> &
   *   import("../Observable").OnSignature<'addlayer'|'removelayer', GroupEvent, Return> &
   *   import("../Observable").CombinedOnSignature<import("../Observable").EventTypes|import("./Base").BaseLayerObjectEventTypes|'addlayer'|'removelayer'|'change:layers', Return>} GroupOnSignature
   */

  /**
   * @typedef {Object} Options
   * @property {number} [opacity=1] Opacity (0, 1).
   * @property {boolean} [visible=true] Visibility.
   * @property {import("../extent.js").Extent} [extent] The bounding extent for layer rendering.  The layer will not be
   * rendered outside of this extent.
   * @property {number} [zIndex] The z-index for layer rendering.  At rendering time, the layers
   * will be ordered, first by Z-index and then by position. When `undefined`, a `zIndex` of 0 is assumed
   * for layers that are added to the map's `layers` collection, or `Infinity` when the layer's `setMap()`
   * method was used.
   * @property {number} [minResolution] The minimum resolution (inclusive) at which this layer will be
   * visible.
   * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
   * be visible.
   * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
   * visible.
   * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
   * be visible.
   * @property {Array<import("./Base.js").default>|Collection<import("./Base.js").default>} [layers] Child layers.
   * @property {Object<string, *>} [properties] Arbitrary observable properties. Can be accessed with `#get()` and `#set()`.
   */

  /**
   * @enum {string}
   * @private
   */
  const Property$1 = {
    LAYERS: 'layers',
  };

  /**
   * @classdesc
   * A {@link module:ol/Collection~Collection} of layers that are handled together.
   *
   * A generic `change` event is triggered when the group/Collection changes.
   *
   * @fires GroupEvent
   * @api
   */
  class LayerGroup extends BaseLayer {
    /**
     * @param {Options} [options] Layer options.
     */
    constructor(options) {
      options = options || {};
      const baseOptions = /** @type {Options} */ (Object.assign({}, options));
      delete baseOptions.layers;

      let layers = options.layers;

      super(baseOptions);

      /***
       * @type {GroupOnSignature<import("../events").EventsKey>}
       */
      this.on;

      /***
       * @type {GroupOnSignature<import("../events").EventsKey>}
       */
      this.once;

      /***
       * @type {GroupOnSignature<void>}
       */
      this.un;

      /**
       * @private
       * @type {Array<import("../events.js").EventsKey>}
       */
      this.layersListenerKeys_ = [];

      /**
       * @private
       * @type {Object<string, Array<import("../events.js").EventsKey>>}
       */
      this.listenerKeys_ = {};

      this.addChangeListener(Property$1.LAYERS, this.handleLayersChanged_);

      if (layers) {
        if (Array.isArray(layers)) {
          layers = new Collection(layers.slice(), {unique: true});
        } else {
          assert(
            typeof (/** @type {?} */ (layers).getArray) === 'function',
            'Expected `layers` to be an array or a `Collection`',
          );
        }
      } else {
        layers = new Collection(undefined, {unique: true});
      }

      this.setLayers(layers);
    }

    /**
     * @private
     */
    handleLayerChange_() {
      this.changed();
    }

    /**
     * @private
     */
    handleLayersChanged_() {
      this.layersListenerKeys_.forEach(unlistenByKey);
      this.layersListenerKeys_.length = 0;

      const layers = this.getLayers();
      this.layersListenerKeys_.push(
        listen(layers, CollectionEventType.ADD, this.handleLayersAdd_, this),
        listen(
          layers,
          CollectionEventType.REMOVE,
          this.handleLayersRemove_,
          this,
        ),
      );

      for (const id in this.listenerKeys_) {
        this.listenerKeys_[id].forEach(unlistenByKey);
      }
      clear(this.listenerKeys_);

      const layersArray = layers.getArray();
      for (let i = 0, ii = layersArray.length; i < ii; i++) {
        const layer = layersArray[i];
        this.registerLayerListeners_(layer);
        this.dispatchEvent(new GroupEvent(GroupEventType.ADDLAYER, layer));
      }
      this.changed();
    }

    /**
     * @param {BaseLayer} layer The layer.
     */
    registerLayerListeners_(layer) {
      const listenerKeys = [
        listen(
          layer,
          ObjectEventType.PROPERTYCHANGE,
          this.handleLayerChange_,
          this,
        ),
        listen(layer, EventType.CHANGE, this.handleLayerChange_, this),
      ];

      if (layer instanceof LayerGroup) {
        listenerKeys.push(
          listen(layer, GroupEventType.ADDLAYER, this.handleLayerGroupAdd_, this),
          listen(
            layer,
            GroupEventType.REMOVELAYER,
            this.handleLayerGroupRemove_,
            this,
          ),
        );
      }

      this.listenerKeys_[getUid(layer)] = listenerKeys;
    }

    /**
     * @param {GroupEvent} event The layer group event.
     */
    handleLayerGroupAdd_(event) {
      this.dispatchEvent(new GroupEvent(GroupEventType.ADDLAYER, event.layer));
    }

    /**
     * @param {GroupEvent} event The layer group event.
     */
    handleLayerGroupRemove_(event) {
      this.dispatchEvent(new GroupEvent(GroupEventType.REMOVELAYER, event.layer));
    }

    /**
     * @param {import("../Collection.js").CollectionEvent<import("./Base.js").default>} collectionEvent CollectionEvent.
     * @private
     */
    handleLayersAdd_(collectionEvent) {
      const layer = collectionEvent.element;
      this.registerLayerListeners_(layer);
      this.dispatchEvent(new GroupEvent(GroupEventType.ADDLAYER, layer));
      this.changed();
    }

    /**
     * @param {import("../Collection.js").CollectionEvent<import("./Base.js").default>} collectionEvent CollectionEvent.
     * @private
     */
    handleLayersRemove_(collectionEvent) {
      const layer = collectionEvent.element;
      const key = getUid(layer);
      this.listenerKeys_[key].forEach(unlistenByKey);
      delete this.listenerKeys_[key];
      this.dispatchEvent(new GroupEvent(GroupEventType.REMOVELAYER, layer));
      this.changed();
    }

    /**
     * Returns the {@link module:ol/Collection~Collection collection} of {@link module:ol/layer/Layer~Layer layers}
     * in this group.
     * @return {!Collection<import("./Base.js").default>} Collection of
     *   {@link module:ol/layer/Base~BaseLayer layers} that are part of this group.
     * @observable
     * @api
     */
    getLayers() {
      return /** @type {!Collection<import("./Base.js").default>} */ (
        this.get(Property$1.LAYERS)
      );
    }

    /**
     * Set the {@link module:ol/Collection~Collection collection} of {@link module:ol/layer/Layer~Layer layers}
     * in this group.
     * @param {!Collection<import("./Base.js").default>} layers Collection of
     *   {@link module:ol/layer/Base~BaseLayer layers} that are part of this group.
     * @observable
     * @api
     */
    setLayers(layers) {
      const collection = this.getLayers();
      if (collection) {
        const currentLayers = collection.getArray();
        for (let i = 0, ii = currentLayers.length; i < ii; ++i) {
          this.dispatchEvent(
            new GroupEvent(GroupEventType.REMOVELAYER, currentLayers[i]),
          );
        }
      }

      this.set(Property$1.LAYERS, layers);
    }

    /**
     * @param {Array<import("./Layer.js").default>} [array] Array of layers (to be modified in place).
     * @return {Array<import("./Layer.js").default>} Array of layers.
     * @override
     */
    getLayersArray(array) {
      array = array !== undefined ? array : [];
      this.getLayers().forEach(function (layer) {
        layer.getLayersArray(array);
      });
      return array;
    }

    /**
     * Get the layer states list and use this groups z-index as the default
     * for all layers in this and nested groups, if it is unset at this point.
     * If dest is not provided and this group's z-index is undefined
     * 0 is used a the default z-index.
     * @param {Array<import("./Layer.js").State>} [dest] Optional list
     * of layer states (to be modified in place).
     * @return {Array<import("./Layer.js").State>} List of layer states.
     * @override
     */
    getLayerStatesArray(dest) {
      const states = dest !== undefined ? dest : [];
      const pos = states.length;

      this.getLayers().forEach(function (layer) {
        layer.getLayerStatesArray(states);
      });

      const ownLayerState = this.getLayerState();
      let defaultZIndex = ownLayerState.zIndex;
      if (!dest && ownLayerState.zIndex === undefined) {
        defaultZIndex = 0;
      }
      for (let i = pos, ii = states.length; i < ii; i++) {
        const layerState = states[i];
        layerState.opacity *= ownLayerState.opacity;
        layerState.visible = layerState.visible && ownLayerState.visible;
        layerState.maxResolution = Math.min(
          layerState.maxResolution,
          ownLayerState.maxResolution,
        );
        layerState.minResolution = Math.max(
          layerState.minResolution,
          ownLayerState.minResolution,
        );
        layerState.minZoom = Math.max(layerState.minZoom, ownLayerState.minZoom);
        layerState.maxZoom = Math.min(layerState.maxZoom, ownLayerState.maxZoom);
        if (ownLayerState.extent !== undefined) {
          if (layerState.extent !== undefined) {
            layerState.extent = getIntersection(
              layerState.extent,
              ownLayerState.extent,
            );
          } else {
            layerState.extent = ownLayerState.extent;
          }
        }
        if (layerState.zIndex === undefined) {
          layerState.zIndex = defaultZIndex;
        }
      }

      return states;
    }

    /**
     * @return {import("../source/Source.js").State} Source state.
     * @override
     */
    getSourceState() {
      return 'ready';
    }
  }

  /**
   * @module ol/renderer/Map
   */

  /**
   * @template T
   * @typedef HitMatch
   * @property {import("../Feature.js").FeatureLike} feature Feature.
   * @property {import("../layer/Layer.js").default} layer Layer.
   * @property {import("../geom/SimpleGeometry.js").default} geometry Geometry.
   * @property {number} distanceSq Squared distance.
   * @property {import("./vector.js").FeatureCallback<T>} callback Callback.
   */

  /**
   * @abstract
   */
  class MapRenderer extends Disposable {
    /**
     * @param {import("../Map.js").default} map Map.
     */
    constructor(map) {
      super();

      /**
       * @private
       * @type {import("../Map.js").default}
       */
      this.map_ = map;
    }

    /**
     * @abstract
     * @param {import("../render/EventType.js").default} type Event type.
     * @param {import("../Map.js").FrameState} frameState Frame state.
     */
    dispatchRenderEvent(type, frameState) {
      abstract();
    }

    /**
     * @param {import("../Map.js").FrameState} frameState FrameState.
     * @protected
     */
    calculateMatrices2D(frameState) {
      const viewState = frameState.viewState;
      const coordinateToPixelTransform = frameState.coordinateToPixelTransform;
      const pixelToCoordinateTransform = frameState.pixelToCoordinateTransform;

      compose(
        coordinateToPixelTransform,
        frameState.size[0] / 2,
        frameState.size[1] / 2,
        1 / viewState.resolution,
        -1 / viewState.resolution,
        -viewState.rotation,
        -viewState.center[0],
        -viewState.center[1],
      );

      makeInverse(pixelToCoordinateTransform, coordinateToPixelTransform);
    }

    /**
     * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
     * @param {import("../Map.js").FrameState} frameState FrameState.
     * @param {number} hitTolerance Hit tolerance in pixels.
     * @param {boolean} checkWrapped Check for wrapped geometries.
     * @param {import("./vector.js").FeatureCallback<T>} callback Feature callback.
     * @param {S} thisArg Value to use as `this` when executing `callback`.
     * @param {function(this: U, import("../layer/Layer.js").default): boolean} layerFilter Layer filter
     *     function, only layers which are visible and for which this function
     *     returns `true` will be tested for features.  By default, all visible
     *     layers will be tested.
     * @param {U} thisArg2 Value to use as `this` when executing `layerFilter`.
     * @return {T|undefined} Callback result.
     * @template S,T,U
     */
    forEachFeatureAtCoordinate(
      coordinate,
      frameState,
      hitTolerance,
      checkWrapped,
      callback,
      thisArg,
      layerFilter,
      thisArg2,
    ) {
      let result;
      const viewState = frameState.viewState;

      /**
       * @param {boolean} managed Managed layer.
       * @param {import("../Feature.js").FeatureLike} feature Feature.
       * @param {import("../layer/Layer.js").default} layer Layer.
       * @param {import("../geom/Geometry.js").default} geometry Geometry.
       * @return {T|undefined} Callback result.
       */
      function forEachFeatureAtCoordinate(managed, feature, layer, geometry) {
        return callback.call(thisArg, feature, managed ? layer : null, geometry);
      }

      const projection = viewState.projection;

      const translatedCoordinate = wrapX$1(coordinate.slice(), projection);
      const offsets = [[0, 0]];
      if (projection.canWrapX() && checkWrapped) {
        const projectionExtent = projection.getExtent();
        const worldWidth = getWidth(projectionExtent);
        offsets.push([-worldWidth, 0], [worldWidth, 0]);
      }

      const layerStates = frameState.layerStatesArray;
      const numLayers = layerStates.length;

      const matches = /** @type {Array<HitMatch<T>>} */ ([]);
      const tmpCoord = [];
      for (let i = 0; i < offsets.length; i++) {
        for (let j = numLayers - 1; j >= 0; --j) {
          const layerState = layerStates[j];
          const layer = layerState.layer;
          if (
            layer.hasRenderer() &&
            inView(layerState, viewState) &&
            layerFilter.call(thisArg2, layer)
          ) {
            const layerRenderer = layer.getRenderer();
            const source = layer.getSource();
            if (layerRenderer && source) {
              const coordinates = source.getWrapX()
                ? translatedCoordinate
                : coordinate;
              const callback = forEachFeatureAtCoordinate.bind(
                null,
                layerState.managed,
              );
              tmpCoord[0] = coordinates[0] + offsets[i][0];
              tmpCoord[1] = coordinates[1] + offsets[i][1];
              result = layerRenderer.forEachFeatureAtCoordinate(
                tmpCoord,
                frameState,
                hitTolerance,
                callback,
                matches,
              );
            }
            if (result) {
              return result;
            }
          }
        }
      }
      if (matches.length === 0) {
        return undefined;
      }
      const order = 1 / matches.length;
      matches.forEach((m, i) => (m.distanceSq += i * order));
      matches.sort((a, b) => a.distanceSq - b.distanceSq);
      matches.some((m) => {
        return (result = m.callback(m.feature, m.layer, m.geometry));
      });
      return result;
    }

    /**
     * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
     * @param {import("../Map.js").FrameState} frameState FrameState.
     * @param {number} hitTolerance Hit tolerance in pixels.
     * @param {boolean} checkWrapped Check for wrapped geometries.
     * @param {function(this: U, import("../layer/Layer.js").default): boolean} layerFilter Layer filter
     *     function, only layers which are visible and for which this function
     *     returns `true` will be tested for features.  By default, all visible
     *     layers will be tested.
     * @param {U} thisArg Value to use as `this` when executing `layerFilter`.
     * @return {boolean} Is there a feature at the given coordinate?
     * @template U
     */
    hasFeatureAtCoordinate(
      coordinate,
      frameState,
      hitTolerance,
      checkWrapped,
      layerFilter,
      thisArg,
    ) {
      const hasFeature = this.forEachFeatureAtCoordinate(
        coordinate,
        frameState,
        hitTolerance,
        checkWrapped,
        TRUE,
        this,
        layerFilter,
        thisArg,
      );

      return hasFeature !== undefined;
    }

    /**
     * @return {import("../Map.js").default} Map.
     */
    getMap() {
      return this.map_;
    }

    /**
     * Render.
     * @abstract
     * @param {?import("../Map.js").FrameState} frameState Frame state.
     */
    renderFrame(frameState) {
      abstract();
    }

    /**
     * @param {import("../Map.js").FrameState} frameState Frame state.
     * @protected
     */
    scheduleExpireIconCache(frameState) {
      if (shared.canExpireCache()) {
        frameState.postRenderFunctions.push(expireIconCache);
      }
    }
  }

  /**
   * @param {import("../Map.js").default} map Map.
   * @param {import("../Map.js").FrameState} frameState Frame state.
   */
  function expireIconCache(map, frameState) {
    shared.expire();
  }

  /**
   * @module ol/renderer/Composite
   */

  /**
   * @classdesc
   * Canvas map renderer.
   * @api
   */
  class CompositeMapRenderer extends MapRenderer {
    /**
     * @param {import("../Map.js").default} map Map.
     */
    constructor(map) {
      super(map);

      /**
       * @private
       * @type {import("../events.js").EventsKey}
       */
      this.fontChangeListenerKey_ = listen(
        checkedFonts,
        ObjectEventType.PROPERTYCHANGE,
        map.redrawText,
        map,
      );

      /**
       * @private
       * @type {HTMLDivElement}
       */
      this.element_ = WORKER_OFFSCREEN_CANVAS
        ? createMockDiv()
        : document.createElement('div');
      const style = this.element_.style;
      style.position = 'absolute';
      style.width = '100%';
      style.height = '100%';
      style.zIndex = '0';

      this.element_.className = CLASS_UNSELECTABLE + ' ol-layers';

      const container = map.getViewport();
      if (container) {
        // maps in a worker do not have a viewport.
        container.insertBefore(this.element_, container.firstChild || null);
      }

      /**
       * @private
       * @type {Array<HTMLElement>}
       */
      this.children_ = [];

      /**
       * @private
       * @type {boolean}
       */
      this.renderedVisible_ = true;
    }

    /**
     * @param {import("../render/EventType.js").default} type Event type.
     * @param {import("../Map.js").FrameState} frameState Frame state.
     * @override
     */
    dispatchRenderEvent(type, frameState) {
      const map = this.getMap();
      if (map.hasListener(type)) {
        const event = new RenderEvent(type, undefined, frameState);
        map.dispatchEvent(event);
      }
    }

    /**
     * @override
     */
    disposeInternal() {
      unlistenByKey(this.fontChangeListenerKey_);
      this.element_.remove();
      super.disposeInternal();
    }

    /**
     * Render.
     * @param {?import("../Map.js").FrameState} frameState Frame state.
     * @override
     */
    renderFrame(frameState) {
      if (!frameState) {
        if (this.renderedVisible_) {
          this.element_.style.display = 'none';
          this.renderedVisible_ = false;
        }
        return;
      }

      this.calculateMatrices2D(frameState);
      this.dispatchRenderEvent(RenderEventType.PRECOMPOSE, frameState);

      const layerStatesArray = frameState.layerStatesArray.sort(
        (a, b) => a.zIndex - b.zIndex,
      );
      const declutter = layerStatesArray.some(
        (layerState) =>
          layerState.layer instanceof BaseVectorLayer &&
          layerState.layer.getDeclutter(),
      );
      if (declutter) {
        // Some layers need decluttering, turn on deferred rendering hint
        frameState.declutter = {};
      }
      const viewState = frameState.viewState;

      this.children_.length = 0;

      const renderedLayerStates = [];
      let previousElement = null;
      for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) {
        const layerState = layerStatesArray[i];
        frameState.layerIndex = i;

        const layer = layerState.layer;
        const sourceState = layer.getSourceState();
        if (
          !inView(layerState, viewState) ||
          (sourceState != 'ready' && sourceState != 'undefined')
        ) {
          layer.unrender();
          continue;
        }

        const element = layer.render(frameState, previousElement);
        if (!element) {
          continue;
        }
        if (element !== previousElement) {
          this.children_.push(element);
          previousElement = element;
        }

        renderedLayerStates.push(layerState);
      }

      this.declutter(frameState, renderedLayerStates);

      replaceChildren(this.element_, this.children_);

      const map = this.getMap();
      const mapCanvas = map.getTargetElement();
      if (isCanvas(mapCanvas)) {
        // Canvas composition when container is a canvas
        const mapContext = mapCanvas.getContext('2d');
        for (const container of this.children_) {
          const canvas = container.firstElementChild || container;
          const backgroundColor = container.style.backgroundColor;
          if (backgroundColor && (!isCanvas(canvas) || canvas.width > 0)) {
            mapContext.fillStyle = backgroundColor;
            mapContext.fillRect(0, 0, mapCanvas.width, mapCanvas.height);
          }
          if (isCanvas(canvas) && canvas.width > 0) {
            mapContext.save();
            const opacity = container.style.opacity || canvas.style.opacity;
            mapContext.globalAlpha = opacity === '' ? 1 : Number(opacity);
            const transform = canvas.style.transform;
            if (transform) {
              // Get the transform parameters from the style's transform matrix
              mapContext.transform(
                .../** @type {[number, number, number, number, number, number]} */ (
                  fromString$1(transform)
                ),
              );
            } else {
              const w = parseFloat(canvas.style.width) / canvas.width;
              const h = parseFloat(canvas.style.height) / canvas.height;
              mapContext.transform(w, 0, 0, h, 0, 0);
            }
            mapContext.drawImage(canvas, 0, 0);
            mapContext.restore();
          }
        }
      }

      this.dispatchRenderEvent(RenderEventType.POSTCOMPOSE, frameState);

      if (!this.renderedVisible_) {
        this.element_.style.display = '';
        this.renderedVisible_ = true;
      }

      this.scheduleExpireIconCache(frameState);
    }

    /**
     * @param {import("../Map.js").FrameState} frameState Frame state.
     * @param {Array<import('../layer/Layer.js').State>} layerStates Layers.
     */
    declutter(frameState, layerStates) {
      if (!frameState.declutter) {
        return;
      }
      for (let i = layerStates.length - 1; i >= 0; --i) {
        const layerState = layerStates[i];
        const layer = layerState.layer;
        if (layer.getDeclutter()) {
          layer.renderDeclutter(frameState, layerState);
        }
      }
      layerStates.forEach((layerState) =>
        layerState.layer.renderDeferred(frameState),
      );
    }
  }

  /**
   * @module ol/Map
   */

  /**
   * State of the current frame. Only `pixelRatio`, `time` and `viewState` should
   * be used in applications.
   * @typedef {Object} FrameState
   * @property {number} pixelRatio The pixel ratio of the frame.
   * @property {number} time The time when rendering of the frame was requested.
   * @property {import("./View.js").State} viewState The state of the current view.
   * @property {boolean} animate Animate.
   * @property {import("./transform.js").Transform} coordinateToPixelTransform CoordinateToPixelTransform.
   * @property {Object<string, import("rbush").default<import('./render/canvas/Executor.js').DeclutterEntry>>|null} declutter
   * Declutter trees by declutter group.
   * When null, no decluttering is needed because no layers have decluttering enabled.
   * @property {null|import("./extent.js").Extent} extent Extent (in view projection coordinates).
   * @property {import("./extent.js").Extent} [nextExtent] Next extent during an animation series.
   * @property {number} index Index.
   * @property {Array<import("./layer/Layer.js").State>} layerStatesArray LayerStatesArray.
   * @property {number} layerIndex LayerIndex.
   * @property {import("./transform.js").Transform} pixelToCoordinateTransform PixelToCoordinateTransform.
   * @property {Array<PostRenderFunction>} postRenderFunctions PostRenderFunctions.
   * @property {import("./size.js").Size} size Size.
   * @property {TileQueue} tileQueue TileQueue.
   * @property {!Object<string, Object<string, boolean>>} usedTiles UsedTiles.
   * @property {Array<number>} viewHints ViewHints.
   * @property {!Object<string, Object<string, boolean>>} wantedTiles WantedTiles.
   * @property {string} mapId The id of the map.
   * @property {Object<string, boolean>} renderTargets Identifiers of previously rendered elements.
   */

  /**
   * @typedef {function(Map, FrameState): any} PostRenderFunction
   */

  /**
   * @typedef {Object} AtPixelOptions
   * @property {undefined|function(import("./layer/Layer.js").default<import("./source/Source").default>): boolean} [layerFilter] Layer filter
   * function. The filter function will receive one argument, the
   * {@link module:ol/layer/Layer~Layer layer-candidate} and it should return a boolean value.
   * Only layers which are visible and for which this function returns `true`
   * will be tested for features. By default, all visible layers will be tested.
   * @property {number} [hitTolerance=0] Hit-detection tolerance in css pixels. Pixels
   * inside the radius around the given position will be checked for features.
   * @property {boolean} [checkWrapped=true] Check-Wrapped Will check for wrapped geometries inside the range of
   *   +/- 1 world width. Works only if a projection is used that can be wrapped.
   */

  /**
   * @typedef {Object} MapOptionsInternal
   * @property {Collection<import("./control/Control.js").default>} [controls] Controls.
   * @property {Collection<import("./interaction/Interaction.js").default>} [interactions] Interactions.
   * @property {HTMLElement|Document} keyboardEventTarget KeyboardEventTarget.
   * @property {Collection<import("./Overlay.js").default>} overlays Overlays.
   * @property {Object<string, *>} values Values.
   */

  /**
   * @typedef {import("./ObjectEventType").Types|'change:layergroup'|'change:size'|'change:target'|'change:view'} MapObjectEventTypes
   */

  /***
   * @template Return
   * @typedef {import("./Observable").OnSignature<import("./Observable").EventTypes, import("./events/Event.js").default, Return> &
   *    import("./Observable").OnSignature<MapObjectEventTypes, import("./Object").ObjectEvent, Return> &
   *    import("./Observable").OnSignature<import("./MapBrowserEventType").Types, import("./MapBrowserEvent").default, Return> &
   *    import("./Observable").OnSignature<import("./MapEventType").Types, import("./MapEvent").default, Return> &
   *    import("./Observable").OnSignature<import("./render/EventType").MapRenderEventTypes, import("./render/Event").default, Return> &
   *    import("./Observable").CombinedOnSignature<import("./Observable").EventTypes|MapObjectEventTypes|
   *      import("./MapBrowserEventType").Types|import("./MapEventType").Types|
   *      import("./render/EventType").MapRenderEventTypes, Return>} MapEventHandler
   */

  /**
   * Object literal with config options for the map.
   * @typedef {Object} MapOptions
   * @property {Collection<import("./control/Control.js").default>|Array<import("./control/Control.js").default>} [controls]
   * Controls initially added to the map. If not specified,
   * {@link module:ol/control/defaults.defaults} is used. In a worker, no controls are added by default.
   * @property {number} [pixelRatio=window.devicePixelRatio] The ratio between
   * physical pixels and device-independent pixels (dips) on the device.
   * @property {Collection<import("./interaction/Interaction.js").default>|Array<import("./interaction/Interaction.js").default>} [interactions]
   * Interactions that are initially added to the map. If not specified,
   * {@link module:ol/interaction/defaults.defaults} is used. In a worker, no interactions are added by default.
   * @property {HTMLElement|Document|string} [keyboardEventTarget] The element to
   * listen to keyboard events on. This determines when the `KeyboardPan` and
   * `KeyboardZoom` interactions trigger. For example, if this option is set to
   * `document` the keyboard interactions will always trigger. If this option is
   * not specified, the element the library listens to keyboard events on is the
   * map target (i.e. the user-provided div for the map). If this is not
   * `document`, the target element needs to be focused for key events to be
   * emitted, requiring that the target element has a `tabindex` attribute.
   * @property {Array<import("./layer/Base.js").default>|Collection<import("./layer/Base.js").default>|LayerGroup} [layers]
   * Layers. If this is not defined, a map with no layers will be rendered. Note
   * that layers are rendered in the order supplied, so if you want, for example,
   * a vector layer to appear on top of a tile layer, it must come after the tile
   * layer.
   * @property {number} [maxTilesLoading=16] Maximum number tiles to load
   * simultaneously.
   * @property {number} [moveTolerance=1] The minimum distance in pixels the
   * cursor must move to be detected as a map move event instead of a click.
   * Increasing this value can make it easier to click on the map.
   * @property {Collection<import("./Overlay.js").default>|Array<import("./Overlay.js").default>} [overlays]
   * Overlays initially added to the map. By default, no overlays are added.
   * @property {HTMLElement|string|HTMLCanvasElement|OffscreenCanvas} [target] The container for the map, either the
   * element itself or the `id` of the element. If not specified at construction
   * time, {@link module:ol/Map~Map#setTarget} must be called for the map to be
   * rendered. If passed by element, the container can be in a secondary document.
   * For use in workers or when exporting a map, use an `OffscreenCanvas` or `HTMLCanvasElement` as target,
   * with a width and height in physical pixels, optionally multiplied by and a scale transform matching
   * the map's pixel ratio.
   * For accessibility (focus and keyboard events for map navigation), the `target` element must have a
   *  properly configured `tabindex` attribute. If the `target` element is inside a Shadow DOM, the
   *  `tabindex` atribute must be set on the custom element's host element.
   * **Note:** CSS `transform` support for the target element is limited to `scale`.
   * @property {View|Promise<import("./View.js").ViewOptions>} [view] The map's view.  No layer sources will be
   * fetched unless this is specified at construction time or through
   * {@link module:ol/Map~Map#setView}.
   */

  /**
   * @param {import("./layer/Base.js").default} layer Layer.
   */
  function removeLayerMapProperty(layer) {
    if (layer instanceof Layer) {
      layer.setMapInternal(null);
      return;
    }
    if (layer instanceof LayerGroup) {
      layer.getLayers().forEach(removeLayerMapProperty);
    }
  }

  /**
   * @param {import("./layer/Base.js").default} layer Layer.
   * @param {Map} map Map.
   */
  function setLayerMapProperty(layer, map) {
    if (layer instanceof Layer) {
      layer.setMapInternal(map);
      return;
    }
    if (layer instanceof LayerGroup) {
      const layers = layer.getLayers().getArray();
      for (let i = 0, ii = layers.length; i < ii; ++i) {
        setLayerMapProperty(layers[i], map);
      }
    }
  }

  /**
   * @classdesc
   * The map is the core component of OpenLayers. For a map to render, a view,
   * one or more layers, and a target container are needed:
   *
   *     import Map from 'ol/Map.js';
   *     import View from 'ol/View.js';
   *     import TileLayer from 'ol/layer/Tile.js';
   *     import OSM from 'ol/source/OSM.js';
   *
   *     const map = new Map({
   *       view: new View({
   *         center: [0, 0],
   *         zoom: 1,
   *       }),
   *       layers: [
   *         new TileLayer({
   *           source: new OSM(),
   *         }),
   *       ],
   *       target: 'map',
   *     });
   *
   * The above snippet creates a map using a {@link module:ol/layer/Tile~TileLayer} to
   * display {@link module:ol/source/OSM~OSM} OSM data and render it to a DOM
   * element with the id `map`.
   *
   * The constructor places a viewport container (with CSS class name
   * `ol-viewport`) in the target element (see `getViewport()`), and then two
   * further elements within the viewport: one with CSS class name
   * `ol-overlaycontainer-stopevent` for controls and some overlays, and one with
   * CSS class name `ol-overlaycontainer` for other overlays (see the `stopEvent`
   * option of {@link module:ol/Overlay~Overlay} for the difference). The map
   * itself is placed in a further element within the viewport.
   *
   * Layers are stored as a {@link module:ol/Collection~Collection} in
   * layerGroups. A top-level group is provided by the library. This is what is
   * accessed by `getLayerGroup` and `setLayerGroup`. Layers entered in the
   * options are added to this group, and `addLayer` and `removeLayer` change the
   * layer collection in the group. `getLayers` is a convenience function for
   * `getLayerGroup().getLayers()`. Note that {@link module:ol/layer/Group~LayerGroup}
   * is a subclass of {@link module:ol/layer/Base~BaseLayer}, so layers entered in the
   * options or added with `addLayer` can be groups, which can contain further
   * groups, and so on.
   *
   * @fires import("./MapBrowserEvent.js").MapBrowserEvent
   * @fires import("./MapEvent.js").MapEvent
   * @fires import("./render/Event.js").default#precompose
   * @fires import("./render/Event.js").default#postcompose
   * @fires import("./render/Event.js").default#rendercomplete
   * @api
   */
  class Map extends BaseObject {
    /**
     * @param {MapOptions} [options] Map options.
     */
    constructor(options) {
      super();

      options = options || {};

      /***
       * @type {MapEventHandler<import("./events").EventsKey>}
       */
      this.on;

      /***
       * @type {MapEventHandler<import("./events").EventsKey>}
       */
      this.once;

      /***
       * @type {MapEventHandler<void>}
       */
      this.un;

      const optionsInternal = createOptionsInternal(options);

      /**
       * @private
       * @type {boolean}
       */
      this.renderComplete_ = false;

      /**
       * @private
       * @type {boolean}
       */
      this.loaded_ = true;

      /** @private */
      this.boundHandleBrowserEvent_ = this.handleBrowserEvent.bind(this);

      /**
       * @type {number}
       * @private
       */
      this.maxTilesLoading_ =
        options.maxTilesLoading !== undefined ? options.maxTilesLoading : 16;

      /**
       * @private
       * @type {number}
       */
      this.pixelRatio_ =
        options.pixelRatio !== undefined
          ? options.pixelRatio
          : DEVICE_PIXEL_RATIO;

      /**
       * @private
       * @type {ReturnType<typeof setTimeout>}
       */
      this.postRenderTimeoutHandle_;

      /**
       * @private
       * @type {number|undefined}
       */
      this.animationDelayKey_;

      /**
       * @private
       */
      this.animationDelay_ = this.animationDelay_.bind(this);

      /**
       * @private
       * @type {import("./transform.js").Transform}
       */
      this.coordinateToPixelTransform_ = create();

      /**
       * @private
       * @type {import("./transform.js").Transform}
       */
      this.pixelToCoordinateTransform_ = create();

      /**
       * @private
       * @type {number}
       */
      this.frameIndex_ = 0;

      /**
       * @private
       * @type {?FrameState}
       */
      this.frameState_ = null;

      /**
       * The extent at the previous 'moveend' event.
       * @private
       * @type {import("./extent.js").Extent}
       */
      this.previousExtent_ = null;

      /**
       * @private
       * @type {?import("./events.js").EventsKey}
       */
      this.viewPropertyListenerKey_ = null;

      /**
       * @private
       * @type {?import("./events.js").EventsKey}
       */
      this.viewChangeListenerKey_ = null;

      /**
       * @private
       * @type {?Array<import("./events.js").EventsKey>}
       */
      this.layerGroupPropertyListenerKeys_ = null;

      /**
       * @private
       * @type {!HTMLElement}
       */
      if (!WORKER_OFFSCREEN_CANVAS) {
        this.viewport_ = document.createElement('div');
        this.viewport_.className =
          'ol-viewport' + ('ontouchstart' in window ? ' ol-touch' : '');
        this.viewport_.style.position = 'relative';
        this.viewport_.style.overflow = 'hidden';
        this.viewport_.style.width = '100%';
        this.viewport_.style.height = '100%';

        /**
         * @private
         * @type {!HTMLElement}
         */
        this.overlayContainer_ = document.createElement('div');
        this.overlayContainer_.style.position = 'absolute';
        this.overlayContainer_.style.zIndex = '0';
        this.overlayContainer_.style.width = '100%';
        this.overlayContainer_.style.height = '100%';
        this.overlayContainer_.style.pointerEvents = 'none';
        this.overlayContainer_.className = 'ol-overlaycontainer';
        this.viewport_.appendChild(this.overlayContainer_);

        /**
         * @private
         * @type {!HTMLElement}
         */
        this.overlayContainerStopEvent_ = document.createElement('div');
        this.overlayContainerStopEvent_.style.position = 'absolute';
        this.overlayContainerStopEvent_.style.zIndex = '0';
        this.overlayContainerStopEvent_.style.width = '100%';
        this.overlayContainerStopEvent_.style.height = '100%';
        this.overlayContainerStopEvent_.style.pointerEvents = 'none';
        this.overlayContainerStopEvent_.className =
          'ol-overlaycontainer-stopevent';
        this.viewport_.appendChild(this.overlayContainerStopEvent_);
      }

      /**
       * @private
       * @type {MapBrowserEventHandler}
       */
      this.mapBrowserEventHandler_ = null;

      /**
       * @private
       * @type {number}
       */
      this.moveTolerance_ = options.moveTolerance;

      /**
       * @private
       * @type {HTMLElement|Document}
       */
      this.keyboardEventTarget_ = optionsInternal.keyboardEventTarget;

      /**
       * @private
       * @type {?Array<import("./events.js").EventsKey>}
       */
      this.targetChangeHandlerKeys_ = null;

      /**
       * @private
       * @type {HTMLElement|null}
       */
      this.targetElement_ = null;

      if (!WORKER_OFFSCREEN_CANVAS) {
        /**
         * @private
         * @type {ResizeObserver}
         */
        this.resizeObserver_ = new ResizeObserver(() => this.updateSize());
      }

      /**
       * @type {Collection<import("./control/Control.js").default>}
       * @protected
       */
      this.controls =
        optionsInternal.controls ||
        (WORKER_OFFSCREEN_CANVAS ? new Collection() : defaults$1());

      /**
       * @type {Collection<import("./interaction/Interaction.js").default>}
       * @protected
       */
      this.interactions =
        optionsInternal.interactions ||
        (WORKER_OFFSCREEN_CANVAS
          ? new Collection()
          : defaults({
              onFocusOnly: true,
            }));

      /**
       * @type {Collection<import("./Overlay.js").default>}
       * @private
       */
      this.overlays_ = optionsInternal.overlays;

      /**
       * A lookup of overlays by id.
       * @private
       * @type {Object<string, import("./Overlay.js").default>}
       */
      this.overlayIdIndex_ = {};

      /**
       * @type {import("./renderer/Map.js").default|null}
       * @private
       */
      this.renderer_ = null;

      /**
       * @private
       * @type {!Array<PostRenderFunction>}
       */
      this.postRenderFunctions_ = [];

      /**
       * @private
       * @type {TileQueue}
       */
      this.tileQueue_ = new TileQueue(
        this.getTilePriority.bind(this),
        this.handleTileChange_.bind(this),
      );

      this.addChangeListener(
        MapProperty.LAYERGROUP,
        this.handleLayerGroupChanged_,
      );
      this.addChangeListener(MapProperty.VIEW, this.handleViewChanged_);
      this.addChangeListener(MapProperty.SIZE, this.handleSizeChanged_);
      this.addChangeListener(MapProperty.TARGET, this.handleTargetChanged_);

      // setProperties will trigger the rendering of the map if the map
      // is "defined" already.
      this.setProperties(optionsInternal.values);

      const map = this;
      if (options.view && !(options.view instanceof View)) {
        options.view.then(function (viewOptions) {
          map.setView(new View(viewOptions));
        });
      }

      this.controls.addEventListener(
        CollectionEventType.ADD,
        /**
         * @param {import("./Collection.js").CollectionEvent<import("./control/Control.js").default>} event CollectionEvent
         */
        (event) => {
          event.element.setMap(this);
        },
      );

      this.controls.addEventListener(
        CollectionEventType.REMOVE,
        /**
         * @param {import("./Collection.js").CollectionEvent<import("./control/Control.js").default>} event CollectionEvent.
         */
        (event) => {
          event.element.setMap(null);
        },
      );

      this.interactions.addEventListener(
        CollectionEventType.ADD,
        /**
         * @param {import("./Collection.js").CollectionEvent<import("./interaction/Interaction.js").default>} event CollectionEvent.
         */
        (event) => {
          event.element.setMap(this);
        },
      );

      this.interactions.addEventListener(
        CollectionEventType.REMOVE,
        /**
         * @param {import("./Collection.js").CollectionEvent<import("./interaction/Interaction.js").default>} event CollectionEvent.
         */
        (event) => {
          event.element.setMap(null);
        },
      );

      this.overlays_.addEventListener(
        CollectionEventType.ADD,
        /**
         * @param {import("./Collection.js").CollectionEvent<import("./Overlay.js").default>} event CollectionEvent.
         */
        (event) => {
          this.addOverlayInternal_(event.element);
        },
      );

      this.overlays_.addEventListener(
        CollectionEventType.REMOVE,
        /**
         * @param {import("./Collection.js").CollectionEvent<import("./Overlay.js").default>} event CollectionEvent.
         */
        (event) => {
          const id = event.element.getId();
          if (id !== undefined) {
            delete this.overlayIdIndex_[id.toString()];
          }
          event.element.setMap(null);
        },
      );

      this.controls.forEach(
        /**
         * @param {import("./control/Control.js").default} control Control.
         */
        (control) => {
          control.setMap(this);
        },
      );

      this.interactions.forEach(
        /**
         * @param {import("./interaction/Interaction.js").default} interaction Interaction.
         */
        (interaction) => {
          interaction.setMap(this);
        },
      );

      this.overlays_.forEach(this.addOverlayInternal_.bind(this));
    }

    /**
     * Add the given control to the map.
     * @param {import("./control/Control.js").default} control Control.
     * @api
     */
    addControl(control) {
      this.getControls().push(control);
    }

    /**
     * Add the given interaction to the map. If you want to add an interaction
     * at another point of the collection use `getInteractions()` and the methods
     * available on {@link module:ol/Collection~Collection}. This can be used to
     * stop the event propagation from the handleEvent function. The interactions
     * get to handle the events in the reverse order of this collection.
     * @param {import("./interaction/Interaction.js").default} interaction Interaction to add.
     * @api
     */
    addInteraction(interaction) {
      this.getInteractions().push(interaction);
    }

    /**
     * Adds the given layer to the top of this map. If you want to add a layer
     * elsewhere in the stack, use `getLayers()` and the methods available on
     * {@link module:ol/Collection~Collection}.
     * @param {import("./layer/Base.js").default} layer Layer.
     * @api
     */
    addLayer(layer) {
      const layers = this.getLayerGroup().getLayers();
      layers.push(layer);
    }

    /**
     * @param {import("./layer/Group.js").GroupEvent} event The layer add event.
     * @private
     */
    handleLayerAdd_(event) {
      setLayerMapProperty(event.layer, this);
    }

    /**
     * Add the given overlay to the map.
     * @param {import("./Overlay.js").default} overlay Overlay.
     * @api
     */
    addOverlay(overlay) {
      this.getOverlays().push(overlay);
    }

    /**
     * This deals with map's overlay collection changes.
     * @param {import("./Overlay.js").default} overlay Overlay.
     * @private
     */
    addOverlayInternal_(overlay) {
      const id = overlay.getId();
      if (id !== undefined) {
        this.overlayIdIndex_[id.toString()] = overlay;
      }
      overlay.setMap(this);
    }

    /**
     *
     * Clean up.
     * @override
     */
    disposeInternal() {
      this.controls.clear();
      this.interactions.clear();
      this.overlays_.clear();
      this.resizeObserver_?.disconnect();
      this.setTarget(null);
      super.disposeInternal();
    }

    /**
     * Detect features that intersect a pixel on the viewport, and execute a
     * callback with each intersecting feature. Layers included in the detection can
     * be configured through the `layerFilter` option in `options`.
     * For polygons without a fill, only the stroke will be used for hit detection.
     * Polygons must have a fill style applied to ensure that pixels inside a polygon are detected.
     * The fill can be transparent.
     * @param {import("./pixel.js").Pixel} pixel Pixel.
     * @param {function(import("./Feature.js").FeatureLike, import("./layer/Layer.js").default<import("./source/Source").default>, import("./geom/SimpleGeometry.js").default): T} callback Feature callback. The callback will be
     *     called with two arguments. The first argument is one
     *     {@link module:ol/Feature~Feature feature} or
     *     {@link module:ol/render/Feature~RenderFeature render feature} at the pixel, the second is
     *     the {@link module:ol/layer/Layer~Layer layer} of the feature and will be null for
     *     unmanaged layers. To stop detection, callback functions can return a
     *     truthy value.
     * @param {AtPixelOptions} [options] Optional options.
     * @return {T|undefined} Callback result, i.e. the return value of last
     * callback execution, or the first truthy callback return value.
     * @template T
     * @api
     */
    forEachFeatureAtPixel(pixel, callback, options) {
      if (!this.frameState_ || !this.renderer_) {
        return;
      }
      const coordinate = this.getCoordinateFromPixelInternal(pixel);
      options = options !== undefined ? options : {};
      const hitTolerance =
        options.hitTolerance !== undefined ? options.hitTolerance : 0;
      const layerFilter =
        options.layerFilter !== undefined ? options.layerFilter : TRUE;
      const checkWrapped = options.checkWrapped !== false;
      return this.renderer_.forEachFeatureAtCoordinate(
        coordinate,
        this.frameState_,
        hitTolerance,
        checkWrapped,
        callback,
        null,
        layerFilter,
        null,
      );
    }

    /**
     * Get all features that intersect a pixel on the viewport.
     * For polygons without a fill, only the stroke will be used for hit detection.
     * Polygons must have a fill style applied to ensure that pixels inside a polygon are detected.
     * The fill can be transparent.
     * @param {import("./pixel.js").Pixel} pixel Pixel.
     * @param {AtPixelOptions} [options] Optional options.
     * @return {Array<import("./Feature.js").FeatureLike>} The detected features or
     * an empty array if none were found.
     * @api
     */
    getFeaturesAtPixel(pixel, options) {
      const features = [];
      this.forEachFeatureAtPixel(
        pixel,
        function (feature) {
          features.push(feature);
        },
        options,
      );
      return features;
    }

    /**
     * Get all layers from all layer groups.
     * @return {Array<import("./layer/Layer.js").default>} Layers.
     * @api
     */
    getAllLayers() {
      const layers = [];
      function addLayersFrom(layerGroup) {
        layerGroup.forEach(function (layer) {
          if (layer instanceof LayerGroup) {
            addLayersFrom(layer.getLayers());
          } else {
            layers.push(layer);
          }
        });
      }
      addLayersFrom(this.getLayers());
      return layers;
    }

    /**
     * Detect if features intersect a pixel on the viewport. Layers included in the
     * detection can be configured through the `layerFilter` option.
     * For polygons without a fill, only the stroke will be used for hit detection.
     * Polygons must have a fill style applied to ensure that pixels inside a polygon are detected.
     * The fill can be transparent.
     * @param {import("./pixel.js").Pixel} pixel Pixel.
     * @param {AtPixelOptions} [options] Optional options.
     * @return {boolean} Is there a feature at the given pixel?
     * @api
     */
    hasFeatureAtPixel(pixel, options) {
      if (!this.frameState_ || !this.renderer_) {
        return false;
      }
      const coordinate = this.getCoordinateFromPixelInternal(pixel);
      options = options !== undefined ? options : {};
      const layerFilter =
        options.layerFilter !== undefined ? options.layerFilter : TRUE;
      const hitTolerance =
        options.hitTolerance !== undefined ? options.hitTolerance : 0;
      const checkWrapped = options.checkWrapped !== false;
      return this.renderer_.hasFeatureAtCoordinate(
        coordinate,
        this.frameState_,
        hitTolerance,
        checkWrapped,
        layerFilter,
        null,
      );
    }

    /**
     * Returns the coordinate in user projection for a browser event.
     * @param {MouseEvent} event Event.
     * @return {import("./coordinate.js").Coordinate} Coordinate.
     * @api
     */
    getEventCoordinate(event) {
      return this.getCoordinateFromPixel(this.getEventPixel(event));
    }

    /**
     * Returns the coordinate in view projection for a browser event.
     * @param {MouseEvent} event Event.
     * @return {import("./coordinate.js").Coordinate} Coordinate.
     */
    getEventCoordinateInternal(event) {
      return this.getCoordinateFromPixelInternal(this.getEventPixel(event));
    }

    /**
     * Returns the map pixel position for a browser event relative to the viewport.
     * @param {UIEvent|{clientX: number, clientY: number}} event Event.
     * @return {import("./pixel.js").Pixel} Pixel.
     * @api
     */
    getEventPixel(event) {
      const viewport = this.viewport_;
      const viewportPosition = viewport.getBoundingClientRect();
      const viewportSize = this.getSize();
      const scaleX = viewportPosition.width / viewportSize[0];
      const scaleY = viewportPosition.height / viewportSize[1];
      const eventPosition =
        //FIXME Are we really calling this with a TouchEvent anywhere?
        'changedTouches' in event
          ? /** @type {TouchEvent} */ (event).changedTouches[0]
          : /** @type {MouseEvent} */ (event);

      return [
        (eventPosition.clientX - viewportPosition.left) / scaleX,
        (eventPosition.clientY - viewportPosition.top) / scaleY,
      ];
    }

    /**
     * Get the target in which this map is rendered.
     * Note that this returns what is entered as an option or in setTarget:
     * if that was an element, it returns an element; if a string, it returns that.
     * @return {HTMLElement|string|undefined} The Element or id of the Element that the
     *     map is rendered in.
     * @observable
     * @api
     */
    getTarget() {
      return /** @type {HTMLElement|string|undefined} */ (
        this.get(MapProperty.TARGET)
      );
    }

    /**
     * Get the DOM element into which this map is rendered. In contrast to
     * `getTarget` this method always return an `Element`, or `null` if the
     * map has no target.
     * @return {HTMLElement} The element that the map is rendered in.
     * @api
     */
    getTargetElement() {
      return this.targetElement_;
    }

    /**
     * Get the coordinate for a given pixel.  This returns a coordinate in the
     * user projection.
     * @param {import("./pixel.js").Pixel} pixel Pixel position in the map viewport.
     * @return {import("./coordinate.js").Coordinate} The coordinate for the pixel position.
     * @api
     */
    getCoordinateFromPixel(pixel) {
      return toUserCoordinate(
        this.getCoordinateFromPixelInternal(pixel),
        this.getView().getProjection(),
      );
    }

    /**
     * Get the coordinate for a given pixel.  This returns a coordinate in the
     * map view projection.
     * @param {import("./pixel.js").Pixel} pixel Pixel position in the map viewport.
     * @return {import("./coordinate.js").Coordinate} The coordinate for the pixel position.
     */
    getCoordinateFromPixelInternal(pixel) {
      const frameState = this.frameState_;
      if (!frameState) {
        return null;
      }
      return apply(frameState.pixelToCoordinateTransform, pixel.slice());
    }

    /**
     * Get the map controls. Modifying this collection changes the controls
     * associated with the map.
     * @return {Collection<import("./control/Control.js").default>} Controls.
     * @api
     */
    getControls() {
      return this.controls;
    }

    /**
     * Get the map overlays. Modifying this collection changes the overlays
     * associated with the map.
     * @return {Collection<import("./Overlay.js").default>} Overlays.
     * @api
     */
    getOverlays() {
      return this.overlays_;
    }

    /**
     * Get an overlay by its identifier (the value returned by overlay.getId()).
     * Note that the index treats string and numeric identifiers as the same. So
     * `map.getOverlayById(2)` will return an overlay with id `'2'` or `2`.
     * @param {string|number} id Overlay identifier.
     * @return {import("./Overlay.js").default|null} Overlay.
     * @api
     */
    getOverlayById(id) {
      const overlay = this.overlayIdIndex_[id.toString()];
      return overlay !== undefined ? overlay : null;
    }

    /**
     * Get the map interactions. Modifying this collection changes the interactions
     * associated with the map.
     *
     * Interactions are used for e.g. pan, zoom and rotate.
     * @return {Collection<import("./interaction/Interaction.js").default>} Interactions.
     * @api
     */
    getInteractions() {
      return this.interactions;
    }

    /**
     * Get the layergroup associated with this map.
     * @return {LayerGroup} A layer group containing the layers in this map.
     * @observable
     * @api
     */
    getLayerGroup() {
      return /** @type {LayerGroup} */ (this.get(MapProperty.LAYERGROUP));
    }

    /**
     * Clear any existing layers and add layers to the map.
     * @param {Array<import("./layer/Base.js").default>|Collection<import("./layer/Base.js").default>} layers The layers to be added to the map.
     * @api
     */
    setLayers(layers) {
      const group = this.getLayerGroup();
      if (layers instanceof Collection) {
        group.setLayers(layers);
        return;
      }

      const collection = group.getLayers();
      collection.clear();
      collection.extend(layers);
    }

    /**
     * Get the collection of layers associated with this map.
     * @return {!Collection<import("./layer/Base.js").default>} Layers.
     * @api
     */
    getLayers() {
      const layers = this.getLayerGroup().getLayers();
      return layers;
    }

    /**
     * @return {boolean} Layers have sources that are still loading.
     */
    getLoadingOrNotReady() {
      const layerStatesArray = this.getLayerGroup().getLayerStatesArray();
      for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) {
        const state = layerStatesArray[i];
        if (!state.visible) {
          continue;
        }
        const renderer = state.layer.getRenderer();
        if (renderer && !renderer.ready) {
          return true;
        }
        const source = state.layer.getSource();
        if (source && source.loading) {
          return true;
        }
      }
      return false;
    }

    /**
     * Get the pixel for a coordinate.  This takes a coordinate in the user
     * projection and returns the corresponding pixel.
     * @param {import("./coordinate.js").Coordinate} coordinate A map coordinate.
     * @return {import("./pixel.js").Pixel} A pixel position in the map viewport.
     * @api
     */
    getPixelFromCoordinate(coordinate) {
      const viewCoordinate = fromUserCoordinate(
        coordinate,
        this.getView().getProjection(),
      );
      return this.getPixelFromCoordinateInternal(viewCoordinate);
    }

    /**
     * Get the pixel for a coordinate.  This takes a coordinate in the map view
     * projection and returns the corresponding pixel.
     * @param {import("./coordinate.js").Coordinate} coordinate A map coordinate.
     * @return {import("./pixel.js").Pixel} A pixel position in the map viewport.
     */
    getPixelFromCoordinateInternal(coordinate) {
      const frameState = this.frameState_;
      if (!frameState) {
        return null;
      }
      return apply(
        frameState.coordinateToPixelTransform,
        coordinate.slice(0, 2),
      );
    }

    /**
     * Get the pixel ratio of the rendered map.
     * @return {number} Pixel ratio.
     * @api
     */
    getPixelRatio() {
      return this.pixelRatio_;
    }

    /**
     * Set the pixel ratio of the rendered map.
     * @param {number} pixelRatio Pixel ratio.
     * @api
     */
    setPixelRatio(pixelRatio) {
      if (this.pixelRatio_ === pixelRatio) {
        return;
      }
      this.pixelRatio_ = pixelRatio;
      this.render();
    }

    /**
     * Get the map renderer.
     * @return {import("./renderer/Map.js").default|null} Renderer
     */
    getRenderer() {
      return this.renderer_;
    }

    /**
     * Get the size of this map.
     * @return {import("./size.js").Size|undefined} The size in pixels of the map in the DOM.
     * @observable
     * @api
     */
    getSize() {
      return /** @type {import("./size.js").Size|undefined} */ (
        this.get(MapProperty.SIZE)
      );
    }

    /**
     * Get the view associated with this map. A view manages properties such as
     * center and resolution.
     * @return {View} The view that controls this map.
     * @observable
     * @api
     */
    getView() {
      return /** @type {View} */ (this.get(MapProperty.VIEW));
    }

    /**
     * Get the element that serves as the map viewport.
     * @return {HTMLElement} Viewport.
     * @api
     */
    getViewport() {
      return this.viewport_;
    }

    /**
     * Get the element that serves as the container for overlays.  Elements added to
     * this container will let mousedown and touchstart events through to the map,
     * so clicks and gestures on an overlay will trigger {@link module:ol/MapBrowserEvent~MapBrowserEvent}
     * events.
     * @return {!HTMLElement} The map's overlay container.
     */
    getOverlayContainer() {
      return this.overlayContainer_;
    }

    /**
     * Get the element that serves as a container for overlays that don't allow
     * event propagation. Elements added to this container won't let mousedown and
     * touchstart events through to the map, so clicks and gestures on an overlay
     * don't trigger any {@link module:ol/MapBrowserEvent~MapBrowserEvent}.
     * @return {!HTMLElement} The map's overlay container that stops events.
     */
    getOverlayContainerStopEvent() {
      return this.overlayContainerStopEvent_;
    }

    /**
     * @return {!Document} The document where the map is displayed.
     */
    getOwnerDocument() {
      const targetElement = this.getTargetElement();
      return targetElement ? targetElement.ownerDocument : document;
    }

    /**
     * @param {import("./Tile.js").default} tile Tile.
     * @param {string} tileSourceKey Tile source key.
     * @param {import("./coordinate.js").Coordinate} tileCenter Tile center.
     * @param {number} tileResolution Tile resolution.
     * @return {number} Tile priority.
     */
    getTilePriority(tile, tileSourceKey, tileCenter, tileResolution) {
      return getTilePriority(
        this.frameState_,
        tile,
        tileSourceKey,
        tileCenter,
        tileResolution,
      );
    }

    /**
     * @param {PointerEvent|KeyboardEvent|WheelEvent} browserEvent Browser event.
     * @param {string} [type] Type.
     */
    handleBrowserEvent(browserEvent, type) {
      type = type || browserEvent.type;
      const mapBrowserEvent = new MapBrowserEvent(type, this, browserEvent);
      this.handleMapBrowserEvent(mapBrowserEvent);
    }

    /**
     * @param {MapBrowserEvent} mapBrowserEvent The event to handle.
     */
    handleMapBrowserEvent(mapBrowserEvent) {
      if (!this.frameState_) {
        // With no view defined, we cannot translate pixels into geographical
        // coordinates so interactions cannot be used.
        return;
      }
      const originalEvent = mapBrowserEvent.originalEvent;
      const eventType = originalEvent.type;
      if (
        eventType === PointerEventType.POINTERDOWN ||
        eventType === EventType.WHEEL ||
        eventType === EventType.KEYDOWN
      ) {
        const doc = this.getOwnerDocument();
        const rootNode = this.viewport_.getRootNode
          ? this.viewport_.getRootNode()
          : doc;
        const target = /** @type {Node} */ (originalEvent.target);

        const currentDoc =
          rootNode instanceof ShadowRoot
            ? rootNode.host === target
              ? rootNode.host.ownerDocument
              : rootNode
            : rootNode === doc
              ? doc.documentElement
              : rootNode;
        if (
          // Abort if the target is a child of the container for elements whose events are not meant
          // to be handled by map interactions.
          this.overlayContainerStopEvent_.contains(target) ||
          // Abort if the event target is a child of the container that is no longer in the page.
          // It's possible for the target to no longer be in the page if it has been removed in an
          // event listener, this might happen in a Control that recreates it's content based on
          // user interaction either manually or via a render in something like https://reactjs.org/
          !currentDoc.contains(target)
        ) {
          return;
        }
      }
      mapBrowserEvent.frameState = this.frameState_;
      if (this.dispatchEvent(mapBrowserEvent) !== false) {
        const interactionsArray = this.getInteractions().getArray().slice();
        for (let i = interactionsArray.length - 1; i >= 0; i--) {
          const interaction = interactionsArray[i];
          if (
            interaction.getMap() !== this ||
            !interaction.getActive() ||
            !this.getTargetElement()
          ) {
            continue;
          }
          const cont = interaction.handleEvent(mapBrowserEvent);
          if (!cont || mapBrowserEvent.propagationStopped) {
            break;
          }
        }
      }
    }

    /**
     * @protected
     */
    handlePostRender() {
      const frameState = this.frameState_;

      // Manage the tile queue
      // Image loads are expensive and a limited resource, so try to use them
      // efficiently:
      // * When the view is static we allow a large number of parallel tile loads
      //   to complete the frame as quickly as possible.
      // * When animating or interacting, image loads can cause janks, so we reduce
      //   the maximum number of loads per frame and limit the number of parallel
      //   tile loads to remain reactive to view changes and to reduce the chance of
      //   loading tiles that will quickly disappear from view.
      const tileQueue = this.tileQueue_;
      if (!tileQueue.isEmpty()) {
        let maxTotalLoading = this.maxTilesLoading_;
        let maxNewLoads = maxTotalLoading;
        if (frameState) {
          const hints = frameState.viewHints;
          if (hints[ViewHint.ANIMATING] || hints[ViewHint.INTERACTING]) {
            const lowOnFrameBudget = Date.now() - frameState.time > 8;
            maxTotalLoading = lowOnFrameBudget ? 0 : 8;
            maxNewLoads = lowOnFrameBudget ? 0 : 2;
          }
        }
        if (tileQueue.getTilesLoading() < maxTotalLoading) {
          tileQueue.reprioritize(); // FIXME only call if view has changed
          tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads);
        }
      }

      if (frameState && this.renderer_ && !frameState.animate) {
        if (this.renderComplete_) {
          if (this.hasListener(RenderEventType.RENDERCOMPLETE)) {
            this.renderer_.dispatchRenderEvent(
              RenderEventType.RENDERCOMPLETE,
              frameState,
            );
          }
          if (this.loaded_ === false) {
            this.loaded_ = true;
            this.dispatchEvent(
              new MapEvent(MapEventType.LOADEND, this, frameState),
            );
          }
        } else if (this.loaded_ === true) {
          this.loaded_ = false;
          this.dispatchEvent(
            new MapEvent(MapEventType.LOADSTART, this, frameState),
          );
        }
      }

      const postRenderFunctions = this.postRenderFunctions_;
      if (frameState) {
        for (let i = 0, ii = postRenderFunctions.length; i < ii; ++i) {
          postRenderFunctions[i](this, frameState);
        }
      }
      postRenderFunctions.length = 0;
    }

    /**
     * @private
     */
    handleSizeChanged_() {
      if (this.getView() && !this.getView().getAnimating()) {
        this.getView().resolveConstraints(0);
      }

      this.render();
    }

    /**
     * @private
     */
    handleTargetChanged_() {
      if (this.mapBrowserEventHandler_) {
        for (let i = 0, ii = this.targetChangeHandlerKeys_.length; i < ii; ++i) {
          unlistenByKey(this.targetChangeHandlerKeys_[i]);
        }
        this.targetChangeHandlerKeys_ = null;
        this.viewport_.removeEventListener(
          EventType.CONTEXTMENU,
          this.boundHandleBrowserEvent_,
        );
        this.viewport_.removeEventListener(
          EventType.WHEEL,
          this.boundHandleBrowserEvent_,
        );
        this.mapBrowserEventHandler_.dispose();
        this.mapBrowserEventHandler_ = null;
        this.viewport_.remove();
      }

      if (this.targetElement_ && !isCanvas(this.targetElement_)) {
        this.resizeObserver_?.unobserve(this.targetElement_);
        const rootNode = this.targetElement_.getRootNode();
        if (rootNode instanceof ShadowRoot) {
          this.resizeObserver_.unobserve(rootNode.host);
        }
        this.setSize(undefined);
      }

      // target may be undefined, null, a string or an Element.
      // If it's a string we convert it to an Element before proceeding.
      // If it's not now an Element we remove the viewport from the DOM.
      // If it's an Element we append the viewport element to it.

      const target = this.getTarget();
      const targetElement =
        typeof target === 'string' ? document.getElementById(target) : target;
      this.targetElement_ = targetElement;
      if (!targetElement) {
        if (this.renderer_) {
          clearTimeout(this.postRenderTimeoutHandle_);
          this.postRenderTimeoutHandle_ = undefined;
          this.postRenderFunctions_.length = 0;
          this.renderer_.dispose();
          this.renderer_ = null;
        }
        if (this.animationDelayKey_) {
          cancelAnimationFrame(this.animationDelayKey_);
          this.animationDelayKey_ = undefined;
        }
      } else {
        if (!isCanvas(targetElement)) {
          targetElement.appendChild(this.viewport_);
        }
        if (!this.renderer_) {
          this.renderer_ = new CompositeMapRenderer(this);
        }

        if (!isCanvas(targetElement)) {
          this.mapBrowserEventHandler_ = new MapBrowserEventHandler(
            this,
            this.moveTolerance_,
          );
          for (const key in MapBrowserEventType) {
            this.mapBrowserEventHandler_.addEventListener(
              MapBrowserEventType[key],
              this.handleMapBrowserEvent.bind(this),
            );
          }
          this.viewport_.addEventListener(
            EventType.CONTEXTMENU,
            this.boundHandleBrowserEvent_,
            false,
          );
          this.viewport_.addEventListener(
            EventType.WHEEL,
            this.boundHandleBrowserEvent_,
            PASSIVE_EVENT_LISTENERS ? {passive: false} : false,
          );

          let keyboardEventTarget;
          if (!this.keyboardEventTarget_) {
            // check if map target is in shadowDOM, if yes use host element as target
            const targetRoot = targetElement.getRootNode();
            const targetCandidate =
              targetRoot instanceof ShadowRoot ? targetRoot.host : targetElement;
            keyboardEventTarget = targetCandidate;
          } else {
            keyboardEventTarget = this.keyboardEventTarget_;
          }

          this.targetChangeHandlerKeys_ = [
            listen(
              keyboardEventTarget,
              EventType.KEYDOWN,
              this.handleBrowserEvent,
              this,
            ),
            listen(
              keyboardEventTarget,
              EventType.KEYPRESS,
              this.handleBrowserEvent,
              this,
            ),
          ];
          if (targetElement instanceof HTMLElement) {
            const rootNode = targetElement.getRootNode();
            if (rootNode instanceof ShadowRoot) {
              this.resizeObserver_.observe(rootNode.host);
            }
            this.resizeObserver_?.observe(targetElement);
          }
        }

        this.updateSize();
      }
      // updateSize calls setSize, so no need to call this.render
      // ourselves here.
    }

    /**
     * @private
     */
    handleTileChange_() {
      this.render();
    }

    /**
     * @private
     */
    handleViewPropertyChanged_() {
      this.render();
    }

    /**
     * @private
     */
    handleViewChanged_() {
      if (this.viewPropertyListenerKey_) {
        unlistenByKey(this.viewPropertyListenerKey_);
        this.viewPropertyListenerKey_ = null;
      }
      if (this.viewChangeListenerKey_) {
        unlistenByKey(this.viewChangeListenerKey_);
        this.viewChangeListenerKey_ = null;
      }
      const view = this.getView();
      if (view) {
        this.updateViewportSize_(this.getSize());

        this.viewPropertyListenerKey_ = listen(
          view,
          ObjectEventType.PROPERTYCHANGE,
          this.handleViewPropertyChanged_,
          this,
        );
        this.viewChangeListenerKey_ = listen(
          view,
          EventType.CHANGE,
          this.handleViewPropertyChanged_,
          this,
        );

        view.resolveConstraints(0);
      }
      this.render();
    }

    /**
     * @private
     */
    handleLayerGroupChanged_() {
      if (this.layerGroupPropertyListenerKeys_) {
        this.layerGroupPropertyListenerKeys_.forEach(unlistenByKey);
        this.layerGroupPropertyListenerKeys_ = null;
      }
      const layerGroup = this.getLayerGroup();
      if (layerGroup) {
        this.handleLayerAdd_(new GroupEvent('addlayer', layerGroup));
        this.layerGroupPropertyListenerKeys_ = [
          listen(layerGroup, ObjectEventType.PROPERTYCHANGE, this.render, this),
          listen(layerGroup, EventType.CHANGE, this.render, this),
          listen(layerGroup, 'addlayer', this.handleLayerAdd_, this),
          listen(layerGroup, 'removelayer', this.handleLayerRemove_, this),
        ];
      }
      this.render();
    }

    /**
     * @return {boolean} Is rendered.
     */
    isRendered() {
      return !!this.frameState_;
    }

    /**
     * @private
     */
    animationDelay_() {
      this.animationDelayKey_ = undefined;
      this.renderFrame_(Date.now());
    }

    /**
     * Requests an immediate render in a synchronous manner.
     * @api
     */
    renderSync() {
      if (this.animationDelayKey_) {
        cancelAnimationFrame(this.animationDelayKey_);
      }
      this.animationDelay_();
    }

    /**
     * Redraws all text after new fonts have loaded
     */
    redrawText() {
      if (!this.frameState_) {
        return;
      }
      const layerStates = this.frameState_.layerStatesArray;
      for (let i = 0, ii = layerStates.length; i < ii; ++i) {
        const layer = layerStates[i].layer;
        if (layer.hasRenderer()) {
          layer.getRenderer().handleFontsChanged();
        }
      }
    }

    /**
     * Request a map rendering (at the next animation frame).
     * @api
     */
    render() {
      if (this.renderer_ && this.animationDelayKey_ === undefined) {
        this.animationDelayKey_ = requestAnimationFrame(this.animationDelay_);
      }
    }

    /**
     * Remove the given control from the map.
     * @param {import("./control/Control.js").default} control Control.
     * @return {import("./control/Control.js").default|undefined} The removed control (or undefined
     *     if the control was not found).
     * @api
     */
    removeControl(control) {
      return this.getControls().remove(control);
    }

    /**
     * Remove the given interaction from the map.
     * @param {import("./interaction/Interaction.js").default} interaction Interaction to remove.
     * @return {import("./interaction/Interaction.js").default|undefined} The removed interaction (or
     *     undefined if the interaction was not found).
     * @api
     */
    removeInteraction(interaction) {
      return this.getInteractions().remove(interaction);
    }

    /**
     * Removes the given layer from the map.
     * @param {import("./layer/Base.js").default} layer Layer.
     * @return {import("./layer/Base.js").default|undefined} The removed layer (or undefined if the
     *     layer was not found).
     * @api
     */
    removeLayer(layer) {
      const layers = this.getLayerGroup().getLayers();
      return layers.remove(layer);
    }

    /**
     * @param {import("./layer/Group.js").GroupEvent} event The layer remove event.
     * @private
     */
    handleLayerRemove_(event) {
      removeLayerMapProperty(event.layer);
    }

    /**
     * Remove the given overlay from the map.
     * @param {import("./Overlay.js").default} overlay Overlay.
     * @return {import("./Overlay.js").default|undefined} The removed overlay (or undefined
     *     if the overlay was not found).
     * @api
     */
    removeOverlay(overlay) {
      return this.getOverlays().remove(overlay);
    }

    /**
     * @param {number} time Time.
     * @private
     */
    renderFrame_(time) {
      const size = this.getSize();
      const view = this.getView();
      const previousFrameState = this.frameState_;
      /** @type {?FrameState} */
      let frameState = null;
      if (size !== undefined && hasArea(size) && view && view.isDef()) {
        const viewHints = view.getHints(
          this.frameState_ ? this.frameState_.viewHints : undefined,
        );
        const viewState = view.getState();
        frameState = {
          animate: false,
          coordinateToPixelTransform: this.coordinateToPixelTransform_,
          declutter: null,
          extent: getForViewAndSize(
            viewState.center,
            viewState.resolution,
            viewState.rotation,
            size,
          ),
          index: this.frameIndex_++,
          layerIndex: 0,
          layerStatesArray: this.getLayerGroup().getLayerStatesArray(),
          pixelRatio: this.pixelRatio_,
          pixelToCoordinateTransform: this.pixelToCoordinateTransform_,
          postRenderFunctions: [],
          size: size,
          tileQueue: this.tileQueue_,
          time: time,
          usedTiles: {},
          viewState: viewState,
          viewHints: viewHints,
          wantedTiles: {},
          mapId: getUid(this),
          renderTargets: {},
        };
        if (viewState.nextCenter && viewState.nextResolution) {
          const rotation = isNaN(viewState.nextRotation)
            ? viewState.rotation
            : viewState.nextRotation;

          frameState.nextExtent = getForViewAndSize(
            viewState.nextCenter,
            viewState.nextResolution,
            rotation,
            size,
          );
        }
      }

      this.frameState_ = frameState;
      this.renderer_.renderFrame(frameState);

      if (frameState) {
        if (frameState.animate) {
          this.render();
        }
        Array.prototype.push.apply(
          this.postRenderFunctions_,
          frameState.postRenderFunctions,
        );

        if (previousFrameState) {
          const moveStart =
            !this.previousExtent_ ||
            (!isEmpty(this.previousExtent_) &&
              !equals$1(frameState.extent, this.previousExtent_));
          if (moveStart) {
            this.dispatchEvent(
              new MapEvent(MapEventType.MOVESTART, this, previousFrameState),
            );
            this.previousExtent_ = createOrUpdateEmpty(this.previousExtent_);
          }
        }

        const idle =
          this.previousExtent_ &&
          !frameState.viewHints[ViewHint.ANIMATING] &&
          !frameState.viewHints[ViewHint.INTERACTING] &&
          !equals$1(frameState.extent, this.previousExtent_);

        if (idle) {
          this.dispatchEvent(
            new MapEvent(MapEventType.MOVEEND, this, frameState),
          );
          clone(frameState.extent, this.previousExtent_);
        }
      }

      this.dispatchEvent(new MapEvent(MapEventType.POSTRENDER, this, frameState));

      this.renderComplete_ =
        (this.hasListener(MapEventType.LOADSTART) ||
          this.hasListener(MapEventType.LOADEND) ||
          this.hasListener(RenderEventType.RENDERCOMPLETE)) &&
        !this.tileQueue_.getTilesLoading() &&
        !this.tileQueue_.getCount() &&
        !this.getLoadingOrNotReady();

      if (!this.postRenderTimeoutHandle_) {
        this.postRenderTimeoutHandle_ = setTimeout(() => {
          this.postRenderTimeoutHandle_ = undefined;
          this.handlePostRender();
        }, 0);
      }
    }

    /**
     * Sets the layergroup of this map.
     * @param {LayerGroup} layerGroup A layer group containing the layers in this map.
     * @observable
     * @api
     */
    setLayerGroup(layerGroup) {
      const oldLayerGroup = this.getLayerGroup();
      if (oldLayerGroup) {
        this.handleLayerRemove_(new GroupEvent('removelayer', oldLayerGroup));
      }
      this.set(MapProperty.LAYERGROUP, layerGroup);
    }

    /**
     * Set the size of this map.
     * @param {import("./size.js").Size|undefined} size The size in pixels of the map in the DOM.
     * @observable
     * @api
     */
    setSize(size) {
      this.set(MapProperty.SIZE, size);
    }

    /**
     * Set the target element to render this map into.
     * For accessibility (focus and keyboard events for map navigation), the `target` element must have a
     *  properly configured `tabindex` attribute. If the `target` element is inside a Shadow DOM, the
     *  `tabindex` atribute must be set on the custom element's host element.
     * @param {HTMLElement|string} [target] The Element or id of the Element
     *     that the map is rendered in.
     * @observable
     * @api
     */
    setTarget(target) {
      this.set(MapProperty.TARGET, target);
    }

    /**
     * Set the view for this map.
     * @param {View|Promise<import("./View.js").ViewOptions>|null} view The view that controls this map.
     * It is also possible to pass a promise that resolves to options for constructing a view.  This
     * alternative allows view properties to be resolved by sources or other components that load
     * view-related metadata.
     * @observable
     * @api
     */
    setView(view) {
      if (!view || view instanceof View) {
        this.set(MapProperty.VIEW, view);
        return;
      }
      this.set(MapProperty.VIEW, new View());

      const map = this;
      view.then(function (viewOptions) {
        map.setView(new View(viewOptions));
      });
    }

    /**
     * Force a recalculation of the map viewport size.  This should be called when
     * third-party code changes the size of the map viewport.
     * @api
     */
    updateSize() {
      const targetElement = this.getTargetElement();

      let size = undefined;
      if (targetElement) {
        let width, height;
        if (isCanvas(targetElement)) {
          const transform = targetElement.getContext('2d').getTransform();
          // Use scale components of the transform to calculate the size in CSS pixels
          width = targetElement.width / transform.a;
          height = targetElement.height / transform.d;
        } else {
          const computedStyle = getComputedStyle(targetElement);
          width =
            targetElement.offsetWidth -
            parseFloat(computedStyle['borderLeftWidth']) -
            parseFloat(computedStyle['paddingLeft']) -
            parseFloat(computedStyle['paddingRight']) -
            parseFloat(computedStyle['borderRightWidth']);
          height =
            targetElement.offsetHeight -
            parseFloat(computedStyle['borderTopWidth']) -
            parseFloat(computedStyle['paddingTop']) -
            parseFloat(computedStyle['paddingBottom']) -
            parseFloat(computedStyle['borderBottomWidth']);
        }
        if (!isNaN(width) && !isNaN(height)) {
          size = [Math.max(0, width), Math.max(0, height)];
          if (
            !hasArea(size) &&
            !!(
              targetElement.offsetWidth ||
              targetElement.offsetHeight ||
              targetElement.getClientRects().length
            )
          ) {
            warn(
              "No map visible because the map container's width or height are 0.",
            );
          }
        }
      }

      const oldSize = this.getSize();
      if (size && (!oldSize || !equals$2(size, oldSize))) {
        this.setSize(size);
        this.updateViewportSize_(size);
      }
    }

    /**
     * Recomputes the viewport size and save it on the view object (if any)
     * @param {import("./size.js").Size|undefined} size The size.
     * @private
     */
    updateViewportSize_(size) {
      const view = this.getView();
      if (view) {
        view.setViewportSize(size);
      }
    }
  }

  /**
   * @param {MapOptions} options Map options.
   * @return {MapOptionsInternal} Internal map options.
   */
  function createOptionsInternal(options) {
    /**
     * @type {HTMLElement|Document}
     */
    let keyboardEventTarget = null;
    if (options.keyboardEventTarget !== undefined) {
      keyboardEventTarget =
        typeof options.keyboardEventTarget === 'string'
          ? document.getElementById(options.keyboardEventTarget)
          : options.keyboardEventTarget;
    }

    /**
     * @type {Object<string, *>}
     */
    const values = {};

    const layerGroup =
      options.layers &&
      typeof (/** @type {?} */ (options.layers).getLayers) === 'function'
        ? /** @type {LayerGroup} */ (options.layers)
        : new LayerGroup({
            layers:
              /** @type {Collection<import("./layer/Base.js").default>|Array<import("./layer/Base.js").default>} */ (
                options.layers
              ),
          });
    values[MapProperty.LAYERGROUP] = layerGroup;

    values[MapProperty.TARGET] = options.target;

    values[MapProperty.VIEW] =
      options.view instanceof View ? options.view : new View();

    /** @type {Collection<import("./control/Control.js").default>} */
    let controls;
    if (options.controls !== undefined) {
      if (Array.isArray(options.controls)) {
        controls = new Collection(options.controls.slice());
      } else {
        assert(
          typeof (/** @type {?} */ (options.controls).getArray) === 'function',
          'Expected `controls` to be an array or an `ol/Collection.js`',
        );
        controls = options.controls;
      }
    }

    /** @type {Collection<import("./interaction/Interaction").default>} */
    let interactions;
    if (options.interactions !== undefined) {
      if (Array.isArray(options.interactions)) {
        interactions = new Collection(options.interactions.slice());
      } else {
        assert(
          typeof (/** @type {?} */ (options.interactions).getArray) ===
            'function',
          'Expected `interactions` to be an array or an `ol/Collection.js`',
        );
        interactions = options.interactions;
      }
    }

    /** @type {Collection<import("./Overlay.js").default>} */
    let overlays;
    if (options.overlays !== undefined) {
      if (Array.isArray(options.overlays)) {
        overlays = new Collection(options.overlays.slice());
      } else {
        assert(
          typeof (/** @type {?} */ (options.overlays).getArray) === 'function',
          'Expected `overlays` to be an array or an `ol/Collection.js`',
        );
        overlays = options.overlays;
      }
    } else {
      overlays = new Collection();
    }

    return {
      controls: controls,
      interactions: interactions,
      keyboardEventTarget: keyboardEventTarget,
      overlays: overlays,
      values: values,
    };
  }

  /**
   * @module ol/Overlay
   */

  /**
   * @typedef {'bottom-left' | 'bottom-center' | 'bottom-right' | 'center-left' | 'center-center' | 'center-right' | 'top-left' | 'top-center' | 'top-right'} Positioning
   * The overlay position: `'bottom-left'`, `'bottom-center'`,  `'bottom-right'`,
   * `'center-left'`, `'center-center'`, `'center-right'`, `'top-left'`,
   * `'top-center'`, or `'top-right'`.
   */

  /**
   * @typedef {Object} Options
   * @property {number|string} [id] Set the overlay id. The overlay id can be used
   * with the {@link module:ol/Map~Map#getOverlayById} method.
   * @property {HTMLElement} [element] The overlay element.
   * @property {Array<number>} [offset=[0, 0]] Offsets in pixels used when positioning
   * the overlay. The first element in the
   * array is the horizontal offset. A positive value shifts the overlay right.
   * The second element in the array is the vertical offset. A positive value
   * shifts the overlay down.
   * @property {import("./coordinate.js").Coordinate} [position] The overlay position
   * in map projection.
   * @property {Positioning} [positioning='top-left'] Defines how
   * the overlay is actually positioned with respect to its `position` property.
   * Possible values are `'bottom-left'`, `'bottom-center'`, `'bottom-right'`,
   * `'center-left'`, `'center-center'`, `'center-right'`, `'top-left'`,
   * `'top-center'`, and `'top-right'`.
   * @property {boolean} [stopEvent=true] Whether event propagation to the map
   * viewport should be stopped. If `true` the overlay is placed in the same
   * container as that of the controls (CSS class name
   * `ol-overlaycontainer-stopevent`); if `false` it is placed in the container
   * with CSS class name specified by the `className` property.
   * @property {boolean} [insertFirst=true] Whether the overlay is inserted first
   * in the overlay container, or appended. If the overlay is placed in the same
   * container as that of the controls (see the `stopEvent` option) you will
   * probably set `insertFirst` to `true` so the overlay is displayed below the
   * controls.
   * @property {PanIntoViewOptions|boolean} [autoPan=false] Pan the map when calling
   * `setPosition`, so that the overlay is entirely visible in the current viewport.
   * @property {string} [className='ol-overlay-container ol-selectable'] CSS class
   * name.
   */

  /**
   * @typedef {Object} PanOptions
   * @property {number} [duration=1000] The duration of the animation in
   * milliseconds.
   * @property {function(number):number} [easing] The easing function to use. Can
   * be one from {@link module:ol/easing} or a custom function.
   * Default is {@link module:ol/easing.inAndOut}.
   */

  /**
   * @typedef {Object} PanIntoViewOptions
   * @property {PanOptions} [animation={}] The animation parameters for the pan
   * @property {number} [margin=20] The margin (in pixels) between the
   * overlay and the borders of the map when panning into view.
   */

  /**
   * @enum {string}
   * @protected
   */
  const Property = {
    ELEMENT: 'element',
    MAP: 'map',
    OFFSET: 'offset',
    POSITION: 'position',
    POSITIONING: 'positioning',
  };

  /**
   * @typedef {import("./ObjectEventType").Types|'change:element'|'change:map'|'change:offset'|'change:position'|
   *   'change:positioning'} OverlayObjectEventTypes
   */

  /***
   * @template Return
   * @typedef {import("./Observable").OnSignature<import("./Observable").EventTypes, import("./events/Event.js").default, Return> &
   *   import("./Observable").OnSignature<OverlayObjectEventTypes, import("./Object").ObjectEvent, Return> &
   *   import("./Observable").CombinedOnSignature<import("./Observable").EventTypes|OverlayObjectEventTypes, Return>} OverlayOnSignature
   */

  /**
   * @classdesc
   * An element to be displayed over the map and attached to a single map
   * location.  Like {@link module:ol/control/Control~Control}, Overlays are
   * visible widgets. Unlike Controls, they are not in a fixed position on the
   * screen, but are tied to a geographical coordinate, so panning the map will
   * move an Overlay but not a Control.
   *
   * Example:
   *
   *     import Overlay from 'ol/Overlay.js';
   *
   *     // ...
   *     const popup = new Overlay({
   *       element: document.getElementById('popup'),
   *     });
   *     popup.setPosition(coordinate);
   *     map.addOverlay(popup);
   *
   * @api
   */
  class Overlay extends BaseObject {
    /**
     * @param {Options} options Overlay options.
     */
    constructor(options) {
      super();

      /***
       * @type {OverlayOnSignature<import("./events").EventsKey>}
       */
      this.on;

      /***
       * @type {OverlayOnSignature<import("./events").EventsKey>}
       */
      this.once;

      /***
       * @type {OverlayOnSignature<void>}
       */
      this.un;

      /**
       * @protected
       * @type {Options}
       */
      this.options = options;

      /**
       * @protected
       * @type {number|string|undefined}
       */
      this.id = options.id;

      /**
       * @protected
       * @type {boolean}
       */
      this.insertFirst =
        options.insertFirst !== undefined ? options.insertFirst : true;

      /**
       * @protected
       * @type {boolean}
       */
      this.stopEvent = options.stopEvent !== undefined ? options.stopEvent : true;

      /**
       * @protected
       * @type {HTMLElement}
       */
      this.element = document.createElement('div');
      this.element.className =
        options.className !== undefined
          ? options.className
          : 'ol-overlay-container ' + CLASS_SELECTABLE;
      this.element.style.position = 'absolute';
      this.element.style.pointerEvents = 'auto';

      /**
       * @protected
       * @type {PanIntoViewOptions|undefined}
       */
      this.autoPan = options.autoPan === true ? {} : options.autoPan || undefined;

      /**
       * @protected
       * @type {{transform_: string,
       *         visible: boolean}}
       */
      this.rendered = {
        transform_: '',
        visible: true,
      };

      /**
       * @protected
       * @type {?import("./events.js").EventsKey}
       */
      this.mapPostrenderListenerKey = null;

      this.addChangeListener(Property.ELEMENT, this.handleElementChanged);
      this.addChangeListener(Property.MAP, this.handleMapChanged);
      this.addChangeListener(Property.OFFSET, this.handleOffsetChanged);
      this.addChangeListener(Property.POSITION, this.handlePositionChanged);
      this.addChangeListener(Property.POSITIONING, this.handlePositioningChanged);

      if (options.element !== undefined) {
        this.setElement(options.element);
      }

      this.setOffset(options.offset !== undefined ? options.offset : [0, 0]);

      this.setPositioning(options.positioning || 'top-left');

      if (options.position !== undefined) {
        this.setPosition(options.position);
      }
    }

    /**
     * Get the DOM element of this overlay.
     * @return {HTMLElement|undefined} The Element containing the overlay.
     * @observable
     * @api
     */
    getElement() {
      return /** @type {HTMLElement|undefined} */ (this.get(Property.ELEMENT));
    }

    /**
     * Get the overlay identifier which is set on constructor.
     * @return {number|string|undefined} Id.
     * @api
     */
    getId() {
      return this.id;
    }

    /**
     * Get the map associated with this overlay.
     * @return {import("./Map.js").default|null} The map that the
     * overlay is part of.
     * @observable
     * @api
     */
    getMap() {
      return /** @type {import("./Map.js").default|null} */ (
        this.get(Property.MAP) || null
      );
    }

    /**
     * Get the offset of this overlay.
     * @return {Array<number>} The offset.
     * @observable
     * @api
     */
    getOffset() {
      return /** @type {Array<number>} */ (this.get(Property.OFFSET));
    }

    /**
     * Get the current position of this overlay.
     * @return {import("./coordinate.js").Coordinate|undefined} The spatial point that the overlay is
     *     anchored at.
     * @observable
     * @api
     */
    getPosition() {
      return /** @type {import("./coordinate.js").Coordinate|undefined} */ (
        this.get(Property.POSITION)
      );
    }

    /**
     * Get the current positioning of this overlay.
     * @return {Positioning} How the overlay is positioned
     *     relative to its point on the map.
     * @observable
     * @api
     */
    getPositioning() {
      return /** @type {Positioning} */ (this.get(Property.POSITIONING));
    }

    /**
     * @protected
     */
    handleElementChanged() {
      removeChildren(this.element);
      const element = this.getElement();
      if (element) {
        this.element.appendChild(element);
      }
    }

    /**
     * @protected
     */
    handleMapChanged() {
      if (this.mapPostrenderListenerKey) {
        this.element?.remove();
        unlistenByKey(this.mapPostrenderListenerKey);
        this.mapPostrenderListenerKey = null;
      }
      const map = this.getMap();
      if (map) {
        this.mapPostrenderListenerKey = listen(
          map,
          MapEventType.POSTRENDER,
          this.render,
          this,
        );
        this.updatePixelPosition();
        const container = this.stopEvent
          ? map.getOverlayContainerStopEvent()
          : map.getOverlayContainer();
        if (this.insertFirst) {
          container.insertBefore(this.element, container.childNodes[0] || null);
        } else {
          container.appendChild(this.element);
        }
        this.performAutoPan();
      }
    }

    /**
     * @protected
     */
    render() {
      this.updatePixelPosition();
    }

    /**
     * @protected
     */
    handleOffsetChanged() {
      this.updatePixelPosition();
    }

    /**
     * @protected
     */
    handlePositionChanged() {
      this.updatePixelPosition();
      this.performAutoPan();
    }

    /**
     * @protected
     */
    handlePositioningChanged() {
      this.updatePixelPosition();
    }

    /**
     * Set the DOM element to be associated with this overlay.
     * @param {HTMLElement|undefined} element The Element containing the overlay.
     * @observable
     * @api
     */
    setElement(element) {
      this.set(Property.ELEMENT, element);
    }

    /**
     * Set the map to be associated with this overlay.
     * @param {import("./Map.js").default|null} map The map that the
     * overlay is part of. Pass `null` to just remove the overlay from the current map.
     * @observable
     * @api
     */
    setMap(map) {
      this.set(Property.MAP, map);
    }

    /**
     * Set the offset for this overlay.
     * @param {Array<number>} offset Offset.
     * @observable
     * @api
     */
    setOffset(offset) {
      this.set(Property.OFFSET, offset);
    }

    /**
     * Set the position for this overlay. If the position is `undefined` the
     * overlay is hidden.
     * @param {import("./coordinate.js").Coordinate|undefined} position The spatial point that the overlay
     *     is anchored at.
     * @observable
     * @api
     */
    setPosition(position) {
      this.set(Property.POSITION, position);
    }

    /**
     * Pan the map so that the overlay is entirely visible in the current viewport
     * (if necessary) using the configured autoPan parameters
     * @protected
     */
    performAutoPan() {
      if (this.autoPan) {
        this.panIntoView(this.autoPan);
      }
    }

    /**
     * Pan the map so that the overlay is entirely visible in the current viewport
     * (if necessary).
     * @param {PanIntoViewOptions} [panIntoViewOptions] Options for the pan action
     * @api
     */
    panIntoView(panIntoViewOptions) {
      const map = this.getMap();

      if (!map || !map.getTargetElement() || !this.get(Property.POSITION)) {
        return;
      }

      const mapRect = this.getRect(map.getTargetElement(), map.getSize());
      const element = this.getElement();
      const overlayRect = this.getRect(element, [
        outerWidth(element),
        outerHeight(element),
      ]);

      panIntoViewOptions = panIntoViewOptions || {};

      const myMargin =
        panIntoViewOptions.margin === undefined ? 20 : panIntoViewOptions.margin;
      if (!containsExtent(mapRect, overlayRect)) {
        // the overlay is not completely inside the viewport, so pan the map
        const offsetLeft = overlayRect[0] - mapRect[0];
        const offsetRight = mapRect[2] - overlayRect[2];
        const offsetTop = overlayRect[1] - mapRect[1];
        const offsetBottom = mapRect[3] - overlayRect[3];

        const delta = [0, 0];
        if (offsetLeft < 0) {
          // move map to the left
          delta[0] = offsetLeft - myMargin;
        } else if (offsetRight < 0) {
          // move map to the right
          delta[0] = Math.abs(offsetRight) + myMargin;
        }
        if (offsetTop < 0) {
          // move map up
          delta[1] = offsetTop - myMargin;
        } else if (offsetBottom < 0) {
          // move map down
          delta[1] = Math.abs(offsetBottom) + myMargin;
        }

        if (delta[0] !== 0 || delta[1] !== 0) {
          const center = /** @type {import("./coordinate.js").Coordinate} */ (
            map.getView().getCenterInternal()
          );
          const centerPx = map.getPixelFromCoordinateInternal(center);
          if (!centerPx) {
            return;
          }
          const newCenterPx = [centerPx[0] + delta[0], centerPx[1] + delta[1]];

          const panOptions = panIntoViewOptions.animation || {};
          map.getView().animateInternal({
            center: map.getCoordinateFromPixelInternal(newCenterPx),
            duration: panOptions.duration,
            easing: panOptions.easing,
          });
        }
      }
    }

    /**
     * Get the extent of an element relative to the document
     * @param {HTMLElement} element The element.
     * @param {import("./size.js").Size} size The size of the element.
     * @return {import("./extent.js").Extent} The extent.
     * @protected
     */
    getRect(element, size) {
      const box = element.getBoundingClientRect();
      const offsetX = box.left + window.pageXOffset;
      const offsetY = box.top + window.pageYOffset;
      return [offsetX, offsetY, offsetX + size[0], offsetY + size[1]];
    }

    /**
     * Set the positioning for this overlay.
     * @param {Positioning} positioning how the overlay is
     *     positioned relative to its point on the map.
     * @observable
     * @api
     */
    setPositioning(positioning) {
      this.set(Property.POSITIONING, positioning);
    }

    /**
     * Modify the visibility of the element.
     * @param {boolean} visible Element visibility.
     * @protected
     */
    setVisible(visible) {
      if (this.rendered.visible !== visible) {
        this.element.style.display = visible ? '' : 'none';
        this.rendered.visible = visible;
      }
    }

    /**
     * Update pixel position.
     * @protected
     */
    updatePixelPosition() {
      const map = this.getMap();
      const position = this.getPosition();
      if (!map || !map.isRendered() || !position) {
        this.setVisible(false);
        return;
      }

      const pixel = map.getPixelFromCoordinate(position);
      const mapSize = map.getSize();
      this.updateRenderedPosition(pixel, mapSize);
    }

    /**
     * @param {import("./pixel.js").Pixel} pixel The pixel location.
     * @param {import("./size.js").Size|undefined} mapSize The map size.
     * @protected
     */
    updateRenderedPosition(pixel, mapSize) {
      const style = this.element.style;
      const offset = this.getOffset();

      const positioning = this.getPositioning();

      this.setVisible(true);

      const x = `${pixel[0] + offset[0]}px`;
      const y = `${pixel[1] + offset[1]}px`;
      let posX = '0%';
      let posY = '0%';
      if (
        positioning == 'bottom-right' ||
        positioning == 'center-right' ||
        positioning == 'top-right'
      ) {
        posX = '-100%';
      } else if (
        positioning == 'bottom-center' ||
        positioning == 'center-center' ||
        positioning == 'top-center'
      ) {
        posX = '-50%';
      }
      if (
        positioning == 'bottom-left' ||
        positioning == 'bottom-center' ||
        positioning == 'bottom-right'
      ) {
        posY = '-100%';
      } else if (
        positioning == 'center-left' ||
        positioning == 'center-center' ||
        positioning == 'center-right'
      ) {
        posY = '-50%';
      }
      const transform = `translate(${posX}, ${posY}) translate(${x}, ${y})`;
      if (this.rendered.transform_ != transform) {
        this.rendered.transform_ = transform;
        style.transform = transform;
      }
    }

    /**
     * returns the options this Overlay has been created with
     * @return {Options} overlay options
     */
    getOptions() {
      return this.options;
    }
  }

  /**
   * @module ol/TileRange
   */

  /**
   * A representation of a contiguous block of tiles.  A tile range is specified
   * by its min/max tile coordinates and is inclusive of coordinates.
   */
  class TileRange {
    /**
     * @param {number} minX Minimum X.
     * @param {number} maxX Maximum X.
     * @param {number} minY Minimum Y.
     * @param {number} maxY Maximum Y.
     */
    constructor(minX, maxX, minY, maxY) {
      /**
       * @type {number}
       */
      this.minX = minX;

      /**
       * @type {number}
       */
      this.maxX = maxX;

      /**
       * @type {number}
       */
      this.minY = minY;

      /**
       * @type {number}
       */
      this.maxY = maxY;
    }

    /**
     * @param {import("./tilecoord.js").TileCoord} tileCoord Tile coordinate.
     * @return {boolean} Contains tile coordinate.
     */
    contains(tileCoord) {
      return this.containsXY(tileCoord[1], tileCoord[2]);
    }

    /**
     * @param {TileRange} tileRange Tile range.
     * @return {boolean} Contains.
     */
    containsTileRange(tileRange) {
      return (
        this.minX <= tileRange.minX &&
        tileRange.maxX <= this.maxX &&
        this.minY <= tileRange.minY &&
        tileRange.maxY <= this.maxY
      );
    }

    /**
     * @param {number} x Tile coordinate x.
     * @param {number} y Tile coordinate y.
     * @return {boolean} Contains coordinate.
     */
    containsXY(x, y) {
      return this.minX <= x && x <= this.maxX && this.minY <= y && y <= this.maxY;
    }

    /**
     * @param {TileRange} tileRange Tile range.
     * @return {boolean} Equals.
     */
    equals(tileRange) {
      return (
        this.minX == tileRange.minX &&
        this.minY == tileRange.minY &&
        this.maxX == tileRange.maxX &&
        this.maxY == tileRange.maxY
      );
    }

    /**
     * @param {TileRange} tileRange Tile range.
     */
    extend(tileRange) {
      if (tileRange.minX < this.minX) {
        this.minX = tileRange.minX;
      }
      if (tileRange.maxX > this.maxX) {
        this.maxX = tileRange.maxX;
      }
      if (tileRange.minY < this.minY) {
        this.minY = tileRange.minY;
      }
      if (tileRange.maxY > this.maxY) {
        this.maxY = tileRange.maxY;
      }
    }

    /**
     * @return {number} Height.
     */
    getHeight() {
      return this.maxY - this.minY + 1;
    }

    /**
     * @return {import("./size.js").Size} Size.
     */
    getSize() {
      return [this.getWidth(), this.getHeight()];
    }

    /**
     * @return {number} Width.
     */
    getWidth() {
      return this.maxX - this.minX + 1;
    }

    /**
     * @param {TileRange} tileRange Tile range.
     * @return {boolean} Intersects.
     */
    intersects(tileRange) {
      return (
        this.minX <= tileRange.maxX &&
        this.maxX >= tileRange.minX &&
        this.minY <= tileRange.maxY &&
        this.maxY >= tileRange.minY
      );
    }
  }

  /**
   * @param {number} minX Minimum X.
   * @param {number} maxX Maximum X.
   * @param {number} minY Minimum Y.
   * @param {number} maxY Maximum Y.
   * @param {TileRange} [tileRange] TileRange.
   * @return {TileRange} Tile range.
   */
  function createOrUpdate$1(minX, maxX, minY, maxY, tileRange) {
    if (tileRange !== undefined) {
      tileRange.minX = minX;
      tileRange.maxX = maxX;
      tileRange.minY = minY;
      tileRange.maxY = maxY;
      return tileRange;
    }
    return new TileRange(minX, maxX, minY, maxY);
  }

  /**
   * @module ol/DataTile
   */

  /**
   * @typedef {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|HTMLVideoElement|ImageBitmap} ImageLike
   */

  /**
   * @typedef {Uint8Array|Uint8ClampedArray|Float32Array|DataView} ArrayLike
   */

  /**
   * Data that can be used with a DataTile.
   * @typedef {ArrayLike|ImageLike} Data
   */

  /**
   * @param {Data} data Tile data.
   * @return {ImageLike|null} The image-like data.
   */
  function asImageLike(data) {
    return data instanceof Image ||
      data instanceof HTMLCanvasElement ||
      data instanceof HTMLVideoElement ||
      data instanceof ImageBitmap
      ? data
      : null;
  }

  /**
   * This is set as the cancellation reason when a tile is disposed.
   */
  const disposedError = new Error('disposed');

  /**
   * @type {import('./size.js').Size}
   */
  const defaultSize = [256, 256];

  /**
   * @typedef {Object} Options
   * @property {import("./tilecoord.js").TileCoord} tileCoord Tile coordinate.
   * @property {function(): Promise<Data>} loader Data loader.  For loaders that generate images,
   * the promise should not resolve until the image is loaded.
   * @property {number} [transition=250] A duration for tile opacity
   * transitions in milliseconds. A duration of 0 disables the opacity transition.
   * @property {boolean} [interpolate=false] Use interpolated values when resampling.  By default,
   * the nearest neighbor is used when resampling.
   * @property {import('./size.js').Size} [size=[256, 256]] Tile size.
   * @property {AbortController} [controller] An abort controller.
   * @api
   */

  class DataTile extends Tile {
    /**
     * @param {Options} options Tile options.
     */
    constructor(options) {
      const state = TileState.IDLE;

      super(options.tileCoord, state, {
        transition: options.transition,
        interpolate: options.interpolate,
      });

      /**
       * @type {function(): Promise<Data>}
       * @private
       */
      this.loader_ = options.loader;

      /**
       * @type {Data}
       * @private
       */
      this.data_ = null;

      /**
       * @type {Error}
       * @private
       */
      this.error_ = null;

      /**
       * @type {import('./size.js').Size|null}
       * @private
       */
      this.size_ = options.size || null;

      /**
       * @type {AbortController|null}
       * @private
       */
      this.controller_ = options.controller || null;
    }

    /**
     * Get the tile size.
     * @return {import('./size.js').Size} Tile size.
     */
    getSize() {
      if (this.size_) {
        return this.size_;
      }
      const imageData = asImageLike(this.data_);
      if (imageData) {
        return [imageData.width, imageData.height];
      }
      return defaultSize;
    }

    /**
     * Get the data for the tile.
     * @return {Data} Tile data.
     * @api
     */
    getData() {
      return this.data_;
    }

    /**
     * Get any loading error.
     * @return {Error} Loading error.
     * @api
     */
    getError() {
      return this.error_;
    }

    /**
     * Load the tile data.
     * @api
     * @override
     */
    load() {
      if (this.state !== TileState.IDLE && this.state !== TileState.ERROR) {
        return;
      }
      this.state = TileState.LOADING;
      this.changed();

      const self = this;
      this.loader_()
        .then(function (data) {
          self.data_ = data;
          self.state = TileState.LOADED;
          self.changed();
        })
        .catch(function (error) {
          self.error_ = error;
          self.state = TileState.ERROR;
          self.changed();
        });
    }

    /**
     * Clean up.
     * @override
     */
    disposeInternal() {
      if (this.controller_) {
        this.controller_.abort(disposedError);
        this.controller_ = null;
      }
      super.disposeInternal();
    }
  }

  /**
   * @module ol/reproj
   */

  let brokenDiagonalRendering_;

  /**
   * @type {Array<HTMLCanvasElement|OffscreenCanvas>}
   */
  const canvasPool = [];

  /**
   * This draws a small triangle into a canvas by setting the triangle as the clip region
   * and then drawing a (too large) rectangle
   *
   * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} ctx The context in which to draw the triangle
   * @param {number} u1 The x-coordinate of the second point. The first point is 0,0.
   * @param {number} v1 The y-coordinate of the second point.
   * @param {number} u2 The x-coordinate of the third point.
   * @param {number} v2 The y-coordinate of the third point.
   */
  function drawTestTriangle(ctx, u1, v1, u2, v2) {
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(u1, v1);
    ctx.lineTo(u2, v2);
    ctx.closePath();
    ctx.save();
    ctx.clip();
    ctx.fillRect(0, 0, Math.max(u1, u2) + 1, Math.max(v1, v2));
    ctx.restore();
  }

  /**
   * Given the data from getImageData, see if the right values appear at the provided offset.
   * Returns true if either the color or transparency is off
   *
   * @param {Uint8ClampedArray} data The data returned from getImageData
   * @param {number} offset The pixel offset from the start of data.
   * @return {boolean} true if the diagonal rendering is broken
   */
  function verifyBrokenDiagonalRendering(data, offset) {
    // the values ought to be close to the rgba(210, 0, 0, 0.75)
    return (
      Math.abs(data[offset * 4] - 210) > 2 ||
      Math.abs(data[offset * 4 + 3] - 0.75 * 255) > 2
    );
  }

  /**
   * Determines if the current browser configuration can render triangular clip regions correctly.
   * This value is cached so the function is only expensive the first time called.
   * Firefox on Windows (as of now) does not if HWA is enabled. See https://bugzilla.mozilla.org/show_bug.cgi?id=1606976
   * Chrome works, and everything seems to work on OSX and Android. This function caches the
   * result. I suppose that it is conceivably possible that a browser might flip modes while the app is
   * running, but lets hope not.
   *
   * @return {boolean} true if the Diagonal Rendering is broken.
   */
  function isBrokenDiagonalRendering() {
    if (brokenDiagonalRendering_ === undefined) {
      const ctx = createCanvasContext2D(6, 6, canvasPool);
      ctx.globalCompositeOperation = 'lighter';
      ctx.fillStyle = 'rgba(210, 0, 0, 0.75)';
      drawTestTriangle(ctx, 4, 5, 4, 0);
      drawTestTriangle(ctx, 4, 5, 0, 5);
      const data = ctx.getImageData(0, 0, 3, 3).data;
      brokenDiagonalRendering_ =
        verifyBrokenDiagonalRendering(data, 0) ||
        verifyBrokenDiagonalRendering(data, 4) ||
        verifyBrokenDiagonalRendering(data, 8);
      releaseCanvas(ctx);
      canvasPool.push(ctx.canvas);
    }

    return brokenDiagonalRendering_;
  }

  /**
   * Calculates ideal resolution to use from the source in order to achieve
   * pixel mapping as close as possible to 1:1 during reprojection.
   * The resolution is calculated regardless of what resolutions
   * are actually available in the dataset (TileGrid, Image, ...).
   *
   * @param {import("./proj/Projection.js").default} sourceProj Source projection.
   * @param {import("./proj/Projection.js").default} targetProj Target projection.
   * @param {import("./coordinate.js").Coordinate} targetCenter Target center.
   * @param {number} targetResolution Target resolution.
   * @return {number} The best resolution to use. Can be +-Infinity, NaN or 0.
   */
  function calculateSourceResolution(
    sourceProj,
    targetProj,
    targetCenter,
    targetResolution,
  ) {
    const sourceCenter = transform(targetCenter, targetProj, sourceProj);

    // calculate the ideal resolution of the source data
    let sourceResolution = getPointResolution(
      targetProj,
      targetResolution,
      targetCenter,
    );

    const targetMetersPerUnit = targetProj.getMetersPerUnit();
    if (targetMetersPerUnit !== undefined) {
      sourceResolution *= targetMetersPerUnit;
    }
    const sourceMetersPerUnit = sourceProj.getMetersPerUnit();
    if (sourceMetersPerUnit !== undefined) {
      sourceResolution /= sourceMetersPerUnit;
    }

    // Based on the projection properties, the point resolution at the specified
    // coordinates may be slightly different. We need to reverse-compensate this
    // in order to achieve optimal results.

    const sourceExtent = sourceProj.getExtent();
    if (!sourceExtent || containsCoordinate(sourceExtent, sourceCenter)) {
      const compensationFactor =
        getPointResolution(sourceProj, sourceResolution, sourceCenter) /
        sourceResolution;
      if (isFinite(compensationFactor) && compensationFactor > 0) {
        sourceResolution /= compensationFactor;
      }
    }

    return sourceResolution;
  }

  /**
   * Calculates ideal resolution to use from the source in order to achieve
   * pixel mapping as close as possible to 1:1 during reprojection.
   * The resolution is calculated regardless of what resolutions
   * are actually available in the dataset (TileGrid, Image, ...).
   *
   * @param {import("./proj/Projection.js").default} sourceProj Source projection.
   * @param {import("./proj/Projection.js").default} targetProj Target projection.
   * @param {import("./extent.js").Extent} targetExtent Target extent
   * @param {number} targetResolution Target resolution.
   * @return {number} The best resolution to use. Can be +-Infinity, NaN or 0.
   */
  function calculateSourceExtentResolution(
    sourceProj,
    targetProj,
    targetExtent,
    targetResolution,
  ) {
    const targetCenter = getCenter(targetExtent);
    let sourceResolution = calculateSourceResolution(
      sourceProj,
      targetProj,
      targetCenter,
      targetResolution,
    );

    if (!isFinite(sourceResolution) || sourceResolution <= 0) {
      forEachCorner(targetExtent, function (corner) {
        sourceResolution = calculateSourceResolution(
          sourceProj,
          targetProj,
          corner,
          targetResolution,
        );
        return isFinite(sourceResolution) && sourceResolution > 0;
      });
    }

    return sourceResolution;
  }

  /**
   * @typedef {Object} ImageExtent
   * @property {import("./extent.js").Extent} extent Extent.
   * @property {import("./extent.js").Extent} [clipExtent] Clip extent.
   * @property {import('./DataTile.js').ImageLike} image Image.
   */

  /**
   * Renders the source data into new canvas based on the triangulation.
   *
   * @param {number} width Width of the canvas.
   * @param {number} height Height of the canvas.
   * @param {number} pixelRatio Pixel ratio.
   * @param {number} sourceResolution Source resolution.
   * @param {import("./extent.js").Extent} sourceExtent Extent of the data source.
   * @param {number} targetResolution Target resolution.
   * @param {import("./extent.js").Extent} targetExtent Target extent.
   * @param {import("./reproj/Triangulation.js").default} triangulation Calculated triangulation.
   * @param {Array<ImageExtent>} sources Array of sources.
   * @param {number} gutter Gutter of the sources.
   * @param {boolean} [renderEdges] Render reprojection edges.
   * @param {boolean} [interpolate] Use linear interpolation when resampling.
   * @param {boolean} [drawSingle] Draw single source images directly without stitchContext.
   * @param {boolean} [clipExtent] Clip stitchContext to sourceExtent.
   * @return {HTMLCanvasElement|OffscreenCanvas} Canvas with reprojected data.
   */
  function render(
    width,
    height,
    pixelRatio,
    sourceResolution,
    sourceExtent,
    targetResolution,
    targetExtent,
    triangulation,
    sources,
    gutter,
    renderEdges,
    interpolate,
    drawSingle,
    clipExtent,
  ) {
    const context = createCanvasContext2D(
      Math.round(pixelRatio * width),
      Math.round(pixelRatio * height),
      canvasPool,
    );

    if (!interpolate) {
      context.imageSmoothingEnabled = false;
    }

    if (sources.length === 0) {
      return context.canvas;
    }

    context.scale(pixelRatio, pixelRatio);

    function pixelRound(value) {
      return Math.round(value * pixelRatio) / pixelRatio;
    }

    context.globalCompositeOperation = 'lighter';

    const sourceDataExtent = createEmpty();
    sources.forEach(function (src, i, arr) {
      extend$1(sourceDataExtent, src.extent);
    });

    let stitchContext;
    const stitchScale = pixelRatio / sourceResolution;
    // Round up Float32 scale values to prevent interpolation in Firefox.
    const inverseScale = (interpolate ? 1 : 1 + Math.pow(2, -24)) / stitchScale;

    {
      stitchContext = createCanvasContext2D(
        Math.round(getWidth(sourceDataExtent) * stitchScale),
        Math.round(getHeight(sourceDataExtent) * stitchScale),
        canvasPool,
      );

      if (!interpolate) {
        stitchContext.imageSmoothingEnabled = false;
      }

      sources.forEach(function (src, i, arr) {
        // This test should never fail -- but it does. Need to find a fix the upstream condition
        if (src.image.width > 0 && src.image.height > 0) {
          if (src.clipExtent) {
            stitchContext.save();
            const xPos = (src.clipExtent[0] - sourceDataExtent[0]) * stitchScale;
            const yPos = -(src.clipExtent[3] - sourceDataExtent[3]) * stitchScale;
            const width = getWidth(src.clipExtent) * stitchScale;
            const height = getHeight(src.clipExtent) * stitchScale;
            stitchContext.rect(
              interpolate ? xPos : Math.round(xPos),
              interpolate ? yPos : Math.round(yPos),
              interpolate ? width : Math.round(xPos + width) - Math.round(xPos),
              interpolate ? height : Math.round(yPos + height) - Math.round(yPos),
            );
            stitchContext.clip();
          }

          const xPos = (src.extent[0] - sourceDataExtent[0]) * stitchScale;
          const yPos = -(src.extent[3] - sourceDataExtent[3]) * stitchScale;
          const srcWidth = getWidth(src.extent) * stitchScale;
          const srcHeight = getHeight(src.extent) * stitchScale;
          stitchContext.drawImage(
            src.image,
            gutter,
            gutter,
            src.image.width - 2 * gutter,
            src.image.height - 2 * gutter,
            interpolate ? xPos : Math.round(xPos),
            interpolate ? yPos : Math.round(yPos),
            interpolate
              ? srcWidth
              : Math.round(xPos + srcWidth) - Math.round(xPos),
            interpolate
              ? srcHeight
              : Math.round(yPos + srcHeight) - Math.round(yPos),
          );

          if (src.clipExtent) {
            stitchContext.restore();
          }
        }
      });
    }
    const targetTopLeft = getTopLeft(targetExtent);

    triangulation.getTriangles().forEach(function (triangle, i, arr) {
      /* Calculate affine transform (src -> dst)
       * Resulting matrix can be used to transform coordinate
       * from `sourceProjection` to destination pixels.
       *
       * To optimize number of context calls and increase numerical stability,
       * we also do the following operations:
       * trans(-topLeftExtentCorner), scale(1 / targetResolution), scale(1, -1)
       * here before solving the linear system so [ui, vi] are pixel coordinates.
       *
       * Src points: xi, yi
       * Dst points: ui, vi
       * Affine coefficients: aij
       *
       * | x0 y0 1  0  0 0 |   |a00|   |u0|
       * | x1 y1 1  0  0 0 |   |a01|   |u1|
       * | x2 y2 1  0  0 0 | x |a02| = |u2|
       * |  0  0 0 x0 y0 1 |   |a10|   |v0|
       * |  0  0 0 x1 y1 1 |   |a11|   |v1|
       * |  0  0 0 x2 y2 1 |   |a12|   |v2|
       */
      const source = triangle.source;
      const target = triangle.target;
      let x0 = source[0][0],
        y0 = source[0][1];
      let x1 = source[1][0],
        y1 = source[1][1];
      let x2 = source[2][0],
        y2 = source[2][1];
      // Make sure that everything is on pixel boundaries
      const u0 = pixelRound((target[0][0] - targetTopLeft[0]) / targetResolution);
      const v0 = pixelRound(
        -(target[0][1] - targetTopLeft[1]) / targetResolution,
      );
      const u1 = pixelRound((target[1][0] - targetTopLeft[0]) / targetResolution);
      const v1 = pixelRound(
        -(target[1][1] - targetTopLeft[1]) / targetResolution,
      );
      const u2 = pixelRound((target[2][0] - targetTopLeft[0]) / targetResolution);
      const v2 = pixelRound(
        -(target[2][1] - targetTopLeft[1]) / targetResolution,
      );

      // Shift all the source points to improve numerical stability
      // of all the subsequent calculations. The [x0, y0] is used here.
      // This is also used to simplify the linear system.
      const sourceNumericalShiftX = x0;
      const sourceNumericalShiftY = y0;
      x0 = 0;
      y0 = 0;
      x1 -= sourceNumericalShiftX;
      y1 -= sourceNumericalShiftY;
      x2 -= sourceNumericalShiftX;
      y2 -= sourceNumericalShiftY;

      const augmentedMatrix = [
        [x1, y1, 0, 0, u1 - u0],
        [x2, y2, 0, 0, u2 - u0],
        [0, 0, x1, y1, v1 - v0],
        [0, 0, x2, y2, v2 - v0],
      ];
      const affineCoefs = solveLinearSystem(augmentedMatrix);
      if (!affineCoefs) {
        return;
      }

      context.save();
      context.beginPath();

      if (isBrokenDiagonalRendering() || !interpolate) {
        // Make sure that all lines are horizontal or vertical
        context.moveTo(u1, v1);
        // This is the diagonal line. Do it in 4 steps
        const steps = 4;
        const ud = u0 - u1;
        const vd = v0 - v1;
        for (let step = 0; step < steps; step++) {
          // Go horizontally
          context.lineTo(
            u1 + pixelRound(((step + 1) * ud) / steps),
            v1 + pixelRound((step * vd) / (steps - 1)),
          );
          // Go vertically
          if (step != steps - 1) {
            context.lineTo(
              u1 + pixelRound(((step + 1) * ud) / steps),
              v1 + pixelRound(((step + 1) * vd) / (steps - 1)),
            );
          }
        }
        // We are almost at u0r, v0r
        context.lineTo(u2, v2);
      } else {
        context.moveTo(u1, v1);
        context.lineTo(u0, v0);
        context.lineTo(u2, v2);
      }

      context.clip();

      context.transform(
        affineCoefs[0],
        affineCoefs[2],
        affineCoefs[1],
        affineCoefs[3],
        u0,
        v0,
      );

      context.translate(
        sourceDataExtent[0] - sourceNumericalShiftX,
        sourceDataExtent[3] - sourceNumericalShiftY,
      );

      let image;
      if (stitchContext) {
        image = stitchContext.canvas;
        context.scale(inverseScale, -inverseScale);
      } else {
        const source = sources[0];
        const extent = source.extent;
        image = source.image;
        context.scale(
          getWidth(extent) / image.width,
          -getHeight(extent) / image.height,
        );
      }

      context.drawImage(image, 0, 0);
      context.restore();
    });

    if (stitchContext) {
      releaseCanvas(stitchContext);
      canvasPool.push(stitchContext.canvas);
    }

    if (renderEdges) {
      context.save();

      context.globalCompositeOperation = 'source-over';
      context.strokeStyle = 'black';
      context.lineWidth = 1;

      triangulation.getTriangles().forEach(function (triangle, i, arr) {
        const target = triangle.target;
        const u0 = (target[0][0] - targetTopLeft[0]) / targetResolution;
        const v0 = -(target[0][1] - targetTopLeft[1]) / targetResolution;
        const u1 = (target[1][0] - targetTopLeft[0]) / targetResolution;
        const v1 = -(target[1][1] - targetTopLeft[1]) / targetResolution;
        const u2 = (target[2][0] - targetTopLeft[0]) / targetResolution;
        const v2 = -(target[2][1] - targetTopLeft[1]) / targetResolution;

        context.beginPath();
        context.moveTo(u1, v1);
        context.lineTo(u0, v0);
        context.lineTo(u2, v2);
        context.closePath();
        context.stroke();
      });

      context.restore();
    }
    return context.canvas;
  }

  /**
   * @module ol/reproj/Triangulation
   */

  /**
   * Single triangle; consists of 3 source points and 3 target points.
   * @typedef {Object} Triangle
   * @property {Array<import("../coordinate.js").Coordinate>} source Source.
   * @property {Array<import("../coordinate.js").Coordinate>} target Target.
   */

  /**
   * Maximum number of subdivision steps during raster reprojection triangulation.
   * Prevents high memory usage and large number of proj4 calls (for certain
   * transformations and areas). At most `2*(2^this)` triangles are created for
   * each triangulated extent (tile/image).
   * @type {number}
   */
  const MAX_SUBDIVISION = 10;

  /**
   * Maximum allowed size of triangle relative to world width. When transforming
   * corners of world extent between certain projections, the resulting
   * triangulation seems to have zero error and no subdivision is performed. If
   * the triangle width is more than this (relative to world width; 0-1),
   * subdivison is forced (up to `MAX_SUBDIVISION`). Default is `0.25`.
   * @type {number}
   */
  const MAX_TRIANGLE_WIDTH = 0.25;

  /**
   * @classdesc
   * Class containing triangulation of the given target extent.
   * Used for determining source data and the reprojection itself.
   */
  class Triangulation {
    /**
     * @param {import("../proj/Projection.js").default} sourceProj Source projection.
     * @param {import("../proj/Projection.js").default} targetProj Target projection.
     * @param {import("../extent.js").Extent} targetExtent Target extent to triangulate.
     * @param {import("../extent.js").Extent} maxSourceExtent Maximal source extent that can be used.
     * @param {number} errorThreshold Acceptable error (in source units).
     * @param {?number} destinationResolution The (optional) resolution of the destination.
     * @param {import("../transform.js").Transform} [sourceMatrix] Source transform matrix.
     */
    constructor(
      sourceProj,
      targetProj,
      targetExtent,
      maxSourceExtent,
      errorThreshold,
      destinationResolution,
      sourceMatrix,
    ) {
      /**
       * @type {import("../proj/Projection.js").default}
       * @private
       */
      this.sourceProj_ = sourceProj;

      /**
       * @type {import("../proj/Projection.js").default}
       * @private
       */
      this.targetProj_ = targetProj;

      /** @type {!Object<string, import("../coordinate.js").Coordinate>} */
      let transformInvCache = {};
      const transformInv = sourceMatrix
        ? createTransformFromCoordinateTransform((input) =>
            apply(
              sourceMatrix,
              transform(input, this.targetProj_, this.sourceProj_),
            ),
          )
        : getTransform(this.targetProj_, this.sourceProj_);

      /**
       * @param {import("../coordinate.js").Coordinate} c A coordinate.
       * @return {import("../coordinate.js").Coordinate} Transformed coordinate.
       * @private
       */
      this.transformInv_ = function (c) {
        const key = c[0] + '/' + c[1];
        if (!transformInvCache[key]) {
          transformInvCache[key] = transformInv(c);
        }
        return transformInvCache[key];
      };

      /**
       * @type {import("../extent.js").Extent}
       * @private
       */
      this.maxSourceExtent_ = maxSourceExtent;

      /**
       * @type {number}
       * @private
       */
      this.errorThresholdSquared_ = errorThreshold * errorThreshold;

      /**
       * @type {Array<Triangle>}
       * @private
       */
      this.triangles_ = [];

      /**
       * Indicates that the triangulation crosses edge of the source projection.
       * @type {boolean}
       * @private
       */
      this.wrapsXInSource_ = false;

      /**
       * @type {boolean}
       * @private
       */
      this.canWrapXInSource_ =
        this.sourceProj_.canWrapX() &&
        !!maxSourceExtent &&
        !!this.sourceProj_.getExtent() &&
        getWidth(maxSourceExtent) >= getWidth(this.sourceProj_.getExtent());

      /**
       * @type {?number}
       * @private
       */
      this.sourceWorldWidth_ = this.sourceProj_.getExtent()
        ? getWidth(this.sourceProj_.getExtent())
        : null;

      /**
       * @type {?number}
       * @private
       */
      this.targetWorldWidth_ = this.targetProj_.getExtent()
        ? getWidth(this.targetProj_.getExtent())
        : null;

      const destinationTopLeft = getTopLeft(targetExtent);
      const destinationTopRight = getTopRight(targetExtent);
      const destinationBottomRight = getBottomRight(targetExtent);
      const destinationBottomLeft = getBottomLeft(targetExtent);
      const sourceTopLeft = this.transformInv_(destinationTopLeft);
      const sourceTopRight = this.transformInv_(destinationTopRight);
      const sourceBottomRight = this.transformInv_(destinationBottomRight);
      const sourceBottomLeft = this.transformInv_(destinationBottomLeft);

      /*
       * The maxSubdivision controls how many splittings of the target area can
       * be done. The idea here is to do a linear mapping of the target areas
       * but the actual overall reprojection (can be) extremely non-linear. The
       * default value of MAX_SUBDIVISION was chosen based on mapping a 256x256
       * tile size. However this function is also called to remap canvas rendered
       * layers which can be much larger. This calculation increases the maxSubdivision
       * value by the right factor so that each 256x256 pixel area has
       * MAX_SUBDIVISION divisions.
       */
      const maxSubdivision =
        MAX_SUBDIVISION +
        (destinationResolution
          ? Math.max(
              0,
              Math.ceil(
                Math.log2(
                  getArea(targetExtent) /
                    (destinationResolution * destinationResolution * 256 * 256),
                ),
              ),
            )
          : 0);

      this.addQuad_(
        destinationTopLeft,
        destinationTopRight,
        destinationBottomRight,
        destinationBottomLeft,
        sourceTopLeft,
        sourceTopRight,
        sourceBottomRight,
        sourceBottomLeft,
        maxSubdivision,
      );

      if (this.wrapsXInSource_) {
        let leftBound = Infinity;
        this.triangles_.forEach(function (triangle, i, arr) {
          leftBound = Math.min(
            leftBound,
            triangle.source[0][0],
            triangle.source[1][0],
            triangle.source[2][0],
          );
        });

        // Shift triangles to be as close to `leftBound` as possible
        // (if the distance is more than `worldWidth / 2` it can be closer.
        this.triangles_.forEach((triangle) => {
          if (
            Math.max(
              triangle.source[0][0],
              triangle.source[1][0],
              triangle.source[2][0],
            ) -
              leftBound >
            this.sourceWorldWidth_ / 2
          ) {
            const newTriangle = [
              [triangle.source[0][0], triangle.source[0][1]],
              [triangle.source[1][0], triangle.source[1][1]],
              [triangle.source[2][0], triangle.source[2][1]],
            ];
            if (newTriangle[0][0] - leftBound > this.sourceWorldWidth_ / 2) {
              newTriangle[0][0] -= this.sourceWorldWidth_;
            }
            if (newTriangle[1][0] - leftBound > this.sourceWorldWidth_ / 2) {
              newTriangle[1][0] -= this.sourceWorldWidth_;
            }
            if (newTriangle[2][0] - leftBound > this.sourceWorldWidth_ / 2) {
              newTriangle[2][0] -= this.sourceWorldWidth_;
            }

            // Rarely (if the extent contains both the dateline and prime meridian)
            // the shift can in turn break some triangles.
            // Detect this here and don't shift in such cases.
            const minX = Math.min(
              newTriangle[0][0],
              newTriangle[1][0],
              newTriangle[2][0],
            );
            const maxX = Math.max(
              newTriangle[0][0],
              newTriangle[1][0],
              newTriangle[2][0],
            );
            if (maxX - minX < this.sourceWorldWidth_ / 2) {
              triangle.source = newTriangle;
            }
          }
        });
      }

      transformInvCache = {};
    }

    /**
     * Adds triangle to the triangulation.
     * @param {import("../coordinate.js").Coordinate} a The target a coordinate.
     * @param {import("../coordinate.js").Coordinate} b The target b coordinate.
     * @param {import("../coordinate.js").Coordinate} c The target c coordinate.
     * @param {import("../coordinate.js").Coordinate} aSrc The source a coordinate.
     * @param {import("../coordinate.js").Coordinate} bSrc The source b coordinate.
     * @param {import("../coordinate.js").Coordinate} cSrc The source c coordinate.
     * @private
     */
    addTriangle_(a, b, c, aSrc, bSrc, cSrc) {
      this.triangles_.push({
        source: [aSrc, bSrc, cSrc],
        target: [a, b, c],
      });
    }

    /**
     * Adds quad (points in clock-wise order) to the triangulation
     * (and reprojects the vertices) if valid.
     * Performs quad subdivision if needed to increase precision.
     *
     * @param {import("../coordinate.js").Coordinate} a The target a coordinate.
     * @param {import("../coordinate.js").Coordinate} b The target b coordinate.
     * @param {import("../coordinate.js").Coordinate} c The target c coordinate.
     * @param {import("../coordinate.js").Coordinate} d The target d coordinate.
     * @param {import("../coordinate.js").Coordinate} aSrc The source a coordinate.
     * @param {import("../coordinate.js").Coordinate} bSrc The source b coordinate.
     * @param {import("../coordinate.js").Coordinate} cSrc The source c coordinate.
     * @param {import("../coordinate.js").Coordinate} dSrc The source d coordinate.
     * @param {number} maxSubdivision Maximal allowed subdivision of the quad.
     * @private
     */
    addQuad_(a, b, c, d, aSrc, bSrc, cSrc, dSrc, maxSubdivision) {
      const sourceQuadExtent = boundingExtent([aSrc, bSrc, cSrc, dSrc]);
      const sourceCoverageX = this.sourceWorldWidth_
        ? getWidth(sourceQuadExtent) / this.sourceWorldWidth_
        : null;
      const sourceWorldWidth = /** @type {number} */ (this.sourceWorldWidth_);

      // when the quad is wrapped in the source projection
      // it covers most of the projection extent, but not fully
      const wrapsX =
        this.sourceProj_.canWrapX() &&
        sourceCoverageX > 0.5 &&
        sourceCoverageX < 1;

      let needsSubdivision = false;

      if (maxSubdivision > 0) {
        if (this.targetProj_.isGlobal() && this.targetWorldWidth_) {
          const targetQuadExtent = boundingExtent([a, b, c, d]);
          const targetCoverageX =
            getWidth(targetQuadExtent) / this.targetWorldWidth_;
          needsSubdivision =
            targetCoverageX > MAX_TRIANGLE_WIDTH || needsSubdivision;
        }
        if (!wrapsX && this.sourceProj_.isGlobal() && sourceCoverageX) {
          needsSubdivision =
            sourceCoverageX > MAX_TRIANGLE_WIDTH || needsSubdivision;
        }
      }

      if (!needsSubdivision && this.maxSourceExtent_) {
        if (
          isFinite(sourceQuadExtent[0]) &&
          isFinite(sourceQuadExtent[1]) &&
          isFinite(sourceQuadExtent[2]) &&
          isFinite(sourceQuadExtent[3])
        ) {
          if (!intersects$1(sourceQuadExtent, this.maxSourceExtent_)) {
            // whole quad outside source projection extent -> ignore
            return;
          }
        }
      }

      let isNotFinite = 0;

      if (!needsSubdivision) {
        if (
          !isFinite(aSrc[0]) ||
          !isFinite(aSrc[1]) ||
          !isFinite(bSrc[0]) ||
          !isFinite(bSrc[1]) ||
          !isFinite(cSrc[0]) ||
          !isFinite(cSrc[1]) ||
          !isFinite(dSrc[0]) ||
          !isFinite(dSrc[1])
        ) {
          if (maxSubdivision > 0) {
            needsSubdivision = true;
          } else {
            // It might be the case that only 1 of the points is infinite. In this case
            // we can draw a single triangle with the other three points
            isNotFinite =
              (!isFinite(aSrc[0]) || !isFinite(aSrc[1]) ? 8 : 0) +
              (!isFinite(bSrc[0]) || !isFinite(bSrc[1]) ? 4 : 0) +
              (!isFinite(cSrc[0]) || !isFinite(cSrc[1]) ? 2 : 0) +
              (!isFinite(dSrc[0]) || !isFinite(dSrc[1]) ? 1 : 0);
            if (
              isNotFinite != 1 &&
              isNotFinite != 2 &&
              isNotFinite != 4 &&
              isNotFinite != 8
            ) {
              return;
            }
          }
        }
      }

      if (maxSubdivision > 0) {
        if (!needsSubdivision) {
          const center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2];
          const centerSrc = this.transformInv_(center);

          let dx;
          if (wrapsX) {
            const centerSrcEstimX =
              (modulo(aSrc[0], sourceWorldWidth) +
                modulo(cSrc[0], sourceWorldWidth)) /
              2;
            dx = centerSrcEstimX - modulo(centerSrc[0], sourceWorldWidth);
          } else {
            dx = (aSrc[0] + cSrc[0]) / 2 - centerSrc[0];
          }
          const dy = (aSrc[1] + cSrc[1]) / 2 - centerSrc[1];
          const centerSrcErrorSquared = dx * dx + dy * dy;
          needsSubdivision = centerSrcErrorSquared > this.errorThresholdSquared_;
        }
        if (needsSubdivision) {
          if (Math.abs(a[0] - c[0]) <= Math.abs(a[1] - c[1])) {
            // split horizontally (top & bottom)
            const bc = [(b[0] + c[0]) / 2, (b[1] + c[1]) / 2];
            const bcSrc = this.transformInv_(bc);
            const da = [(d[0] + a[0]) / 2, (d[1] + a[1]) / 2];
            const daSrc = this.transformInv_(da);

            this.addQuad_(
              a,
              b,
              bc,
              da,
              aSrc,
              bSrc,
              bcSrc,
              daSrc,
              maxSubdivision - 1,
            );
            this.addQuad_(
              da,
              bc,
              c,
              d,
              daSrc,
              bcSrc,
              cSrc,
              dSrc,
              maxSubdivision - 1,
            );
          } else {
            // split vertically (left & right)
            const ab = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
            const abSrc = this.transformInv_(ab);
            const cd = [(c[0] + d[0]) / 2, (c[1] + d[1]) / 2];
            const cdSrc = this.transformInv_(cd);

            this.addQuad_(
              a,
              ab,
              cd,
              d,
              aSrc,
              abSrc,
              cdSrc,
              dSrc,
              maxSubdivision - 1,
            );
            this.addQuad_(
              ab,
              b,
              c,
              cd,
              abSrc,
              bSrc,
              cSrc,
              cdSrc,
              maxSubdivision - 1,
            );
          }
          return;
        }
      }

      if (wrapsX) {
        if (!this.canWrapXInSource_) {
          return;
        }
        this.wrapsXInSource_ = true;
      }

      // Exactly zero or one of *Src is not finite
      // The triangles must have the diagonal line as the first side
      // This is to allow easy code in reproj.s to make it straight for broken
      // browsers that can't handle diagonal clipping
      if ((isNotFinite & 0xb) == 0) {
        this.addTriangle_(a, c, d, aSrc, cSrc, dSrc);
      }
      if ((isNotFinite & 0xe) == 0) {
        this.addTriangle_(a, c, b, aSrc, cSrc, bSrc);
      }
      if (isNotFinite) {
        // Try the other two triangles
        if ((isNotFinite & 0xd) == 0) {
          this.addTriangle_(b, d, a, bSrc, dSrc, aSrc);
        }
        if ((isNotFinite & 0x7) == 0) {
          this.addTriangle_(b, d, c, bSrc, dSrc, cSrc);
        }
      }
    }

    /**
     * Calculates extent of the `source` coordinates from all the triangles.
     *
     * @return {import("../extent.js").Extent} Calculated extent.
     */
    calculateSourceExtent() {
      const extent = createEmpty();

      this.triangles_.forEach(function (triangle, i, arr) {
        const src = triangle.source;
        extendCoordinate(extent, src[0]);
        extendCoordinate(extent, src[1]);
        extendCoordinate(extent, src[2]);
      });

      return extent;
    }

    /**
     * @return {Array<Triangle>} Array of the calculated triangles.
     */
    getTriangles() {
      return this.triangles_;
    }
  }

  /**
   * @module ol/reproj/common
   */

  /**
   * Default maximum allowed threshold  (in pixels) for reprojection
   * triangulation.
   * @type {number}
   */
  const ERROR_THRESHOLD = 0.5;

  /**
   * @module ol/reproj/Tile
   */


  /**
   * @typedef {function(number, number, number, number) : (import("../ImageTile.js").default)} FunctionType
   */

  /**
   * @typedef {Object} TileOffset
   * @property {import("../ImageTile.js").default} [tile] Tile.
   * @property {function(): import("../ImageTile.js").default} getTile Tile getter.
   * @property {number} offset Offset.
   */

  /**
   * @classdesc
   * Class encapsulating single reprojected tile.
   * See {@link module:ol/source/TileImage~TileImage}.
   *
   */
  class ReprojTile extends Tile {
    /**
     * @param {import("../proj/Projection.js").default} sourceProj Source projection.
     * @param {import("../tilegrid/TileGrid.js").default} sourceTileGrid Source tile grid.
     * @param {import("../proj/Projection.js").default} targetProj Target projection.
     * @param {import("../tilegrid/TileGrid.js").default} targetTileGrid Target tile grid.
     * @param {import("../tilecoord.js").TileCoord} tileCoord Coordinate of the tile.
     * @param {import("../tilecoord.js").TileCoord} wrappedTileCoord Coordinate of the tile wrapped in X.
     * @param {number} pixelRatio Pixel ratio.
     * @param {number} gutter Gutter of the source tiles.
     * @param {FunctionType} getTileFunction
     *     Function returning source tiles (z, x, y, pixelRatio).
     * @param {number} [errorThreshold] Acceptable reprojection error (in px).
     * @param {boolean} [renderEdges] Render reprojection edges.
     * @param {import("../Tile.js").Options} [options] Tile options.
     */
    constructor(
      sourceProj,
      sourceTileGrid,
      targetProj,
      targetTileGrid,
      tileCoord,
      wrappedTileCoord,
      pixelRatio,
      gutter,
      getTileFunction,
      errorThreshold,
      renderEdges,
      options,
    ) {
      super(tileCoord, TileState.IDLE, options);

      /**
       * @private
       * @type {boolean}
       */
      this.renderEdges_ = renderEdges !== undefined ? renderEdges : false;

      /**
       * @private
       * @type {number}
       */
      this.pixelRatio_ = pixelRatio;

      /**
       * @private
       * @type {number}
       */
      this.gutter_ = gutter;

      /**
       * @private
       * @type {HTMLCanvasElement|OffscreenCanvas}
       */
      this.canvas_ = null;

      /**
       * @private
       * @type {import("../tilegrid/TileGrid.js").default}
       */
      this.sourceTileGrid_ = sourceTileGrid;

      /**
       * @private
       * @type {import("../tilegrid/TileGrid.js").default}
       */
      this.targetTileGrid_ = targetTileGrid;

      /**
       * @private
       * @type {import("../tilecoord.js").TileCoord}
       */
      this.wrappedTileCoord_ = wrappedTileCoord ? wrappedTileCoord : tileCoord;

      /**
       * @private
       * @type {!Array<TileOffset>}
       */
      this.sourceTiles_ = [];

      /**
       * @private
       * @type {?Array<import("../events.js").EventsKey>}
       */
      this.sourcesListenerKeys_ = null;

      /**
       * @private
       * @type {number}
       */
      this.sourceZ_ = 0;

      /**
       * @private
       * @type {import("../extent.js").Extent}
       */
      this.clipExtent_ = sourceProj.canWrapX()
        ? sourceProj.getExtent()
        : undefined;

      const targetExtent = targetTileGrid.getTileCoordExtent(
        this.wrappedTileCoord_,
      );
      const maxTargetExtent = this.targetTileGrid_.getExtent();
      let maxSourceExtent = this.sourceTileGrid_.getExtent();

      const limitedTargetExtent = maxTargetExtent
        ? getIntersection(targetExtent, maxTargetExtent)
        : targetExtent;

      if (getArea(limitedTargetExtent) === 0) {
        // Tile is completely outside range -> EMPTY
        // TODO: is it actually correct that the source even creates the tile ?
        this.state = TileState.EMPTY;
        return;
      }

      const sourceProjExtent = sourceProj.getExtent();
      if (sourceProjExtent) {
        if (!maxSourceExtent) {
          maxSourceExtent = sourceProjExtent;
        } else {
          maxSourceExtent = getIntersection(maxSourceExtent, sourceProjExtent);
        }
      }

      const targetResolution = targetTileGrid.getResolution(
        this.wrappedTileCoord_[0],
      );

      const sourceResolution = calculateSourceExtentResolution(
        sourceProj,
        targetProj,
        limitedTargetExtent,
        targetResolution,
      );

      if (!isFinite(sourceResolution) || sourceResolution <= 0) {
        // invalid sourceResolution -> EMPTY
        // probably edges of the projections when no extent is defined
        this.state = TileState.EMPTY;
        return;
      }

      const errorThresholdInPixels =
        errorThreshold !== undefined ? errorThreshold : ERROR_THRESHOLD;

      /**
       * @private
       * @type {!import("./Triangulation.js").default}
       */
      this.triangulation_ = new Triangulation(
        sourceProj,
        targetProj,
        limitedTargetExtent,
        maxSourceExtent,
        sourceResolution * errorThresholdInPixels,
        targetResolution,
      );

      if (this.triangulation_.getTriangles().length === 0) {
        // no valid triangles -> EMPTY
        this.state = TileState.EMPTY;
        return;
      }

      this.sourceZ_ = sourceTileGrid.getZForResolution(sourceResolution);
      let sourceExtent = this.triangulation_.calculateSourceExtent();

      if (maxSourceExtent) {
        if (sourceProj.canWrapX()) {
          sourceExtent[1] = clamp(
            sourceExtent[1],
            maxSourceExtent[1],
            maxSourceExtent[3],
          );
          sourceExtent[3] = clamp(
            sourceExtent[3],
            maxSourceExtent[1],
            maxSourceExtent[3],
          );
        } else {
          sourceExtent = getIntersection(sourceExtent, maxSourceExtent);
        }
      }

      if (!getArea(sourceExtent)) {
        this.state = TileState.EMPTY;
      } else {
        let worldWidth = 0;
        let worldsAway = 0;
        if (sourceProj.canWrapX()) {
          worldWidth = getWidth(sourceProjExtent);
          worldsAway = Math.floor(
            (sourceExtent[0] - sourceProjExtent[0]) / worldWidth,
          );
        }

        const sourceExtents = wrapAndSliceX(
          sourceExtent.slice(),
          sourceProj,
          true,
        );
        sourceExtents.forEach((extent) => {
          const sourceRange = sourceTileGrid.getTileRangeForExtentAndZ(
            extent,
            this.sourceZ_,
          );

          for (let srcX = sourceRange.minX; srcX <= sourceRange.maxX; srcX++) {
            for (let srcY = sourceRange.minY; srcY <= sourceRange.maxY; srcY++) {
              const offset = worldsAway * worldWidth;
              this.sourceTiles_.push({
                getTile: () =>
                  getTileFunction(this.sourceZ_, srcX, srcY, pixelRatio),
                offset,
              });
            }
          }
          ++worldsAway;
        });

        if (this.sourceTiles_.length === 0) {
          this.state = TileState.EMPTY;
        }
      }
    }

    /**
     * Get the HTML Canvas element for this tile.
     * @return {HTMLCanvasElement|OffscreenCanvas} Canvas.
     */
    getImage() {
      return this.canvas_;
    }

    /**
     * @private
     */
    reproject_() {
      const sources = [];
      this.sourceTiles_.forEach((source) => {
        const tile = source.tile;
        if (tile && tile.getState() == TileState.LOADED) {
          const extent = this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord);
          extent[0] += source.offset;
          extent[2] += source.offset;
          const clipExtent = this.clipExtent_?.slice();
          if (clipExtent) {
            clipExtent[0] += source.offset;
            clipExtent[2] += source.offset;
          }
          sources.push({
            extent: extent,
            clipExtent: clipExtent,
            image: tile.getImage(),
          });
        }
      });
      this.sourceTiles_.length = 0;

      if (sources.length === 0) {
        this.state = TileState.ERROR;
      } else {
        const z = this.wrappedTileCoord_[0];
        const size = this.targetTileGrid_.getTileSize(z);
        const width = typeof size === 'number' ? size : size[0];
        const height = typeof size === 'number' ? size : size[1];
        const targetResolution = this.targetTileGrid_.getResolution(z);
        const sourceResolution = this.sourceTileGrid_.getResolution(
          this.sourceZ_,
        );

        const targetExtent = this.targetTileGrid_.getTileCoordExtent(
          this.wrappedTileCoord_,
        );

        this.canvas_ = render(
          width,
          height,
          this.pixelRatio_,
          sourceResolution,
          this.sourceTileGrid_.getExtent(),
          targetResolution,
          targetExtent,
          this.triangulation_,
          sources,
          this.gutter_,
          this.renderEdges_,
          this.interpolate,
        );

        this.state = TileState.LOADED;
      }
      this.changed();
    }

    /**
     * Load not yet loaded URI.
     * @override
     */
    load() {
      for (const sourceTile of this.sourceTiles_) {
        sourceTile.tile = sourceTile.getTile();
      }
      if (this.state == TileState.IDLE) {
        this.state = TileState.LOADING;
        this.changed();

        let leftToLoad = 0;

        this.sourcesListenerKeys_ = [];
        this.sourceTiles_.forEach(({tile}) => {
          const state = tile.getState();
          if (state == TileState.IDLE || state == TileState.LOADING) {
            leftToLoad++;

            const sourceListenKey = listen(tile, EventType.CHANGE, (e) => {
              const state = tile.getState();
              if (
                state == TileState.LOADED ||
                state == TileState.ERROR ||
                state == TileState.EMPTY
              ) {
                unlistenByKey(sourceListenKey);
                leftToLoad--;
                if (leftToLoad === 0) {
                  this.unlistenSources_();
                  this.reproject_();
                }
              }
            });
            this.sourcesListenerKeys_.push(sourceListenKey);
          }
        });

        if (leftToLoad === 0) {
          setTimeout(this.reproject_.bind(this), 0);
        } else {
          this.sourceTiles_.forEach(function ({tile}, i, arr) {
            const state = tile.getState();
            if (state == TileState.IDLE) {
              tile.load();
            }
          });
        }
      }
    }

    /**
     * @private
     */
    unlistenSources_() {
      this.sourcesListenerKeys_.forEach(unlistenByKey);
      this.sourcesListenerKeys_ = null;
    }

    /**
     * Remove from the cache due to expiry
     * @override
     */
    release() {
      if (this.canvas_) {
        releaseCanvas(
          /** @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} */ (
            this.canvas_.getContext('2d')
          ),
        );
        canvasPool.push(this.canvas_);
        this.canvas_ = null;
      }
      this.sourceTiles_.length = 0;
      super.release();
    }
  }

  /**
   * @module ol/structs/LRUCache
   */


  /**
   * @typedef {Object} Entry
   * @property {string} key_ Key.
   * @property {Entry|null} newer Newer.
   * @property {Entry|null} older Older.
   * @property {*} value_ Value.
   */

  /**
   * @classdesc
   * Implements a Least-Recently-Used cache where the keys do not conflict with
   * Object's properties (e.g. 'hasOwnProperty' is not allowed as a key). Expiring
   * items from the cache is the responsibility of the user.
   *
   * @fires import("../events/Event.js").default
   * @template T
   */
  class LRUCache {
    /**
     * @param {number} [highWaterMark] High water mark.
     */
    constructor(highWaterMark) {
      /**
       * Desired max cache size after expireCache(). If set to 0, no cache entries
       * will be pruned at all.
       * @type {number}
       */
      this.highWaterMark = highWaterMark !== undefined ? highWaterMark : 2048;

      /**
       * @private
       * @type {number}
       */
      this.count_ = 0;

      /**
       * @private
       * @type {!Object<string, Entry>}
       */
      this.entries_ = {};

      /**
       * @private
       * @type {?Entry}
       */
      this.oldest_ = null;

      /**
       * @private
       * @type {?Entry}
       */
      this.newest_ = null;
    }

    deleteOldest() {
      const entry = this.pop();
      if (entry instanceof Disposable) {
        entry.dispose();
      }
    }

    /**
     * @return {boolean} Can expire cache.
     */
    canExpireCache() {
      return this.highWaterMark > 0 && this.getCount() > this.highWaterMark;
    }

    /**
     * Expire the cache. When the cache entry is a {@link module:ol/Disposable~Disposable},
     * the entry will be disposed.
     * @param {!Object<string, boolean>} [keep] Keys to keep. To be implemented by subclasses.
     */
    expireCache(keep) {
      while (this.canExpireCache()) {
        this.deleteOldest();
      }
    }

    /**
     * FIXME empty description for jsdoc
     */
    clear() {
      while (this.oldest_) {
        this.deleteOldest();
      }
    }

    /**
     * @param {string} key Key.
     * @return {boolean} Contains key.
     */
    containsKey(key) {
      return this.entries_.hasOwnProperty(key);
    }

    /**
     * @param {function(T, string, LRUCache<T>): ?} f The function
     *     to call for every entry from the oldest to the newer. This function takes
     *     3 arguments (the entry value, the entry key and the LRUCache object).
     *     The return value is ignored.
     */
    forEach(f) {
      let entry = this.oldest_;
      while (entry) {
        f(entry.value_, entry.key_, this);
        entry = entry.newer;
      }
    }

    /**
     * @param {string} key Key.
     * @param {*} [options] Options (reserved for subclasses).
     * @return {T} Value.
     */
    get(key, options) {
      const entry = this.entries_[key];
      assert(
        entry !== undefined,
        'Tried to get a value for a key that does not exist in the cache',
      );
      if (entry === this.newest_) {
        return entry.value_;
      }
      if (entry === this.oldest_) {
        this.oldest_ = /** @type {Entry} */ (this.oldest_.newer);
        this.oldest_.older = null;
      } else {
        entry.newer.older = entry.older;
        entry.older.newer = entry.newer;
      }
      entry.newer = null;
      entry.older = this.newest_;
      this.newest_.newer = entry;
      this.newest_ = entry;
      return entry.value_;
    }

    /**
     * Remove an entry from the cache.
     * @param {string} key The entry key.
     * @return {T} The removed entry.
     */
    remove(key) {
      const entry = this.entries_[key];
      assert(
        entry !== undefined,
        'Tried to get a value for a key that does not exist in the cache',
      );
      if (entry === this.newest_) {
        this.newest_ = /** @type {Entry} */ (entry.older);
        if (this.newest_) {
          this.newest_.newer = null;
        }
      } else if (entry === this.oldest_) {
        this.oldest_ = /** @type {Entry} */ (entry.newer);
        if (this.oldest_) {
          this.oldest_.older = null;
        }
      } else {
        entry.newer.older = entry.older;
        entry.older.newer = entry.newer;
      }
      delete this.entries_[key];
      --this.count_;
      return entry.value_;
    }

    /**
     * @return {number} Count.
     */
    getCount() {
      return this.count_;
    }

    /**
     * @return {Array<string>} Keys.
     */
    getKeys() {
      const keys = new Array(this.count_);
      let i = 0;
      let entry;
      for (entry = this.newest_; entry; entry = entry.older) {
        keys[i++] = entry.key_;
      }
      return keys;
    }

    /**
     * @return {Array<T>} Values.
     */
    getValues() {
      const values = new Array(this.count_);
      let i = 0;
      let entry;
      for (entry = this.newest_; entry; entry = entry.older) {
        values[i++] = entry.value_;
      }
      return values;
    }

    /**
     * @return {T} Last value.
     */
    peekLast() {
      return this.oldest_.value_;
    }

    /**
     * @return {string} Last key.
     */
    peekLastKey() {
      return this.oldest_.key_;
    }

    /**
     * Get the key of the newest item in the cache.  Throws if the cache is empty.
     * @return {string} The newest key.
     */
    peekFirstKey() {
      return this.newest_.key_;
    }

    /**
     * Return an entry without updating least recently used time.
     * @param {string} key Key.
     * @return {T|undefined} Value.
     */
    peek(key) {
      return this.entries_[key]?.value_;
    }

    /**
     * @return {T} value Value.
     */
    pop() {
      const entry = this.oldest_;
      delete this.entries_[entry.key_];
      if (entry.newer) {
        entry.newer.older = null;
      }
      this.oldest_ = /** @type {Entry} */ (entry.newer);
      if (!this.oldest_) {
        this.newest_ = null;
      }
      --this.count_;
      return entry.value_;
    }

    /**
     * @param {string} key Key.
     * @param {T} value Value.
     */
    replace(key, value) {
      this.get(key); // update `newest_`
      this.entries_[key].value_ = value;
    }

    /**
     * @param {string} key Key.
     * @param {T} value Value.
     */
    set(key, value) {
      assert(
        !(key in this.entries_),
        'Tried to set a value for a key that is used already',
      );
      const entry = {
        key_: key,
        newer: null,
        older: this.newest_,
        value_: value,
      };
      if (!this.newest_) {
        this.oldest_ = entry;
      } else {
        this.newest_.newer = entry;
      }
      this.newest_ = entry;
      this.entries_[key] = entry;
      ++this.count_;
    }

    /**
     * Set a maximum number of entries for the cache.
     * @param {number} size Cache size.
     * @api
     */
    setSize(size) {
      this.highWaterMark = size;
    }
  }

  /**
   * @module ol/tilecoord
   */


  /**
   * An array of three numbers representing the location of a tile in a tile
   * grid. The order is `z` (zoom level), `x` (column), and `y` (row).
   * @typedef {Array<number>} TileCoord
   * @api
   */

  /**
   * @param {number} z Z.
   * @param {number} x X.
   * @param {number} y Y.
   * @param {TileCoord} [tileCoord] Tile coordinate.
   * @return {TileCoord} Tile coordinate.
   */
  function createOrUpdate(z, x, y, tileCoord) {
    if (tileCoord !== undefined) {
      tileCoord[0] = z;
      tileCoord[1] = x;
      tileCoord[2] = y;
      return tileCoord;
    }
    return [z, x, y];
  }

  /**
   * @param {number} z Z.
   * @param {number} x X.
   * @param {number} y Y.
   * @return {string} Key.
   */
  function getKeyZXY(z, x, y) {
    return z + '/' + x + '/' + y;
  }

  /**
   * @param {import("./source/Tile.js").default} source The tile source.
   * @param {string} sourceKey The source key.
   * @param {number} z The tile z level.
   * @param {number} x The tile x level.
   * @param {number} y The tile y level.
   * @return {string} The cache key.
   */
  function getCacheKey(source, sourceKey, z, x, y) {
    return `${getUid(source)},${sourceKey},${getKeyZXY(z, x, y)}`;
  }

  /**
   * @param {TileCoord} tileCoord Tile coord.
   * @return {number} Hash.
   */
  function hash(tileCoord) {
    return hashZXY(tileCoord[0], tileCoord[1], tileCoord[2]);
  }

  /**
   * @param {number} z The tile z coordinate.
   * @param {number} x The tile x coordinate.
   * @param {number} y The tile y coordinate.
   * @return {number} Hash.
   */
  function hashZXY(z, x, y) {
    return (x << z) + y;
  }

  /**
   * @param {TileCoord} tileCoord Tile coordinate.
   * @param {!import("./tilegrid/TileGrid.js").default} tileGrid Tile grid.
   * @return {boolean} Tile coordinate is within extent and zoom level range.
   */
  function withinExtentAndZ(tileCoord, tileGrid) {
    const z = tileCoord[0];
    const x = tileCoord[1];
    const y = tileCoord[2];

    if (tileGrid.getMinZoom() > z || z > tileGrid.getMaxZoom()) {
      return false;
    }
    const tileRange = tileGrid.getFullTileRange(z);
    if (!tileRange) {
      return true;
    }
    return tileRange.containsXY(x, y);
  }

  /**
   * @module ol/renderer/canvas/TileLayer
   */

  /**
   * @typedef {Object<number, Set<import("../../Tile.js").default>>} TileLookup
   */

  /**
   * Add a tile to the lookup.
   * @param {TileLookup} tilesByZ Lookup of tiles by zoom level.
   * @param {import("../../Tile.js").default} tile A tile.
   * @param {number} z The zoom level.
   * @return {boolean} The tile was added to the lookup.
   */
  function addTileToLookup(tilesByZ, tile, z) {
    if (!(z in tilesByZ)) {
      tilesByZ[z] = new Set([tile]);
      return true;
    }
    const set = tilesByZ[z];
    const existing = set.has(tile);
    if (!existing) {
      set.add(tile);
    }
    return !existing;
  }

  /**
   * Remove a tile from the lookup.
   * @param {TileLookup} tilesByZ Lookup of tiles by zoom level.
   * @param {import("../../Tile.js").default} tile A tile.
   * @param {number} z The zoom level.
   * @return {boolean} The tile was removed from the lookup.
   */
  function removeTileFromLookup(tilesByZ, tile, z) {
    const set = tilesByZ[z];
    if (set) {
      return set.delete(tile);
    }
    return false;
  }

  /**
   * @param {import("../../Map.js").FrameState} frameState Frame state.
   * @param {import("../../extent.js").Extent} extent The frame extent.
   * @return {import("../../extent.js").Extent} Frame extent intersected with layer extents.
   */
  function getRenderExtent(frameState, extent) {
    const layerState = frameState.layerStatesArray[frameState.layerIndex];
    if (layerState.extent) {
      extent = getIntersection(
        extent,
        fromUserExtent(layerState.extent, frameState.viewState.projection),
      );
    }
    const source = /** @type {import("../../source/Tile.js").default} */ (
      layerState.layer.getRenderSource()
    );
    if (!source.getWrapX()) {
      const gridExtent = source
        .getTileGridForProjection(frameState.viewState.projection)
        .getExtent();
      if (gridExtent) {
        extent = getIntersection(extent, gridExtent);
      }
    }
    return extent;
  }

  /**
   * @typedef {Object} Options
   * @property {number} [cacheSize=512] The cache size.
   */

  /**
   * @classdesc
   * Canvas renderer for tile layers.
   * @api
   * @template {import("../../layer/Tile.js").default|import("../../layer/VectorTile.js").default} [LayerType=import("../../layer/Tile.js").default<import("../../source/Tile.js").default>|import("../../layer/VectorTile.js").default]
   * @extends {CanvasLayerRenderer<LayerType>}
   */
  class CanvasTileLayerRenderer extends CanvasLayerRenderer {
    /**
     * @param {LayerType} tileLayer Tile layer.
     * @param {Options} [options] Options.
     */
    constructor(tileLayer, options) {
      super(tileLayer);

      options = options || {};

      /**
       * Rendered extent has changed since the previous `renderFrame()` call
       * @type {boolean}
       */
      this.extentChanged = true;

      /**
       * The last call to `renderFrame` was completed with all tiles loaded
       * @type {boolean}
       */
      this.renderComplete = false;

      /**
       * @private
       * @type {?import("../../extent.js").Extent}
       */
      this.renderedExtent_ = null;

      /**
       * @protected
       * @type {number}
       */
      this.renderedPixelRatio;

      /**
       * @protected
       * @type {import("../../proj/Projection.js").default|null}
       */
      this.renderedProjection = null;

      /**
       * @protected
       * @type {!Array<import("../../Tile.js").default>}
       */
      this.renderedTiles = [];

      /**
       * @private
       * @type {string}
       */
      this.renderedSourceKey_;

      /**
       * @private
       * @type {number}
       */
      this.renderedSourceRevision_;

      /**
       * @protected
       * @type {import("../../extent.js").Extent}
       */
      this.tempExtent = createEmpty();

      /**
       * @private
       * @type {import("../../TileRange.js").default}
       */
      this.tempTileRange_ = new TileRange(0, 0, 0, 0);

      /**
       * @type {import("../../tilecoord.js").TileCoord}
       * @private
       */
      this.tempTileCoord_ = createOrUpdate(0, 0, 0);

      const cacheSize = options.cacheSize !== undefined ? options.cacheSize : 512;

      /**
       * @type {import("../../structs/LRUCache.js").default<import("../../Tile.js").default>}
       * @private
       */
      this.tileCache_ = new LRUCache(cacheSize);

      /**
       * @type {import("../../structs/LRUCache.js").default<import("../../Tile.js").default|null>}
       * @private
       */
      this.sourceTileCache_ = null;

      this.maxStaleKeys = cacheSize * 0.5;
    }

    /**
     * @return {LRUCache} Tile cache.
     */
    getTileCache() {
      return this.tileCache_;
    }

    /**
     * @return {LRUCache} Tile cache.
     */
    getSourceTileCache() {
      if (!this.sourceTileCache_) {
        this.sourceTileCache_ = new LRUCache(512);
      }
      return this.sourceTileCache_;
    }

    /**
     * Get a tile from the cache or create one if needed.
     *
     * @param {number} z Tile coordinate z.
     * @param {number} x Tile coordinate x.
     * @param {number} y Tile coordinate y.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @return {import("../../Tile.js").default|null} Tile (or null if outside source extent).
     * @protected
     */
    getOrCreateTile(z, x, y, frameState) {
      const tileCache = this.tileCache_;
      const tileLayer = this.getLayer();
      const tileSource = tileLayer.getSource();
      const cacheKey = getCacheKey(tileSource, tileSource.getKey(), z, x, y);

      /** @type {import("../../Tile.js").default} */
      let tile;

      if (tileCache.containsKey(cacheKey)) {
        tile = tileCache.get(cacheKey);
      } else {
        const projection = frameState.viewState.projection;
        const sourceProjection = tileSource.getProjection();
        tile = tileSource.getTile(
          z,
          x,
          y,
          frameState.pixelRatio,
          projection,
          !sourceProjection || equivalent$1(sourceProjection, projection)
            ? undefined
            : this.getSourceTileCache(),
        );
        if (!tile) {
          return null;
        }
        tileCache.set(cacheKey, tile);
      }
      return tile;
    }

    /**
     * @param {number} z Tile coordinate z.
     * @param {number} x Tile coordinate x.
     * @param {number} y Tile coordinate y.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @return {import("../../Tile.js").default|null} Tile (or null if outside source extent).
     * @protected
     */
    getTile(z, x, y, frameState) {
      const tile = this.getOrCreateTile(z, x, y, frameState);
      if (!tile) {
        return null;
      }
      return tile;
    }

    /**
     * @param {import("../../pixel.js").Pixel} pixel Pixel.
     * @return {Uint8ClampedArray} Data at the pixel location.
     * @override
     */
    getData(pixel) {
      const frameState = this.frameState;
      if (!frameState) {
        return null;
      }

      const layer = this.getLayer();
      const coordinate = apply(
        frameState.pixelToCoordinateTransform,
        pixel.slice(),
      );

      const layerExtent = layer.getExtent();
      if (layerExtent) {
        if (!containsCoordinate(layerExtent, coordinate)) {
          return null;
        }
      }

      const viewState = frameState.viewState;
      const source = layer.getRenderSource();
      const tileGrid = source.getTileGridForProjection(viewState.projection);
      const tilePixelRatio = source.getTilePixelRatio(frameState.pixelRatio);

      for (
        let z = tileGrid.getZForResolution(viewState.resolution);
        z >= tileGrid.getMinZoom();
        --z
      ) {
        const tileCoord = tileGrid.getTileCoordForCoordAndZ(coordinate, z);
        const tile = this.getTile(z, tileCoord[1], tileCoord[2], frameState);
        if (!tile || tile.getState() !== TileState.LOADED) {
          continue;
        }

        const tileOrigin = tileGrid.getOrigin(z);
        const tileSize = toSize(tileGrid.getTileSize(z));
        const tileResolution = tileGrid.getResolution(z);

        /**
         * @type {import('../../DataTile.js').ImageLike}
         */
        let image;
        if (tile instanceof ImageTile || tile instanceof ReprojTile) {
          image = tile.getImage();
        } else if (tile instanceof DataTile) {
          image = asImageLike(tile.getData());
          if (!image) {
            continue;
          }
        } else {
          continue;
        }

        const col = Math.floor(
          tilePixelRatio *
            ((coordinate[0] - tileOrigin[0]) / tileResolution -
              tileCoord[1] * tileSize[0]),
        );

        const row = Math.floor(
          tilePixelRatio *
            ((tileOrigin[1] - coordinate[1]) / tileResolution -
              tileCoord[2] * tileSize[1]),
        );

        const gutter = Math.round(
          tilePixelRatio * source.getGutterForProjection(viewState.projection),
        );

        return this.getImageData(image, col + gutter, row + gutter);
      }

      return null;
    }

    /**
     * Determine whether render should be called.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @return {boolean} Layer is ready to be rendered.
     * @override
     */
    prepareFrame(frameState) {
      if (!this.renderedProjection) {
        this.renderedProjection = frameState.viewState.projection;
      } else if (frameState.viewState.projection !== this.renderedProjection) {
        this.tileCache_.clear();
        this.renderedProjection = frameState.viewState.projection;
      }

      const source = this.getLayer().getSource();
      if (!source) {
        return false;
      }
      const sourceRevision = source.getRevision();
      if (!this.renderedSourceRevision_) {
        this.renderedSourceRevision_ = sourceRevision;
      } else if (this.renderedSourceRevision_ !== sourceRevision) {
        this.renderedSourceRevision_ = sourceRevision;
        if (this.renderedSourceKey_ === source.getKey()) {
          this.tileCache_.clear();
          this.sourceTileCache_?.clear();
        }
      }
      return true;
    }

    /**
     * Determine whether tiles for next extent should be enqueued for rendering.
     * @return {boolean} Rendering tiles for next extent is supported.
     * @protected
     */
    enqueueTilesForNextExtent() {
      return true;
    }

    /**
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @param {import("../../extent.js").Extent} extent The extent to be rendered.
     * @param {number} initialZ The zoom level.
     * @param {TileLookup} tilesByZ Lookup of tiles by zoom level.
     * @param {number} preload Number of additional levels to load.
     */
    enqueueTiles(frameState, extent, initialZ, tilesByZ, preload) {
      const viewState = frameState.viewState;
      const tileLayer = this.getLayer();
      const tileSource = tileLayer.getRenderSource();
      const tileGrid = tileSource.getTileGridForProjection(viewState.projection);

      const tileSourceKey = getUid(tileSource);
      if (!(tileSourceKey in frameState.wantedTiles)) {
        frameState.wantedTiles[tileSourceKey] = {};
      }

      const wantedTiles = frameState.wantedTiles[tileSourceKey];

      const map = tileLayer.getMapInternal();
      const minZ = Math.max(
        initialZ - preload,
        tileGrid.getMinZoom(),
        tileGrid.getZForResolution(
          Math.min(
            tileLayer.getMaxResolution(),
            map
              ? map
                  .getView()
                  .getResolutionForZoom(Math.max(tileLayer.getMinZoom(), 0))
              : tileGrid.getResolution(0),
          ),
          tileSource.zDirection,
        ),
      );
      const rotation = viewState.rotation;
      const viewport = rotation
        ? getRotatedViewport(
            viewState.center,
            viewState.resolution,
            rotation,
            frameState.size,
          )
        : undefined;
      for (let z = initialZ; z >= minZ; --z) {
        const tileRange = tileGrid.getTileRangeForExtentAndZ(
          extent,
          z,
          this.tempTileRange_,
        );

        const tileResolution = tileGrid.getResolution(z);

        for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
          for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
            if (
              rotation &&
              !tileGrid.tileCoordIntersectsViewport([z, x, y], viewport)
            ) {
              continue;
            }
            const tile = this.getTile(z, x, y, frameState);
            if (!tile) {
              continue;
            }
            const added = addTileToLookup(tilesByZ, tile, z);
            if (!added) {
              continue;
            }

            const tileQueueKey = tile.getKey();
            wantedTiles[tileQueueKey] = true;

            if (tile.getState() === TileState.IDLE) {
              if (!frameState.tileQueue.isKeyQueued(tileQueueKey)) {
                const tileCoord = createOrUpdate(z, x, y, this.tempTileCoord_);
                frameState.tileQueue.enqueue([
                  tile,
                  tileSourceKey,
                  tileGrid.getTileCoordCenter(tileCoord),
                  tileResolution,
                ]);
              }
            }
          }
        }
      }
    }

    /**
     * Look for tiles covering the provided tile coordinate at an alternate
     * zoom level.  Loaded tiles will be added to the provided tile texture lookup.
     * @param {import("../../tilecoord.js").TileCoord} tileCoord The target tile coordinate.
     * @param {TileLookup} tilesByZ Lookup of tiles by zoom level.
     * @return {boolean} The tile coordinate is covered by loaded tiles at the alternate zoom level.
     * @private
     */
    findStaleTile_(tileCoord, tilesByZ) {
      const tileCache = this.tileCache_;
      const z = tileCoord[0];
      const x = tileCoord[1];
      const y = tileCoord[2];
      const staleKeys = this.getStaleKeys();
      for (let i = 0; i < staleKeys.length; ++i) {
        const cacheKey = getCacheKey(
          this.getLayer().getSource(),
          staleKeys[i],
          z,
          x,
          y,
        );
        if (tileCache.containsKey(cacheKey)) {
          const tile = tileCache.peek(cacheKey);
          if (tile.getState() === TileState.LOADED) {
            tile.endTransition(getUid(this));
            addTileToLookup(tilesByZ, tile, z);
            return true;
          }
        }
      }
      return false;
    }

    /**
     * Look for tiles covering the provided tile coordinate at an alternate
     * zoom level.  Loaded tiles will be added to the provided tile texture lookup.
     * @param {import("../../tilegrid/TileGrid.js").default} tileGrid The tile grid.
     * @param {import("../../tilecoord.js").TileCoord} tileCoord The target tile coordinate.
     * @param {number} altZ The alternate zoom level.
     * @param {TileLookup} tilesByZ Lookup of tiles by zoom level.
     * @return {boolean} The tile coordinate is covered by loaded tiles at the alternate zoom level.
     * @private
     */
    findAltTiles_(tileGrid, tileCoord, altZ, tilesByZ) {
      const tileRange = tileGrid.getTileRangeForTileCoordAndZ(
        tileCoord,
        altZ,
        this.tempTileRange_,
      );

      if (!tileRange) {
        return false;
      }

      let covered = true;
      const tileCache = this.tileCache_;
      const source = this.getLayer().getRenderSource();
      const sourceKey = source.getKey();
      for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
        for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
          const cacheKey = getCacheKey(source, sourceKey, altZ, x, y);
          let loaded = false;
          if (tileCache.containsKey(cacheKey)) {
            const tile = tileCache.peek(cacheKey);
            if (tile.getState() === TileState.LOADED) {
              addTileToLookup(tilesByZ, tile, altZ);
              loaded = true;
            }
          }
          if (!loaded) {
            covered = false;
          }
        }
      }
      return covered;
    }

    /**
     * Render the layer.
     *
     * The frame rendering logic has three parts:
     *
     *  1. Enqueue tiles
     *  2. Find alt tiles for those that are not yet loaded
     *  3. Render loaded tiles
     *
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @param {HTMLElement} target Target that may be used to render content to.
     * @return {HTMLElement} The rendered element.
     * @override
     */
    renderFrame(frameState, target) {
      this.renderComplete = true;
      /**
       * TODO:
       *  maybe skip transition when not fully opaque
       *  decide if this.renderComplete is useful
       */

      const layerState = frameState.layerStatesArray[frameState.layerIndex];
      const viewState = frameState.viewState;
      const projection = viewState.projection;
      const viewResolution = viewState.resolution;
      const viewCenter = viewState.center;
      const pixelRatio = frameState.pixelRatio;

      const tileLayer = this.getLayer();
      const tileSource = tileLayer.getSource();
      const tileGrid = tileSource.getTileGridForProjection(projection);
      const z = tileGrid.getZForResolution(viewResolution, tileSource.zDirection);
      const tileResolution = tileGrid.getResolution(z);

      const sourceKey = tileSource.getKey();
      if (!this.renderedSourceKey_) {
        this.renderedSourceKey_ = sourceKey;
      } else if (this.renderedSourceKey_ !== sourceKey) {
        this.prependStaleKey(this.renderedSourceKey_);
        this.renderedSourceKey_ = sourceKey;
      }

      let frameExtent = frameState.extent;
      const tilePixelRatio = tileSource.getTilePixelRatio(pixelRatio);

      this.prepareContainer(frameState, target);

      // desired dimensions of the canvas in pixels
      const width = this.context.canvas.width;
      const height = this.context.canvas.height;

      const layerExtent =
        layerState.extent && fromUserExtent(layerState.extent);
      if (layerExtent) {
        frameExtent = getIntersection(
          frameExtent,
          fromUserExtent(layerState.extent),
        );
      }

      const dx = (tileResolution * width) / 2 / tilePixelRatio;
      const dy = (tileResolution * height) / 2 / tilePixelRatio;
      const canvasExtent = [
        viewCenter[0] - dx,
        viewCenter[1] - dy,
        viewCenter[0] + dx,
        viewCenter[1] + dy,
      ];

      /**
       * @type {TileLookup}
       */
      const tilesByZ = {};

      this.renderedTiles.length = 0;

      /**
       * Part 1: Enqueue tiles
       */

      const preload = tileLayer.getPreload();
      if (frameState.nextExtent && this.enqueueTilesForNextExtent()) {
        const targetZ = tileGrid.getZForResolution(
          viewState.nextResolution,
          tileSource.zDirection,
        );
        const nextExtent = getRenderExtent(frameState, frameState.nextExtent);
        this.enqueueTiles(frameState, nextExtent, targetZ, tilesByZ, preload);
      }

      const renderExtent = getRenderExtent(frameState, frameExtent);
      this.enqueueTiles(frameState, renderExtent, z, tilesByZ, 0);
      if (preload > 0) {
        setTimeout(() => {
          this.enqueueTiles(
            frameState,
            renderExtent,
            z - 1,
            tilesByZ,
            preload - 1,
          );
        }, 0);
      }

      if (!(z in tilesByZ)) {
        return this.container;
      }

      /**
       * Part 2: Find alt tiles for those that are not yet loaded
       */

      const uid = getUid(this);
      const time = frameState.time;

      // look for cached tiles to use if a target tile is not ready
      for (const tile of tilesByZ[z]) {
        const tileState = tile.getState();
        if (tileState === TileState.EMPTY) {
          continue;
        }
        const tileCoord = tile.tileCoord;

        if (tileState === TileState.LOADED) {
          const alpha = tile.getAlpha(uid, time);
          if (alpha === 1) {
            // no need to look for alt tiles
            tile.endTransition(uid);
            continue;
          }
        }
        if (tileState !== TileState.ERROR) {
          this.renderComplete = false;
        }

        const hasStaleTile = this.findStaleTile_(tileCoord, tilesByZ);
        if (hasStaleTile) {
          // use the stale tile before the new tile's transition has completed
          removeTileFromLookup(tilesByZ, tile, z);
          frameState.animate = true;
          continue;
        }

        // first look for child tiles (at z + 1)
        const coveredByChildren = this.findAltTiles_(
          tileGrid,
          tileCoord,
          z + 1,
          tilesByZ,
        );

        if (coveredByChildren) {
          continue;
        }

        // next look for parent tiles
        const minZoom = tileGrid.getMinZoom();
        for (let parentZ = z - 1; parentZ >= minZoom; --parentZ) {
          const coveredByParent = this.findAltTiles_(
            tileGrid,
            tileCoord,
            parentZ,
            tilesByZ,
          );

          if (coveredByParent) {
            break;
          }
        }
      }

      /**
       * Part 3: Render loaded tiles
       */

      const canvasScale =
        ((tileResolution / viewResolution) * pixelRatio) / tilePixelRatio;

      const context = this.getRenderContext(frameState);

      // set scale transform for calculating tile positions on the canvas
      compose(
        this.tempTransform,
        width / 2,
        height / 2,
        canvasScale,
        canvasScale,
        0,
        -width / 2,
        -height / 2,
      );

      if (layerState.extent) {
        this.clipUnrotated(context, frameState, layerExtent);
      }

      if (!tileSource.getInterpolate()) {
        context.imageSmoothingEnabled = false;
      }

      this.preRender(context, frameState);

      /** @type {Array<number>} */
      const zs = Object.keys(tilesByZ).map(Number);
      zs.sort(ascending);

      let currentClip;
      const clips = [];
      const clipZs = [];
      for (let i = zs.length - 1; i >= 0; --i) {
        const currentZ = zs[i];
        const currentTilePixelSize = tileSource.getTilePixelSize(
          currentZ,
          pixelRatio,
          projection,
        );
        const currentResolution = tileGrid.getResolution(currentZ);
        const currentScale = currentResolution / tileResolution;
        const dx = currentTilePixelSize[0] * currentScale * canvasScale;
        const dy = currentTilePixelSize[1] * currentScale * canvasScale;
        const originTileCoord = tileGrid.getTileCoordForCoordAndZ(
          getTopLeft(canvasExtent),
          currentZ,
        );
        const originTileExtent = tileGrid.getTileCoordExtent(originTileCoord);
        const origin = apply(this.tempTransform, [
          (tilePixelRatio * (originTileExtent[0] - canvasExtent[0])) /
            tileResolution,
          (tilePixelRatio * (canvasExtent[3] - originTileExtent[3])) /
            tileResolution,
        ]);
        const tileGutter =
          tilePixelRatio * tileSource.getGutterForProjection(projection);
        for (const tile of tilesByZ[currentZ]) {
          if (tile.getState() !== TileState.LOADED) {
            continue;
          }
          const tileCoord = tile.tileCoord;

          // Calculate integer positions and sizes so that tiles align
          const xIndex = originTileCoord[1] - tileCoord[1];
          const nextX = Math.round(origin[0] - (xIndex - 1) * dx);
          const yIndex = originTileCoord[2] - tileCoord[2];
          const nextY = Math.round(origin[1] - (yIndex - 1) * dy);
          const x = Math.round(origin[0] - xIndex * dx);
          const y = Math.round(origin[1] - yIndex * dy);
          const w = nextX - x;
          const h = nextY - y;
          const transition = zs.length === 1;

          let contextSaved = false;

          // Clip mask for regions in this tile that already filled by a higher z tile
          currentClip = [x, y, x + w, y, x + w, y + h, x, y + h];
          for (let i = 0, ii = clips.length; i < ii; ++i) {
            if (!transition && currentZ < clipZs[i]) {
              const clip = clips[i];
              if (
                intersects$1(
                  [x, y, x + w, y + h],
                  [clip[0], clip[3], clip[4], clip[7]],
                )
              ) {
                if (!contextSaved) {
                  context.save();
                  contextSaved = true;
                }
                context.beginPath();
                // counter-clockwise (outer ring) for current tile
                context.moveTo(currentClip[0], currentClip[1]);
                context.lineTo(currentClip[2], currentClip[3]);
                context.lineTo(currentClip[4], currentClip[5]);
                context.lineTo(currentClip[6], currentClip[7]);
                // clockwise (inner ring) for higher z tile
                context.moveTo(clip[6], clip[7]);
                context.lineTo(clip[4], clip[5]);
                context.lineTo(clip[2], clip[3]);
                context.lineTo(clip[0], clip[1]);
                context.clip();
              }
            }
          }
          clips.push(currentClip);
          clipZs.push(currentZ);

          this.drawTile(tile, frameState, x, y, w, h, tileGutter, transition);
          if (contextSaved) {
            context.restore();
          }
          this.renderedTiles.unshift(tile);

          // TODO: decide if this is necessary
          this.updateUsedTiles(frameState.usedTiles, tileSource, tile);
        }
      }

      this.renderedResolution = tileResolution;
      this.extentChanged =
        !this.renderedExtent_ || !equals$1(this.renderedExtent_, canvasExtent);
      this.renderedExtent_ = canvasExtent;
      this.renderedPixelRatio = pixelRatio;

      this.postRender(this.context, frameState);

      if (layerState.extent) {
        context.restore();
      }
      context.imageSmoothingEnabled = true;

      if (this.renderComplete) {
        /**
         * @param {import("../../Map.js").default} map Map.
         * @param {import("../../Map.js").FrameState} frameState Frame state.
         */
        const postRenderFunction = (map, frameState) => {
          const tileSourceKey = getUid(tileSource);
          const wantedTiles = frameState.wantedTiles[tileSourceKey];
          const tilesCount = wantedTiles ? Object.keys(wantedTiles).length : 0;
          this.updateCacheSize(tilesCount);
          this.tileCache_.expireCache();
          this.sourceTileCache_?.expireCache();
        };

        frameState.postRenderFunctions.push(postRenderFunction);
      }

      // this normally is `div.ol-layer` and is a mocked div in worker
      return this.container;
    }

    /**
     * Increases the cache size if needed
     * @param {number} tileCount Minimum number of tiles needed.
     */
    updateCacheSize(tileCount) {
      this.tileCache_.highWaterMark = Math.max(
        this.tileCache_.highWaterMark,
        tileCount * 2,
      );
    }

    /**
     * @param {import("../../Tile.js").default} tile Tile.
     * @param {import("../../Map.js").FrameState} frameState Frame state.
     * @param {number} x Left of the tile.
     * @param {number} y Top of the tile.
     * @param {number} w Width of the tile.
     * @param {number} h Height of the tile.
     * @param {number} gutter Tile gutter.
     * @param {boolean} transition Apply an alpha transition.
     * @protected
     */
    drawTile(tile, frameState, x, y, w, h, gutter, transition) {
      let image;
      if (tile instanceof DataTile) {
        image = asImageLike(tile.getData());
        if (!image) {
          throw new Error('Rendering array data is not yet supported');
        }
      } else {
        image = this.getTileImage(
          /** @type {import("../../ImageTile.js").default} */ (tile),
        );
      }

      if (!image) {
        return;
      }
      const context = this.getRenderContext(frameState);
      const uid = getUid(this);
      const layerState = frameState.layerStatesArray[frameState.layerIndex];
      const alpha =
        layerState.opacity *
        (transition ? tile.getAlpha(uid, frameState.time) : 1);
      const alphaChanged = alpha !== context.globalAlpha;
      if (alphaChanged) {
        context.save();
        context.globalAlpha = alpha;
      }
      context.drawImage(
        image,
        gutter,
        gutter,
        image.width - 2 * gutter,
        image.height - 2 * gutter,
        x,
        y,
        w,
        h,
      );

      if (alphaChanged) {
        context.restore();
      }
      if (alpha !== layerState.opacity) {
        frameState.animate = true;
      } else if (transition) {
        tile.endTransition(uid);
      }
    }

    /**
     * @return {HTMLCanvasElement|OffscreenCanvas} Image
     */
    getImage() {
      const context = this.context;
      return context ? context.canvas : null;
    }

    /**
     * Get the image from a tile.
     * @param {import("../../ImageTile.js").default} tile Tile.
     * @return {HTMLCanvasElement|OffscreenCanvas|HTMLImageElement|HTMLVideoElement} Image.
     * @protected
     */
    getTileImage(tile) {
      return tile.getImage();
    }

    /**
     * @param {!Object<string, !Object<string, boolean>>} usedTiles Used tiles.
     * @param {import("../../source/Tile.js").default} tileSource Tile source.
     * @param {import('../../Tile.js').default} tile Tile.
     * @protected
     */
    updateUsedTiles(usedTiles, tileSource, tile) {
      // FIXME should we use tilesToDrawByZ instead?
      const tileSourceKey = getUid(tileSource);
      if (!(tileSourceKey in usedTiles)) {
        usedTiles[tileSourceKey] = {};
      }
      usedTiles[tileSourceKey][tile.getKey()] = true;
    }
  }

  /**
   * @module ol/layer/TileProperty
   */

  /**
   * @enum {string}
   */
  var TileProperty = {
    PRELOAD: 'preload',
    USE_INTERIM_TILES_ON_ERROR: 'useInterimTilesOnError',
  };

  /**
   * @module ol/layer/BaseTile
   */

  /***
   * @template Return
   * @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> &
   *   import("../Observable").OnSignature<import("./Base").BaseLayerObjectEventTypes|
   *     import("./Layer.js").LayerEventType|'change:preload'|'change:useInterimTilesOnError', import("../Object").ObjectEvent, Return> &
   *   import("../Observable").OnSignature<import("../render/EventType").LayerRenderEventTypes, import("../render/Event").default, Return> &
   *   import("../Observable").CombinedOnSignature<import("../Observable").EventTypes|import("./Base").BaseLayerObjectEventTypes|
   *   import("./Layer.js").LayerEventType|'change:preload'|'change:useInterimTilesOnError'|import("../render/EventType").LayerRenderEventTypes, Return>} BaseTileLayerOnSignature
   */

  /**
   * @template {import("../source/Tile.js").default} TileSourceType
   * @typedef {Object} Options
   * @property {string} [className='ol-layer'] A CSS class name to set to the layer element.
   * @property {number} [opacity=1] Opacity (0, 1).
   * @property {boolean} [visible=true] Visibility.
   * @property {import("../extent.js").Extent} [extent] The bounding extent for layer rendering.  The layer will not be
   * rendered outside of this extent.
   * @property {number} [zIndex] The z-index for layer rendering.  At rendering time, the layers
   * will be ordered, first by Z-index and then by position. When `undefined`, a `zIndex` of 0 is assumed
   * for layers that are added to the map's `layers` collection, or `Infinity` when the layer's `setMap()`
   * method was used.
   * @property {number} [minResolution] The minimum resolution (inclusive) at which this layer will be
   * visible.
   * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
   * be visible.
   * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
   * visible.
   * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
   * be visible.
   * @property {number} [preload=0] Preload. Load low-resolution tiles up to `preload` levels. `0`
   * means no preloading.
   * @property {TileSourceType} [source] Source for this layer.
   * @property {import("../Map.js").default} [map] Sets the layer as overlay on a map. The map will not manage
   * this layer in its layers collection, and the layer will be rendered on top. This is useful for
   * temporary layers. The standard way to add a layer to a map and have it managed by the map is to
   * use {@link import("../Map.js").default#addLayer map.addLayer()}.
   * @property {import("./Base.js").BackgroundColor} [background] Background color for the layer. If not specified, no background
   * will be rendered.
   * @property {boolean} [useInterimTilesOnError=true] Deprecated.  Use interim tiles on error.
   * @property {Object<string, *>} [properties] Arbitrary observable properties. Can be accessed with `#get()` and `#set()`.
   * @property {number} [cacheSize=512] The internal tile cache size.  This needs to be large enough to render
   * two zoom levels worth of tiles.
   */

  /**
   * @classdesc
   * For layer sources that provide pre-rendered, tiled images in grids that are
   * organized by zoom levels for specific resolutions.
   * Note that any property set in the options is set as a {@link module:ol/Object~BaseObject}
   * property on the layer object; for example, setting `title: 'My Title'` in the
   * options means that `title` is observable, and has get/set accessors.
   *
   * @template {import("../source/Tile.js").default} TileSourceType
   * @template {import("../renderer/Layer.js").default} RendererType
   * @extends {Layer<TileSourceType, RendererType>}
   * @api
   */
  class BaseTileLayer extends Layer {
    /**
     * @param {Options<TileSourceType>} [options] Tile layer options.
     */
    constructor(options) {
      options = options ? options : {};

      const baseOptions = Object.assign({}, options);

      const cacheSize = options.cacheSize;
      delete options.cacheSize;

      delete baseOptions.preload;
      delete baseOptions.useInterimTilesOnError;
      super(baseOptions);

      /***
       * @type {BaseTileLayerOnSignature<import("../events").EventsKey>}
       */
      this.on;

      /***
       * @type {BaseTileLayerOnSignature<import("../events").EventsKey>}
       */
      this.once;

      /***
       * @type {BaseTileLayerOnSignature<void>}
       */
      this.un;

      /**
       * @type {number|undefined}
       * @private
       */
      this.cacheSize_ = cacheSize;

      this.setPreload(options.preload !== undefined ? options.preload : 0);
      this.setUseInterimTilesOnError(
        options.useInterimTilesOnError !== undefined
          ? options.useInterimTilesOnError
          : true,
      );
    }

    /**
     * @return {number|undefined} The suggested cache size
     * @protected
     */
    getCacheSize() {
      return this.cacheSize_;
    }

    /**
     * Return the level as number to which we will preload tiles up to.
     * @return {number} The level to preload tiles up to.
     * @observable
     * @api
     */
    getPreload() {
      return /** @type {number} */ (this.get(TileProperty.PRELOAD));
    }

    /**
     * Set the level as number to which we will preload tiles up to.
     * @param {number} preload The level to preload tiles up to.
     * @observable
     * @api
     */
    setPreload(preload) {
      this.set(TileProperty.PRELOAD, preload);
    }

    /**
     * Deprecated.  Whether we use interim tiles on error.
     * @return {boolean} Use interim tiles on error.
     * @observable
     * @api
     */
    getUseInterimTilesOnError() {
      return /** @type {boolean} */ (
        this.get(TileProperty.USE_INTERIM_TILES_ON_ERROR)
      );
    }

    /**
     * Deprecated.  Set whether we use interim tiles on error.
     * @param {boolean} useInterimTilesOnError Use interim tiles on error.
     * @observable
     * @api
     */
    setUseInterimTilesOnError(useInterimTilesOnError) {
      this.set(TileProperty.USE_INTERIM_TILES_ON_ERROR, useInterimTilesOnError);
    }

    /**
     * Get data for a pixel location.  The return type depends on the source data.  For image tiles,
     * a four element RGBA array will be returned.  For data tiles, the array length will match the
     * number of bands in the dataset.  For requests outside the layer extent, `null` will be returned.
     * Data for a image tiles can only be retrieved if the source's `crossOrigin` property is set.
     *
     * ```js
     * // display layer data on every pointer move
     * map.on('pointermove', (event) => {
     *   console.log(layer.getData(event.pixel));
     * });
     * ```
     * @param {import("../pixel").Pixel} pixel Pixel.
     * @return {Uint8ClampedArray|Uint8Array|Float32Array|DataView|null} Pixel data.
     * @api
     * @override
     */
    getData(pixel) {
      return super.getData(pixel);
    }
  }

  /**
   * @module ol/layer/Tile
   */

  /**
   * @classdesc
   * For layer sources that provide pre-rendered, tiled images in grids that are
   * organized by zoom levels for specific resolutions.
   * Note that any property set in the options is set as a {@link module:ol/Object~BaseObject}
   * property on the layer object; for example, setting `title: 'My Title'` in the
   * options means that `title` is observable, and has get/set accessors.
   *
   * @template {import("../source/Tile.js").default} [TileSourceType=import("../source/Tile.js").default]
   * @extends BaseTileLayer<TileSourceType, CanvasTileLayerRenderer>
   * @api
   */
  class TileLayer extends BaseTileLayer {
    /**
     * @param {import("./BaseTile.js").Options<TileSourceType>} [options] Tile layer options.
     */
    constructor(options) {
      super(options);
    }

    /**
     * @override
     */
    createRenderer() {
      return new CanvasTileLayerRenderer(this, {
        cacheSize: this.getCacheSize(),
      });
    }
  }

  /**
   * @module ol/tilegrid/TileGrid
   */

  /**
   * @private
   * @type {import("../tilecoord.js").TileCoord}
   */
  const tmpTileCoord = [0, 0, 0];

  /**
   * Number of decimal digits to consider in integer values when rounding.
   * @type {number}
   */
  const DECIMALS = 5;

  /**
   * @typedef {Object} Options
   * @property {import("../extent.js").Extent} [extent] Extent for the tile grid. No tiles outside this
   * extent will be requested by {@link module:ol/source/Tile~TileSource} sources. When no `origin` or
   * `origins` are configured, the `origin` will be set to the top-left corner of the extent.
   * @property {number} [minZoom=0] Minimum zoom.
   * @property {import("../coordinate.js").Coordinate} [origin] The tile grid origin, i.e. where the `x`
   * and `y` axes meet (`[z, 0, 0]`). Tile coordinates increase left to right and downwards. If not
   * specified, `extent` or `origins` must be provided.
   * @property {Array<import("../coordinate.js").Coordinate>} [origins] Tile grid origins, i.e. where
   * the `x` and `y` axes meet (`[z, 0, 0]`), for each zoom level. If given, the array length
   * should match the length of the `resolutions` array, i.e. each resolution can have a different
   * origin. Tile coordinates increase left to right and downwards. If not specified, `extent` or
   * `origin` must be provided.
   * @property {!Array<number>} resolutions Resolutions. The array index of each resolution needs
   * to match the zoom level. This means that even if a `minZoom` is configured, the resolutions
   * array will have a length of `maxZoom + 1`.
   * @property {Array<import("../size.js").Size>} [sizes] Number of tile rows and columns
   * of the grid for each zoom level. If specified the values
   * define each zoom level's extent together with the `origin` or `origins`.
   * A grid `extent` can be configured in addition, and will further limit the extent
   * for which tile requests are made by sources. If the bottom-left corner of
   * an extent is used as `origin` or `origins`, then the `y` value must be
   * negative because OpenLayers tile coordinates use the top left as the origin.
   * @property {number|import("../size.js").Size} [tileSize] Tile size.
   * Default is `[256, 256]`.
   * @property {Array<number|import("../size.js").Size>} [tileSizes] Tile sizes. If given, the array length
   * should match the length of the `resolutions` array, i.e. each resolution can have a different
   * tile size.
   */

  /**
   * @classdesc
   * Base class for setting the grid pattern for sources accessing tiled-image
   * servers.
   * @api
   */
  class TileGrid {
    /**
     * @param {Options} options Tile grid options.
     */
    constructor(options) {
      /**
       * @protected
       * @type {number}
       */
      this.minZoom = options.minZoom !== undefined ? options.minZoom : 0;

      /**
       * @private
       * @type {!Array<number>}
       */
      this.resolutions_ = options.resolutions;
      assert(
        isSorted(
          this.resolutions_,
          /**
           * @param {number} a First resolution
           * @param {number} b Second resolution
           * @return {number} Comparison result
           */
          (a, b) => b - a),
        '`resolutions` must be sorted in descending order',
      );

      // check if we've got a consistent zoom factor and origin
      let zoomFactor;
      if (!options.origins) {
        for (let i = 0, ii = this.resolutions_.length - 1; i < ii; ++i) {
          if (!zoomFactor) {
            zoomFactor = this.resolutions_[i] / this.resolutions_[i + 1];
          } else {
            if (this.resolutions_[i] / this.resolutions_[i + 1] !== zoomFactor) {
              zoomFactor = undefined;
              break;
            }
          }
        }
      }

      /**
       * @private
       * @type {number|undefined}
       */
      this.zoomFactor_ = zoomFactor;

      /**
       * @protected
       * @type {number}
       */
      this.maxZoom = this.resolutions_.length - 1;

      /**
       * @private
       * @type {import("../coordinate.js").Coordinate|null}
       */
      this.origin_ = options.origin !== undefined ? options.origin : null;

      /**
       * @private
       * @type {Array<import("../coordinate.js").Coordinate>}
       */
      this.origins_ = null;
      if (options.origins !== undefined) {
        this.origins_ = options.origins;
        assert(
          this.origins_.length == this.resolutions_.length,
          'Number of `origins` and `resolutions` must be equal',
        );
      }

      const extent = options.extent;

      if (extent !== undefined && !this.origin_ && !this.origins_) {
        this.origin_ = getTopLeft(extent);
      }

      assert(
        (!this.origin_ && this.origins_) || (this.origin_ && !this.origins_),
        'Either `origin` or `origins` must be configured, never both',
      );

      /**
       * @private
       * @type {Array<number|import("../size.js").Size>}
       */
      this.tileSizes_ = null;
      if (options.tileSizes !== undefined) {
        this.tileSizes_ = options.tileSizes;
        assert(
          this.tileSizes_.length == this.resolutions_.length,
          'Number of `tileSizes` and `resolutions` must be equal',
        );
      }

      /**
       * @private
       * @type {number|import("../size.js").Size}
       */
      this.tileSize_ =
        options.tileSize !== undefined
          ? options.tileSize
          : !this.tileSizes_
            ? DEFAULT_TILE_SIZE
            : null;
      assert(
        (!this.tileSize_ && this.tileSizes_) ||
          (this.tileSize_ && !this.tileSizes_),
        'Either `tileSize` or `tileSizes` must be configured, never both',
      );

      /**
       * @private
       * @type {import("../extent.js").Extent}
       */
      this.extent_ = extent !== undefined ? extent : null;

      /**
       * @private
       * @type {Array<import("../TileRange.js").default>}
       */
      this.fullTileRanges_ = null;

      /**
       * @private
       * @type {import("../size.js").Size}
       */
      this.tmpSize_ = [0, 0];

      /**
       * @private
       * @type {import("../extent.js").Extent}
       */
      this.tmpExtent_ = [0, 0, 0, 0];

      if (options.sizes !== undefined) {
        this.fullTileRanges_ = options.sizes.map((size, z) => {
          const tileRange = new TileRange(
            Math.min(0, size[0]),
            Math.max(size[0] - 1, -1),
            Math.min(0, size[1]),
            Math.max(size[1] - 1, -1),
          );
          if (extent) {
            const restrictedTileRange = this.getTileRangeForExtentAndZ(extent, z);
            tileRange.minX = Math.max(restrictedTileRange.minX, tileRange.minX);
            tileRange.maxX = Math.min(restrictedTileRange.maxX, tileRange.maxX);
            tileRange.minY = Math.max(restrictedTileRange.minY, tileRange.minY);
            tileRange.maxY = Math.min(restrictedTileRange.maxY, tileRange.maxY);
          }
          return tileRange;
        });
      } else if (extent) {
        this.calculateTileRanges_(extent);
      }
    }

    /**
     * Call a function with each tile coordinate for a given extent and zoom level.
     *
     * @param {import("../extent.js").Extent} extent Extent.
     * @param {number} zoom Integer zoom level.
     * @param {function(import("../tilecoord.js").TileCoord): void} callback Function called with each tile coordinate.
     * @api
     */
    forEachTileCoord(extent, zoom, callback) {
      const tileRange = this.getTileRangeForExtentAndZ(extent, zoom);
      for (let i = tileRange.minX, ii = tileRange.maxX; i <= ii; ++i) {
        for (let j = tileRange.minY, jj = tileRange.maxY; j <= jj; ++j) {
          callback([zoom, i, j]);
        }
      }
    }

    /**
     * @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
     * @param {function(number, import("../TileRange.js").default): boolean} callback Callback.
     * @param {import("../TileRange.js").default} [tempTileRange] Temporary import("../TileRange.js").default object.
     * @param {import("../extent.js").Extent} [tempExtent] Temporary import("../extent.js").Extent object.
     * @return {boolean} Callback succeeded.
     */
    forEachTileCoordParentTileRange(
      tileCoord,
      callback,
      tempTileRange,
      tempExtent,
    ) {
      let tileRange, x, y;
      let tileCoordExtent = null;
      let z = tileCoord[0] - 1;
      if (this.zoomFactor_ === 2) {
        x = tileCoord[1];
        y = tileCoord[2];
      } else {
        tileCoordExtent = this.getTileCoordExtent(tileCoord, tempExtent);
      }
      while (z >= this.minZoom) {
        if (x !== undefined && y !== undefined) {
          x = Math.floor(x / 2);
          y = Math.floor(y / 2);
          tileRange = createOrUpdate$1(x, x, y, y, tempTileRange);
        } else {
          tileRange = this.getTileRangeForExtentAndZ(
            tileCoordExtent,
            z,
            tempTileRange,
          );
        }
        if (callback(z, tileRange)) {
          return true;
        }
        --z;
      }
      return false;
    }

    /**
     * Get the extent for this tile grid, if it was configured.
     * @return {import("../extent.js").Extent} Extent.
     * @api
     */
    getExtent() {
      return this.extent_;
    }

    /**
     * Get the maximum zoom level for the grid.
     * @return {number} Max zoom.
     * @api
     */
    getMaxZoom() {
      return this.maxZoom;
    }

    /**
     * Get the minimum zoom level for the grid.
     * @return {number} Min zoom.
     * @api
     */
    getMinZoom() {
      return this.minZoom;
    }

    /**
     * Get the origin for the grid at the given zoom level.
     * @param {number} z Integer zoom level.
     * @return {import("../coordinate.js").Coordinate} Origin.
     * @api
     */
    getOrigin(z) {
      if (this.origin_) {
        return this.origin_;
      }
      return this.origins_[z];
    }

    /**
     * Get the list of origins for the grid.
     * @return {Array<import("../coordinate.js").Coordinate>|null} Origin.
     */
    getOrigins() {
      return this.origins_;
    }

    /**
     * Get the resolution for the given zoom level.
     * @param {number} z Integer zoom level.
     * @return {number} Resolution.
     * @api
     */
    getResolution(z) {
      return this.resolutions_[z];
    }

    /**
     * Get the list of resolutions for the tile grid.
     * @return {Array<number>} Resolutions.
     * @api
     */
    getResolutions() {
      return this.resolutions_;
    }

    /**
     * @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
     * @param {import("../TileRange.js").default} [tempTileRange] Temporary import("../TileRange.js").default object.
     * @param {import("../extent.js").Extent} [tempExtent] Temporary import("../extent.js").Extent object.
     * @return {import("../TileRange.js").default|null} Tile range.
     */
    getTileCoordChildTileRange(tileCoord, tempTileRange, tempExtent) {
      if (tileCoord[0] < this.maxZoom) {
        if (this.zoomFactor_ === 2) {
          const minX = tileCoord[1] * 2;
          const minY = tileCoord[2] * 2;
          return createOrUpdate$1(
            minX,
            minX + 1,
            minY,
            minY + 1,
            tempTileRange,
          );
        }
        const tileCoordExtent = this.getTileCoordExtent(
          tileCoord,
          tempExtent || this.tmpExtent_,
        );
        return this.getTileRangeForExtentAndZ(
          tileCoordExtent,
          tileCoord[0] + 1,
          tempTileRange,
        );
      }
      return null;
    }

    /**
     * @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
     * @param {number} z Integer zoom level.
     * @param {import("../TileRange.js").default} [tempTileRange] Temporary import("../TileRange.js").default object.
     * @return {import("../TileRange.js").default|null} Tile range.
     */
    getTileRangeForTileCoordAndZ(tileCoord, z, tempTileRange) {
      if (z > this.maxZoom || z < this.minZoom) {
        return null;
      }

      const tileCoordZ = tileCoord[0];
      const tileCoordX = tileCoord[1];
      const tileCoordY = tileCoord[2];

      if (z === tileCoordZ) {
        return createOrUpdate$1(
          tileCoordX,
          tileCoordY,
          tileCoordX,
          tileCoordY,
          tempTileRange,
        );
      }

      if (this.zoomFactor_) {
        const factor = Math.pow(this.zoomFactor_, z - tileCoordZ);
        const minX = Math.floor(tileCoordX * factor);
        const minY = Math.floor(tileCoordY * factor);
        if (z < tileCoordZ) {
          return createOrUpdate$1(minX, minX, minY, minY, tempTileRange);
        }

        const maxX = Math.floor(factor * (tileCoordX + 1)) - 1;
        const maxY = Math.floor(factor * (tileCoordY + 1)) - 1;
        return createOrUpdate$1(minX, maxX, minY, maxY, tempTileRange);
      }

      const tileCoordExtent = this.getTileCoordExtent(tileCoord, this.tmpExtent_);
      return this.getTileRangeForExtentAndZ(tileCoordExtent, z, tempTileRange);
    }

    /**
     * Get a tile range for the given extent and integer zoom level.
     * @param {import("../extent.js").Extent} extent Extent.
     * @param {number} z Integer zoom level.
     * @param {import("../TileRange.js").default} [tempTileRange] Temporary tile range object.
     * @return {import("../TileRange.js").default} Tile range.
     */
    getTileRangeForExtentAndZ(extent, z, tempTileRange) {
      this.getTileCoordForXYAndZ_(extent[0], extent[3], z, false, tmpTileCoord);
      const minX = tmpTileCoord[1];
      const minY = tmpTileCoord[2];
      this.getTileCoordForXYAndZ_(extent[2], extent[1], z, true, tmpTileCoord);
      const maxX = tmpTileCoord[1];
      const maxY = tmpTileCoord[2];
      return createOrUpdate$1(minX, maxX, minY, maxY, tempTileRange);
    }

    /**
     * @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
     * @return {import("../coordinate.js").Coordinate} Tile center.
     */
    getTileCoordCenter(tileCoord) {
      const origin = this.getOrigin(tileCoord[0]);
      const resolution = this.getResolution(tileCoord[0]);
      const tileSize = toSize(this.getTileSize(tileCoord[0]), this.tmpSize_);
      return [
        origin[0] + (tileCoord[1] + 0.5) * tileSize[0] * resolution,
        origin[1] - (tileCoord[2] + 0.5) * tileSize[1] * resolution,
      ];
    }

    /**
     * Get the extent of a tile coordinate.
     *
     * @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
     * @param {import("../extent.js").Extent} [tempExtent] Temporary extent object.
     * @return {import("../extent.js").Extent} Extent.
     * @api
     */
    getTileCoordExtent(tileCoord, tempExtent) {
      const origin = this.getOrigin(tileCoord[0]);
      const resolution = this.getResolution(tileCoord[0]);
      const tileSize = toSize(this.getTileSize(tileCoord[0]), this.tmpSize_);
      const minX = origin[0] + tileCoord[1] * tileSize[0] * resolution;
      const minY = origin[1] - (tileCoord[2] + 1) * tileSize[1] * resolution;
      const maxX = minX + tileSize[0] * resolution;
      const maxY = minY + tileSize[1] * resolution;
      return createOrUpdate$2(minX, minY, maxX, maxY, tempExtent);
    }

    /**
     * Get the tile coordinate for the given map coordinate and resolution.  This
     * method considers that coordinates that intersect tile boundaries should be
     * assigned the higher tile coordinate.
     *
     * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
     * @param {number} resolution Resolution.
     * @param {import("../tilecoord.js").TileCoord} [opt_tileCoord] Destination import("../tilecoord.js").TileCoord object.
     * @return {import("../tilecoord.js").TileCoord} Tile coordinate.
     * @api
     */
    getTileCoordForCoordAndResolution(coordinate, resolution, opt_tileCoord) {
      return this.getTileCoordForXYAndResolution_(
        coordinate[0],
        coordinate[1],
        resolution,
        false,
        opt_tileCoord,
      );
    }

    /**
     * Note that this method should not be called for resolutions that correspond
     * to an integer zoom level.  Instead call the `getTileCoordForXYAndZ_` method.
     * @param {number} x X.
     * @param {number} y Y.
     * @param {number} resolution Resolution (for a non-integer zoom level).
     * @param {boolean} reverseIntersectionPolicy Instead of letting edge
     *     intersections go to the higher tile coordinate, let edge intersections
     *     go to the lower tile coordinate.
     * @param {import("../tilecoord.js").TileCoord} [opt_tileCoord] Temporary import("../tilecoord.js").TileCoord object.
     * @return {import("../tilecoord.js").TileCoord} Tile coordinate.
     * @private
     */
    getTileCoordForXYAndResolution_(
      x,
      y,
      resolution,
      reverseIntersectionPolicy,
      opt_tileCoord,
    ) {
      const z = this.getZForResolution(resolution);
      const scale = resolution / this.getResolution(z);
      const origin = this.getOrigin(z);
      const tileSize = toSize(this.getTileSize(z), this.tmpSize_);

      let tileCoordX = (scale * (x - origin[0])) / resolution / tileSize[0];
      let tileCoordY = (scale * (origin[1] - y)) / resolution / tileSize[1];

      if (reverseIntersectionPolicy) {
        tileCoordX = ceil(tileCoordX, DECIMALS) - 1;
        tileCoordY = ceil(tileCoordY, DECIMALS) - 1;
      } else {
        tileCoordX = floor(tileCoordX, DECIMALS);
        tileCoordY = floor(tileCoordY, DECIMALS);
      }

      return createOrUpdate(z, tileCoordX, tileCoordY, opt_tileCoord);
    }

    /**
     * Although there is repetition between this method and `getTileCoordForXYAndResolution_`,
     * they should have separate implementations.  This method is for integer zoom
     * levels.  The other method should only be called for resolutions corresponding
     * to non-integer zoom levels.
     * @param {number} x Map x coordinate.
     * @param {number} y Map y coordinate.
     * @param {number} z Integer zoom level.
     * @param {boolean} reverseIntersectionPolicy Instead of letting edge
     *     intersections go to the higher tile coordinate, let edge intersections
     *     go to the lower tile coordinate.
     * @param {import("../tilecoord.js").TileCoord} [opt_tileCoord] Temporary import("../tilecoord.js").TileCoord object.
     * @return {import("../tilecoord.js").TileCoord} Tile coordinate.
     * @private
     */
    getTileCoordForXYAndZ_(x, y, z, reverseIntersectionPolicy, opt_tileCoord) {
      const origin = this.getOrigin(z);
      const resolution = this.getResolution(z);
      const tileSize = toSize(this.getTileSize(z), this.tmpSize_);

      let tileCoordX = (x - origin[0]) / resolution / tileSize[0];
      let tileCoordY = (origin[1] - y) / resolution / tileSize[1];

      if (reverseIntersectionPolicy) {
        tileCoordX = ceil(tileCoordX, DECIMALS) - 1;
        tileCoordY = ceil(tileCoordY, DECIMALS) - 1;
      } else {
        tileCoordX = floor(tileCoordX, DECIMALS);
        tileCoordY = floor(tileCoordY, DECIMALS);
      }

      return createOrUpdate(z, tileCoordX, tileCoordY, opt_tileCoord);
    }

    /**
     * Get a tile coordinate given a map coordinate and zoom level.
     * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
     * @param {number} z Integer zoom level, e.g. the result of a `getZForResolution()` method call
     * @param {import("../tilecoord.js").TileCoord} [opt_tileCoord] Destination import("../tilecoord.js").TileCoord object.
     * @return {import("../tilecoord.js").TileCoord} Tile coordinate.
     * @api
     */
    getTileCoordForCoordAndZ(coordinate, z, opt_tileCoord) {
      return this.getTileCoordForXYAndZ_(
        coordinate[0],
        coordinate[1],
        z,
        false,
        opt_tileCoord,
      );
    }

    /**
     * @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
     * @return {number} Tile resolution.
     */
    getTileCoordResolution(tileCoord) {
      return this.resolutions_[tileCoord[0]];
    }

    /**
     * Get the tile size for a zoom level. The type of the return value matches the
     * `tileSize` or `tileSizes` that the tile grid was configured with. To always
     * get an {@link import("../size.js").Size}, run the result through {@link module:ol/size.toSize}.
     * @param {number} z Z.
     * @return {number|import("../size.js").Size} Tile size.
     * @api
     */
    getTileSize(z) {
      if (this.tileSize_) {
        return this.tileSize_;
      }
      return this.tileSizes_[z];
    }

    /**
     * @param {number} z Zoom level.
     * @return {import("../TileRange.js").default|null} Extent tile range for the specified zoom level.
     */
    getFullTileRange(z) {
      if (!this.fullTileRanges_) {
        return this.extent_
          ? this.getTileRangeForExtentAndZ(this.extent_, z)
          : null;
      }
      return this.fullTileRanges_[z];
    }

    /**
     * @param {number} resolution Resolution.
     * @param {number|import("../array.js").NearestDirectionFunction} [opt_direction]
     *     If 0, the nearest resolution will be used.
     *     If 1, the nearest higher resolution (lower Z) will be used. If -1, the
     *     nearest lower resolution (higher Z) will be used. Default is 0.
     *     Use a {@link module:ol/array~NearestDirectionFunction} for more precise control.
     *
     * For example to change tile Z at the midpoint of zoom levels
     * ```js
     * function(value, high, low) {
     *   return value - low * Math.sqrt(high / low);
     * }
     * ```
     * @return {number} Z.
     * @api
     */
    getZForResolution(resolution, opt_direction) {
      const z = linearFindNearest(
        this.resolutions_,
        resolution,
        opt_direction || 0,
      );
      return clamp(z, this.minZoom, this.maxZoom);
    }

    /**
     * The tile with the provided tile coordinate intersects the given viewport.
     * @param {import('../tilecoord.js').TileCoord} tileCoord Tile coordinate.
     * @param {Array<number>} viewport Viewport as returned from {@link module:ol/extent.getRotatedViewport}.
     * @return {boolean} The tile with the provided tile coordinate intersects the given viewport.
     */
    tileCoordIntersectsViewport(tileCoord, viewport) {
      return intersectsLinearRing(
        viewport,
        0,
        viewport.length,
        2,
        this.getTileCoordExtent(tileCoord),
      );
    }

    /**
     * @param {!import("../extent.js").Extent} extent Extent for this tile grid.
     * @private
     */
    calculateTileRanges_(extent) {
      const length = this.resolutions_.length;
      const fullTileRanges = new Array(length);
      for (let z = this.minZoom; z < length; ++z) {
        fullTileRanges[z] = this.getTileRangeForExtentAndZ(extent, z);
      }
      this.fullTileRanges_ = fullTileRanges;
    }
  }

  /**
   * @module ol/tilegrid
   */

  /**
   * @param {import("./proj/Projection.js").default} projection Projection.
   * @return {!TileGrid} Default tile grid for the
   * passed projection.
   */
  function getForProjection(projection) {
    let tileGrid = projection.getDefaultTileGrid();
    if (!tileGrid) {
      tileGrid = createForProjection(projection);
      projection.setDefaultTileGrid(tileGrid);
    }
    return tileGrid;
  }

  /**
   * @param {TileGrid} tileGrid Tile grid.
   * @param {import("./tilecoord.js").TileCoord} tileCoord Tile coordinate.
   * @param {import("./proj/Projection.js").default} projection Projection.
   * @return {import("./tilecoord.js").TileCoord} Tile coordinate.
   */
  function wrapX(tileGrid, tileCoord, projection) {
    const z = tileCoord[0];
    const center = tileGrid.getTileCoordCenter(tileCoord);
    const projectionExtent = extentFromProjection(projection);
    if (!containsCoordinate(projectionExtent, center)) {
      const worldWidth = getWidth(projectionExtent);
      const worldsAway = Math.ceil(
        (projectionExtent[0] - center[0]) / worldWidth,
      );
      center[0] += worldWidth * worldsAway;
      return tileGrid.getTileCoordForCoordAndZ(center, z);
    }
    return tileCoord;
  }

  /**
   * @param {import("./extent.js").Extent} extent Extent.
   * @param {number} [maxZoom] Maximum zoom level (default is
   *     DEFAULT_MAX_ZOOM).
   * @param {number|import("./size.js").Size} [tileSize] Tile size (default uses
   *     DEFAULT_TILE_SIZE).
   * @param {import("./extent.js").Corner} [corner] Extent corner (default is `'top-left'`).
   * @return {!TileGrid} TileGrid instance.
   */
  function createForExtent(extent, maxZoom, tileSize, corner) {
    corner = corner !== undefined ? corner : 'top-left';

    const resolutions = resolutionsFromExtent(extent, maxZoom, tileSize);

    return new TileGrid({
      extent: extent,
      origin: getCorner(extent, corner),
      resolutions: resolutions,
      tileSize: tileSize,
    });
  }

  /**
   * @typedef {Object} XYZOptions
   * @property {import("./extent.js").Extent} [extent] Extent for the tile grid. The origin for an XYZ tile grid is the
   * top-left corner of the extent. If `maxResolution` is not provided the zero level of the grid is defined by the resolution
   * at which one tile fits in the provided extent. If not provided, the extent of the EPSG:3857 projection is used.
   * @property {number} [maxResolution] Resolution at level zero.
   * @property {number} [maxZoom] Maximum zoom. The default is `42`. This determines the number of levels
   * in the grid set. For example, a `maxZoom` of 21 means there are 22 levels in the grid set.
   * @property {number} [minZoom=0] Minimum zoom.
   * @property {number|import("./size.js").Size} [tileSize=[256, 256]] Tile size in pixels.
   */

  /**
   * Creates a tile grid with a standard XYZ tiling scheme.
   * @param {XYZOptions} [options] Tile grid options.
   * @return {!TileGrid} Tile grid instance.
   * @api
   */
  function createXYZ(options) {
    const xyzOptions = options || {};

    const extent = xyzOptions.extent || get$1('EPSG:3857').getExtent();

    const gridOptions = {
      extent: extent,
      minZoom: xyzOptions.minZoom,
      tileSize: xyzOptions.tileSize,
      resolutions: resolutionsFromExtent(
        extent,
        xyzOptions.maxZoom,
        xyzOptions.tileSize,
        xyzOptions.maxResolution,
      ),
    };
    return new TileGrid(gridOptions);
  }

  /**
   * Create a resolutions array from an extent.  A zoom factor of 2 is assumed.
   * @param {import("./extent.js").Extent} extent Extent.
   * @param {number} [maxZoom] Maximum zoom level (default is
   *     DEFAULT_MAX_ZOOM).
   * @param {number|import("./size.js").Size} [tileSize] Tile size (default uses
   *     DEFAULT_TILE_SIZE).
   * @param {number} [maxResolution] Resolution at level zero.
   * @return {!Array<number>} Resolutions array.
   */
  function resolutionsFromExtent(extent, maxZoom, tileSize, maxResolution) {
    maxZoom = maxZoom !== undefined ? maxZoom : DEFAULT_MAX_ZOOM;
    tileSize = toSize(tileSize !== undefined ? tileSize : DEFAULT_TILE_SIZE);

    const height = getHeight(extent);
    const width = getWidth(extent);

    maxResolution =
      maxResolution > 0
        ? maxResolution
        : Math.max(width / tileSize[0], height / tileSize[1]);

    const length = maxZoom + 1;
    const resolutions = new Array(length);
    for (let z = 0; z < length; ++z) {
      resolutions[z] = maxResolution / Math.pow(2, z);
    }
    return resolutions;
  }

  /**
   * @param {import("./proj.js").ProjectionLike} projection Projection.
   * @param {number} [maxZoom] Maximum zoom level (default is
   *     DEFAULT_MAX_ZOOM).
   * @param {number|import("./size.js").Size} [tileSize] Tile size (default uses
   *     DEFAULT_TILE_SIZE).
   * @param {import("./extent.js").Corner} [corner] Extent corner (default is `'top-left'`).
   * @return {!TileGrid} TileGrid instance.
   */
  function createForProjection(projection, maxZoom, tileSize, corner) {
    const extent = extentFromProjection(projection);
    return createForExtent(extent, maxZoom, tileSize, corner);
  }

  /**
   * Generate a tile grid extent from a projection.  If the projection has an
   * extent, it is used.  If not, a global extent is assumed.
   * @param {import("./proj.js").ProjectionLike} projection Projection.
   * @return {import("./extent.js").Extent} Extent.
   */
  function extentFromProjection(projection) {
    projection = get$1(projection);
    let extent = projection.getExtent();
    if (!extent) {
      const half =
        (180 * METERS_PER_UNIT$1.degrees) / projection.getMetersPerUnit();
      extent = createOrUpdate$2(-half, -half, half, half);
    }
    return extent;
  }

  /**
   * @module ol/uri
   */


  const zRegEx = /\{z\}/g;
  const xRegEx = /\{x\}/g;
  const yRegEx = /\{y\}/g;
  const dashYRegEx = /\{-y\}/g;

  /**
   * @param {string} template The URL template.  Should have `{x}`, `{y}`, and `{z}` placeholders.  If
   * the template has a `{-y}` placeholder, the `maxY` parameter must be supplied.
   * @param {number} z The tile z coordinate.
   * @param {number} x The tile x coordinate.
   * @param {number} y The tile y coordinate.
   * @param {number} [maxY] The maximum y coordinate at the given z level.
   * @return {string} The URL.
   */
  function renderXYZTemplate(template, z, x, y, maxY) {
    return template
      .replace(zRegEx, z.toString())
      .replace(xRegEx, x.toString())
      .replace(yRegEx, y.toString())
      .replace(dashYRegEx, function () {
        if (maxY === undefined) {
          throw new Error(
            'If the URL template has a {-y} placeholder, the grid extent must be known',
          );
        }
        return (maxY - y).toString();
      });
  }

  /**
   * @param {string} url URL.
   * @return {Array<string>} Array of urls.
   */
  function expandUrl(url) {
    const urls = [];
    let match = /\{([a-z])-([a-z])\}/.exec(url);
    if (match) {
      // char range
      const startCharCode = match[1].charCodeAt(0);
      const stopCharCode = match[2].charCodeAt(0);
      let charCode;
      for (charCode = startCharCode; charCode <= stopCharCode; ++charCode) {
        urls.push(url.replace(match[0], String.fromCharCode(charCode)));
      }
      return urls;
    }
    match = /\{(\d+)-(\d+)\}/.exec(url);
    if (match) {
      // number range
      const stop = parseInt(match[2], 10);
      for (let i = parseInt(match[1], 10); i <= stop; i++) {
        urls.push(url.replace(match[0], i.toString()));
      }
      return urls;
    }
    urls.push(url);
    return urls;
  }

  /**
   * @module ol/tileurlfunction
   */

  /**
   * @param {string} template Template.
   * @param {import("./tilegrid/TileGrid.js").default|null} tileGrid Tile grid.
   * @return {import("./Tile.js").UrlFunction} Tile URL function.
   */
  function createFromTemplate(template, tileGrid) {
    return (
      /**
       * @param {import("./tilecoord.js").TileCoord} tileCoord Tile Coordinate.
       * @param {number} pixelRatio Pixel ratio.
       * @param {import("./proj/Projection.js").default} projection Projection.
       * @return {string|undefined} Tile URL.
       */
      function (tileCoord, pixelRatio, projection) {
        if (!tileCoord) {
          return undefined;
        }
        let maxY;
        const z = tileCoord[0];
        if (tileGrid) {
          // The `{-y}` placeholder only works for sources that have a tile grid at construction
          const range = tileGrid.getFullTileRange(z);
          if (range) {
            maxY = range.getHeight() - 1;
          }
        }
        return renderXYZTemplate(template, z, tileCoord[1], tileCoord[2], maxY);
      }
    );
  }

  /**
   * @param {Array<string>} templates Templates.
   * @param {import("./tilegrid/TileGrid.js").default} tileGrid Tile grid.
   * @return {import("./Tile.js").UrlFunction} Tile URL function.
   */
  function createFromTemplates(templates, tileGrid) {
    const len = templates.length;
    const tileUrlFunctions = new Array(len);
    for (let i = 0; i < len; ++i) {
      tileUrlFunctions[i] = createFromTemplate(templates[i], tileGrid);
    }
    return createFromTileUrlFunctions(tileUrlFunctions);
  }

  /**
   * @param {Array<import("./Tile.js").UrlFunction>} tileUrlFunctions Tile URL Functions.
   * @return {import("./Tile.js").UrlFunction} Tile URL function.
   */
  function createFromTileUrlFunctions(tileUrlFunctions) {
    if (tileUrlFunctions.length === 1) {
      return tileUrlFunctions[0];
    }
    return (
      /**
       * @param {import("./tilecoord.js").TileCoord} tileCoord Tile Coordinate.
       * @param {number} pixelRatio Pixel ratio.
       * @param {import("./proj/Projection.js").default} projection Projection.
       * @return {string|undefined} Tile URL.
       */
      function (tileCoord, pixelRatio, projection) {
        if (!tileCoord) {
          return undefined;
        }
        const h = hash(tileCoord);
        const index = modulo(h, tileUrlFunctions.length);
        return tileUrlFunctions[index](tileCoord, pixelRatio, projection);
      }
    );
  }

  /**
   * @module ol/source/Tile
   */

  /***
   * @template Return
   * @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> &
   *   import("../Observable").OnSignature<import("../ObjectEventType").Types, import("../Object").ObjectEvent, Return> &
   *   import("../Observable").OnSignature<import("./TileEventType").TileSourceEventTypes, TileSourceEvent, Return> &
   *   import("../Observable").CombinedOnSignature<import("../Observable").EventTypes|import("../ObjectEventType").Types|
   *     import("./TileEventType").TileSourceEventTypes, Return>} TileSourceOnSignature
   */

  /**
   * @typedef {Object} Options
   * @property {import("./Source.js").AttributionLike} [attributions] Attributions.
   * @property {boolean} [attributionsCollapsible=true] Attributions are collapsible.
   * @property {number} [cacheSize] Deprecated.  Use the cacheSize option on the layer instead.
   * @property {number} [tilePixelRatio] TilePixelRatio.
   * @property {import("../proj.js").ProjectionLike} [projection] Projection.
   * @property {import("./Source.js").State} [state] State.
   * @property {import("../tilegrid/TileGrid.js").default} [tileGrid] TileGrid.
   * @property {boolean} [wrapX=false] WrapX.
   * @property {number} [transition] Transition.
   * @property {string} [key] Key.
   * @property {number|import("../array.js").NearestDirectionFunction} [zDirection=0] ZDirection.
   * @property {boolean} [interpolate=false] Use interpolated values when resampling.  By default,
   * the nearest neighbor is used when resampling.
   */

  /**
   * @classdesc
   * Abstract base class; normally only used for creating subclasses and not
   * instantiated in apps.
   * Base class for sources providing images divided into a tile grid.
   *
   * @template {import("../Tile.js").default} [TileType=import("../Tile.js").default]
   * @abstract
   * @api
   */
  class TileSource extends Source {
    /**
     * @param {Options} options SourceTile source options.
     */
    constructor(options) {
      super({
        attributions: options.attributions,
        attributionsCollapsible: options.attributionsCollapsible,
        projection: options.projection,
        state: options.state,
        wrapX: options.wrapX,
        interpolate: options.interpolate,
      });

      /***
       * @type {TileSourceOnSignature<import("../events").EventsKey>}
       */
      this.on;

      /***
       * @type {TileSourceOnSignature<import("../events").EventsKey>}
       */
      this.once;

      /***
       * @type {TileSourceOnSignature<void>}
       */
      this.un;

      /**
       * @private
       * @type {number}
       */
      this.tilePixelRatio_ =
        options.tilePixelRatio !== undefined ? options.tilePixelRatio : 1;

      /**
       * @type {import("../tilegrid/TileGrid.js").default|null}
       * @protected
       */
      this.tileGrid = options.tileGrid !== undefined ? options.tileGrid : null;

      const tileSize = [256, 256];
      if (this.tileGrid) {
        toSize(this.tileGrid.getTileSize(this.tileGrid.getMinZoom()), tileSize);
      }

      /**
       * @protected
       * @type {import("../size.js").Size}
       */
      this.tmpSize = [0, 0];

      /**
       * @private
       * @type {string}
       */
      this.key_ = options.key || getUid(this);

      /**
       * @protected
       * @type {import("../Tile.js").Options}
       */
      this.tileOptions = {
        transition: options.transition,
        interpolate: options.interpolate,
      };

      /**
       * zDirection hint, read by the renderer. Indicates which resolution should be used
       * by a renderer if the views resolution does not match any resolution of the tile source.
       * If 0, the nearest resolution will be used. If 1, the nearest lower resolution
       * will be used. If -1, the nearest higher resolution will be used.
       * @type {number|import("../array.js").NearestDirectionFunction}
       */
      this.zDirection = options.zDirection ? options.zDirection : 0;
    }

    /**
     * @param {import("../proj/Projection.js").default} projection Projection.
     * @return {number} Gutter.
     */
    getGutterForProjection(projection) {
      return 0;
    }

    /**
     * Return the key to be used for all tiles in the source.
     * @return {string} The key for all tiles.
     */
    getKey() {
      return this.key_;
    }

    /**
     * Set the value to be used as the key for all tiles in the source.
     * @param {string} key The key for tiles.
     * @protected
     */
    setKey(key) {
      if (this.key_ !== key) {
        this.key_ = key;
        this.changed();
      }
    }

    /**
     * @param {import("../proj/Projection").default} [projection] Projection.
     * @return {Array<number>|null} Resolutions.
     * @override
     */
    getResolutions(projection) {
      const tileGrid = projection
        ? this.getTileGridForProjection(projection)
        : this.tileGrid;
      if (!tileGrid) {
        return null;
      }
      return tileGrid.getResolutions();
    }

    /**
     * @abstract
     * @param {number} z Tile coordinate z.
     * @param {number} x Tile coordinate x.
     * @param {number} y Tile coordinate y.
     * @param {number} pixelRatio Pixel ratio.
     * @param {import("../proj/Projection.js").default} projection Projection.
     * @param {import("../structs/LRUCache.js").default<import("../Tile.js").default>} [tileCache] Tile cache.
     * @return {TileType|null} Tile.
     */
    getTile(z, x, y, pixelRatio, projection, tileCache) {
      return abstract();
    }

    /**
     * Return the tile grid of the tile source.
     * @return {import("../tilegrid/TileGrid.js").default|null} Tile grid.
     * @api
     */
    getTileGrid() {
      return this.tileGrid;
    }

    /**
     * @param {import("../proj/Projection.js").default} projection Projection.
     * @return {!import("../tilegrid/TileGrid.js").default} Tile grid.
     */
    getTileGridForProjection(projection) {
      if (!this.tileGrid) {
        return getForProjection(projection);
      }
      return this.tileGrid;
    }

    /**
     * Get the tile pixel ratio for this source. Subclasses may override this
     * method, which is meant to return a supported pixel ratio that matches the
     * provided `pixelRatio` as close as possible.
     * @param {number} pixelRatio Pixel ratio.
     * @return {number} Tile pixel ratio.
     */
    getTilePixelRatio(pixelRatio) {
      return this.tilePixelRatio_;
    }

    /**
     * @param {number} z Z.
     * @param {number} pixelRatio Pixel ratio.
     * @param {import("../proj/Projection.js").default} projection Projection.
     * @return {import("../size.js").Size} Tile size.
     */
    getTilePixelSize(z, pixelRatio, projection) {
      const tileGrid = this.getTileGridForProjection(projection);
      const tilePixelRatio = this.getTilePixelRatio(pixelRatio);
      const tileSize = toSize(tileGrid.getTileSize(z), this.tmpSize);
      if (tilePixelRatio == 1) {
        return tileSize;
      }
      return scale(tileSize, tilePixelRatio, this.tmpSize);
    }

    /**
     * Returns a tile coordinate wrapped around the x-axis. When the tile coordinate
     * is outside the resolution and extent range of the tile grid, `null` will be
     * returned.
     * @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
     * @param {import("../proj/Projection.js").default} [projection] Projection.
     * @return {import("../tilecoord.js").TileCoord} Tile coordinate to be passed to the tileUrlFunction or
     *     null if no tile URL should be created for the passed `tileCoord`.
     */
    getTileCoordForTileUrlFunction(tileCoord, projection) {
      const gridProjection =
        projection !== undefined ? projection : this.getProjection();
      const tileGrid =
        projection !== undefined
          ? this.getTileGridForProjection(gridProjection)
          : this.tileGrid || this.getTileGridForProjection(gridProjection);
      if (this.getWrapX() && gridProjection.isGlobal()) {
        tileCoord = wrapX(tileGrid, tileCoord, gridProjection);
      }
      return withinExtentAndZ(tileCoord, tileGrid) ? tileCoord : null;
    }

    /**
     * Remove all cached reprojected tiles from the source. The next render cycle will create new tiles.
     * @api
     */
    clear() {}

    /**
     * @override
     */
    refresh() {
      this.clear();
      super.refresh();
    }
  }

  /**
   * @classdesc
   * Events emitted by {@link module:ol/source/Tile~TileSource} instances are instances of this
   * type.
   */
  class TileSourceEvent extends BaseEvent {
    /**
     * @param {string} type Type.
     * @param {import("../Tile.js").default} tile The tile.
     */
    constructor(type, tile) {
      super(type);

      /**
       * The tile related to the event.
       * @type {import("../Tile.js").default}
       * @api
       */
      this.tile = tile;
    }
  }

  /**
   * @module ol/source/TileEventType
   */

  /**
   * @enum {string}
   */
  var TileEventType = {
    /**
     * Triggered when a tile starts loading.
     * @event module:ol/source/Tile.TileSourceEvent#tileloadstart
     * @api
     */
    TILELOADSTART: 'tileloadstart',

    /**
     * Triggered when a tile finishes loading, either when its data is loaded,
     * or when loading was aborted because the tile is no longer needed.
     * @event module:ol/source/Tile.TileSourceEvent#tileloadend
     * @api
     */
    TILELOADEND: 'tileloadend',

    /**
     * Triggered if tile loading results in an error. Note that this is not the
     * right place to re-fetch tiles. See {@link module:ol/ImageTile~ImageTile#load}
     * for details.
     * @event module:ol/source/Tile.TileSourceEvent#tileloaderror
     * @api
     */
    TILELOADERROR: 'tileloaderror',
  };

  /**
   * @typedef {'tileloadstart'|'tileloadend'|'tileloaderror'} TileSourceEventTypes
   */

  /**
   * @module ol/source/UrlTile
   */

  /**
   * @typedef {Object} Options
   * @property {import("./Source.js").AttributionLike} [attributions] Attributions.
   * @property {boolean} [attributionsCollapsible=true] Attributions are collapsible.
   * @property {number} [cacheSize] Deprecated.  Use the cacheSize option on the layer instead.
   * @property {import("../proj.js").ProjectionLike} [projection] Projection.
   * @property {import("./Source.js").State} [state] State.
   * @property {import("../tilegrid/TileGrid.js").default} [tileGrid] TileGrid.
   * @property {import("../Tile.js").LoadFunction} tileLoadFunction TileLoadFunction.
   * @property {number} [tilePixelRatio] TilePixelRatio.
   * @property {import("../Tile.js").UrlFunction} [tileUrlFunction] Deprecated.  Use an ImageTile source and provide a function
   * for the url option instead.
   * @property {string} [url] Url.
   * @property {Array<string>} [urls] Urls.
   * @property {boolean} [wrapX=true] WrapX.
   * @property {number} [transition] Transition.
   * @property {string} [key] Key.
   * @property {number|import("../array.js").NearestDirectionFunction} [zDirection=0] ZDirection.
   * @property {boolean} [interpolate=false] Use interpolated values when resampling.  By default,
   * the nearest neighbor is used when resampling.
   */

  /**
   * @deprecated Use the ol/source/ImageTile.js instead.
   *
   * @fires import("./Tile.js").TileSourceEvent
   */
  class UrlTile extends TileSource {
    /**
     * @param {Options} options Image tile options.
     */
    constructor(options) {
      super({
        attributions: options.attributions,
        cacheSize: options.cacheSize,
        projection: options.projection,
        state: options.state,
        tileGrid: options.tileGrid,
        tilePixelRatio: options.tilePixelRatio,
        wrapX: options.wrapX,
        transition: options.transition,
        interpolate: options.interpolate,
        key: options.key,
        attributionsCollapsible: options.attributionsCollapsible,
        zDirection: options.zDirection,
      });

      /**
       * @private
       * @type {boolean}
       */
      this.generateTileUrlFunction_ =
        this.tileUrlFunction === UrlTile.prototype.tileUrlFunction;

      /**
       * @protected
       * @type {import("../Tile.js").LoadFunction}
       */
      this.tileLoadFunction = options.tileLoadFunction;

      if (options.tileUrlFunction) {
        this.tileUrlFunction = options.tileUrlFunction;
      }

      /**
       * @protected
       * @type {!Array<string>|null}
       */
      this.urls = null;

      if (options.urls) {
        this.setUrls(options.urls);
      } else if (options.url) {
        this.setUrl(options.url);
      }

      /**
       * @private
       * @type {!Object<string, boolean>}
       */
      this.tileLoadingKeys_ = {};
    }

    /**
     * Deprecated.  Use an ImageTile source instead.
     * Return the tile load function of the source.
     * @return {import("../Tile.js").LoadFunction} TileLoadFunction
     * @api
     */
    getTileLoadFunction() {
      return this.tileLoadFunction;
    }

    /**
     * Deprecated.  Use an ImageTile source instead.
     * Return the tile URL function of the source.
     * @return {import("../Tile.js").UrlFunction} TileUrlFunction
     * @api
     */
    getTileUrlFunction() {
      return Object.getPrototypeOf(this).tileUrlFunction === this.tileUrlFunction
        ? this.tileUrlFunction.bind(this)
        : this.tileUrlFunction;
    }

    /**
     * Deprecated.  Use an ImageTile source instead.
     * Return the URLs used for this source.
     * When a tileUrlFunction is used instead of url or urls,
     * null will be returned.
     * @return {!Array<string>|null} URLs.
     * @api
     */
    getUrls() {
      return this.urls;
    }

    /**
     * Handle tile change events.
     * @param {import("../events/Event.js").default} event Event.
     * @protected
     */
    handleTileChange(event) {
      const tile = /** @type {import("../Tile.js").default} */ (event.target);
      const uid = getUid(tile);
      const tileState = tile.getState();
      let type;
      if (tileState == TileState.LOADING) {
        this.tileLoadingKeys_[uid] = true;
        type = TileEventType.TILELOADSTART;
      } else if (uid in this.tileLoadingKeys_) {
        delete this.tileLoadingKeys_[uid];
        type =
          tileState == TileState.ERROR
            ? TileEventType.TILELOADERROR
            : tileState == TileState.LOADED
              ? TileEventType.TILELOADEND
              : undefined;
      }
      if (type != undefined) {
        this.dispatchEvent(new TileSourceEvent(type, tile));
      }
    }

    /**
     * Deprecated.  Use an ImageTile source instead.
     * Set the tile load function of the source.
     * @param {import("../Tile.js").LoadFunction} tileLoadFunction Tile load function.
     * @api
     */
    setTileLoadFunction(tileLoadFunction) {
      this.tileLoadFunction = tileLoadFunction;
      this.changed();
    }

    /**
     * Deprecated.  Use an ImageTile source instead.
     * Set the tile URL function of the source.
     * @param {import("../Tile.js").UrlFunction} tileUrlFunction Tile URL function.
     * @param {string} [key] Optional new tile key for the source.
     * @api
     */
    setTileUrlFunction(tileUrlFunction, key) {
      this.tileUrlFunction = tileUrlFunction;
      if (typeof key !== 'undefined') {
        this.setKey(key);
      } else {
        this.changed();
      }
    }

    /**
     * Set the URL to use for requests.
     * @param {string} url URL.
     * @api
     */
    setUrl(url) {
      const urls = expandUrl(url);
      this.urls = urls;
      this.setUrls(urls);
    }

    /**
     * Deprecated.  Use an ImageTile source instead.
     * Set the URLs to use for requests.
     * @param {Array<string>} urls URLs.
     * @api
     */
    setUrls(urls) {
      this.urls = urls;
      const key = urls.join('\n');
      if (this.generateTileUrlFunction_) {
        this.setTileUrlFunction(createFromTemplates(urls, this.tileGrid), key);
      } else {
        this.setKey(key);
      }
    }

    /**
     * @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
     * @param {number} pixelRatio Pixel ratio.
     * @param {import("../proj/Projection.js").default} projection Projection.
     * @return {string|undefined} Tile URL.
     */
    tileUrlFunction(tileCoord, pixelRatio, projection) {
      return undefined;
    }
  }

  /**
   * @module ol/source/TileImage
   */

  /**
   * @typedef {Object} Options
   * @property {import("./Source.js").AttributionLike} [attributions] Attributions.
   * @property {boolean} [attributionsCollapsible=true] Attributions are collapsible.
   * @property {number} [cacheSize] Deprecated.  Use the cacheSize option on the layer instead.
   * @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images.  Note that
   * you must provide a `crossOrigin` value if you want to access pixel data with the Canvas renderer.
   * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.
   * @property {ReferrerPolicy} [referrerPolicy] The `referrerPolicy` property for loaded images.
   * @property {boolean} [interpolate=true] Use interpolated values when resampling.  By default,
   * linear interpolation is used when resampling.  Set to false to use the nearest neighbor instead.
   * @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection.
   * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels).
   * Higher values can increase reprojection performance, but decrease precision.
   * @property {import("./Source.js").State} [state] Source state.
   * @property {typeof import("../ImageTile.js").default} [tileClass] Class used to instantiate image tiles.
   * Default is {@link module:ol/ImageTile~ImageTile}.
   * @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid.
   * @property {import("../Tile.js").LoadFunction} [tileLoadFunction] Optional function to load a tile given a URL. The default is
   * ```js
   * function(imageTile, src) {
   *   imageTile.getImage().src = src;
   * };
   * ```
   * @property {number} [tilePixelRatio=1] The pixel ratio used by the tile service. For example, if the tile
   * service advertizes 256px by 256px tiles but actually sends 512px
   * by 512px images (for retina/hidpi devices) then `tilePixelRatio`
   * should be set to `2`.
   * @property {import("../Tile.js").UrlFunction} [tileUrlFunction] Deprecated.  Use an ImageTile source and provide a function
   * for the url option instead.
   * @property {string} [url] URL template. Must include `{x}`, `{y}` or `{-y}`, and `{z}` placeholders.
   * A `{?-?}` template pattern, for example `subdomain{a-f}.domain.com`, may be
   * used instead of defining each one separately in the `urls` option.
   * @property {Array<string>} [urls] An array of URL templates.
   * @property {boolean} [wrapX] Whether to wrap the world horizontally. The default, is to
   * request out-of-bounds tiles from the server. When set to `false`, only one
   * world will be rendered. When set to `true`, tiles will be requested for one
   * world only, but they will be wrapped horizontally to render multiple worlds.
   * @property {number} [transition] Duration of the opacity transition for rendering.
   * To disable the opacity transition, pass `transition: 0`.
   * @property {string} [key] Optional tile key for proper cache fetching
   * @property {number|import("../array.js").NearestDirectionFunction} [zDirection=0]
   * Choose whether to use tiles with a higher or lower zoom level when between integer
   * zoom levels. See {@link module:ol/tilegrid/TileGrid~TileGrid#getZForResolution}.
   */

  /**
   * @deprecated Use the ol/source/ImageTile.js instead.
   *
   * @fires import("./Tile.js").TileSourceEvent
   * @api
   */
  class TileImage extends UrlTile {
    /**
     * @param {!Options} options Image tile options.
     */
    constructor(options) {
      super({
        attributions: options.attributions,
        cacheSize: options.cacheSize,
        projection: options.projection,
        state: options.state,
        tileGrid: options.tileGrid,
        tileLoadFunction: options.tileLoadFunction
          ? options.tileLoadFunction
          : defaultTileLoadFunction,
        tilePixelRatio: options.tilePixelRatio,
        tileUrlFunction: options.tileUrlFunction,
        url: options.url,
        urls: options.urls,
        wrapX: options.wrapX,
        transition: options.transition,
        interpolate:
          options.interpolate !== undefined ? options.interpolate : true,
        key: options.key,
        attributionsCollapsible: options.attributionsCollapsible,
        zDirection: options.zDirection,
      });

      /**
       * @protected
       * @type {?string}
       */
      this.crossOrigin =
        options.crossOrigin !== undefined ? options.crossOrigin : null;

      /**
       * @protected
       * @type {ReferrerPolicy}
       */
      this.referrerPolicy = options.referrerPolicy;

      /**
       * @protected
       * @type {typeof ImageTile}
       */
      this.tileClass =
        options.tileClass !== undefined ? options.tileClass : ImageTile;

      /**
       * @protected
       * @type {!Object<string, import("../tilegrid/TileGrid.js").default>}
       */
      this.tileGridForProjection = {};

      /**
       * @private
       * @type {number|undefined}
       */
      this.reprojectionErrorThreshold_ = options.reprojectionErrorThreshold;

      /**
       * @private
       * @type {boolean}
       */
      this.renderReprojectionEdges_ = false;
    }

    /**
     * @param {import("../proj/Projection.js").default} projection Projection.
     * @return {number} Gutter.
     * @override
     */
    getGutterForProjection(projection) {
      if (
        this.getProjection() &&
        projection &&
        !equivalent$1(this.getProjection(), projection)
      ) {
        return 0;
      }
      return this.getGutter();
    }

    /**
     * @return {number} Gutter.
     */
    getGutter() {
      return 0;
    }

    /**
     * Return the key to be used for all tiles in the source.
     * @return {string} The key for all tiles.
     * @override
     */
    getKey() {
      let key = super.getKey();
      if (!this.getInterpolate()) {
        key += ':disable-interpolation';
      }
      return key;
    }

    /**
     * @param {import("../proj/Projection.js").default} projection Projection.
     * @return {!import("../tilegrid/TileGrid.js").default} Tile grid.
     * @override
     */
    getTileGridForProjection(projection) {
      const thisProj = this.getProjection();
      if (this.tileGrid && (!thisProj || equivalent$1(thisProj, projection))) {
        return this.tileGrid;
      }
      const projKey = getUid(projection);
      if (!(projKey in this.tileGridForProjection)) {
        this.tileGridForProjection[projKey] =
          getForProjection(projection);
      }
      return this.tileGridForProjection[projKey];
    }

    /**
     * @param {number} z Tile coordinate z.
     * @param {number} x Tile coordinate x.
     * @param {number} y Tile coordinate y.
     * @param {number} pixelRatio Pixel ratio.
     * @param {import("../proj/Projection.js").default} projection Projection.
     * @param {string} key The key set on the tile.
     * @return {!ImageTile} Tile.
     * @private
     */
    createTile_(z, x, y, pixelRatio, projection, key) {
      const tileCoord = [z, x, y];
      const urlTileCoord = this.getTileCoordForTileUrlFunction(
        tileCoord,
        projection,
      );
      const tileUrl = urlTileCoord
        ? this.tileUrlFunction(urlTileCoord, pixelRatio, projection)
        : undefined;
      const tile = new this.tileClass(
        tileCoord,
        tileUrl !== undefined ? TileState.IDLE : TileState.EMPTY,
        tileUrl !== undefined ? tileUrl : '',
        {
          crossOrigin: this.crossOrigin,
          referrerPolicy: this.referrerPolicy,
        },
        this.tileLoadFunction,
        this.tileOptions,
      );
      tile.key = key;
      tile.addEventListener(EventType.CHANGE, this.handleTileChange.bind(this));
      return tile;
    }

    /**
     * @param {number} z Tile coordinate z.
     * @param {number} x Tile coordinate x.
     * @param {number} y Tile coordinate y.
     * @param {number} pixelRatio Pixel ratio.
     * @param {import("../proj/Projection.js").default} projection Projection.
     * @param {import("../structs/LRUCache.js").default<import("../Tile.js").default>} [tileCache] Tile cache.
     * @return {!(ImageTile|ReprojTile)} Tile.
     * @override
     */
    getTile(z, x, y, pixelRatio, projection, tileCache) {
      const sourceProjection = this.getProjection();
      if (
        !sourceProjection ||
        !projection ||
        equivalent$1(sourceProjection, projection)
      ) {
        return this.getTileInternal(
          z,
          x,
          y,
          pixelRatio,
          sourceProjection || projection,
        );
      }
      const tileCoord = [z, x, y];
      const key = this.getKey();
      const sourceTileGrid = this.getTileGridForProjection(sourceProjection);
      const targetTileGrid = this.getTileGridForProjection(projection);
      const wrappedTileCoord = this.getTileCoordForTileUrlFunction(
        tileCoord,
        projection,
      );
      const tile = new ReprojTile(
        sourceProjection,
        sourceTileGrid,
        projection,
        targetTileGrid,
        tileCoord,
        wrappedTileCoord,
        this.getTilePixelRatio(pixelRatio),
        this.getGutter(),
        (z, x, y, pixelRatio) =>
          this.getTileInternal(z, x, y, pixelRatio, sourceProjection, tileCache),
        this.reprojectionErrorThreshold_,
        this.renderReprojectionEdges_,
        this.tileOptions,
      );
      tile.key = key;
      return tile;
    }

    /**
     * @param {number} z Tile coordinate z.
     * @param {number} x Tile coordinate x.
     * @param {number} y Tile coordinate y.
     * @param {number} pixelRatio Pixel ratio.
     * @param {!import("../proj/Projection.js").default} projection Projection.
     * @param {import("../structs/LRUCache.js").default<import("../Tile.js").default>} [tileCache] Tile cache.
     * @return {!ImageTile} Tile.
     * @protected
     */
    getTileInternal(z, x, y, pixelRatio, projection, tileCache) {
      const key = this.getKey();
      const cacheKey = getCacheKey(this, key, z, x, y);
      if (tileCache && tileCache.containsKey(cacheKey)) {
        const tile = /** @type {!ImageTile} */ (tileCache.get(cacheKey));
        return tile;
      }
      const tile = this.createTile_(z, x, y, pixelRatio, projection, key);
      tileCache?.set(cacheKey, tile);
      return tile;
    }

    /**
     * Sets whether to render reprojection edges or not (usually for debugging).
     * @param {boolean} render Render the edges.
     * @api
     */
    setRenderReprojectionEdges(render) {
      if (this.renderReprojectionEdges_ == render) {
        return;
      }
      this.renderReprojectionEdges_ = render;
      this.changed();
    }

    /**
     * Sets the tile grid to use when reprojecting the tiles to the given
     * projection instead of the default tile grid for the projection.
     *
     * This can be useful when the default tile grid cannot be created
     * (e.g. projection has no extent defined) or
     * for optimization reasons (custom tile size, resolutions, ...).
     *
     * @param {import("../proj.js").ProjectionLike} projection Projection.
     * @param {import("../tilegrid/TileGrid.js").default} tilegrid Tile grid to use for the projection.
     * @api
     */
    setTileGridForProjection(projection, tilegrid) {
      const proj = get$1(projection);
      if (proj) {
        const projKey = getUid(proj);
        if (!(projKey in this.tileGridForProjection)) {
          this.tileGridForProjection[projKey] = tilegrid;
        }
      }
    }
  }

  /**
   * @param {ImageTile} imageTile Image tile.
   * @param {string} src Source.
   */
  function defaultTileLoadFunction(imageTile, src) {
    if (WORKER_OFFSCREEN_CANVAS) {
      // special treatment for offscreen canvas
      const crossOrigin = imageTile.getCrossOrigin();

      /** @type {RequestMode} */
      let mode = 'same-origin';
      /** @type {RequestCredentials} */
      let credentials = 'same-origin';
      if (crossOrigin === 'anonymous' || crossOrigin === '') {
        mode = 'cors';
        credentials = 'omit';
      } else if (crossOrigin === 'use-credentials') {
        mode = 'cors';
        credentials = 'include';
      }

      const options = {
        mode,
        credentials,
        referrerPolicy: imageTile.getReferrerPolicy(),
      };

      fetch(src, options)
        .then((response) => {
          if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
          }
          return response.blob();
        })
        .then((blob) => {
          return createImageBitmap(blob);
        })
        .then((imageBitmap) => {
          const canvas = imageTile.getImage();
          canvas.width = imageBitmap.width;
          canvas.height = imageBitmap.height;
          const ctx = /** @type {OffscreenCanvas} */ (canvas).getContext('2d');
          ctx.drawImage(imageBitmap, 0, 0);
          imageBitmap.close?.();
          // mock the image 'load' event
          canvas.dispatchEvent(new Event('load'));
        })
        .catch(() => {
          const canvas = imageTile.getImage();
          canvas.dispatchEvent(new Event('error'));
        });
      return;
    }

    /** @type {HTMLImageElement|HTMLVideoElement} */ (imageTile.getImage()).src =
      src;
  }

  /**
   * @module ol/source/XYZ
   */


  /**
   * @typedef {Object} Options
   * @property {import("./Source.js").AttributionLike} [attributions] Attributions.
   * @property {boolean} [attributionsCollapsible=true] Attributions are collapsible.
   * @property {number} [cacheSize] Deprecated.  Use the cacheSize option on the layer instead.
   * @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images.  Note that
   * you must provide a `crossOrigin` value if you want to access pixel data with the Canvas renderer.
   * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.
   * @property {ReferrerPolicy} [referrerPolicy] The `referrerPolicy` property for loaded images.
   * @property {boolean} [interpolate=true] Use interpolated values when resampling.  By default,
   * linear interpolation is used when resampling.  Set to false to use the nearest neighbor instead.
   * @property {import("../proj.js").ProjectionLike} [projection='EPSG:3857'] Projection.
   * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels).
   * Higher values can increase reprojection performance, but decrease precision.
   * @property {number} [maxZoom=42] Optional max zoom level. Not used if `tileGrid` is provided.
   * @property {number} [minZoom=0] Optional min zoom level. Not used if `tileGrid` is provided.
   * @property {number} [maxResolution] Optional tile grid resolution at level zero. Not used if `tileGrid` is provided.
   * @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid.
   * @property {import("../Tile.js").LoadFunction} [tileLoadFunction] Deprecated.  Use an ImageTile source with a loader
   * instead.  Optional function to load a tile given a URL. The default is
   * ```js
   * function(imageTile, src) {
   *   imageTile.getImage().src = src;
   * };
   * ```
   * @property {number} [tilePixelRatio=1] The pixel ratio used by the tile service.
   * For example, if the tile service advertizes 256px by 256px tiles but actually sends 512px
   * by 512px images (for retina/hidpi devices) then `tilePixelRatio`
   * should be set to `2`.
   * @property {number|import("../size.js").Size} [tileSize=[256, 256]] The tile size used by the tile service.
   * Not used if `tileGrid` is provided.
   * @property {number} [gutter=0] The size in pixels of the gutter around image tiles to ignore.
   * This allows artifacts of rendering at tile edges to be ignored.
   * Supported images should be wider and taller than the tile size by a value of `2 x gutter`.
   * @property {import("../Tile.js").UrlFunction} [tileUrlFunction] Deprecated.  Use an ImageTile source and provide a function
   * for the url option instead.
   * @property {string} [url] URL template. Must include `{x}`, `{y}` or `{-y}`,
   * and `{z}` placeholders. A `{?-?}` template pattern, for example `subdomain{a-f}.domain.com`,
   * may be used instead of defining each one separately in the `urls` option.
   * @property {Array<string>} [urls] Deprecated.  Use an ImageTile source and provide an array of URLs for the
   * url option instead.
   * @property {boolean} [wrapX=true] Whether to wrap the world horizontally.
   * @property {number} [transition=250] Duration of the opacity transition for rendering.
   * To disable the opacity transition, pass `transition: 0`.
   * @property {number|import("../array.js").NearestDirectionFunction} [zDirection=0]
   * Choose whether to use tiles with a higher or lower zoom level when between integer
   * zoom levels. See {@link module:ol/tilegrid/TileGrid~TileGrid#getZForResolution}.
   */

  /**
   * @classdesc
   * Layer source for tile data with URLs in a set XYZ format that are
   * defined in a URL template. By default, this follows the widely-used
   * Google grid where `x` 0 and `y` 0 are in the top left. Grids like
   * TMS where `x` 0 and `y` 0 are in the bottom left can be used by
   * using the `{-y}` placeholder in the URL template, so long as the
   * source does not have a custom tile grid. In this case
   * a `tileUrlFunction` can be used, such as:
   * ```js
   *  tileUrlFunction: function(coordinate) {
   *    return 'http://mapserver.com/' + coordinate[0] + '/' +
   *      coordinate[1] + '/' + (-coordinate[2] - 1) + '.png';
   *  }
   * ```
   * @api
   */
  class XYZ extends TileImage {
    /**
     * @param {Options} [options] XYZ options.
     */
    constructor(options) {
      options = options || {};

      const projection =
        options.projection !== undefined ? options.projection : 'EPSG:3857';

      const tileGrid =
        options.tileGrid !== undefined
          ? options.tileGrid
          : createXYZ({
              extent: extentFromProjection(projection),
              maxResolution: options.maxResolution,
              maxZoom: options.maxZoom,
              minZoom: options.minZoom,
              tileSize: options.tileSize,
            });

      super({
        attributions: options.attributions,
        cacheSize: options.cacheSize,
        crossOrigin: options.crossOrigin,
        referrerPolicy: options.referrerPolicy,
        interpolate: options.interpolate,
        projection: projection,
        reprojectionErrorThreshold: options.reprojectionErrorThreshold,
        tileGrid: tileGrid,
        tileLoadFunction: options.tileLoadFunction,
        tilePixelRatio: options.tilePixelRatio,
        tileUrlFunction: options.tileUrlFunction,
        url: options.url,
        urls: options.urls,
        wrapX: options.wrapX !== undefined ? options.wrapX : true,
        transition: options.transition,
        attributionsCollapsible: options.attributionsCollapsible,
        zDirection: options.zDirection,
      });

      /**
       * @private
       * @type {number}
       */
      this.gutter_ = options.gutter !== undefined ? options.gutter : 0;
    }

    /**
     * @return {number} Gutter.
     * @override
     */
    getGutter() {
      return this.gutter_;
    }
  }

  /**
   * @module ol/source/OSM
   */


  /**
   * The attribution containing a link to the OpenStreetMap Copyright and License
   * page.
   * @const
   * @type {string}
   * @api
   */
  const ATTRIBUTION =
    '&#169; ' +
    '<a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> ' +
    'contributors.';

  /**
   * @typedef {Object} Options
   * @property {import("./Source.js").AttributionLike} [attributions] Attributions.
   * @property {number} [cacheSize] Deprecated.  Use the cacheSize option on the layer instead.
   * @property {null|string} [crossOrigin='anonymous'] The `crossOrigin` attribute for loaded images.  Note that
   * you must provide a `crossOrigin` value if you want to access pixel data with the Canvas renderer.
   * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.
   * @property {ReferrerPolicy} [referrerPolicy='origin-when-cross-origin'] The `referrerPolicy` property for loaded images.
   * @property {boolean} [interpolate=true] Use interpolated values when resampling.  By default,
   * linear interpolation is used when resampling.  Set to false to use the nearest neighbor instead.
   * @property {number} [maxZoom=19] Max zoom.
   * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels).
   * Higher values can increase reprojection performance, but decrease precision.
   * @property {import("../Tile.js").LoadFunction} [tileLoadFunction] Optional function to load a tile given a URL. The default is
   * ```js
   * function(imageTile, src) {
   *   imageTile.getImage().src = src;
   * };
   * ```
   * @property {number} [transition=250] Duration of the opacity transition for rendering.
   * To disable the opacity transition, pass `transition: 0`.
   * @property {string} [url='https://tile.openstreetmap.org/{z}/{x}/{y}.png'] URL template.
   * Must include `{x}`, `{y}` or `{-y}`, and `{z}` placeholders.
   * @property {boolean} [wrapX=true] Whether to wrap the world horizontally.
   * @property {number|import("../array.js").NearestDirectionFunction} [zDirection=0]
   * Choose whether to use tiles with a higher or lower zoom level when between integer
   * zoom levels. See {@link module:ol/tilegrid/TileGrid~TileGrid#getZForResolution}.
   */

  /**
   * @classdesc
   * Layer source for the OpenStreetMap tile server.
   * @api
   */
  class OSM extends XYZ {
    /**
     * @param {Options} [options] Open Street Map options.
     */
    constructor(options) {
      options = options || {};

      let attributions;
      if (options.attributions !== undefined) {
        attributions = options.attributions;
      } else {
        attributions = [ATTRIBUTION];
      }

      const url =
        options.url !== undefined
          ? options.url
          : 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';

      super({
        attributions: attributions,
        attributionsCollapsible: false,
        cacheSize: options.cacheSize,
        crossOrigin:
          options.crossOrigin !== undefined ? options.crossOrigin : 'anonymous',
        referrerPolicy: options.referrerPolicy || 'origin-when-cross-origin',
        interpolate: options.interpolate,
        maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19,
        reprojectionErrorThreshold: options.reprojectionErrorThreshold,
        tileLoadFunction: options.tileLoadFunction,
        transition: options.transition,
        url: url,
        wrapX: options.wrapX,
        zDirection: options.zDirection,
      });
    }
  }

  /**
   * @module ol/format/Feature
   */

  /**
   * @typedef {Object} ReadOptions
   * @property {import("../proj.js").ProjectionLike} [dataProjection] Projection of the data we are reading.
   * If not provided, the projection will be derived from the data (where possible) or
   * the `dataProjection` of the format is assigned (where set). If the projection
   * can not be derived from the data and if no `dataProjection` is set for a format,
   * the features will not be reprojected.
   * @property {import("../extent.js").Extent} [extent] Tile extent in map units of the tile being read.
   * This is only required when reading data with tile pixels as geometry units. When configured,
   * a `dataProjection` with `TILE_PIXELS` as `units` and the tile's pixel extent as `extent` needs to be
   * provided.
   * @property {import("../proj.js").ProjectionLike} [featureProjection] Projection of the feature geometries
   * created by the format reader. If not provided, features will be returned in the
   * `dataProjection`.
   */

  /**
   * @typedef {Object} WriteOptions
   * @property {import("../proj.js").ProjectionLike} [dataProjection] Projection of the data we are writing.
   * If not provided, the `dataProjection` of the format is assigned (where set).
   * If no `dataProjection` is set for a format, the features will be returned
   * in the `featureProjection`.
   * @property {import("../proj.js").ProjectionLike} [featureProjection] Projection of the feature geometries
   * that will be serialized by the format writer. If not provided, geometries are assumed
   * to be in the `dataProjection` if that is set; in other words, they are not transformed.
   * @property {boolean} [rightHanded] When writing geometries, follow the right-hand
   * rule for linear ring orientation.  This means that polygons will have counter-clockwise
   * exterior rings and clockwise interior rings.  By default, coordinates are serialized
   * as they are provided at construction.  If `true`, the right-hand rule will
   * be applied.  If `false`, the left-hand rule will be applied (clockwise for
   * exterior and counter-clockwise for interior rings).  Note that not all
   * formats support this.  The GeoJSON format does use this property when writing
   * geometries.
   * @property {number} [decimals] Maximum number of decimal places for coordinates.
   * Coordinates are stored internally as floats, but floating-point arithmetic can create
   * coordinates with a large number of decimal places, not generally wanted on output.
   * Set a number here to round coordinates. Can also be used to ensure that
   * coordinates read in can be written back out with the same number of decimals.
   * Default is no rounding.
   */

  /**
   * @typedef {'arraybuffer' | 'json' | 'text' | 'xml'} Type
   */

  /**
   * @typedef {Object} SimpleGeometryObject
   * @property {import('../geom/Geometry.js').Type} type Type.
   * @property {Array<number>} flatCoordinates Flat coordinates.
   * @property {Array<number>|Array<Array<number>>} [ends] Ends or endss.
   * @property {import('../geom/Geometry.js').GeometryLayout} [layout] Layout.
   */

  /**
   * @typedef {Array<GeometryObject>} GeometryCollectionObject
   */

  /**
   * @typedef {SimpleGeometryObject|GeometryCollectionObject} GeometryObject
   */

  /**
   * @typedef {Object} FeatureObject
   * @property {string|number} [id] Id.
   * @property {GeometryObject} [geometry] Geometry.
   * @property {Object<string, *>} [properties] Properties.
   */

  /***
   * @template {import('../Feature.js').FeatureLike} T
   * @typedef {T extends RenderFeature ? typeof RenderFeature : typeof Feature} FeatureToFeatureClass
   */

  /***
   * @template {import("../Feature.js").FeatureClass} T
   * @typedef {T[keyof T] extends RenderFeature ? RenderFeature : Feature} FeatureClassToFeature
   */

  /**
   * @classdesc
   * Abstract base class; normally only used for creating subclasses and not
   * instantiated in apps.
   * Base class for feature formats.
   * {@link module:ol/format/Feature~FeatureFormat} subclasses provide the ability to decode and encode
   * {@link module:ol/Feature~Feature} objects from a variety of commonly used geospatial
   * file formats.  See the documentation for each format for more details.
   *
   * @template {import('../Feature.js').FeatureLike} [FeatureType=import("../Feature.js").default]
   * @abstract
   * @api
   */
  class FeatureFormat {
    constructor() {
      /**
       * @protected
       * @type {import("../proj/Projection.js").default|undefined}
       */
      this.dataProjection = undefined;

      /**
       * @protected
       * @type {import("../proj/Projection.js").default|undefined}
       */
      this.defaultFeatureProjection = undefined;

      /**
       * @protected
       * @type {FeatureToFeatureClass<FeatureType>}
       */
      this.featureClass = /** @type {FeatureToFeatureClass<FeatureType>} */ (
        Feature
      );

      /**
       * A list media types supported by the format in descending order of preference.
       * @type {Array<string>}
       */
      this.supportedMediaTypes = null;
    }

    /**
     * Adds the data projection to the read options.
     * @param {Document|Element|Object|string} source Source.
     * @param {ReadOptions} [options] Options.
     * @return {ReadOptions|undefined} Options.
     * @protected
     */
    getReadOptions(source, options) {
      if (options) {
        let dataProjection = options.dataProjection
          ? get$1(options.dataProjection)
          : this.readProjection(source);
        if (
          options.extent &&
          dataProjection &&
          dataProjection.getUnits() === 'tile-pixels'
        ) {
          dataProjection = get$1(dataProjection);
          dataProjection.setWorldExtent(options.extent);
        }
        options = {
          dataProjection: dataProjection,
          featureProjection: options.featureProjection,
        };
      }
      return this.adaptOptions(options);
    }

    /**
     * Sets the `dataProjection` on the options, if no `dataProjection`
     * is set.
     * @param {WriteOptions|ReadOptions|undefined} options
     *     Options.
     * @protected
     * @return {WriteOptions|ReadOptions|undefined}
     *     Updated options.
     */
    adaptOptions(options) {
      return Object.assign(
        {
          dataProjection: this.dataProjection,
          featureProjection: this.defaultFeatureProjection,
          featureClass: this.featureClass,
        },
        options,
      );
    }

    /**
     * @abstract
     * @return {Type} The format type.
     */
    getType() {
      return abstract();
    }

    /**
     * Read a single feature from a source.
     *
     * @abstract
     * @param {Document|Element|Object|string} source Source.
     * @param {ReadOptions} [options] Read options.
     * @return {FeatureType|Array<FeatureType>} Feature.
     */
    readFeature(source, options) {
      return abstract();
    }

    /**
     * Read all features from a source.
     *
     * @abstract
     * @param {Document|Element|ArrayBuffer|Object|string} source Source.
     * @param {ReadOptions} [options] Read options.
     * @return {Array<FeatureType>} Features.
     */
    readFeatures(source, options) {
      return abstract();
    }

    /**
     * Read a single geometry from a source.
     *
     * @abstract
     * @param {Document|Element|Object|string} source Source.
     * @param {ReadOptions} [options] Read options.
     * @return {import("../geom/Geometry.js").default} Geometry.
     */
    readGeometry(source, options) {
      return abstract();
    }

    /**
     * Read the projection from a source.
     *
     * @abstract
     * @param {Document|Element|Object|string} source Source.
     * @return {import("../proj/Projection.js").default|undefined} Projection.
     */
    readProjection(source) {
      return abstract();
    }

    /**
     * Encode a feature in this format.
     *
     * @abstract
     * @param {Feature} feature Feature.
     * @param {WriteOptions} [options] Write options.
     * @return {string|ArrayBuffer} Result.
     */
    writeFeature(feature, options) {
      return abstract();
    }

    /**
     * Encode an array of features in this format.
     *
     * @abstract
     * @param {Array<Feature>} features Features.
     * @param {WriteOptions} [options] Write options.
     * @return {string|ArrayBuffer} Result.
     */
    writeFeatures(features, options) {
      return abstract();
    }

    /**
     * Write a single geometry in this format.
     *
     * @abstract
     * @param {import("../geom/Geometry.js").default} geometry Geometry.
     * @param {WriteOptions} [options] Write options.
     * @return {string|ArrayBuffer} Result.
     */
    writeGeometry(geometry, options) {
      return abstract();
    }
  }

  /**
   * @template {import("../geom/Geometry.js").default|RenderFeature} T
   * @param {T} geometry Geometry.
   * @param {boolean} write Set to true for writing, false for reading.
   * @param {WriteOptions|ReadOptions} [options] Options.
   * @return {T} Transformed geometry.
   */
  function transformGeometryWithOptions(geometry, write, options) {
    const featureProjection = options
      ? get$1(options.featureProjection)
      : null;
    const dataProjection = options ? get$1(options.dataProjection) : null;

    let transformed = geometry;
    if (
      featureProjection &&
      dataProjection &&
      !equivalent$1(featureProjection, dataProjection)
    ) {
      if (write) {
        transformed = /** @type {T} */ (geometry.clone());
      }
      const fromProjection = write ? featureProjection : dataProjection;
      const toProjection = write ? dataProjection : featureProjection;
      if (fromProjection.getUnits() === 'tile-pixels') {
        transformed.transform(fromProjection, toProjection);
      } else {
        transformed.applyTransform(getTransform(fromProjection, toProjection));
      }
    }
    if (
      write &&
      options &&
      /** @type {WriteOptions} */ (options).decimals !== undefined
    ) {
      const power = Math.pow(10, /** @type {WriteOptions} */ (options).decimals);
      // if decimals option on write, round each coordinate appropriately
      /**
       * @param {Array<number>} coordinates Coordinates.
       * @return {Array<number>} Transformed coordinates.
       */
      const transform = function (coordinates) {
        for (let i = 0, ii = coordinates.length; i < ii; ++i) {
          coordinates[i] = Math.round(coordinates[i] * power) / power;
        }
        return coordinates;
      };
      if (transformed === geometry) {
        transformed = /** @type {T} */ (geometry.clone());
      }
      transformed.applyTransform(transform);
    }
    return transformed;
  }

  const GeometryConstructor = {
    Point: Point,
    LineString: LineString,
    Polygon: Polygon,
    MultiPoint: MultiPoint,
    MultiLineString: MultiLineString,
    MultiPolygon: MultiPolygon,
  };

  function orientFlatCoordinates(flatCoordinates, ends, stride) {
    if (Array.isArray(ends[0])) {
      // MultiPolagon
      if (!linearRingssAreOriented(flatCoordinates, 0, ends, stride)) {
        flatCoordinates = flatCoordinates.slice();
        orientLinearRingsArray(flatCoordinates, 0, ends, stride);
      }
      return flatCoordinates;
    }
    if (!linearRingsAreOriented(flatCoordinates, 0, ends, stride)) {
      flatCoordinates = flatCoordinates.slice();
      orientLinearRings(flatCoordinates, 0, ends, stride);
    }
    return flatCoordinates;
  }

  /**
   * @param {FeatureObject} object Feature object.
   * @param {WriteOptions|ReadOptions} [options] Options.
   * @return {RenderFeature|Array<RenderFeature>} Render feature.
   */
  function createRenderFeature(object, options) {
    const geometry = object.geometry;
    if (!geometry) {
      return [];
    }
    if (Array.isArray(geometry)) {
      return geometry
        .map((geometry) => createRenderFeature({...object, geometry}))
        .flat();
    }

    const geometryType =
      geometry.type === 'MultiPolygon' ? 'Polygon' : geometry.type;
    if (geometryType === 'GeometryCollection' || geometryType === 'Circle') {
      throw new Error('Unsupported geometry type: ' + geometryType);
    }

    const stride = geometry.layout.length;
    return transformGeometryWithOptions(
      new RenderFeature(
        geometryType,
        geometryType === 'Polygon'
          ? orientFlatCoordinates(geometry.flatCoordinates, geometry.ends, stride)
          : geometry.flatCoordinates,
        geometry.ends?.flat(),
        stride,
        object.properties || {},
        object.id,
      ).enableSimplifyTransformed(),
      false,
      options,
    );
  }

  /**
   * @param {GeometryObject|null} object Geometry object.
   * @param {WriteOptions|ReadOptions} [options] Options.
   * @return {import("../geom/Geometry.js").default} Geometry.
   */
  function createGeometry(object, options) {
    if (!object) {
      return null;
    }
    if (Array.isArray(object)) {
      const geometries = object.map((geometry) =>
        createGeometry(geometry, options),
      );
      return new GeometryCollection(geometries);
    }
    const Geometry = GeometryConstructor[object.type];
    return transformGeometryWithOptions(
      new Geometry(object.flatCoordinates, object.layout || 'XY', object.ends),
      false,
      options,
    );
  }

  /**
   * @module ol/format/JSONFeature
   */

  /**
   * @classdesc
   * Abstract base class; normally only used for creating subclasses and not
   * instantiated in apps.
   * Base class for JSON feature formats.
   *
   * @template {import('../Feature.js').FeatureLike} [FeatureType=import("../Feature.js").default]
   * @extends {FeatureFormat<FeatureType>}
   * @abstract
   */
  class JSONFeature extends FeatureFormat {
    constructor() {
      super();
    }

    /**
     * @return {import("./Feature.js").Type} Format.
     * @override
     */
    getType() {
      return 'json';
    }

    /**
     * Read a feature.  Only works for a single feature. Use `readFeatures` to
     * read a feature collection.
     *
     * @param {ArrayBuffer|Document|Element|Object|string} source Source.
     * @param {import("./Feature.js").ReadOptions} [options] Read options.
     * @return {FeatureType|Array<FeatureType>} Feature.
     * @api
     * @override
     */
    readFeature(source, options) {
      return this.readFeatureFromObject(
        getObject(source),
        this.getReadOptions(source, options),
      );
    }

    /**
     * Read all features.  Works with both a single feature and a feature
     * collection.
     *
     * @param {ArrayBuffer|Document|Element|Object|string} source Source.
     * @param {import("./Feature.js").ReadOptions} [options] Read options.
     * @return {Array<FeatureType>} Features.
     * @api
     * @override
     */
    readFeatures(source, options) {
      return this.readFeaturesFromObject(
        getObject(source),
        this.getReadOptions(source, options),
      );
    }

    /**
     * @abstract
     * @param {Object} object Object.
     * @param {import("./Feature.js").ReadOptions} [options] Read options.
     * @protected
     * @return {FeatureType|Array<FeatureType>} Feature.
     */
    readFeatureFromObject(object, options) {
      return abstract();
    }

    /**
     * @abstract
     * @param {Object} object Object.
     * @param {import("./Feature.js").ReadOptions} [options] Read options.
     * @protected
     * @return {Array<FeatureType>} Features.
     */
    readFeaturesFromObject(object, options) {
      return abstract();
    }

    /**
     * Read a geometry.
     *
     * @param {ArrayBuffer|Document|Element|Object|string} source Source.
     * @param {import("./Feature.js").ReadOptions} [options] Read options.
     * @return {import("../geom/Geometry.js").default} Geometry.
     * @api
     * @override
     */
    readGeometry(source, options) {
      return this.readGeometryFromObject(
        getObject(source),
        this.getReadOptions(source, options),
      );
    }

    /**
     * @abstract
     * @param {Object} object Object.
     * @param {import("./Feature.js").ReadOptions} [options] Read options.
     * @protected
     * @return {import("../geom/Geometry.js").default} Geometry.
     */
    readGeometryFromObject(object, options) {
      return abstract();
    }

    /**
     * Read the projection.
     *
     * @param {ArrayBuffer|Document|Element|Object|string} source Source.
     * @return {import("../proj/Projection.js").default} Projection.
     * @api
     * @override
     */
    readProjection(source) {
      return this.readProjectionFromObject(getObject(source));
    }

    /**
     * @abstract
     * @param {Object} object Object.
     * @protected
     * @return {import("../proj/Projection.js").default} Projection.
     */
    readProjectionFromObject(object) {
      return abstract();
    }

    /**
     * Encode a feature as string.
     *
     * @param {import("../Feature.js").default} feature Feature.
     * @param {import("./Feature.js").WriteOptions} [options] Write options.
     * @return {string} Encoded feature.
     * @api
     * @override
     */
    writeFeature(feature, options) {
      return JSON.stringify(this.writeFeatureObject(feature, options));
    }

    /**
     * @abstract
     * @param {import("../Feature.js").default} feature Feature.
     * @param {import("./Feature.js").WriteOptions} [options] Write options.
     * @return {Object} Object.
     */
    writeFeatureObject(feature, options) {
      return abstract();
    }

    /**
     * Encode an array of features as string.
     *
     * @param {Array<import("../Feature.js").default>} features Features.
     * @param {import("./Feature.js").WriteOptions} [options] Write options.
     * @return {string} Encoded features.
     * @api
     * @override
     */
    writeFeatures(features, options) {
      return JSON.stringify(this.writeFeaturesObject(features, options));
    }

    /**
     * @abstract
     * @param {Array<import("../Feature.js").default>} features Features.
     * @param {import("./Feature.js").WriteOptions} [options] Write options.
     * @return {Object} Object.
     */
    writeFeaturesObject(features, options) {
      return abstract();
    }

    /**
     * Encode a geometry as string.
     *
     * @param {import("../geom/Geometry.js").default} geometry Geometry.
     * @param {import("./Feature.js").WriteOptions} [options] Write options.
     * @return {string} Encoded geometry.
     * @api
     * @override
     */
    writeGeometry(geometry, options) {
      return JSON.stringify(this.writeGeometryObject(geometry, options));
    }

    /**
     * @abstract
     * @param {import("../geom/Geometry.js").default} geometry Geometry.
     * @param {import("./Feature.js").WriteOptions} [options] Write options.
     * @return {Object} Object.
     */
    writeGeometryObject(geometry, options) {
      return abstract();
    }
  }

  /**
   * @param {Document|Element|Object|string} source Source.
   * @return {Object} Object.
   */
  function getObject(source) {
    if (typeof source === 'string') {
      const object = JSON.parse(source);
      return object ? /** @type {Object} */ (object) : null;
    }
    if (source !== null) {
      return source;
    }
    return null;
  }

  /**
   * @module ol/format/GeoJSON
   */


  /**
   * @typedef {import("geojson").GeoJSON} GeoJSONObject
   * @typedef {import("geojson").Feature} GeoJSONFeature
   * @typedef {import("geojson").FeatureCollection} GeoJSONFeatureCollection
   * @typedef {import("geojson").Geometry} GeoJSONGeometry
   * @typedef {import("geojson").Point} GeoJSONPoint
   * @typedef {import("geojson").LineString} GeoJSONLineString
   * @typedef {import("geojson").Polygon} GeoJSONPolygon
   * @typedef {import("geojson").MultiPoint} GeoJSONMultiPoint
   * @typedef {import("geojson").MultiLineString} GeoJSONMultiLineString
   * @typedef {import("geojson").MultiPolygon} GeoJSONMultiPolygon
   * @typedef {import("geojson").GeometryCollection} GeoJSONGeometryCollection
   */

  /**
   * @template {import("../Feature.js").FeatureLike} [FeatureType=import("../Feature.js").default]
   * @typedef {Object} Options
   *
   * @property {import("../proj.js").ProjectionLike} [dataProjection='EPSG:4326'] Default data projection.
   * @property {import("../proj.js").ProjectionLike} [featureProjection] Projection for features read or
   * written by the format.  Options passed to read or write methods will take precedence.
   * @property {string} [geometryName] Geometry name to use when creating features.
   * @property {boolean} [extractGeometryName=false] Certain GeoJSON providers include
   * the geometry_name field in the feature GeoJSON. If set to `true` the GeoJSON reader
   * will look for that field to set the geometry name. If both this field is set to `true`
   * and a `geometryName` is provided, the `geometryName` will take precedence.
   * @property {import('./Feature.js').FeatureToFeatureClass<FeatureType>} [featureClass] Feature class
   * to be used when reading features. The default is {@link module:ol/Feature~Feature}. If performance is
   * the primary concern, and features are not going to be modified or round-tripped through the format,
   * consider using {@link module:ol/render/Feature~RenderFeature}
   */

  /**
   * @classdesc
   * Feature format for reading and writing data in the GeoJSON format.
   *
   * @template {import('../Feature.js').FeatureLike} [FeatureType=import("../Feature.js").default]
   * @extends {JSONFeature<FeatureType>}
   * @api
   */
  class GeoJSON extends JSONFeature {
    /**
     * @param {Options<FeatureType>} [options] Options.
     */
    constructor(options) {
      options = options ? options : {};

      super();

      /**
       * @type {import("../proj/Projection.js").default}
       */
      this.dataProjection = get$1(
        options.dataProjection ? options.dataProjection : 'EPSG:4326',
      );

      if (options.featureProjection) {
        /**
         * @type {import("../proj/Projection.js").default}
         */
        this.defaultFeatureProjection = get$1(options.featureProjection);
      }

      if (options.featureClass) {
        this.featureClass = options.featureClass;
      }

      /**
       * Name of the geometry attribute for features.
       * @type {string|undefined}
       * @private
       */
      this.geometryName_ = options.geometryName;

      /**
       * Look for the `geometry_name` in the feature GeoJSON
       * @type {boolean|undefined}
       * @private
       */
      this.extractGeometryName_ = options.extractGeometryName;

      this.supportedMediaTypes = [
        'application/geo+json',
        'application/vnd.geo+json',
      ];
    }

    /**
     * @param {Object} object Object.
     * @param {import("./Feature.js").ReadOptions} [options] Read options.
     * @protected
     * @return {FeatureType|Array<FeatureType>} Feature.
     * @override
     */
    readFeatureFromObject(object, options) {
      /**
       * @type {GeoJSONFeature}
       */
      let geoJSONFeature = null;
      if (object['type'] === 'Feature') {
        geoJSONFeature = /** @type {GeoJSONFeature} */ (object);
      } else {
        geoJSONFeature = {
          'type': 'Feature',
          'geometry': /** @type {GeoJSONGeometry} */ (object),
          'properties': null,
        };
      }

      const geometry = readGeometryInternal(geoJSONFeature['geometry']);
      if (this.featureClass === RenderFeature) {
        return /** @type {FeatureType|Array<FeatureType>} */ (
          createRenderFeature(
            {
              geometry,
              id: geoJSONFeature['id'],
              properties: geoJSONFeature['properties'],
            },
            options,
          )
        );
      }

      const feature = new Feature();
      if (this.geometryName_) {
        feature.setGeometryName(this.geometryName_);
      } else if (this.extractGeometryName_ && geoJSONFeature['geometry_name']) {
        feature.setGeometryName(geoJSONFeature['geometry_name']);
      }
      feature.setGeometry(createGeometry(geometry, options));

      if ('id' in geoJSONFeature) {
        feature.setId(geoJSONFeature['id']);
      }

      if (geoJSONFeature['properties']) {
        feature.setProperties(geoJSONFeature['properties'], true);
      }
      return /** @type {FeatureType|Array<FeatureType>} */ (feature);
    }

    /**
     * @param {Object} object Object.
     * @param {import("./Feature.js").ReadOptions} [options] Read options.
     * @protected
     * @return {Array<FeatureType>} Features.
     * @override
     */
    readFeaturesFromObject(object, options) {
      const geoJSONObject = /** @type {GeoJSONObject} */ (object);
      let features = null;
      if (geoJSONObject['type'] === 'FeatureCollection') {
        const geoJSONFeatureCollection = /** @type {GeoJSONFeatureCollection} */ (
          object
        );
        features = [];
        const geoJSONFeatures = geoJSONFeatureCollection['features'];
        for (let i = 0, ii = geoJSONFeatures.length; i < ii; ++i) {
          const featureObject = this.readFeatureFromObject(
            geoJSONFeatures[i],
            options,
          );
          if (!featureObject) {
            continue;
          }
          features.push(featureObject);
        }
      } else {
        features = [this.readFeatureFromObject(object, options)];
      }
      return /** @type {Array<FeatureType>} */ (features.flat());
    }

    /**
     * @param {GeoJSONGeometry} object Object.
     * @param {import("./Feature.js").ReadOptions} [options] Read options.
     * @protected
     * @return {import("../geom/Geometry.js").default} Geometry.
     * @override
     */
    readGeometryFromObject(object, options) {
      return readGeometry(object, options);
    }

    /**
     * @param {Object} object Object.
     * @protected
     * @return {import("../proj/Projection.js").default} Projection.
     * @override
     */
    readProjectionFromObject(object) {
      const crs = object['crs'];
      let projection;
      if (crs) {
        if (crs['type'] == 'name') {
          projection = get$1(crs['properties']['name']);
        } else if (crs['type'] === 'EPSG') {
          projection = get$1('EPSG:' + crs['properties']['code']);
        } else {
          throw new Error('Unknown SRS type');
        }
      } else {
        projection = this.dataProjection;
      }
      return /** @type {import("../proj/Projection.js").default} */ (projection);
    }

    /**
     * Encode a feature as a GeoJSON Feature object.
     *
     * @param {import("../Feature.js").default} feature Feature.
     * @param {import("./Feature.js").WriteOptions} [options] Write options.
     * @return {GeoJSONFeature} Object.
     * @api
     * @override
     */
    writeFeatureObject(feature, options) {
      options = this.adaptOptions(options);

      /** @type {GeoJSONFeature} */
      const object = {
        'type': 'Feature',
        geometry: null,
        properties: null,
      };

      const id = feature.getId();
      if (id !== undefined) {
        object.id = id;
      }

      if (!feature.hasProperties()) {
        return object;
      }

      const properties = feature.getProperties();
      const geometry = feature.getGeometry();
      if (geometry) {
        object.geometry = writeGeometry(geometry, options);

        delete properties[feature.getGeometryName()];
      }

      if (!isEmpty$1(properties)) {
        object.properties = properties;
      }

      return object;
    }

    /**
     * Encode an array of features as a GeoJSON object.
     *
     * @param {Array<import("../Feature.js").default>} features Features.
     * @param {import("./Feature.js").WriteOptions} [options] Write options.
     * @return {GeoJSONFeatureCollection} GeoJSON Object.
     * @api
     * @override
     */
    writeFeaturesObject(features, options) {
      options = this.adaptOptions(options);
      const objects = [];
      for (let i = 0, ii = features.length; i < ii; ++i) {
        objects.push(this.writeFeatureObject(features[i], options));
      }
      return {
        type: 'FeatureCollection',
        features: objects,
      };
    }

    /**
     * Encode a geometry as a GeoJSON object.
     *
     * @param {import("../geom/Geometry.js").default} geometry Geometry.
     * @param {import("./Feature.js").WriteOptions} [options] Write options.
     * @return {GeoJSONGeometry|GeoJSONGeometryCollection} Object.
     * @api
     * @override
     */
    writeGeometryObject(geometry, options) {
      return writeGeometry(geometry, this.adaptOptions(options));
    }
  }

  /**
   * @param {GeoJSONGeometry|GeoJSONGeometryCollection} object Object.
   * @param {import("./Feature.js").ReadOptions} [options] Read options.
   * @return {import("./Feature.js").GeometryObject} Geometry.
   */
  function readGeometryInternal(object, options) {
    if (!object) {
      return null;
    }

    /** @type {import("./Feature.js").GeometryObject} */
    let geometry;
    switch (object['type']) {
      case 'Point': {
        geometry = readPointGeometry(/** @type {GeoJSONPoint} */ (object));
        break;
      }
      case 'LineString': {
        geometry = readLineStringGeometry(
          /** @type {GeoJSONLineString} */ (object),
        );
        break;
      }
      case 'Polygon': {
        geometry = readPolygonGeometry(/** @type {GeoJSONPolygon} */ (object));
        break;
      }
      case 'MultiPoint': {
        geometry = readMultiPointGeometry(
          /** @type {GeoJSONMultiPoint} */ (object),
        );
        break;
      }
      case 'MultiLineString': {
        geometry = readMultiLineStringGeometry(
          /** @type {GeoJSONMultiLineString} */ (object),
        );
        break;
      }
      case 'MultiPolygon': {
        geometry = readMultiPolygonGeometry(
          /** @type {GeoJSONMultiPolygon} */ (object),
        );
        break;
      }
      case 'GeometryCollection': {
        geometry = readGeometryCollectionGeometry(
          /** @type {GeoJSONGeometryCollection} */ (object),
        );
        break;
      }
      default: {
        throw new Error('Unsupported GeoJSON type: ' + object['type']);
      }
    }
    return geometry;
  }

  /**
   * @param {GeoJSONGeometry|GeoJSONGeometryCollection} object Object.
   * @param {import("./Feature.js").ReadOptions} [options] Read options.
   * @return {import("../geom/Geometry.js").default} Geometry.
   */
  function readGeometry(object, options) {
    const geometryObject = readGeometryInternal(object);
    return createGeometry(geometryObject, options);
  }

  /**
   * @param {GeoJSONGeometryCollection} object Object.
   * @param {import("./Feature.js").ReadOptions} [options] Read options.
   * @return {import("./Feature.js").GeometryCollectionObject} Geometry collection.
   */
  function readGeometryCollectionGeometry(object, options) {
    const geometries = object['geometries'].map(
      /**
       * @param {GeoJSONGeometry} geometry Geometry.
       * @return {import("./Feature.js").GeometryObject} geometry Geometry.
       */
      function (geometry) {
        return readGeometryInternal(geometry);
      },
    );
    return geometries;
  }

  /**
   * @param {GeoJSONPoint} object Input object.
   * @return {import("./Feature.js").GeometryObject} Point geometry.
   */
  function readPointGeometry(object) {
    const flatCoordinates = object['coordinates'];
    return {
      type: 'Point',
      flatCoordinates,
      layout: getLayoutForStride(flatCoordinates.length),
    };
  }

  /**
   * @param {GeoJSONLineString} object Object.
   * @return {import("./Feature.js").GeometryObject} LineString geometry.
   */
  function readLineStringGeometry(object) {
    const coordinates = object['coordinates'];
    const flatCoordinates = coordinates.flat();
    return {
      type: 'LineString',
      flatCoordinates,
      ends: [flatCoordinates.length],
      layout: getLayoutForStride(coordinates[0]?.length || 2),
    };
  }

  /**
   * @param {GeoJSONMultiLineString} object Object.
   * @return {import("./Feature.js").GeometryObject} MultiLineString geometry.
   */
  function readMultiLineStringGeometry(object) {
    const coordinates = object['coordinates'];
    const stride = coordinates[0]?.[0]?.length || 2;
    const flatCoordinates = [];
    const ends = deflateCoordinatesArray(flatCoordinates, 0, coordinates, stride);
    return {
      type: 'MultiLineString',
      flatCoordinates,
      ends,
      layout: getLayoutForStride(stride),
    };
  }

  /**
   * @param {GeoJSONMultiPoint} object Object.
   * @return {import("./Feature.js").GeometryObject} MultiPoint geometry.
   */
  function readMultiPointGeometry(object) {
    const coordinates = object['coordinates'];
    return {
      type: 'MultiPoint',
      flatCoordinates: coordinates.flat(),
      layout: getLayoutForStride(coordinates[0]?.length || 2),
    };
  }

  /**
   * @param {GeoJSONMultiPolygon} object Object.
   * @return {import("./Feature.js").GeometryObject} MultiPolygon geometry.
   */
  function readMultiPolygonGeometry(object) {
    const coordinates = object['coordinates'];
    const flatCoordinates = [];
    const stride = coordinates[0]?.[0]?.[0].length || 2;
    const endss = deflateMultiCoordinatesArray(
      flatCoordinates,
      0,
      coordinates,
      stride,
    );
    return {
      type: 'MultiPolygon',
      flatCoordinates,
      ends: endss,
      layout: getLayoutForStride(stride),
    };
  }

  /**
   * @param {GeoJSONPolygon} object Object.
   * @return {import("./Feature.js").GeometryObject} Polygon.
   */
  function readPolygonGeometry(object) {
    const coordinates = object['coordinates'];
    const flatCoordinates = [];
    const stride = coordinates[0]?.[0]?.length;
    const ends = deflateCoordinatesArray(flatCoordinates, 0, coordinates, stride);
    return {
      type: 'Polygon',
      flatCoordinates,
      ends,
      layout: getLayoutForStride(stride),
    };
  }

  /**
   * @param {import("../geom/Geometry.js").default} geometry Geometry.
   * @param {import("./Feature.js").WriteOptions} [options] Write options.
   * @return {GeoJSONGeometry} GeoJSON geometry.
   */
  function writeGeometry(geometry, options) {
    geometry = transformGeometryWithOptions(geometry, true, options);

    const type = geometry.getType();

    /** @type {GeoJSONGeometry} */
    let geoJSON;
    switch (type) {
      case 'Point': {
        geoJSON = writePointGeometry(
          /** @type {import("../geom/Point.js").default} */ (geometry));
        break;
      }
      case 'LineString': {
        geoJSON = writeLineStringGeometry(
          /** @type {import("../geom/LineString.js").default} */ (geometry));
        break;
      }
      case 'Polygon': {
        geoJSON = writePolygonGeometry(
          /** @type {import("../geom/Polygon.js").default} */ (geometry),
          options,
        );
        break;
      }
      case 'MultiPoint': {
        geoJSON = writeMultiPointGeometry(
          /** @type {import("../geom/MultiPoint.js").default} */ (geometry));
        break;
      }
      case 'MultiLineString': {
        geoJSON = writeMultiLineStringGeometry(
          /** @type {import("../geom/MultiLineString.js").default} */ (geometry));
        break;
      }
      case 'MultiPolygon': {
        geoJSON = writeMultiPolygonGeometry(
          /** @type {import("../geom/MultiPolygon.js").default} */ (geometry),
          options,
        );
        break;
      }
      case 'GeometryCollection': {
        geoJSON = writeGeometryCollectionGeometry(
          /** @type {import("../geom/GeometryCollection.js").default} */ (
            geometry
          ),
          options,
        );
        break;
      }
      case 'Circle': {
        geoJSON = {
          type: 'GeometryCollection',
          geometries: [],
        };
        break;
      }
      default: {
        throw new Error('Unsupported geometry type: ' + type);
      }
    }
    return geoJSON;
  }

  /**
   * @param {import("../geom/GeometryCollection.js").default} geometry Geometry.
   * @param {import("./Feature.js").WriteOptions} [options] Write options.
   * @return {GeoJSONGeometryCollection} GeoJSON geometry collection.
   */
  function writeGeometryCollectionGeometry(geometry, options) {
    options = Object.assign({}, options);
    delete options.featureProjection;
    const geometries = geometry.getGeometriesArray().map(function (geometry) {
      return writeGeometry(geometry, options);
    });
    return {
      type: 'GeometryCollection',
      geometries: geometries,
    };
  }

  /**
   * @param {import("../geom/LineString.js").default} geometry Geometry.
   * @param {import("./Feature.js").WriteOptions} [options] Write options.
   * @return {GeoJSONGeometry} GeoJSON geometry.
   */
  function writeLineStringGeometry(geometry, options) {
    return {
      type: 'LineString',
      coordinates: geometry.getCoordinates(),
    };
  }

  /**
   * @param {import("../geom/MultiLineString.js").default} geometry Geometry.
   * @param {import("./Feature.js").WriteOptions} [options] Write options.
   * @return {GeoJSONGeometry} GeoJSON geometry.
   */
  function writeMultiLineStringGeometry(geometry, options) {
    return {
      type: 'MultiLineString',
      coordinates: geometry.getCoordinates(),
    };
  }

  /**
   * @param {import("../geom/MultiPoint.js").default} geometry Geometry.
   * @param {import("./Feature.js").WriteOptions} [options] Write options.
   * @return {GeoJSONGeometry} GeoJSON geometry.
   */
  function writeMultiPointGeometry(geometry, options) {
    return {
      type: 'MultiPoint',
      coordinates: geometry.getCoordinates(),
    };
  }

  /**
   * @param {import("../geom/MultiPolygon.js").default} geometry Geometry.
   * @param {import("./Feature.js").WriteOptions} [options] Write options.
   * @return {GeoJSONGeometry} GeoJSON geometry.
   */
  function writeMultiPolygonGeometry(geometry, options) {
    let right;
    if (options) {
      right = options.rightHanded;
    }
    return {
      type: 'MultiPolygon',
      coordinates: geometry.getCoordinates(right),
    };
  }

  /**
   * @param {import("../geom/Point.js").default} geometry Geometry.
   * @param {import("./Feature.js").WriteOptions} [options] Write options.
   * @return {GeoJSONGeometry} GeoJSON geometry.
   */
  function writePointGeometry(geometry, options) {
    return {
      type: 'Point',
      coordinates: geometry.getCoordinates(),
    };
  }

  /**
   * @param {import("../geom/Polygon.js").default} geometry Geometry.
   * @param {import("./Feature.js").WriteOptions} [options] Write options.
   * @return {GeoJSONGeometry} GeoJSON geometry.
   */
  function writePolygonGeometry(geometry, options) {
    let right;
    if (options) {
      right = options.rightHanded;
    }
    return {
      type: 'Polygon',
      coordinates: geometry.getCoordinates(right),
    };
  }

  /*!
   * venomaps admin scripts
   */
  (function(){

  	// Variabili globali del modulo
  	let preview_map, preview_getview, preview_getsource;
  	let previewMarkersLayer;
  	let previewRouteLayer;
  	let routeClickListenerKey;
  	let markerTemplate;

  	let sourcesettings = {};
  	let rowdata = {};
  	const defaultIcon = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="30px" height="30px" fill="currentColor" viewBox="0 0 30 30" xml:space="preserve"><path d="M15,1C8.7,1,3.5,6.1,3.5,12.3S8.3,22.8,15,28.7c6.7-5.9,11.5-10.2,11.5-16.4S21.3,1,15,1z M15,17.2 c-2.5,0-4.6-2.1-4.6-4.6c0-2.5,2.1-4.6,4.6-4.6s4.6,2.1,4.6,4.6C19.6,15.1,17.5,17.2,15,17.2z"/></svg>';

  	function init() {
  		initModal();
  		initRows();
  		loadGeolocator();
  		loadPreviewMap();
  		initSettingsPage();
  		initRoutesUI();
  		drawSavedRoutesOnLoad();
  		syncPreviewMarkers();
  		initFormValidation();
  		document.onkeydown = onDisableEnterKey;
  	}

  	// =======================================================
  	// NUOVA SEZIONE: VALIDAZIONE DEL FORM PRIMA DEL SALVATAGGIO
  	// =======================================================
  	function validateRoutesBeforeSave() {
  		const allRouteRows = document.querySelectorAll('#vmap-routes-container .vmap-route-row');
  		let allRoutesAreValid = true;

  		allRouteRows.forEach(row => {
  			const stopsContainer = row.querySelector('.vmap-route-stops-select');
  			const geometryInput = row.querySelector('.vmap-route-geometry');
  			const status = row.querySelector('.vmap-route-status');

  			const stopsCount = stopsContainer ? stopsContainer.querySelectorAll('input[type="checkbox"]:checked').length : 0;
  			const hasGeometry = geometryInput && geometryInput.value.trim() !== '';

  			// Reset stile errore
  			row.classList.remove('invalid-route');
  			if (status) {
  				status.classList.remove('error-message');
  				// Non cancellare il testo se è un messaggio di successo
  				if (status.textContent.startsWith('ERROR:')) {
  					status.textContent = '';
  				}
  			}

  			// CONDIZIONE DI ERRORE: l'utente ha selezionato delle tappe ma non ha generato la geometria
  			if (stopsCount > 0 && !hasGeometry) {
  				allRoutesAreValid = false;
  				row.classList.add('invalid-route');
  				if (status) {
  					status.textContent = 'ERROR: Please click "Preview" to generate the route before saving.';
  					status.classList.add('error-message');
  				}
  			}
  		});
  		return allRoutesAreValid;
  	}

  	function initFormValidation() {
  		const postForm = document.getElementById('post');
  		if (postForm) {
  			postForm.addEventListener('submit', function(event) {
  				const routesAreValid = validateRoutesBeforeSave();
  				if (!routesAreValid) {
  					event.preventDefault(); // Blocca il salvataggio
  					alert('There are incomplete routes. Please generate a preview for each route that has stops selected before saving the post.');
  					const firstError = document.querySelector('.invalid-route');
  					if (firstError) {
  						firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
  					}
  				}
  			});
  		}
  	}
  	
  	function loadPreviewMap(){
  		const styleSelector = document.querySelector('select[name="venomaps_style"]');
  		if (!styleSelector) return;

  		let defaultstyle = getStyleData(styleSelector.value);
  		styleSelector.addEventListener('change', function(){
  			const seturl = getStyleData(this.value);
  			if (seturl) preview_getsource.setUrl(seturl);
  		});

  		const maplat = document.querySelector('input[name="venomaps_lat"]');
  		const maplon = document.querySelector('input[name="venomaps_lon"]');
  		sourcesettings.url = defaultstyle;
  		preview_getsource = new OSM(sourcesettings);
  		const baselayer = new TileLayer({ source: preview_getsource });
  		preview_getview = new View({
  			center: fromLonLat([ Number(maplon.value), Number(maplat.value) ]),
  			zoom: 4
  		});
  		preview_map = new Map({
  			target: 'preview-admin-map',
  			view: preview_getview,
  			layers: [ baselayer ],
  			interactions: defaults({mouseWheelZoom:false})
  		});
  		preview_map.on('moveend', function () {
  			const center = preview_getview.getCenter();
  			const lonlat = toLonLat(center);
  			maplat.value = lonlat[1];
  			maplon.value = lonlat[0];
  		});
  		maplat.addEventListener('change', updateCenter);
  		maplon.addEventListener('change', updateCenter);

  		const routeSource = new VectorSource();
  		previewRouteLayer = new VectorLayer({ source: routeSource, zIndex: 10 });
  		
  		const markersSource = new VectorSource();
  		previewMarkersLayer = new VectorLayer({ source: markersSource, zIndex: 11 });
  		
  		preview_map.addLayer(previewRouteLayer);
  		preview_map.addLayer(previewMarkersLayer);
  	}

  	function syncPreviewMarkers() {

  		if (!previewMarkersLayer) {
  			return;
  		}
  		const source = previewMarkersLayer.getSource();
  		source.clear();
  		const allMarkerRows = document.querySelectorAll('.vmap-wrap-rows .vmap-marker-row');

  		if (allMarkerRows.length === 0) {
  			return;
  		}
  		allMarkerRows.forEach((row, index) => {
  			const markerKey = row.dataset.markerKey;
  			const dataTextarea = row.querySelector('.vmap-modal-set-data');
  			const rawData = dataTextarea ? dataTextarea.value : null;

  			if (!rawData || rawData.trim() === '') {
  				return; // Salta questa iterazione
  			}

  			let data;
  			try {
  				data = JSON.parse(rawData);
  			} catch (e) {
  				console.error(`Riga #${index}: ERRORE nel parsing del JSON.`, e);
  				return; // Salta questa iterazione
  			}

  			if (data && data.lon && data.lat && data.lon.toString().trim() !== '' && data.lat.toString().trim() !== '') {
  				
  				const coordinates = fromLonLat([parseFloat(data.lon), parseFloat(data.lat)]);
  				const feature = new Feature({ geometry: new Point(coordinates) });
  				
  				const style = new Style({
  					image: new CircleStyle({
  						radius: 10,
  						fill: new Fill({ color: 'rgba(255, 100, 50, 0.8)' }),
  						stroke: new Stroke({ color: '#fff', width: 2 })
  					}),
  					text: new Text({
  						text: (parseInt(markerKey, 10) + 1).toString(),
  						fill: new Fill({ color: '#fff' }),
  						font: 'bold 12px sans-serif'
  					})
  				});
  				feature.setStyle(style);
  				
  				source.addFeature(feature);
  			} else {
  				console.warn(`Riga #${index}: Coordinate mancanti o non valide nei dati parsati.`);
  			}
  		});

  	}

  	function drawSavedRoutesOnLoad() {
  		setTimeout(() => {

  			if (!previewRouteLayer) {
  				return;
  			}
  			
  			const allRouteRows = document.querySelectorAll('#vmap-routes-container .vmap-route-row');
  			
  			const routeSource = previewRouteLayer.getSource();
  			routeSource.clear();

  			const format = new GeoJSON({
  				dataProjection: 'EPSG:4326',
  				featureProjection: 'EPSG:3857'
  			});
  			const routeColors = ['#0073aa', '#d9534f', '#5cb85c', '#f0ad4e'];
  			let featuresExist = false;

  			allRouteRows.forEach((row, index) => {
  				console.log(`--- Analizzando la riga di percorso #${index} ---`);
  				const geometryInput = row.querySelector('.vmap-route-geometry');
  				
  				if (geometryInput && geometryInput.value && geometryInput.value.trim() !== '') {
  					console.log(`Riga #${index}: Trovata geometria salvata.`);
  					try {
  						const feature = format.readFeature(geometryInput.value);
  						if (feature.getGeometry().getCoordinates().length > 0) {
  							const style = new Style({
  								stroke: new Stroke({
  									color: routeColors[index % routeColors.length],
  									width: 5,
  									lineCap: 'round', lineJoin: 'round'
  								})
  							});
  							feature.setStyle(style);
  							routeSource.addFeature(feature);
  							featuresExist = true;
  							console.log(`Riga #${index}: Feature del percorso aggiunta con successo.`);
  						}
  					} catch (e) {
  						console.error(`Riga #${index}: ERRORE nel parsare la geometria salvata.`, e);
  					}
  				} else {
  					console.warn(`Riga #${index}: Nessuna geometria salvata trovata.`);
  				}
  			});
  			
  			const finalFeatureCount = routeSource.getFeatures().length;

  			if (featuresExist && finalFeatureCount > 0) {
  				const extent = routeSource.getExtent();
  				preview_map.getView().fit(extent, { padding: [40, 40, 40, 40], duration: 500, maxZoom: 16 });
  			} else {
  				console.warn("Nessuna feature valida da mostrare, lo zoom non viene eseguito.");
  			}
  			console.groupEnd();
  		}, 500);
  	}

  	function reIndexMarkers() {
  		const rows = document.querySelectorAll('.vmap-wrap-rows .vmap-marker-row');
  		rows.forEach((row, index) => {
  			row.dataset.markerKey = index;
  			row.id = `vmap-row-${index}`;
  			
  			// Aggiorna il numero visibile #1, #2...
  			const badge = row.querySelector(".vmap-badge-text");
  			if (badge) badge.innerText = index + 1;

  			// Aggiorna il NAME dell'input nascosto per il PHP
  			const dataInput = row.querySelector(".vmap-modal-set-data");
  			if (dataInput) dataInput.name = `venomaps_data[${index}]`;
  		});
  		
  		// Aggiorna selettori rotte e mappa
  		updateAllRouteSelects();
  		syncPreviewMarkers();
  	}

  	function initRows(){
  		const marker_rows = document.querySelectorAll(".vmap-marker-row");
  		if (!marker_rows.length) return false;
  		markerTemplate = marker_rows[0].cloneNode(true);
  		marker_rows.forEach(row => initRow(row));
  		initRepeatable(markerTemplate);
  	}

  	function initRow(row) {
  		const set_title = row.querySelector(".vmap-modal-set-title");
  		const set_lat = row.querySelector(".vmap-modal-set-lat");
  		const set_lon = row.querySelector(".vmap-modal-set-lon");
  		const removeRows = row.querySelectorAll(".vmap-del-row");

  		row.querySelectorAll(".vmap-edit-marker").forEach(trigger => {
  			trigger.addEventListener("click", () => fillModal(row));
  		});

  		removeRows.forEach(removeRow => {
  			removeRow.addEventListener("click", () => {
  				row.remove();
  				reIndexMarkers();
  			});
  		});

  		// FUNZIONE UNICA PER GESTIRE OGNI CAMBIAMENTO
  		function handleInputChange() {
  			const set_data = row.querySelector(".vmap-modal-set-data");
  			let currentData;
  			try {
  				currentData = set_data.value ? JSON.parse(set_data.value) : {};
  			} catch (e) {
  				currentData = {}; // Inizia da un oggetto vuoto se il JSON non è valido
  			}

  			// Aggiorna l'oggetto dati con i valori correnti degli input
  			currentData.title = set_title.value;
  			currentData.lat = set_lat.value;
  			currentData.lon = set_lon.value;
  			
  			// Risalva la stringa JSON nel textarea
  			set_data.value = JSON.stringify(currentData);

  			// Aggiorna l'interfaccia
  			updateAllRouteSelects(); // Necessario per aggiornare il titolo nei select
  			syncPreviewMarkers();   // Ridisegna TUTTI i marker sulla mappa
  		}

  		// Applica lo stesso handler a tutti gli input
  		if(set_title) set_title.addEventListener('input', handleInputChange);
  		if(set_lat) set_lat.addEventListener("input", handleInputChange);
  		if(set_lon) set_lon.addEventListener("input", handleInputChange);
  	}

  	function updateAllRouteSelects() {
  		const allMarkerRows = document.querySelectorAll('.vmap-wrap-rows .vmap-marker-row');
  		const allRouteStopsContainers = document.querySelectorAll('#vmap-routes-container .vmap-route-stops-select');

  		if (allRouteStopsContainers.length === 0) return;

  		// 1. Raccogli i dati di tutti i marker disponibili
  		const markerOptions = [];
  		allMarkerRows.forEach(row => {
  			const key = row.dataset.markerKey;
  			const titleInput = row.querySelector('.vmap-modal-set-title');
  			const title = titleInput && titleInput.value ? titleInput.value : `Marker #${parseInt(key, 10) + 1}`;
  			markerOptions.push({ value: key, text: title });
  		});

  		// 2. Itera su ogni contenitore di checkbox (per ogni rotta) e aggiornalo
  		allRouteStopsContainers.forEach(container => {
  			// Salva i valori attualmente selezionati
  			const checkedValues = Array.from(container.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
  			
  			// Svuota il contenitore
  			container.innerHTML = '';

  			// Ricostruisci la lista di checkbox
  			const parentRow = container.closest('.vmap-route-row');
  			const routeIndex = parentRow ? parentRow.dataset.index : '0';

  			markerOptions.forEach(opt => {
  				const label = document.createElement('label');
  				const checkbox = document.createElement('input');
  				
  				checkbox.type = 'checkbox';
  				checkbox.value = opt.value;
  				checkbox.name = `venomaps_routes[${routeIndex}][stops][]`;

  				if (checkedValues.includes(opt.value)) {
  					checkbox.checked = true;
  				}

  				label.appendChild(checkbox);
  				label.appendChild(document.createTextNode(` ${opt.text}`));
  				container.appendChild(label);
  			});
  		});
  	}

  	function initRoutesUI() {
  		const container = document.getElementById('vmap-routes-container');
  		if (!container) return;

  		container.addEventListener('click', async function(e) {
  			const previewBtn = e.target.closest('.vmap-preview-route');
  			const deleteBtn = e.target.closest('.vmap-del-route');
  			const addBtn = e.target.closest('#vmap-add-route');

  			if (previewBtn) {
  				e.preventDefault();
  				previewBtn.disabled = true;
  				previewBtn.textContent = 'Loading...';
  				const routeRow = previewBtn.closest('.vmap-route-row');
  				const stopsContainer = routeRow.querySelector('.vmap-route-stops-select');
  				
  				const selectedCheckboxes = stopsContainer.querySelectorAll('input[type="checkbox"]:checked');
  				const selectedMarkerIndexes = Array.from(selectedCheckboxes).map(cb => cb.value);

  				if (selectedMarkerIndexes.length < 2) {
  					alert('Please select at least two stops.');
  					previewBtn.disabled = false;
  					previewBtn.textContent = 'Preview';
  					return;
  				}

  				const allMarkerRows = document.querySelectorAll('.vmap-wrap-rows .vmap-marker-row');
  				const markerDataMap = {};
  				allMarkerRows.forEach(row => {
  					const key = row.dataset.markerKey;
  					try {
  						const data = JSON.parse(row.querySelector('.vmap-modal-set-data').value);
  						if (data && data.lon && data.lat) {
  							markerDataMap[key] = { lon: data.lon, lat: data.lat };
  						}
  					} catch (e) {}
  				});

  				const orderedCoords = selectedMarkerIndexes
  					.map(index => {
  						const marker = markerDataMap[index];
  						if (marker) return `${marker.lon},${marker.lat}`;
  						return null;
  					})
  					.filter(coord => coord !== null);

  				const coordsString = orderedCoords.join(';');
  				if (orderedCoords.length < 2) {
  					alert('The selected stops do not have valid coordinates. Please check each selected marker.');
  					previewBtn.disabled = false;
  					previewBtn.textContent = 'Preview';
  					return;
  				}

  				const bodyParams = new URLSearchParams();
  				bodyParams.append("action", "vmap_fetch_osrm_routes");
  				bodyParams.append("nonce", venomapsAdminVars.nonce);
  				bodyParams.append("coords", coordsString);

  				try {
  					const response = await fetch(venomapsAdminVars.ajax_url, {
  						method: 'POST',
  						headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  						body: bodyParams
  					});
  					const result = await response.json();
  					if (result.success) {
  						drawRoutePreviews(result.data, routeRow);
  					} else {
  						alert('Error fetching route: ' + result.data);
  					}
  				} catch (error) {
  					console.error('AJAX Error:', error);
  					alert('An error occurred while fetching the route.');
  				} finally {
  					previewBtn.disabled = false;
  					previewBtn.textContent = 'Preview';
  				}
  			}

  			if (deleteBtn) {
  				deleteBtn.closest('.vmap-route-row').remove();
  				// updateAllRouteSelects();
  				// syncPreviewMarkers();
  				drawSavedRoutesOnLoad(); 
  			}

  			if (addBtn) {
  				const list = container.querySelector('.vmap-routes-list');
  				const template = container.querySelector('#vmap-route-template');

  				let nextIndex = 0;
  				const allRows = list.querySelectorAll('.vmap-route-row');
  				if (allRows.length > 0) {
  					const lastRow = allRows[allRows.length - 1];
  					nextIndex = parseInt(lastRow.dataset.index, 10) + 1;
  				}
  				const newNum = nextIndex + 1;

  				const newRow = template.querySelector('.vmap-route-row').cloneNode(true);
  				newRow.id = `vmap-route-row-${nextIndex}`;
  				newRow.dataset.index = nextIndex;

  				const strongEl = newRow.querySelector('strong');
  				strongEl.textContent = strongEl.textContent.replace('#__NUM__', `#${newNum}`);

  				// Aggiorna i nomi degli input
  				const geometryInput = newRow.querySelector('.vmap-route-geometry');
  				if (geometryInput) geometryInput.name = `venomaps_routes[${nextIndex}][geometry]`;
  				
  				// Aggiorna il contenitore delle checkbox, svuotandolo e ripopolandolo
  				const stopsContainer = newRow.querySelector('.vmap-route-stops-select');
  				if (stopsContainer) {
  					 stopsContainer.innerHTML = 'No markers available yet.'; // Messaggio temporaneo
  				}

  				list.appendChild(newRow);
  				
  				// Aggiorna tutti i contenitori per aggiungere le opzioni alla nuova riga
  				updateAllRouteSelects();
  			}
  		});
  	}

  	function drawRoutePreviews(routes, targetRow) {
  		if (!previewRouteLayer) return;
  		const routeSource = previewRouteLayer.getSource();
  		routeSource.clear();

  		if (routeClickListenerKey) {
  			unByKey(routeClickListenerKey);
  			routeClickListenerKey = null;
  		}
  		
  		const format = new GeoJSON({ dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' });
  		const primaryStyle = new Style({ stroke: new Stroke({ color: '#0073aa', width: 6, lineCap: 'round', lineJoin: 'round' }) });
  		const alternativeStyle = new Style({ stroke: new Stroke({ color: '#888', width: 4, lineCap: 'round', lineJoin: 'round' }) });
  		
  		// 1. Disegna tutte le rotte e assegna loro lo stile alternativo di default
  		routes.forEach((route) => {
  			const feature = format.readFeature(route.geometry);
  			feature.set('full_route_data', route);
  			feature.set('is_preview', true);
  			feature.set('targetRowId', targetRow.id);
  			feature.setStyle(alternativeStyle); // Iniziano tutte come alternative
  			routeSource.addFeature(feature);
  		});

  		// 2. Determina quale feature deve essere quella "attiva" (primaria)
  		const savedGeometryValue = targetRow.querySelector('.vmap-route-geometry').value;
  		let activeFeature = routeSource.getFeatures()[0] || null; // Default alla prima se non troviamo altro

  		if (savedGeometryValue) {
  			try {
  				// Confronta la geometria salvata con quelle nuove
  				const savedGeomString = JSON.stringify(JSON.parse(savedGeometryValue));
  				for (const feature of routeSource.getFeatures()) {
  					const featureGeomString = JSON.stringify(format.writeGeometryObject(feature.getGeometry()));
  					if (featureGeomString === savedGeomString) {
  						activeFeature = feature; // Trovata! Questa è la nostra feature attiva
  						break;
  					}
  				}
  			} catch(e) { /* Ignora errori */ }
  		}
  		
  		// 3. Applica lo stile primario alla feature attiva e salva i suoi dati
  		if (activeFeature) {
  			activeFeature.setStyle(primaryStyle);
  			const activeGeometry = format.writeGeometryObject(activeFeature.getGeometry());
  			targetRow.querySelector('.vmap-route-geometry').value = JSON.stringify(activeGeometry);
  			targetRow.querySelector('.vmap-route-status').textContent = 'Route selected. Click an alternative to change.';
  		}

  		// 4. Zoom e listener
  		const fullExtent = previewMarkersLayer.getSource().getExtent();
  		extend$1(fullExtent, routeSource.getExtent());
  		preview_map.getView().fit(fullExtent, { padding: [40, 40, 40, 40], duration: 500, maxZoom: 16 });
  		
  		routeClickListenerKey = preview_map.on('singleclick', function(e) {
  			const clickedFeature = preview_map.forEachFeatureAtPixel(e.pixel, function(f) {
  				if (f.get('is_preview')) return f;
  			});

  			if (clickedFeature) {
  				const geometryJson = JSON.stringify(format.writeGeometryObject(clickedFeature.getGeometry()));
  				document.getElementById(clickedFeature.get('targetRowId')).querySelector('.vmap-route-geometry').value = geometryJson;
  				
  				routeSource.getFeatures().forEach(f => {
  					f.setStyle(f === clickedFeature ? primaryStyle : alternativeStyle);
  				});
  			}
  		});
  	}

  	function upFile(component) {
  	    const upbutton = component.querySelector('.vmap-set-uploader');
  	    const field = component.querySelector('.vmap-get-uploader');
  	    const post_id = field ? field.dataset.postId : null;
  	    let om_metaImageFrame;

  	    // Helper per inviare i dati senza ripetere codice
  	    const triggerImport = () => {
  	        const url = field.value.trim();
  	        if (url && post_id) {
  	            const data = {
  	                url: url,
  	                id: post_id
  	            };
  	            sendCsvData(data, component);
  	        }
  	    };

  	    if (field && upbutton) {
  	        // Apertura Media Manager
  	        upbutton.addEventListener("click", function(e) {
  	            e.preventDefault();
  	            
  	            // Riutilizza il frame se esiste
  	            if (om_metaImageFrame) {
  	                om_metaImageFrame.open();
  	                return;
  	            }

  	            om_metaImageFrame = wp.media({
  	                title: 'Select CSV File',
  	                button: { text: 'Use this file' },
  	                multiple: false,
  				    library: {
  				        type: 'text/csv' // Forza la visualizzazione dei soli file CSV
  				    }
  	            });

  	            om_metaImageFrame.on("select", function() {
  	                const attachment = om_metaImageFrame.state().get('selection').first().toJSON();
  	                field.value = attachment.url;
  	                
  	                // Trigger automatico dopo la selezione
  	                triggerImport();
  	            });

  	            om_metaImageFrame.open();
  	        });

  	        // Trigger se l'utente incolla un URL manualmente
  	        field.addEventListener("change", triggerImport);
  	    }

  		component.querySelectorAll('input[name="csv_delimiter"]').forEach(el => {
  		    el.addEventListener('change', triggerImport);
  		});
  	}

  	const allUploaders = document.querySelectorAll(".vmap-uploader");
  	allUploaders.forEach( function(component) {
  		upFile(component); 
  	});

  	function sendCsvData(data, component) {
  	    const spinner = component.querySelector(".spinner");
  	    const responseMsg = component.querySelector(".vmap-response-message");
  	    const delimiterInput = component.querySelector('input[name="csv_delimiter"]:checked');
  	    const delimiter = delimiterInput ? delimiterInput.value : ',';

  	    // UI Feedback
  	    if (responseMsg) responseMsg.classList.remove("active");
  	    if (spinner) spinner.classList.add("is-active");

  	    const formData = new FormData();
  	    formData.append("action", "vmap_set_csv");
  	    formData.append("vmap_nonce", venomapsAdminVars.nonce);
  	    formData.append("url", data.url);
  	    formData.append("post_id", data.id);
  	    formData.append("delimiter", delimiter);

  	    fetch(venomapsAdminVars.ajax_url, {
  	        method: 'POST',
  	        body: formData
  	    })
  	    .then(response => response.json())
  	    .then(res => {
  console.log(res);

  	        if (spinner) spinner.classList.remove("is-active");
  	        
  	        if (responseMsg) {
  	            responseMsg.innerText = res.message || res.data;
  	            responseMsg.classList.add("active");
  	        }

  	        if (res.markers) {
  	            // Se la risposta di WP è wp_send_json_success, i dati sono in res.data
  	            rebuildMarkersUI(res.markers);
  	        }
  	    })
  	    .catch(error => {
  	        console.error('Error:', error);
  	        if (spinner) spinner.classList.remove("is-active");
  	    });
  	}

  	function rebuildMarkersUI(markers) {
  		const row_container = document.querySelector(".vmap-wrap-rows");
  		if (!row_container || !markerTemplate) return;

  		row_container.innerHTML = "";

  		markers.forEach((marker, index) => {
  			const newKey = index;
  			const clone = markerTemplate.cloneNode(true);
  			clone.dataset.markerKey = newKey;
  			clone.id = `vmap-row-${newKey}`;
  			clone.querySelector(".vmap-badge-text").innerText = newKey + 1;

  			const inputdata = clone.querySelector(".vmap-modal-set-data");
  			inputdata.name = `venomaps_data[${newKey}]`;

  			// Set marker data
  			marker.key = newKey + 1;
  			inputdata.value = JSON.stringify(marker);

  			// Update visible inputs
  			const titleInput = clone.querySelector(".vmap-modal-set-title");
  			if (titleInput) titleInput.value = marker.title || "";

  			const latInput = clone.querySelector(".vmap-modal-set-lat");
  			if (latInput) latInput.value = marker.lat || "";

  			const lonInput = clone.querySelector(".vmap-modal-set-lon");
  			if (lonInput) lonInput.value = marker.lon || "";

  			row_container.append(clone);
  			initRow(clone);
  		});

  		updateAllRouteSelects();
  		syncPreviewMarkers();
  	}

  	function loadIconImage(upmarker, url = false) {

  		if (!url || url === 'undefined' || url === '' || url === null) {
  			url = false;
  		}

  		const field = upmarker.querySelector( '.vmap-modal-get-icon' );
  		const upimage = upmarker.querySelector( '.vmap-icon-image' );
  		const defaultimage = upmarker.querySelector( '.vmap-icon-default' );
  		const removemarkers = upmarker.querySelectorAll( '.venomaps_marker_remove_btn' );
  		const color_component = upmarker.querySelector(".vmap-color-component");

  		if (url) {
  			// Mostra immagine custom
  			const img = new Image();
  			img.src = url;
  			img.onload = function () {
  				upimage.innerHTML = `<img src="${url}" style="width:100%; height:auto;">`;
  				upimage.classList.remove("vmap-hidden");
  				defaultimage.classList.add("vmap-hidden");
  				removemarkers.forEach(btn => btn.classList.remove("vmap-invisible"));
  				if (color_component) color_component.classList.add("vmap-hidden");
  			};
  			field.value = url;
  		} else {
  			// Ritorna all'SVG di default
  			field.value = "";
  			defaultimage.innerHTML = defaultIcon;
  			upimage.classList.add("vmap-hidden");
  			upimage.innerHTML = "";
  			defaultimage.classList.remove("vmap-hidden");
  			removemarkers.forEach(btn => btn.classList.add("vmap-invisible"));
  			if (color_component) color_component.classList.remove("vmap-hidden");
  		}
  	}

  	function iconUploader(modal_component){
  		if (!modal_component) return false;
  		const upmarker = modal_component.classList.contains('vmap-icon-uploader') 
  		? modal_component 
  		: modal_component.querySelector(".vmap-icon-uploader");

  		if (!upmarker) return false; // Esci se non trovi nulla
  		const upbuttons = upmarker.querySelectorAll( '.venomaps_marker_upload_btn' );
  		const removemarkers = upmarker.querySelectorAll( '.venomaps_marker_remove_btn' );
  		const color_component = modal_component.querySelector(".vmap-color-component");
  		let get_target;
  		var om_metaImageFrame;
  		removemarkers.forEach(function(removemarker){
  			removemarker.addEventListener("click", function(e){
  				e.preventDefault();
  				removemarker.classList.add("vmap-invisible");
  				color_component.classList.remove("vmap-hidden");
  				loadIconImage(upmarker, false);
  				rowdata.icon = "";
  				get_target = modal_component.dataset.rowTarget || false;
  				if (get_target) updateRowData(get_target);
  			});
  		});
  		upbuttons.forEach(function(upbutton){
  			upbutton.addEventListener("click", function(e){
  				e.preventDefault();
  				om_metaImageFrame = wp.media.frames.om_metaImageFrame = wp.media();
  				om_metaImageFrame.on("select", function(){
  					var media_attachment = om_metaImageFrame.state().get('selection').first().toJSON();
  					if (media_attachment.url) {
  						loadIconImage(upmarker, media_attachment.url);
  						rowdata.icon = media_attachment.url;
  						get_target = modal_component.dataset.rowTarget || false;
  						if (get_target) updateRowData(get_target);
  					}
  				});
  				om_metaImageFrame.open();
  			});
  		});
  		const get_color = upmarker.querySelector( '.vmap-modal-get-color' );
  		if (get_color) {
  			get_color.addEventListener("input", function(){
  				rowdata.color = get_color.value;
  				updateIconStyle(modal_component);
  			});	
  		}
  		const get_size = modal_component.querySelector(".vmap-modal-get-size");
  		if (get_size) {
  			get_size.addEventListener("input", function(){
  				updateIconStyle(modal_component);
  			});
  		}
  	}

  	function fillModal(row){
  		const dialog = document.querySelector("#vmap-modal");
  		dialog.classList.add("active");
  		const modal_component = dialog.querySelector(".vmap-modal-content");

  		modal_component.dataset.rowTarget = row.id;
  		const set_data = row.querySelector(".vmap-modal-set-data");

  		const globalDefaults = JSON.parse(venomapsAdminVars.default_settings);
  		const savedData = set_data.value ? JSON.parse(set_data.value) : {};

  		rowdata = { ...globalDefaults, ...savedData };
  		
  		const rowindex = rowdata.key || (parseInt(row.dataset.markerKey) + 1);
  		modal_component.dataset.index = rowindex;

  		const get_size = modal_component.querySelector(".vmap-modal-get-size");
  		const get_icon = modal_component.querySelector(".vmap-modal-get-icon");
  		const get_color = modal_component.querySelector(".vmap-modal-get-color");
  		const set_color = modal_component.querySelector(".vmap-modal-set-color");
  		const get_infobox = modal_component.querySelector(".vmap-modal-get-infobox");
  		const get_infobox_open = modal_component.querySelector(".vmap-modal-get-infobox-open");
  		const get_header = modal_component.querySelector(".vmap-modal-title");
  		const upmarker = modal_component.querySelector(".vmap-icon-uploader");

  		get_size.value = rowdata.size;
  		get_icon.value = rowdata.icon;
  		get_color.value = rowdata.color;
  		set_color.value = rowdata.color;
  		get_infobox.value = rowdata.infobox;
  		get_infobox_open.checked = rowdata.infobox_open == 1;
  		get_header.innerHTML = `<span class="vmap-badge">#${rowindex}</span> ${rowdata.title}`;

  		(rowdata.icon && rowdata.icon !== 'undefined') ? rowdata.icon : false;

  		loadIconImage(upmarker, rowdata.icon);
  		updateIconStyle(modal_component);
  	}
  	
  	function updateRowData(row_target){
  		if (!row_target) return false;
  		const row = document.getElementById(row_target);
  		if (!row) return false;
  		const set_data = row.querySelector(".vmap-modal-set-data");
  		set_data.value = JSON.stringify(rowdata);	
  	}

  	function updateIconStyle(modal_component){
  		const upimage = modal_component.querySelector( '.vmap-icon-image' );
  		const defaultimage = modal_component.querySelector( '.vmap-icon-default' );
  		upimage.style.width = rowdata.size + "px";
  		defaultimage.style.width = rowdata.size + "px";
  		defaultimage.style.color = rowdata.color;
  	}

  	function initModal(){
  		const dialog = document.querySelector("#vmap-modal");
  		if (!dialog) return false;
  		const modal_component = dialog.querySelector(".vmap-modal-content");
  		const dismiss_modal = dialog.querySelectorAll(".vmap-modal-dismiss");
  		dismiss_modal.forEach(modal_close => {
  			modal_close.addEventListener("click", () => dialog.classList.remove("active"));
  		});
  		const get_size = modal_component.querySelector('.vmap-modal-get-size' );
  		const get_color = modal_component.querySelector('.vmap-modal-get-color' );
  		const get_infobox = modal_component.querySelector(".vmap-modal-get-infobox");
  		const get_infobox_open = modal_component.querySelector('.vmap-modal-get-infobox-open' );
  		const set_color = modal_component.querySelector('.vmap-modal-set-color' );
  		let get_target;
  		get_color.addEventListener("change", function(){
  			rowdata.color = get_color.value;
  			set_color.value = get_color.value;
  			updateIconStyle(modal_component);
  			get_target = modal_component.dataset.rowTarget || false;
  			if (get_target) updateRowData(get_target);
  		});
  		set_color.addEventListener("change", function(){
  			rowdata.color = set_color.value;
  			get_color.value = set_color.value;
  			updateIconStyle(modal_component);
  			get_target = modal_component.dataset.rowTarget || false;
  			if (get_target) updateRowData(get_target);
  		});
  		get_size.addEventListener("change", function(){
  			rowdata.size = get_size.value;
  			updateIconStyle(modal_component);
  			get_target = modal_component.dataset.rowTarget || false;
  			if (get_target) updateRowData(get_target);
  		});
  		get_infobox.addEventListener("change", function(){
  			rowdata.infobox = get_infobox.value;
  			get_target = modal_component.dataset.rowTarget || false;
  			if (get_target) updateRowData(get_target);
  		});
  		get_infobox_open.addEventListener("change", function(){
  			rowdata.infobox_open = get_infobox_open.checked;
  			get_target = modal_component.dataset.rowTarget || false;
  			if (get_target) updateRowData(get_target);
  		});
  		iconUploader(modal_component);
  	}

  	function initRepeatable(clonemaster) {
  		const row_container = document.querySelector(".vmap-wrap-rows");
  		const newmarker_btn = document.querySelector(".wpol-new-marker");

  		if (!clonemaster || !row_container || !newmarker_btn) return false;

  		newmarker_btn.addEventListener("click", function() {
  			// const allRows = row_container.querySelectorAll('.vmap-marker-row');
  			// let maxKey = -1;
  			// allRows.forEach(r => {
  			//     const key = parseInt(r.dataset.markerKey, 10);
  			//     if (key > maxKey) maxKey = key;
  			// });
  			// const newKey = maxKey + 1;
  			// const clone = clonemaster.cloneNode(true);
  			// clone.dataset.markerKey = newKey;
  			// clone.id = `vmap-row-${newKey}`;
  			// clone.querySelector(".vmap-badge-text").innerText = newKey + 1;

  			// const inputdata = clone.querySelector(".vmap-modal-set-data");
  			// inputdata.name = `venomaps_data[${newKey}]`;
  			
  			// // CORREZIONE: Inizializza con un JSON valido ma vuoto
  			// let newMarkerData = JSON.parse(venomapsAdminVars.default_settings);
  			// newMarkerData.lat = '';
  			// newMarkerData.lon = '';
  			// newMarkerData.title = '';
  			// newMarkerData.key = newKey + 1;
  			// inputdata.value = JSON.stringify(newMarkerData);
  			
  			// clone.querySelectorAll("input[type='text']").forEach(input => {
  			//     input.value = "";
  			// });

  			// row_container.append(clone);
  			// initRow(clone);
  			// updateAllRouteSelects();
  			// syncPreviewMarkers();

  	const clone = clonemaster.cloneNode(true);
  	
  	// Pulisce i campi del clone
  	clone.querySelector(".vmap-modal-set-data").value = venomapsAdminVars.default_settings;
  	clone.querySelectorAll("input[type='text']").forEach(input => input.value = "");

  	row_container.append(clone);
  	initRow(clone); // Inizializza i listener sulla nuova riga
  	reIndexMarkers(); // Sistema gli indici per includere la nuova riga

  		});
  	}


  	function initSettingsPage() {
  		const repeatablegroup = document.querySelector(".wpol-repeatable-group");
  		const callrepeatables = document.querySelectorAll(".wpol-call-repeat");
  		if (!repeatablegroup || !callrepeatables.length) return;
  		callrepeatables.forEach(callrepeatable => {
  			callrepeatable.addEventListener("click", function(){
  				const lastItem = repeatablegroup.querySelector(".wpol-repeatable-item:last-child");
  				const lastitemnum = lastItem ? lastItem.dataset.number : -1;
  				const newnum = parseFloat(lastitemnum) + 1;
  				const newItemHtml = `<div class="wpol-repeatable-item wpol-form-group vmap-flex vmap-flex-collapse" data-number="${newnum}"><input type="text" class="all-options" name="venomaps_settings[style][${newnum}][name]" value="map style ${newnum}"> <div class="vmap-flex-grow-1"><input type="url" class="large-text" name="venomaps_settings[style][${newnum}][url]" value="" placeholder="https://provider.ext/{z}/{x}/{y}.png?api_key=..."></div></div>`;
  				repeatablegroup.insertAdjacentHTML('beforeend', newItemHtml);
  			});
  		});

  		// --- NUOVA LOGICA: INIZIALIZZAZIONE MARKER GLOBALI ---
  		const settingsWrapper = document.querySelector("#vmap-global-settings-wrapper");
  		if (settingsWrapper) {
  			// Prepariamo rowdata con i valori attuali dei settings
  			rowdata = {
  				color: settingsWrapper.querySelector('.vmap-modal-get-color').value,
  				size: settingsWrapper.querySelector('.vmap-modal-get-size').value,
  				icon: settingsWrapper.querySelector('.vmap-modal-get-icon').value
  			};

  			// Inizializziamo l'uploader (riutilizza la tua funzione iconUploader)
  			// Passiamo settingsWrapper come se fosse il modal_component
  			iconUploader(settingsWrapper);

  			// Se c'è un'icona salvata, carichiamola subito nell'anteprima
  			// Carica l'anteprima iniziale
  			loadIconImage(settingsWrapper, rowdata.icon);

  			// Listener per aggiornare l'anteprima (Size e Color)
  			const sizeInput = settingsWrapper.querySelector('.vmap-modal-get-size');
  			const colorInput = settingsWrapper.querySelector('.vmap-modal-get-color');

  			if (sizeInput) {
  				sizeInput.addEventListener('input', () => {
  					rowdata.size = sizeInput.value;
  					updateIconStyle(settingsWrapper);
  				});
  			}
  			if (colorInput) {
  				colorInput.addEventListener('input', () => {
  					rowdata.color = colorInput.value;
  					// Aggiorna anche il testo accanto se presente
  					const colorText = settingsWrapper.querySelector('.vmap-modal-set-color');
  					if (colorText) colorText.value = colorInput.value;
  					updateIconStyle(settingsWrapper);
  				});
  			}

  			// Sovrascriviamo leggermente il comportamento di updateRowData 
  			// perché nei settings non abbiamo un textarea JSON da aggiornare ad ogni click,
  			// ma degli input standard che si aggiornano da soli.
  			// Tuttavia, dobbiamo assicurarci che updateIconStyle funzioni:
  			settingsWrapper.querySelectorAll('input').forEach(input => {
  				input.addEventListener('input', () => {
  					rowdata.color = settingsWrapper.querySelector('.vmap-modal-get-color').value;
  					rowdata.size = settingsWrapper.querySelector('.vmap-modal-get-size').value;
  					updateIconStyle(settingsWrapper);
  				});
  			});
  		}

  	}

  	function getStyleData(currentstyle) {
  		const styles = JSON.parse(venomapsAdminVars.styles);
  		var parts = currentstyle.split(/_(.*)/s);
  		const stylegroup = parts[0];
  		const stylemap = parts[1];
  		if (styles[stylegroup] && styles[stylegroup].maps[stylemap]) {
  			return styles[stylegroup].maps[stylemap].url;
  		}
  		return false;
  	}


  	function updateCenter(){
  		const maplat = document.querySelector('input[name="venomaps_lat"]').value;
  		const maplon = document.querySelector('input[name="venomaps_lon"]').value;
  		if (maplat && maplon) {
  			const coords = fromLonLat([ Number(maplon), Number(maplat) ]);
  			preview_getview.animate({center: coords});
  		}
  	}

  	function loadGeolocator() {
  		const geomarker_id = "infomarker_admin";
  		const latinput = document.querySelector(".venomaps-get-lat");
  		const loninput = document.querySelector(".venomaps-get-lon");
  		const responseinput = document.querySelector(".venomaps-response");
  		const getcoords = document.querySelector(".venomaps-get-coordinates");
  		if (!latinput || !loninput || !responseinput || !getcoords) return;

  		var om_map_pos = fromLonLat([-74.005974, 40.712776]);
  		var view = new View({ center: om_map_pos, zoom: 2 });
  		var map = new Map({
  			target: 'wpol-admin-map',
  			view: view,
  			layers: [ new TileLayer({ source: new OSM() }) ],
  			interactions: defaults({mouseWheelZoom:false})
  		});
  		var marker_el = document.getElementById(geomarker_id);
  		var infomarker = new Overlay({
  			position: om_map_pos,
  			positioning: 'center-center',
  			element: marker_el,
  			stopEvent: false
  		});
  		map.addOverlay(infomarker);
  		var dragPan;
  		map.getInteractions().forEach(interaction => {
  			if (interaction instanceof DragPan) dragPan = interaction;
  		});
  		marker_el.addEventListener('mousedown', () => {
  			dragPan.setActive(false);
  			infomarker.set('dragging', true);
  		});
  		map.on("pointermove", (evt) => {
  			if (infomarker.get('dragging')) infomarker.setPosition(evt.coordinate);
  		});
  		map.on("pointerup", (evt) => {
  			if (infomarker.get('dragging')) {
  				dragPan.setActive(true);
  				infomarker.set('dragging', false);
  				var lonlat = toLonLat(evt.coordinate);
  				latinput.value = lonlat[1];
  				loninput.value = lonlat[0];
  			}
  		});
  		function georesponse(response){
  			var lat, lon;
  			responseinput.innerHTML = "";
  			if (response[0]) {
  				lat = response[0].lat;
  				lon = response[0].lon;
  				var newcoord = fromLonLat([lon, lat]);
  				infomarker.setPosition(newcoord);
  				view.setCenter(newcoord);
  				view.setZoom(6);
  			} else {
  				lat = ''; lon = '';
  				responseinput.innerHTML = "Nothing Found";
  			}
  			latinput.value = lat;
  			loninput.value = lon;
  		}
  		getcoords.addEventListener("click", function(){
  			const address = document.querySelector(".venomaps-set-address").value;
  			if (address.length > 3) {
  				getcoords.classList.add("vmap-hidden");
  				var encoded = encodeURIComponent(address);
  				fetch(`https://nominatim.openstreetmap.org/search?q=${encoded}&format=json`)
  					.then(response => response.json())
  					.then(data => {
  						georesponse(data);
  						getcoords.classList.remove("vmap-hidden");
  					});
  			}
  		});
  	}

  	function onDisableEnterKey(evt) {
  		var elem = evt.target;
  		if (evt.keyCode === 13 && (elem.type =='text' || elem.type =='url' || elem.type =='number')) {
  			evt.preventDefault();
  			return false;
  		}
  	}

  	document.addEventListener("DOMContentLoaded", init);
  })();

})();
