import BoundingRectangle from "../Core/BoundingRectangle.js";
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartesian4 from "../Core/Cartesian4.js";
import Cartographic from "../Core/Cartographic.js";
import Check from "../Core/Check.js";
import Color from "../Core/Color.js";
import createGuid from "../Core/createGuid.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import DistanceDisplayCondition from "../Core/DistanceDisplayCondition.js";
import Matrix4 from "../Core/Matrix4.js";
import NearFarScalar from "../Core/NearFarScalar.js";
import Resource from "../Core/Resource.js";
import HeightReference from "./HeightReference.js";
import HorizontalOrigin from "./HorizontalOrigin.js";
import SceneMode from "./SceneMode.js";
import SceneTransforms from "./SceneTransforms.js";
import VerticalOrigin from "./VerticalOrigin.js";
/**
 * A viewport-aligned image positioned in the 3D scene, that is created
 * and rendered using a {@link BillboardCollection}.  A billboard is created and its initial
 * properties are set by calling {@link BillboardCollection#add}.
 * 
 * 

x increases from
   * left to right, and y increases from top to bottom.
   * | *default | *b.pixeloffset = new Cartesian2(50, 25); | 
x points towards the viewer's right, y points up, and
   * z points into the screen.  Eye coordinates use the same scale as world and model coordinates,
   * which is typically meters.
   * | * | * | 
b.eyeOffset = new Cartesian3(0.0, 8000000.0, 0.0);

1.0 does not change the size of the billboard; a scale greater than
   * 1.0 enlarges the billboard; a positive scale less than 1.0 shrinks
   * the billboard.
   * 
0.5, 1.0,
   * and 2.0.
   * 0.0 makes the billboard transparent, and 1.0 makes the billboard opaque.
   * | *default | *alpha : 0.5 | 
value's red, green,
   * blue, and alpha properties as shown in Example 1.  These components range from 0.0
   * (no intensity) to 1.0 (full intensity).
   * @memberof Billboard.prototype
   * @type {Color}
   *
   * @example
   * // Example 1. Assign yellow.
   * b.color = Cesium.Color.YELLOW;
   *
   * @example
   * // Example 2. Make a billboard 50% translucent.
   * b.color = new Cesium.Color(1.0, 1.0, 1.0, 0.5);
   */
  color: {
    get: function () {
      return this._color;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.object("value", value);
      //>>includeEnd('debug');
      const color = this._color;
      if (!Color.equals(color, value)) {
        Color.clone(value, color);
        makeDirty(this, COLOR_INDEX);
      }
    },
  },
  /**
   * Gets or sets the rotation angle in radians.
   * @memberof Billboard.prototype
   * @type {Number}
   */
  rotation: {
    get: function () {
      return this._rotation;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number("value", value);
      //>>includeEnd('debug');
      if (this._rotation !== value) {
        this._rotation = value;
        makeDirty(this, ROTATION_INDEX);
      }
    },
  },
  /**
   * Gets or sets the aligned axis in world space. The aligned axis is the unit vector that the billboard up vector points towards.
   * The default is the zero vector, which means the billboard is aligned to the screen up vector.
   * @memberof Billboard.prototype
   * @type {Cartesian3}
   * @example
   * // Example 1.
   * // Have the billboard up vector point north
   * billboard.alignedAxis = Cesium.Cartesian3.UNIT_Z;
   *
   * @example
   * // Example 2.
   * // Have the billboard point east.
   * billboard.alignedAxis = Cesium.Cartesian3.UNIT_Z;
   * billboard.rotation = -Cesium.Math.PI_OVER_TWO;
   *
   * @example
   * // Example 3.
   * // Reset the aligned axis
   * billboard.alignedAxis = Cesium.Cartesian3.ZERO;
   */
  alignedAxis: {
    get: function () {
      return this._alignedAxis;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.object("value", value);
      //>>includeEnd('debug');
      const alignedAxis = this._alignedAxis;
      if (!Cartesian3.equals(alignedAxis, value)) {
        Cartesian3.clone(value, alignedAxis);
        makeDirty(this, ALIGNED_AXIS_INDEX);
      }
    },
  },
  /**
   * Gets or sets a width for the billboard. If undefined, the image width will be used.
   * @memberof Billboard.prototype
   * @type {Number}
   */
  width: {
    get: function () {
      return defaultValue(this._width, this._imageWidth);
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (defined(value)) {
        Check.typeOf.number("value", value);
      }
      //>>includeEnd('debug');
      if (this._width !== value) {
        this._width = value;
        makeDirty(this, IMAGE_INDEX_INDEX);
      }
    },
  },
  /**
   * Gets or sets a height for the billboard. If undefined, the image height will be used.
   * @memberof Billboard.prototype
   * @type {Number}
   */
  height: {
    get: function () {
      return defaultValue(this._height, this._imageHeight);
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (defined(value)) {
        Check.typeOf.number("value", value);
      }
      //>>includeEnd('debug');
      if (this._height !== value) {
        this._height = value;
        makeDirty(this, IMAGE_INDEX_INDEX);
      }
    },
  },
  /**
   * Gets or sets if the billboard size is in meters or pixels. true to size the billboard in meters;
   * otherwise, the size is in pixels.
   * @memberof Billboard.prototype
   * @type {Boolean}
   * @default false
   */
  sizeInMeters: {
    get: function () {
      return this._sizeInMeters;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.bool("value", value);
      //>>includeEnd('debug');
      if (this._sizeInMeters !== value) {
        this._sizeInMeters = value;
        makeDirty(this, COLOR_INDEX);
      }
    },
  },
  /**
   * Gets or sets the condition specifying at what distance from the camera that this billboard will be displayed.
   * @memberof Billboard.prototype
   * @type {DistanceDisplayCondition}
   * @default undefined
   */
  distanceDisplayCondition: {
    get: function () {
      return this._distanceDisplayCondition;
    },
    set: function (value) {
      if (
        !DistanceDisplayCondition.equals(value, this._distanceDisplayCondition)
      ) {
        //>>includeStart('debug', pragmas.debug);
        if (defined(value)) {
          Check.typeOf.object("value", value);
          if (value.far <= value.near) {
            throw new DeveloperError(
              "far distance must be greater than near distance."
            );
          }
        }
        //>>includeEnd('debug');
        this._distanceDisplayCondition = DistanceDisplayCondition.clone(
          value,
          this._distanceDisplayCondition
        );
        makeDirty(this, DISTANCE_DISPLAY_CONDITION);
      }
    },
  },
  /**
   * Gets or sets the distance from the camera at which to disable the depth test to, for example, prevent clipping against terrain.
   * When set to zero, the depth test is always applied. When set to Number.POSITIVE_INFINITY, the depth test is never applied.
   * @memberof Billboard.prototype
   * @type {Number}
   */
  disableDepthTestDistance: {
    get: function () {
      return this._disableDepthTestDistance;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (defined(value)) {
        Check.typeOf.number("value", value);
        if (value < 0.0) {
          throw new DeveloperError(
            "disableDepthTestDistance must be greater than or equal to 0.0."
          );
        }
      }
      //>>includeEnd('debug');
      if (this._disableDepthTestDistance !== value) {
        this._disableDepthTestDistance = value;
        makeDirty(this, DISABLE_DEPTH_DISTANCE);
      }
    },
  },
  /**
   * Gets or sets the user-defined object returned when the billboard is picked.
   * @memberof Billboard.prototype
   * @type {Object}
   */
  id: {
    get: function () {
      return this._id;
    },
    set: function (value) {
      this._id = value;
      if (defined(this._pickId)) {
        this._pickId.object.id = value;
      }
    },
  },
  /**
   * The primitive to return when picking this billboard.
   * @memberof Billboard.prototype
   * @private
   */
  pickPrimitive: {
    get: function () {
      return this._pickPrimitive;
    },
    set: function (value) {
      this._pickPrimitive = value;
      if (defined(this._pickId)) {
        this._pickId.object.primitive = value;
      }
    },
  },
  /**
   * @private
   */
  pickId: {
    get: function () {
      return this._pickId;
    },
  },
  /**
   * * Gets or sets the image to be used for this billboard. If a texture has already been created for the * given image, the existing texture is used. *
** This property can be set to a loaded Image, a URL which will be loaded as an Image automatically, * a canvas, or another billboard's image property (from the same billboard collection). *
* * @memberof Billboard.prototype * @type {String} * @example * // load an image from a URL * b.image = 'some/image/url.png'; * * // assuming b1 and b2 are billboards in the same billboard collection, * // use the same image for both billboards. * b2.image = b1.image; */ image: { get: function () { return this._imageId; }, set: function (value) { if (!defined(value)) { this._imageIndex = -1; this._imageSubRegion = undefined; this._imageId = undefined; this._image = undefined; this._imageIndexPromise = undefined; makeDirty(this, IMAGE_INDEX_INDEX); } else if (typeof value === "string") { this.setImage(value, value); } else if (value instanceof Resource) { this.setImage(value.url, value); } else if (defined(value.src)) { this.setImage(value.src, value); } else { this.setImage(createGuid(), value); } }, }, /** * Whentrue, this billboard is ready to render, i.e., the image
   * has been downloaded and the WebGL resources are created.
   *
   * @memberof Billboard.prototype
   *
   * @type {Boolean}
   * @readonly
   *
   * @default false
   */
  ready: {
    get: function () {
      return this._imageIndex !== -1;
    },
  },
  /**
   * Keeps track of the position of the billboard based on the height reference.
   * @memberof Billboard.prototype
   * @type {Cartesian3}
   * @private
   */
  _clampedPosition: {
    get: function () {
      return this._actualClampedPosition;
    },
    set: function (value) {
      this._actualClampedPosition = Cartesian3.clone(
        value,
        this._actualClampedPosition
      );
      makeDirty(this, POSITION_INDEX);
    },
  },
  /**
   * Determines whether or not this billboard will be shown or hidden because it was clustered.
   * @memberof Billboard.prototype
   * @type {Boolean}
   * @private
   */
  clusterShow: {
    get: function () {
      return this._clusterShow;
    },
    set: function (value) {
      if (this._clusterShow !== value) {
        this._clusterShow = value;
        makeDirty(this, SHOW_INDEX);
      }
    },
  },
  /**
   * The outline color of this Billboard.  Effective only for SDF billboards like Label glyphs.
   * @memberof Billboard.prototype
   * @type {Color}
   * @private
   */
  outlineColor: {
    get: function () {
      return this._outlineColor;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');
      const outlineColor = this._outlineColor;
      if (!Color.equals(outlineColor, value)) {
        Color.clone(value, outlineColor);
        makeDirty(this, SDF_INDEX);
      }
    },
  },
  /**
   * The outline width of this Billboard in pixels.  Effective only for SDF billboards like Label glyphs.
   * @memberof Billboard.prototype
   * @type {Number}
   * @private
   */
  outlineWidth: {
    get: function () {
      return this._outlineWidth;
    },
    set: function (value) {
      if (this._outlineWidth !== value) {
        this._outlineWidth = value;
        makeDirty(this, SDF_INDEX);
      }
    },
  },
});
Billboard.prototype.getPickId = function (context) {
  if (!defined(this._pickId)) {
    this._pickId = context.createPickId({
      primitive: this._pickPrimitive,
      collection: this._collection,
      id: this._id,
    });
  }
  return this._pickId;
};
Billboard.prototype._updateClamping = function () {
  Billboard._updateClamping(this._billboardCollection, this);
};
const scratchCartographic = new Cartographic();
const scratchPosition = new Cartesian3();
Billboard._updateClamping = function (collection, owner) {
  const scene = collection._scene;
  if (!defined(scene) || !defined(scene.globe)) {
    //>>includeStart('debug', pragmas.debug);
    if (owner._heightReference !== HeightReference.NONE) {
      throw new DeveloperError(
        "Height reference is not supported without a scene and globe."
      );
    }
    //>>includeEnd('debug');
    return;
  }
  const globe = scene.globe;
  const ellipsoid = globe.ellipsoid;
  const surface = globe._surface;
  const mode = scene.frameState.mode;
  const modeChanged = mode !== owner._mode;
  owner._mode = mode;
  if (
    (owner._heightReference === HeightReference.NONE || modeChanged) &&
    defined(owner._removeCallbackFunc)
  ) {
    owner._removeCallbackFunc();
    owner._removeCallbackFunc = undefined;
    owner._clampedPosition = undefined;
  }
  if (
    owner._heightReference === HeightReference.NONE ||
    !defined(owner._position)
  ) {
    return;
  }
  const position = ellipsoid.cartesianToCartographic(owner._position);
  if (!defined(position)) {
    owner._actualClampedPosition = undefined;
    return;
  }
  if (defined(owner._removeCallbackFunc)) {
    owner._removeCallbackFunc();
  }
  function updateFunction(clampedPosition) {
    if (owner._heightReference === HeightReference.RELATIVE_TO_GROUND) {
      if (owner._mode === SceneMode.SCENE3D) {
        const clampedCart = ellipsoid.cartesianToCartographic(
          clampedPosition,
          scratchCartographic
        );
        clampedCart.height += position.height;
        ellipsoid.cartographicToCartesian(clampedCart, clampedPosition);
      } else {
        clampedPosition.x += position.height;
      }
    }
    owner._clampedPosition = Cartesian3.clone(
      clampedPosition,
      owner._clampedPosition
    );
  }
  owner._removeCallbackFunc = surface.updateHeight(position, updateFunction);
  Cartographic.clone(position, scratchCartographic);
  const height = globe.getHeight(position);
  if (defined(height)) {
    scratchCartographic.height = height;
  }
  ellipsoid.cartographicToCartesian(scratchCartographic, scratchPosition);
  updateFunction(scratchPosition);
};
Billboard.prototype._loadImage = function () {
  const atlas = this._billboardCollection._textureAtlas;
  const imageId = this._imageId;
  const image = this._image;
  const imageSubRegion = this._imageSubRegion;
  let imageIndexPromise;
  const that = this;
  function completeImageLoad(index) {
    if (
      that._imageId !== imageId ||
      that._image !== image ||
      !BoundingRectangle.equals(that._imageSubRegion, imageSubRegion)
    ) {
      // another load occurred before this one finished, ignore the index
      return;
    }
    // fill in imageWidth and imageHeight
    const textureCoordinates = atlas.textureCoordinates[index];
    that._imageWidth = atlas.texture.width * textureCoordinates.width;
    that._imageHeight = atlas.texture.height * textureCoordinates.height;
    that._imageIndex = index;
    that._ready = true;
    that._image = undefined;
    that._imageIndexPromise = undefined;
    makeDirty(that, IMAGE_INDEX_INDEX);
  }
  if (defined(image)) {
    // No need to wait on imageIndexPromise since these have already been added to the atlas
    const index = atlas.getImageIndex(imageId);
    if (defined(index)) {
      completeImageLoad(index);
      return;
    }
    imageIndexPromise = atlas.addImage(imageId, image);
  }
  if (defined(imageSubRegion)) {
    imageIndexPromise = atlas.addSubRegion(imageId, imageSubRegion);
  }
  this._imageIndexPromise = imageIndexPromise;
  if (!defined(imageIndexPromise)) {
    return;
  }
  imageIndexPromise.then(completeImageLoad).catch(function (error) {
    console.error(`Error loading image for billboard: ${error}`);
    that._imageIndexPromise = undefined;
  });
};
/**
 * * Sets the image to be used for this billboard. If a texture has already been created for the * given id, the existing texture is used. *
** This function is useful for dynamically creating textures that are shared across many billboards. * Only the first billboard will actually call the function and create the texture, while subsequent * billboards created with the same id will simply re-use the existing texture. *
** To load an image from a URL, setting the {@link Billboard#image} property is more convenient. *
* * @param {String} id The id of the image. This can be any string that uniquely identifies the image. * @param {HTMLImageElement|HTMLCanvasElement|String|Resource|Billboard.CreateImageCallback} image The image to load. This parameter * can either be a loaded Image or Canvas, a URL which will be loaded as an Image automatically, * or a function which will be called to create the image if it hasn't been loaded already. * @example * // create a billboard image dynamically * function drawImage(id) { * // create and draw an image using a canvas * const canvas = document.createElement('canvas'); * const context2D = canvas.getContext('2d'); * // ... draw image * return canvas; * } * // drawImage will be called to create the texture * b.setImage('myImage', drawImage); * * // subsequent billboards created in the same collection using the same id will use the existing * // texture, without the need to create the canvas or draw the image * b2.setImage('myImage', drawImage); */ Billboard.prototype.setImage = function (id, image) { //>>includeStart('debug', pragmas.debug); if (!defined(id)) { throw new DeveloperError("id is required."); } if (!defined(image)) { throw new DeveloperError("image is required."); } //>>includeEnd('debug'); if (this._imageId === id) { return; } this._imageIndex = -1; this._imageSubRegion = undefined; this._imageId = id; this._image = image; if (defined(this._billboardCollection._textureAtlas)) { this._loadImage(); } }; /** * Uses a sub-region of the image with the given id as the image for this billboard, * measured in pixels from the bottom-left. * * @param {String} id The id of the image to use. * @param {BoundingRectangle} subRegion The sub-region of the image. * * @exception {RuntimeError} image with id must be in the atlas */ Billboard.prototype.setImageSubRegion = function (id, subRegion) { //>>includeStart('debug', pragmas.debug); if (!defined(id)) { throw new DeveloperError("id is required."); } if (!defined(subRegion)) { throw new DeveloperError("subRegion is required."); } //>>includeEnd('debug'); if ( this._imageId === id && BoundingRectangle.equals(this._imageSubRegion, subRegion) ) { return; } this._imageIndex = -1; this._imageId = id; this._imageSubRegion = BoundingRectangle.clone(subRegion); if (defined(this._billboardCollection._textureAtlas)) { this._loadImage(); } }; Billboard.prototype._setTranslate = function (value) { //>>includeStart('debug', pragmas.debug); if (!defined(value)) { throw new DeveloperError("value is required."); } //>>includeEnd('debug'); const translate = this._translate; if (!Cartesian2.equals(translate, value)) { Cartesian2.clone(value, translate); makeDirty(this, PIXEL_OFFSET_INDEX); } }; Billboard.prototype._getActualPosition = function () { return defined(this._clampedPosition) ? this._clampedPosition : this._actualPosition; }; Billboard.prototype._setActualPosition = function (value) { if (!defined(this._clampedPosition)) { Cartesian3.clone(value, this._actualPosition); } makeDirty(this, POSITION_INDEX); }; const tempCartesian3 = new Cartesian4(); Billboard._computeActualPosition = function ( billboard, position, frameState, modelMatrix ) { if (defined(billboard._clampedPosition)) { if (frameState.mode !== billboard._mode) { billboard._updateClamping(); } return billboard._clampedPosition; } else if (frameState.mode === SceneMode.SCENE3D) { return position; } Matrix4.multiplyByPoint(modelMatrix, position, tempCartesian3); return SceneTransforms.computeActualWgs84Position(frameState, tempCartesian3); }; const scratchCartesian3 = new Cartesian3(); // This function is basically a stripped-down JavaScript version of BillboardCollectionVS.glsl Billboard._computeScreenSpacePosition = function ( modelMatrix, position, eyeOffset, pixelOffset, scene, result ) { // Model to world coordinates const positionWorld = Matrix4.multiplyByPoint( modelMatrix, position, scratchCartesian3 ); // World to window coordinates const positionWC = SceneTransforms.wgs84WithEyeOffsetToWindowCoordinates( scene, positionWorld, eyeOffset, result ); if (!defined(positionWC)) { return undefined; } // Apply pixel offset Cartesian2.add(positionWC, pixelOffset, positionWC); return positionWC; }; const scratchPixelOffset = new Cartesian2(0.0, 0.0); /** * Computes the screen-space position of the billboard's origin, taking into account eye and pixel offsets. * The screen space origin is the top, left corner of the canvas;x increases from
 * left to right, and y increases from top to bottom.
 *
 * @param {Scene} scene The scene.
 * @param {Cartesian2} [result] The object onto which to store the result.
 * @returns {Cartesian2} The screen-space position of the billboard.
 *
 * @exception {DeveloperError} Billboard must be in a collection.
 *
 * @example
 * console.log(b.computeScreenSpacePosition(scene).toString());
 *
 * @see Billboard#eyeOffset
 * @see Billboard#pixelOffset
 */
Billboard.prototype.computeScreenSpacePosition = function (scene, result) {
  const billboardCollection = this._billboardCollection;
  if (!defined(result)) {
    result = new Cartesian2();
  }
  //>>includeStart('debug', pragmas.debug);
  if (!defined(billboardCollection)) {
    throw new DeveloperError(
      "Billboard must be in a collection.  Was it removed?"
    );
  }
  if (!defined(scene)) {
    throw new DeveloperError("scene is required.");
  }
  //>>includeEnd('debug');
  // pixel offset for screen space computation is the pixelOffset + screen space translate
  Cartesian2.clone(this._pixelOffset, scratchPixelOffset);
  Cartesian2.add(scratchPixelOffset, this._translate, scratchPixelOffset);
  let modelMatrix = billboardCollection.modelMatrix;
  let position = this._position;
  if (defined(this._clampedPosition)) {
    position = this._clampedPosition;
    if (scene.mode !== SceneMode.SCENE3D) {
      // position needs to be in world coordinates
      const projection = scene.mapProjection;
      const ellipsoid = projection.ellipsoid;
      const cart = projection.unproject(position, scratchCartographic);
      position = ellipsoid.cartographicToCartesian(cart, scratchCartesian3);
      modelMatrix = Matrix4.IDENTITY;
    }
  }
  const windowCoordinates = Billboard._computeScreenSpacePosition(
    modelMatrix,
    position,
    this._eyeOffset,
    scratchPixelOffset,
    scene,
    result
  );
  return windowCoordinates;
};
/**
 * Gets a billboard's screen space bounding box centered around screenSpacePosition.
 * @param {Billboard} billboard The billboard to get the screen space bounding box for.
 * @param {Cartesian2} screenSpacePosition The screen space center of the label.
 * @param {BoundingRectangle} [result] The object onto which to store the result.
 * @returns {BoundingRectangle} The screen space bounding box.
 *
 * @private
 */
Billboard.getScreenSpaceBoundingBox = function (
  billboard,
  screenSpacePosition,
  result
) {
  let width = billboard.width;
  let height = billboard.height;
  const scale = billboard.scale;
  width *= scale;
  height *= scale;
  let x = screenSpacePosition.x;
  if (billboard.horizontalOrigin === HorizontalOrigin.RIGHT) {
    x -= width;
  } else if (billboard.horizontalOrigin === HorizontalOrigin.CENTER) {
    x -= width * 0.5;
  }
  let y = screenSpacePosition.y;
  if (
    billboard.verticalOrigin === VerticalOrigin.BOTTOM ||
    billboard.verticalOrigin === VerticalOrigin.BASELINE
  ) {
    y -= height;
  } else if (billboard.verticalOrigin === VerticalOrigin.CENTER) {
    y -= height * 0.5;
  }
  if (!defined(result)) {
    result = new BoundingRectangle();
  }
  result.x = x;
  result.y = y;
  result.width = width;
  result.height = height;
  return result;
};
/**
 * Determines if this billboard equals another billboard.  Billboards are equal if all their properties
 * are equal.  Billboards in different collections can be equal.
 *
 * @param {Billboard} other The billboard to compare for equality.
 * @returns {Boolean} true if the billboards are equal; otherwise, false.
 */
Billboard.prototype.equals = function (other) {
  return (
    this === other ||
    (defined(other) &&
      this._id === other._id &&
      Cartesian3.equals(this._position, other._position) &&
      this._imageId === other._imageId &&
      this._show === other._show &&
      this._scale === other._scale &&
      this._verticalOrigin === other._verticalOrigin &&
      this._horizontalOrigin === other._horizontalOrigin &&
      this._heightReference === other._heightReference &&
      BoundingRectangle.equals(this._imageSubRegion, other._imageSubRegion) &&
      Color.equals(this._color, other._color) &&
      Cartesian2.equals(this._pixelOffset, other._pixelOffset) &&
      Cartesian2.equals(this._translate, other._translate) &&
      Cartesian3.equals(this._eyeOffset, other._eyeOffset) &&
      NearFarScalar.equals(this._scaleByDistance, other._scaleByDistance) &&
      NearFarScalar.equals(
        this._translucencyByDistance,
        other._translucencyByDistance
      ) &&
      NearFarScalar.equals(
        this._pixelOffsetScaleByDistance,
        other._pixelOffsetScaleByDistance
      ) &&
      DistanceDisplayCondition.equals(
        this._distanceDisplayCondition,
        other._distanceDisplayCondition
      ) &&
      this._disableDepthTestDistance === other._disableDepthTestDistance)
  );
};
Billboard.prototype._destroy = function () {
  if (defined(this._customData)) {
    this._billboardCollection._scene.globe._surface.removeTileCustomData(
      this._customData
    );
    this._customData = undefined;
  }
  if (defined(this._removeCallbackFunc)) {
    this._removeCallbackFunc();
    this._removeCallbackFunc = undefined;
  }
  this.image = undefined;
  this._pickId = this._pickId && this._pickId.destroy();
  this._billboardCollection = undefined;
};
/**
 * A function that creates an image.
 * @callback Billboard.CreateImageCallback
 * @param {String} id The identifier of the image to load.
 * @returns {HTMLImageElement|HTMLCanvasElement|Promise