text: import when from "../ThirdParty/when.js";
  import AttributeCompression from "./AttributeCompression.js";
  import BoundingSphere from "./BoundingSphere.js";
  import Cartesian3 from "./Cartesian3.js";
  import Credit from "./Credit.js";
  import defaultValue from "./defaultValue.js";
  import defined from "./defined.js";
  import DeveloperError from "./DeveloperError.js";
  import Event from "./Event.js";
  import GeographicTilingScheme from "./GeographicTilingScheme.js";
  import WebMercatorTilingScheme from "./WebMercatorTilingScheme.js";
  import getJsonFromTypedArray from "./getJsonFromTypedArray.js";
  import HeightmapTerrainData from "./HeightmapTerrainData.js";
  import IndexDatatype from "./IndexDatatype.js";
  import OrientedBoundingBox from "./OrientedBoundingBox.js";
  import QuantizedMeshTerrainData from "./QuantizedMeshTerrainData.js";
  import Request from "./Request.js";
  import RequestType from "./RequestType.js";
  import Resource from "./Resource.js";
  import RuntimeError from "./RuntimeError.js";
  import TerrainProvider from "./TerrainProvider.js";
  import TileAvailability from "./TileAvailability.js";
  import TileProviderError from "./TileProviderError.js";
  
  function LayerInformation(layer) {
    this.resource = layer.resource;
    this.version = layer.version;
    this.isHeightmap = layer.isHeightmap;
    this.tileUrlTemplates = layer.tileUrlTemplates;
    this.availability = layer.availability;
    this.hasVertexNormals = layer.hasVertexNormals;
    this.hasWaterMask = layer.hasWaterMask;
    this.hasMetadata = layer.hasMetadata;
    this.availabilityLevels = layer.availabilityLevels;
    this.availabilityTilesLoaded = layer.availabilityTilesLoaded;
    this.littleEndianExtensionSize = layer.littleEndianExtensionSize;
    this.availabilityPromiseCache = {};
  }
  
  /**
   * A {@link TerrainProvider} that accesses terrain data in a Cesium terrain format.
   *
   * @alias CesiumTerrainProvider
   * @constructor
   *
   * @param {Object} options Object with the following properties:
   * @param {Resource|String|Promise<Resource>|Promise<String>} options.url The URL of the Cesium terrain server.
   * @param {Boolean} [options.requestVertexNormals=false] Flag that indicates if the client should request additional lighting information from the server, in the form of per vertex normals if available.
   * @param {Boolean} [options.requestWaterMask=false] Flag that indicates if the client should request per tile water masks from the server,  if available.
   * @param {Boolean} [options.requestMetadata=true] Flag that indicates if the client should request per tile metadata from the server, if available.
   * @param {Ellipsoid} [options.ellipsoid] The ellipsoid.  If not specified, the WGS84 ellipsoid is used.
   * @param {Credit|String} [options.credit] A credit for the data source, which is displayed on the canvas.
   *
   *
   * @example
   * // Create Arctic DEM terrain with normals.
   * var viewer = new Cesium.Viewer('cesiumContainer', {
   *     terrainProvider : new Cesium.CesiumTerrainProvider({
   *         url : Cesium.IonResource.fromAssetId(3956),
   *         requestVertexNormals : true
   *     })
   * });
   *
   * @see createWorldTerrain
   * @see TerrainProvider
   */
  function CesiumTerrainProvider(options) {
    //>>includeStart('debug', pragmas.debug)
    if (!defined(options) || !defined(options.url)) {
      throw new DeveloperError("options.url is required.");
    }
    //>>includeEnd('debug');
  
    this._heightmapWidth = 65;
    this._heightmapStructure = undefined;
    this._hasWaterMask = false;
    this._hasVertexNormals = false;
    this._ellipsoid = options.ellipsoid;
  
    /**
     * Boolean flag that indicates if the client should request vertex normals from the server.
     * @type {Boolean}
     * @default false
     * @private
     */
    this._requestVertexNormals = defaultValue(
      options.requestVertexNormals,
      false
    );
  
    /**
     * Boolean flag that indicates if the client should request tile watermasks from the server.
     * @type {Boolean}
     * @default false
     * @private
     */
    this._requestWaterMask = defaultValue(options.requestWaterMask, false);
  
    /**
     * Boolean flag that indicates if the client should request tile metadata from the server.
     * @type {Boolean}
     * @default true
     * @private
     */
    this._requestMetadata = defaultValue(options.requestMetadata, true);
  
    this._errorEvent = new Event();
  
    var credit = options.credit;
    if (typeof credit === "string") {
      credit = new Credit(credit);
    }
    this._credit = credit;
  
    this._availability = undefined;
  
    var deferred = when.defer();
    this._ready = false;
    this._readyPromise = deferred;
    this._tileCredits = undefined;
  
    var that = this;
    var lastResource;
    var layerJsonResource;
    var metadataError;
  
    var layers = (this._layers = []);
    var attribution = "";
    var overallAvailability = [];
    var overallMaxZoom = 0;
    when(options.url)
      .then(function (url) {
        var resource = Resource.createIfNeeded(url);
        resource.appendForwardSlash();
        lastResource = resource;
        layerJsonResource = lastResource.getDerivedResource({
          url: "layer.json",
        });
  
        // ion resources have a credits property we can use for additional attribution.
        that._tileCredits = resource.credits;
  
        requestLayerJson();
      })
      .otherwise(function (e) {
        deferred.reject(e);
      });
  
    function parseMetadataSuccess(data) {
      var message;
  
      if (!data.format) {
        message = "The tile format is not specified in the layer.json file.";
        metadataError = TileProviderError.handleError(
          metadataError,
          that,
          that._errorEvent,
          message,
          undefined,
          undefined,
          undefined,
          requestLayerJson
        );
        return;
      }
  
      if (!data.tiles || data.tiles.length === 0) {
        message = "The layer.json file does not specify any tile URL templates.";
        metadataError = TileProviderError.handleError(
          metadataError,
          that,
          that._errorEvent,
          message,
          undefined,
          undefined,
          undefined,
          requestLayerJson
        );
        return;
      }
  
      var hasVertexNormals = false;
      var hasWaterMask = false;
      var hasMetadata = false;
      var littleEndianExtensionSize = true;
      var isHeightmap = false;
      if (data.format === "heightmap-1.0") {
        isHeightmap = true;
        if (!defined(that._heightmapStructure)) {
          that._heightmapStructure = {
            heightScale: 1.0 / 5.0,
            heightOffset: -1000.0,
            elementsPerHeight: 1,
            stride: 1,
            elementMultiplier: 256.0,
            isBigEndian: false,
            lowestEncodedHeight: 0,
            highestEncodedHeight: 256 * 256 - 1,
          };
        }
        hasWaterMask = true;
        that._requestWaterMask = true;
      } else if (data.format.indexOf("quantized-mesh-1.") !== 0) {
        message =
          'The tile format "' + data.format + '" is invalid or not supported.';
        metadataError = TileProviderError.handleError(
          metadataError,
          that,
          that._errorEvent,
          message,
          undefined,
          undefined,
          undefined,
          requestLayerJson
        );
        return;
      }
  
      var tileUrlTemplates = data.tiles;
  
      var maxZoom = data.maxzoom;
      overallMaxZoom = Math.max(overallMaxZoom, maxZoom);
      // Keeps track of which of the availablity containing tiles have been loaded
  
      if (!data.projection || data.projection === "EPSG:4326") {
        that._tilingScheme = new GeographicTilingScheme({
          numberOfLevelZeroTilesX: 2,
          numberOfLevelZeroTilesY: 1,
          ellipsoid: that._ellipsoid,
        });
      } else if (data.projection === "EPSG:3857") {
        that._tilingScheme = new WebMercatorTilingScheme({
          numberOfLevelZeroTilesX: 1,
          numberOfLevelZeroTilesY: 1,
          ellipsoid: that._ellipsoid,
        });
      } else {
        message =
          'The projection "' + data.projection + '" is invalid or not supported.';
        metadataError = TileProviderError.handleError(
          metadataError,
          that,
          that._errorEvent,
          message,
          undefined,
          undefined,
          undefined,
          requestLayerJson
        );
        return;
      }
  
      that._levelZeroMaximumGeometricError = TerrainProvider.getEstimatedLevelZeroGeometricErrorForAHeightmap(
        that._tilingScheme.ellipsoid,
        that._heightmapWidth,
        that._tilingScheme.getNumberOfXTilesAtLevel(0)
      );
      if (!data.scheme || data.scheme === "tms" || data.scheme === "slippyMap") {
        that._scheme = data.scheme;
      } else {
        message = 'The scheme "' + data.scheme + '" is invalid or not supported.';
        metadataError = TileProviderError.handleError(
          metadataError,
          that,
          that._errorEvent,
          message,
          undefined,
          undefined,
          undefined,
          requestLayerJson
        );
        return;
      }
  
      var availabilityTilesLoaded;
  
      // The vertex normals defined in the 'octvertexnormals' extension is identical to the original
      // contents of the original 'vertexnormals' extension.  'vertexnormals' extension is now
      // deprecated, as the extensionLength for this extension was incorrectly using big endian.
      // We maintain backwards compatibility with the legacy 'vertexnormal' implementation
      // by setting the _littleEndianExtensionSize to false. Always prefer 'octvertexnormals'
      // over 'vertexnormals' if both extensions are supported by the server.
      if (
        defined(data.extensions) &&
        data.extensions.indexOf("octvertexnormals") !== -1
      ) {
        hasVertexNormals = true;
      } else if (
        defined(data.extensions) &&
        data.extensions.indexOf("vertexnormals") !== -1
      ) {
        hasVertexNormals = true;
        littleEndianExtensionSize = false;
      }
      if (
        defined(data.extensions) &&
        data.extensions.indexOf("watermask") !== -1
      ) {
        hasWaterMask = true;
      }
      if (
        defined(data.extensions) &&
        data.extensions.indexOf("metadata") !== -1
      ) {
        hasMetadata = true;
      }
  
      var availabilityLevels = data.metadataAvailability;
      var availableTiles = data.available;
      var availability;
      if (defined(availableTiles) && !defined(availabilityLevels)) {
        availability = new TileAvailability(
          that._tilingScheme,
          availableTiles.length
        );
        for (var level = 0; level < availableTiles.length; ++level) {
          var rangesAtLevel = availableTiles[level];
          var yTiles = that._tilingScheme.getNumberOfYTilesAtLevel(level);
          if (!defined(overallAvailability[level])) {
            overallAvailability[level] = [];
          }
  
          for (
            var rangeIndex = 0;
            rangeIndex < rangesAtLevel.length;
            ++rangeIndex
          ) {
            var range = rangesAtLevel[rangeIndex];
            var yStart = yTiles - range.endY - 1;
            var yEnd = yTiles - range.startY - 1;
            overallAvailability[level].push([
              range.startX,
              yStart,
              range.endX,
              yEnd,
            ]);
            availability.addAvailableTileRange(
              level,
              range.startX,
              yStart,
              range.endX,
              yEnd
            );
          }
        }
      } else if (defined(availabilityLevels)) {
        availabilityTilesLoaded = new TileAvailability(
          that._tilingScheme,
          maxZoom
        );
        availability = new TileAvailability(that._tilingScheme, maxZoom);
        overallAvailability[0] = [[0, 0, 1, 0]];
        availability.addAvailableTileRange(0, 0, 0, 1, 0);
      }
  
      that._hasWaterMask = that._hasWaterMask || hasWaterMask;
      that._hasVertexNormals = that._hasVertexNormals || hasVertexNormals;
      that._hasMetadata = that._hasMetadata || hasMetadata;
      if (defined(data.attribution)) {
        if (attribution.length > 0) {
          attribution += " ";
        }
        attribution += data.attribution;
      }
  
      layers.push(
        new LayerInformation({
          resource: lastResource,
          version: data.version,
          isHeightmap: isHeightmap,
          tileUrlTemplates: tileUrlTemplates,
          availability: availability,
          hasVertexNormals: hasVertexNormals,
          hasWaterMask: hasWaterMask,
          hasMetadata: hasMetadata,
          availabilityLevels: availabilityLevels,
          availabilityTilesLoaded: availabilityTilesLoaded,
          littleEndianExtensionSize: littleEndianExtensionSize,
        })
      );
  
      var parentUrl = data.parentUrl;
      if (defined(parentUrl)) {
        if (!defined(availability)) {
          console.log(
            "A layer.json can't have a parentUrl if it does't have an available array."
          );
          return when.resolve();
        }
        lastResource = lastResource.getDerivedResource({
          url: parentUrl,
        });
        lastResource.appendForwardSlash(); // Terrain always expects a directory
        layerJsonResource = lastResource.getDerivedResource({
          url: "layer.json",
        });
        var parentMetadata = layerJsonResource.fetchJson();
        return when(parentMetadata, parseMetadataSuccess, parseMetadataFailure);
      }
  
      return when.resolve();
    }
  
    function parseMetadataFailure(data) {
      var message =
        "An error occurred while accessing " + layerJsonResource.url + ".";
      metadataError = TileProviderError.handleError(
        metadataError,
        that,
        that._errorEvent,
        message,
        undefined,
        undefined,
        undefined,
        requestLayerJson
      );
    }
  
    function metadataSuccess(data) {
      parseMetadataSuccess(data).then(function () {
        if (defined(metadataError)) {
          return;
        }
  
        var length = overallAvailability.length;
        if (length > 0) {
          var availability = (that._availability = new TileAvailability(
            that._tilingScheme,
            overallMaxZoom
          ));
          for (var level = 0; level < length; ++level) {
            var levelRanges = overallAvailability[level];
            for (var i = 0; i < levelRanges.length; ++i) {
              var range = levelRanges[i];
              availability.addAvailableTileRange(
                level,
                range[0],
                range[1],
                range[2],
                range[3]
              );
            }
          }
        }
  
        if (attribution.length > 0) {
          var layerJsonCredit = new Credit(attribution);
  
          if (defined(that._tileCredits)) {
            that._tileCredits.push(layerJsonCredit);
          } else {
            that._tileCredits = [layerJsonCredit];
          }
        }
  
        that._ready = true;
        that._readyPromise.resolve(true);
      });
    }
  
    function metadataFailure(data) {
      // If the metadata is not found, assume this is a pre-metadata heightmap tileset.
      if (defined(data) && data.statusCode === 404) {
        metadataSuccess({
          tilejson: "2.1.0",
          format: "heightmap-1.0",
          version: "1.0.0",
          scheme: "tms",
          tiles: ["{z}/{x}/{y}.terrain?v={version}"],
        });
        return;
      }
      parseMetadataFailure(data);
    }
  
    function requestLayerJson() {
      when(layerJsonResource.fetchJson())
        .then(metadataSuccess)
        .otherwise(metadataFailure);
    }
  }
  
  /**
   * When using the Quantized-Mesh format, a tile may be returned that includes additional extensions, such as PerVertexNormals, watermask, etc.
   * This enumeration defines the unique identifiers for each type of extension data that has been appended to the standard mesh data.
   *
   * @namespace QuantizedMeshExtensionIds
   * @see CesiumTerrainProvider
   * @private
   */
  var QuantizedMeshExtensionIds = {
    /**
     * Oct-Encoded Per-Vertex Normals are included as an extension to the tile mesh
     *
     * @type {Number}
     * @constant
     * @default 1
     */
    OCT_VERTEX_NORMALS: 1,
    /**
     * A watermask is included as an extension to the tile mesh
     *
     * @type {Number}
     * @constant
     * @default 2
     */
    WATER_MASK: 2,
    /**
     * A json object contain metadata about the tile
     *
     * @type {Number}
     * @constant
     * @default 4
     */
    METADATA: 4,
  };
  
  function getRequestHeader(extensionsList) {
    if (!defined(extensionsList) || extensionsList.length === 0) {
      return {
        Accept:
          "application/vnd.quantized-mesh,application/octet-stream;q=0.9,*/*;q=0.01",
      };
    }
    var extensions = extensionsList.join("-");
    return {
      Accept:
        "application/vnd.quantized-mesh;extensions=" +
        extensions +
        ",application/octet-stream;q=0.9,*/*;q=0.01",
    };
  }
  
  function createHeightmapTerrainData(provider, buffer, level, x, y) {
    var heightBuffer = new Uint16Array(
      buffer,
      0,
      provider._heightmapWidth * provider._heightmapWidth
    );
    return new HeightmapTerrainData({
      buffer: heightBuffer,
      childTileMask: new Uint8Array(buffer, heightBuffer.byteLength, 1)[0],
      waterMask: new Uint8Array(
        buffer,
        heightBuffer.byteLength + 1,
        buffer.byteLength - heightBuffer.byteLength - 1
      ),
      width: provider._heightmapWidth,
      height: provider._heightmapWidth,
      structure: provider._heightmapStructure,
      credits: provider._tileCredits,
    });
  }
  
  function createQuantizedMeshTerrainData(provider, buffer, level, x, y, layer) {
    var littleEndianExtensionSize = layer.littleEndianExtensionSize;
    var pos = 0;
    var cartesian3Elements = 3;
    var boundingSphereElements = cartesian3Elements + 1;
    var cartesian3Length = Float64Array.BYTES_PER_ELEMENT * cartesian3Elements;
    var boundingSphereLength =
      Float64Array.BYTES_PER_ELEMENT * boundingSphereElements;
    var encodedVertexElements = 3;
    var encodedVertexLength =
      Uint16Array.BYTES_PER_ELEMENT * encodedVertexElements;
    var triangleElements = 3;
    var bytesPerIndex = Uint16Array.BYTES_PER_ELEMENT;
    var triangleLength = bytesPerIndex * triangleElements;
  
    var view = new DataView(buffer);
    var center = new Cartesian3(
      view.getFloat64(pos, true),
      view.getFloat64(pos + 8, true),
      view.getFloat64(pos + 16, true)
    );
    pos += cartesian3Length;
  
    var minimumHeight = view.getFloat32(pos, true);
    pos += Float32Array.BYTES_PER_ELEMENT;
    var maximumHeight = view.getFloat32(pos, true);
    pos += Float32Array.BYTES_PER_ELEMENT;
  
    var boundingSphere = new BoundingSphere(
      new Cartesian3(
        view.getFloat64(pos, true),
        view.getFloat64(pos + 8, true),
        view.getFloat64(pos + 16, true)
      ),
      view.getFloat64(pos + cartesian3Length, true)
    );
    pos += boundingSphereLength;
  
    var horizonOcclusionPoint = new Cartesian3(
      view.getFloat64(pos, true),
      view.getFloat64(pos + 8, true),
      view.getFloat64(pos + 16, true)
    );
    pos += cartesian3Length;
  
    var vertexCount = view.getUint32(pos, true);
    pos += Uint32Array.BYTES_PER_ELEMENT;
    var encodedVertexBuffer = new Uint16Array(buffer, pos, vertexCount * 3);
    pos += vertexCount * encodedVertexLength;
  
    if (vertexCount > 64 * 1024) {
      // More than 64k vertices, so indices are 32-bit.
      bytesPerIndex = Uint32Array.BYTES_PER_ELEMENT;
      triangleLength = bytesPerIndex * triangleElements;
    }
  
    // Decode the vertex buffer.
    var uBuffer = encodedVertexBuffer.subarray(0, vertexCount);
    var vBuffer = encodedVertexBuffer.subarray(vertexCount, 2 * vertexCount);
    var heightBuffer = encodedVertexBuffer.subarray(
      vertexCount * 2,
      3 * vertexCount
    );
  
    AttributeCompression.zigZagDeltaDecode(uBuffer, vBuffer, heightBuffer);
  
    // skip over any additional padding that was added for 2/4 byte alignment
    if (pos % bytesPerIndex !== 0) {
      pos += bytesPerIndex - (pos % bytesPerIndex);
    }
  
    var triangleCount = view.getUint32(pos, true);
    pos += Uint32Array.BYTES_PER_ELEMENT;
    var indices = IndexDatatype.createTypedArrayFromArrayBuffer(
      vertexCount,
      buffer,
      pos,
      triangleCount * triangleElements
    );
    pos += triangleCount * triangleLength;
  
    // High water mark decoding based on decompressIndices_ in webgl-loader's loader.js.
    // https://code.google.com/p/webgl-loader/source/browse/trunk/samples/loader.js?r=99#55
    // Copyright 2012 Google Inc., Apache 2.0 license.
    var highest = 0;
    var length = indices.length;
    for (var i = 0; i < length; ++i) {
      var code = indices[i];
      indices[i] = highest - code;
      if (code === 0) {
        ++highest;
      }
    }
  
    var westVertexCount = view.getUint32(pos, true);
    pos += Uint32Array.BYTES_PER_ELEMENT;
    var westIndices = IndexDatatype.createTypedArrayFromArrayBuffer(
      vertexCount,
      buffer,
      pos,
      westVertexCount
    );
    pos += westVertexCount * bytesPerIndex;
  
    var southVertexCount = view.getUint32(pos, true);
    pos += Uint32Array.BYTES_PER_ELEMENT;
    var southIndices = IndexDatatype.createTypedArrayFromArrayBuffer(
      vertexCount,
      buffer,
      pos,
      southVertexCount
    );
    pos += southVertexCount * bytesPerIndex;
  
    var eastVertexCount = view.getUint32(pos, true);
    pos += Uint32Array.BYTES_PER_ELEMENT;
    var eastIndices = IndexDatatype.createTypedArrayFromArrayBuffer(
      vertexCount,
      buffer,
      pos,
      eastVertexCount
    );
    pos += eastVertexCount * bytesPerIndex;
  
    var northVertexCount = view.getUint32(pos, true);
    pos += Uint32Array.BYTES_PER_ELEMENT;
    var northIndices = IndexDatatype.createTypedArrayFromArrayBuffer(
      vertexCount,
      buffer,
      pos,
      northVertexCount
    );
    pos += northVertexCount * bytesPerIndex;
  
    var encodedNormalBuffer;
    var waterMaskBuffer;
    while (pos < view.byteLength) {
      var extensionId = view.getUint8(pos, true);
      pos += Uint8Array.BYTES_PER_ELEMENT;
      var extensionLength = view.getUint32(pos, littleEndianExtensionSize);
      pos += Uint32Array.BYTES_PER_ELEMENT;
  
      if (
        extensionId === QuantizedMeshExtensionIds.OCT_VERTEX_NORMALS &&
        provider._requestVertexNormals
      ) {
        encodedNormalBuffer = new Uint8Array(buffer, pos, vertexCount * 2);
      } else if (
        extensionId === QuantizedMeshExtensionIds.WATER_MASK &&
        provider._requestWaterMask
      ) {
        waterMaskBuffer = new Uint8Array(buffer, pos, extensionLength);
      } else if (
        extensionId === QuantizedMeshExtensionIds.METADATA &&
        provider._requestMetadata
      ) {
        var stringLength = view.getUint32(pos, true);
        if (stringLength > 0) {
          var metadata = getJsonFromTypedArray(
            new Uint8Array(buffer),
            pos + Uint32Array.BYTES_PER_ELEMENT,
            stringLength
          );
          var availableTiles = metadata.available;
          if (defined(availableTiles)) {
            for (var offset = 0; offset < availableTiles.length; ++offset) {
              var availableLevel = level + offset + 1;
              var rangesAtLevel = availableTiles[offset];
              var yTiles = provider._tilingScheme.getNumberOfYTilesAtLevel(
                availableLevel
              );
  
              for (
                var rangeIndex = 0;
                rangeIndex < rangesAtLevel.length;
                ++rangeIndex
              ) {
                var range = rangesAtLevel[rangeIndex];
                var yStart = yTiles - range.endY - 1;
                var yEnd = yTiles - range.startY - 1;
                provider.availability.addAvailableTileRange(
                  availableLevel,
                  range.startX,
                  yStart,
                  range.endX,
                  yEnd
                );
                layer.availability.addAvailableTileRange(
                  availableLevel,
                  range.startX,
                  yStart,
                  range.endX,
                  yEnd
                );
              }
            }
          }
        }
        layer.availabilityTilesLoaded.addAvailableTileRange(level, x, y, x, y);
      }
      pos += extensionLength;
    }
  
    var skirtHeight = provider.getLevelMaximumGeometricError(level) * 5.0;
  
    // The skirt is not included in the OBB computation. If this ever
    // causes any rendering artifacts (cracks), they are expected to be
    // minor and in the corners of the screen. It's possible that this
    // might need to be changed - just change to `minimumHeight - skirtHeight`
    // A similar change might also be needed in `upsampleQuantizedTerrainMesh.js`.
    var rectangle = provider._tilingScheme.tileXYToRectangle(x, y, level);
    var orientedBoundingBox = OrientedBoundingBox.fromRectangle(
      rectangle,
      minimumHeight,
      maximumHeight,
      provider._tilingScheme.ellipsoid
    );
  
    return new QuantizedMeshTerrainData({
      center: center,
      minimumHeight: minimumHeight,
      maximumHeight: maximumHeight,
      boundingSphere: boundingSphere,
      orientedBoundingBox: orientedBoundingBox,
      horizonOcclusionPoint: horizonOcclusionPoint,
      quantizedVertices: encodedVertexBuffer,
      encodedNormals: encodedNormalBuffer,
      indices: indices,
      westIndices: westIndices,
      southIndices: southIndices,
      eastIndices: eastIndices,
      northIndices: northIndices,
      westSkirtHeight: skirtHeight,
      southSkirtHeight: skirtHeight,
      eastSkirtHeight: skirtHeight,
      northSkirtHeight: skirtHeight,
      childTileMask: provider.availability.computeChildMaskForTile(level, x, y),
      waterMask: waterMaskBuffer,
      credits: provider._tileCredits,
    });
  }
  
  /**
   * Requests the geometry for a given tile.  This function should not be called before
   * {@link CesiumTerrainProvider#ready} returns true.  The result must include terrain data and
   * may optionally include a water mask and an indication of which child tiles are available.
   *
   * @param {Number} x The X coordinate of the tile for which to request geometry.
   * @param {Number} y The Y coordinate of the tile for which to request geometry.
   * @param {Number} level The level of the tile for which to request geometry.
   * @param {Request} [request] The request object. Intended for internal use only.
   *
   * @returns {Promise.<TerrainData>|undefined} A promise for the requested geometry.  If this method
   *          returns undefined instead of a promise, it is an indication that too many requests are already
   *          pending and the request will be retried later.
   *
   * @exception {DeveloperError} This function must not be called before {@link CesiumTerrainProvider#ready}
   *            returns true.
   */
  CesiumTerrainProvider.prototype.requestTileGeometry = function (
    x,
    y,
    level,
    request
  ) {
    //>>includeStart('debug', pragmas.debug)
    if (!this._ready) {
      throw new DeveloperError(
        "requestTileGeometry must not be called before the terrain provider is ready."
      );
    }
    //>>includeEnd('debug');
  
    var layers = this._layers;
    var layerToUse;
    var layerCount = layers.length;
  
    if (layerCount === 1) {
      // Optimized path for single layers
      layerToUse = layers[0];
    } else {
      for (var i = 0; i < layerCount; ++i) {
        var layer = layers[i];
        if (
          !defined(layer.availability) ||
          layer.availability.isTileAvailable(level, x, y)
        ) {
          layerToUse = layer;
          break;
        }
      }
    }
  
    return requestTileGeometry(this, x, y, level, layerToUse, request);
  };
  
  function requestTileGeometry(provider, x, y, level, layerToUse, request) {
    if (!defined(layerToUse)) {
      return when.reject(new RuntimeError("Terrain tile doesn't exist"));
    }
  
    var urlTemplates = layerToUse.tileUrlTemplates;
    if (urlTemplates.length === 0) {
      return undefined;
    }
  
    // The TileMapService scheme counts from the bottom left
    var terrainY;
    if (!provider._scheme || provider._scheme === "tms") {
      var yTiles = provider._tilingScheme.getNumberOfYTilesAtLevel(level);
      terrainY = yTiles - y - 1;
    } else {
      terrainY = y;
    }
  
    var extensionList = [];
    if (provider._requestVertexNormals && layerToUse.hasVertexNormals) {
      extensionList.push(
        layerToUse.littleEndianExtensionSize
          ? "octvertexnormals"
          : "vertexnormals"
      );
    }
    if (provider._requestWaterMask && layerToUse.hasWaterMask) {
      extensionList.push("watermask");
    }
    if (provider._requestMetadata && layerToUse.hasMetadata) {
      extensionList.push("metadata");
    }
  
    var headers;
    var query;
    var url = urlTemplates[(x + terrainY + level) % urlTemplates.length];
  
    var resource = layerToUse.resource;
    if (
      defined(resource._ionEndpoint) &&
      !defined(resource._ionEndpoint.externalType)
    ) {
      // ion uses query paremeters to request extensions
      if (extensionList.length !== 0) {
        query = { extensions: extensionList.join("-") };
      }
      headers = getRequestHeader(undefined);
    } else {
      //All other terrain servers
      headers = getRequestHeader(extensionList);
    }
  
    var promise = resource
      .getDerivedResource({
        url: url,
        templateValues: {
          version: layerToUse.version,
          z: level,
          x: x,
          y: terrainY,
        },
        queryParameters: query,
        headers: headers,
        request: request,
      })
      .fetchArrayBuffer();
  
    if (!defined(promise)) {
      return undefined;
    }
  
    return promise.then(function (buffer) {
      if (defined(provider._heightmapStructure)) {
        return createHeightmapTerrainData(provider, buffer, level, x, y);
      }
      return createQuantizedMeshTerrainData(
        provider,
        buffer,
        level,
        x,
        y,
        layerToUse
      );
    });
  }
  
  Object.defineProperties(CesiumTerrainProvider.prototype, {
    /**
     * Gets an event that is raised when the terrain provider encounters an asynchronous error.  By subscribing
     * to the event, you will be notified of the error and can potentially recover from it.  Event listeners
     * are passed an instance of {@link TileProviderError}.
     * @memberof CesiumTerrainProvider.prototype
     * @type {Event}
     */
    errorEvent: {
      get: function () {
        return this._errorEvent;
      },
    },
  
    /**
     * Gets the credit to display when this terrain provider is active.  Typically this is used to credit
     * the source of the terrain.  This function should not be called before {@link CesiumTerrainProvider#ready} returns true.
     * @memberof CesiumTerrainProvider.prototype
     * @type {Credit}
     */
    credit: {
      get: function () {
        //>>includeStart('debug', pragmas.debug)
        if (!this._ready) {
          throw new DeveloperError(
            "credit must not be called before the terrain provider is ready."
          );
        }
        //>>includeEnd('debug');
  
        return this._credit;
      },
    },
  
    /**
     * Gets the tiling scheme used by this provider.  This function should
     * not be called before {@link CesiumTerrainProvider#ready} returns true.
     * @memberof CesiumTerrainProvider.prototype
     * @type {GeographicTilingScheme}
     */
    tilingScheme: {
      get: function () {
        //>>includeStart('debug', pragmas.debug)
        if (!this._ready) {
          throw new DeveloperError(
            "tilingScheme must not be called before the terrain provider is ready."
          );
        }
        //>>includeEnd('debug');
  
        return this._tilingScheme;
      },
    },
  
    /**
     * Gets a value indicating whether or not the provider is ready for use.
     * @memberof CesiumTerrainProvider.prototype
     * @type {Boolean}
     */
    ready: {
      get: function () {
        return this._ready;
      },
    },
  
    /**
     * Gets a promise that resolves to true when the provider is ready for use.
     * @memberof CesiumTerrainProvider.prototype
     * @type {Promise.<Boolean>}
     * @readonly
     */
    readyPromise: {
      get: function () {
        return this._readyPromise.promise;
      },
    },
  
    /**
     * Gets a value indicating whether or not the provider includes a water mask.  The water mask
     * indicates which areas of the globe are water rather than land, so they can be rendered
     * as a reflective surface with animated waves.  This function should not be
     * called before {@link CesiumTerrainProvider#ready} returns true.
     * @memberof CesiumTerrainProvider.prototype
     * @type {Boolean}
     * @exception {DeveloperError} This property must not be called before {@link CesiumTerrainProvider#ready}
     */
    hasWaterMask: {
      get: function () {
        //>>includeStart('debug', pragmas.debug)
        if (!this._ready) {
          throw new DeveloperError(
            "hasWaterMask must not be called before the terrain provider is ready."
          );
        }
        //>>includeEnd('debug');
  
        return this._hasWaterMask && this._requestWaterMask;
      },
    },
  
    /**
     * Gets a value indicating whether or not the requested tiles include vertex normals.
     * This function should not be called before {@link CesiumTerrainProvider#ready} returns true.
     * @memberof CesiumTerrainProvider.prototype
     * @type {Boolean}
     * @exception {DeveloperError} This property must not be called before {@link CesiumTerrainProvider#ready}
     */
    hasVertexNormals: {
      get: function () {
        //>>includeStart('debug', pragmas.debug)
        if (!this._ready) {
          throw new DeveloperError(
            "hasVertexNormals must not be called before the terrain provider is ready."
          );
        }
        //>>includeEnd('debug');
  
        // returns true if we can request vertex normals from the server
        return this._hasVertexNormals && this._requestVertexNormals;
      },
    },
  
    /**
     * Gets a value indicating whether or not the requested tiles include metadata.
     * This function should not be called before {@link CesiumTerrainProvider#ready} returns true.
     * @memberof CesiumTerrainProvider.prototype
     * @type {Boolean}
     * @exception {DeveloperError} This property must not be called before {@link CesiumTerrainProvider#ready}
     */
    hasMetadata: {
      get: function () {
        //>>includeStart('debug', pragmas.debug)
        if (!this._ready) {
          throw new DeveloperError(
            "hasMetadata must not be called before the terrain provider is ready."
          );
        }
        //>>includeEnd('debug');
  
        // returns true if we can request metadata from the server
        return this._hasMetadata && this._requestMetadata;
      },
    },
  
    /**
     * Boolean flag that indicates if the client should request vertex normals from the server.
     * Vertex normals data is appended to the standard tile mesh data only if the client requests the vertex normals and
     * if the server provides vertex normals.
     * @memberof CesiumTerrainProvider.prototype
     * @type {Boolean}
     */
    requestVertexNormals: {
      get: function () {
        return this._requestVertexNormals;
      },
    },
  
    /**
     * Boolean flag that indicates if the client should request a watermask from the server.
     * Watermask data is appended to the standard tile mesh data only if the client requests the watermask and
     * if the server provides a watermask.
     * @memberof CesiumTerrainProvider.prototype
     * @type {Boolean}
     */
    requestWaterMask: {
      get: function () {
        return this._requestWaterMask;
      },
    },
  
    /**
     * Boolean flag that indicates if the client should request metadata from the server.
     * Metadata is appended to the standard tile mesh data only if the client requests the metadata and
     * if the server provides a metadata.
     * @memberof CesiumTerrainProvider.prototype
     * @type {Boolean}
     */
    requestMetadata: {
      get: function () {
        return this._requestMetadata;
      },
    },
  
    /**
     * Gets an object that can be used to determine availability of terrain from this provider, such as
     * at points and in rectangles.  This function should not be called before
     * {@link CesiumTerrainProvider#ready} returns true.  This property may be undefined if availability
     * information is not available. Note that this reflects tiles that are known to be available currently.
     * Additional tiles may be discovered to be available in the future, e.g. if availability information
     * exists deeper in the tree rather than it all being discoverable at the root. However, a tile that
     * is available now will not become unavailable in the future.
     * @memberof CesiumTerrainProvider.prototype
     * @type {TileAvailability}
     */
    availability: {
      get: function () {
        //>>includeStart('debug', pragmas.debug)
        if (!this._ready) {
          throw new DeveloperError(
            "availability must not be called before the terrain provider is ready."
          );
        }
        //>>includeEnd('debug');
        return this._availability;
      },
    },
  });
  
  /**
   * Gets the maximum geometric error allowed in a tile at a given level.
   *
   * @param {Number} level The tile level for which to get the maximum geometric error.
   * @returns {Number} The maximum geometric error.
   */
  CesiumTerrainProvider.prototype.getLevelMaximumGeometricError = function (
    level
  ) {
    return this._levelZeroMaximumGeometricError / (1 << level);
  };
  
  /**
   * Determines whether data for a tile is available to be loaded.
   *
   * @param {Number} x The X coordinate of the tile for which to request geometry.
   * @param {Number} y The Y coordinate of the tile for which to request geometry.
   * @param {Number} level The level of the tile for which to request geometry.
   * @returns {Boolean} Undefined if not supported or availability is unknown, otherwise true or false.
   */
  CesiumTerrainProvider.prototype.getTileDataAvailable = function (x, y, level) {
    if (!defined(this._availability)) {
      return undefined;
    }
    if (level > this._availability._maximumLevel) {
      return false;
    }
  
    if (this._availability.isTileAvailable(level, x, y)) {
      // If the tile is listed as available, then we are done
      return true;
    }
    if (!this._hasMetadata) {
      // If we don't have any layers with the metadata extension then we don't have this tile
      return false;
    }
  
    var layers = this._layers;
    var count = layers.length;
    for (var i = 0; i < count; ++i) {
      var layerResult = checkLayer(this, x, y, level, layers[i], i === 0);
      if (layerResult.result) {
        // There is a layer that may or may not have the tile
        return undefined;
      }
    }
  
    return false;
  };
  
  /**
   * Makes sure we load availability data for a tile
   *
   * @param {Number} x The X coordinate of the tile for which to request geometry.
   * @param {Number} y The Y coordinate of the tile for which to request geometry.
   * @param {Number} level The level of the tile for which to request geometry.
   * @returns {undefined|Promise<void>} Undefined if nothing need to be loaded or a Promise that resolves when all required tiles are loaded
   */
  CesiumTerrainProvider.prototype.loadTileDataAvailability = function (
    x,
    y,
    level
  ) {
    if (
      !defined(this._availability) ||
      level > this._availability._maximumLevel ||
      this._availability.isTileAvailable(level, x, y) ||
      !this._hasMetadata
    ) {
      // We know the tile is either available or not available so nothing to wait on
      return undefined;
    }
  
    var layers = this._layers;
    var count = layers.length;
    for (var i = 0; i < count; ++i) {
      var layerResult = checkLayer(this, x, y, level, layers[i], i === 0);
      if (defined(layerResult.promise)) {
        return layerResult.promise;
      }
    }
  };
  
  function getAvailabilityTile(layer, x, y, level) {
    if (level === 0) {
      return;
    }
  
    var availabilityLevels = layer.availabilityLevels;
    var parentLevel =
      level % availabilityLevels === 0
        ? level - availabilityLevels
        : ((level / availabilityLevels) | 0) * availabilityLevels;
    var divisor = 1 << (level - parentLevel);
    var parentX = (x / divisor) | 0;
    var parentY = (y / divisor) | 0;
  
    return {
      level: parentLevel,
      x: parentX,
      y: parentY,
    };
  }
  
  function checkLayer(provider, x, y, level, layer, topLayer) {
    if (!defined(layer.availabilityLevels)) {
      // It's definitely not in this layer
      return {
        result: false,
      };
    }
  
    var cacheKey;
    var deleteFromCache = function () {
      delete layer.availabilityPromiseCache[cacheKey];
    };
    var availabilityTilesLoaded = layer.availabilityTilesLoaded;
    var availability = layer.availability;
  
    var tile = getAvailabilityTile(layer, x, y, level);
    while (defined(tile)) {
      if (
        availability.isTileAvailable(tile.level, tile.x, tile.y) &&
        !availabilityTilesLoaded.isTileAvailable(tile.level, tile.x, tile.y)
      ) {
        var requestPromise;
        if (!topLayer) {
          cacheKey = tile.level + "-" + tile.x + "-" + tile.y;
          requestPromise = layer.availabilityPromiseCache[cacheKey];
          if (!defined(requestPromise)) {
            // For cutout terrain, if this isn't the top layer the availability tiles
            //  may never get loaded, so request it here.
            var request = new Request({
              throttle: false,
              throttleByServer: true,
              type: RequestType.TERRAIN,
            });
            requestPromise = requestTileGeometry(
              provider,
              tile.x,
              tile.y,
              tile.level,
              layer,
              request
            );
            if (defined(requestPromise)) {
              layer.availabilityPromiseCache[cacheKey] = requestPromise;
              requestPromise.then(deleteFromCache);
            }
          }
        }
  
        // The availability tile is available, but not loaded, so there
        //  is still a chance that it may become available at some point
        return {
          result: true,
          promise: requestPromise,
        };
      }
  
      tile = getAvailabilityTile(layer, tile.x, tile.y, tile.level);
    }
  
    return {
      result: false,
    };
  }
  
  // Used for testing
  CesiumTerrainProvider._getAvailabilityTile = getAvailabilityTile;
  export default CesiumTerrainProvider;
  