import { BoundingRectangle } from "../../Source/Cesium.js";
import { BoundingSphere } from "../../Source/Cesium.js";
import { Cartesian2 } from "../../Source/Cesium.js";
import { Cartesian3 } from "../../Source/Cesium.js";
import { Color } from "../../Source/Cesium.js";
import { DistanceDisplayCondition } from "../../Source/Cesium.js";
import { Math as CesiumMath } from "../../Source/Cesium.js";
import { NearFarScalar } from "../../Source/Cesium.js";
import { Rectangle } from "../../Source/Cesium.js";
import { BlendOption } from "../../Source/Cesium.js";
import { Globe } from "../../Source/Cesium.js";
import { HeightReference } from "../../Source/Cesium.js";
import { HorizontalOrigin } from "../../Source/Cesium.js";
import { Label } from "../../Source/Cesium.js";
import { LabelCollection } from "../../Source/Cesium.js";
import { LabelStyle } from "../../Source/Cesium.js";
import { VerticalOrigin } from "../../Source/Cesium.js";
import createGlobe from "../createGlobe.js";
import createScene from "../createScene.js";

describe(
  "Scene/LabelCollection",
  function () {
    // TODO: rendering tests for pixel offset, eye offset, horizontal origin, vertical origin, font, style, outlineColor, outlineWidth, and fillColor properties

    var scene;
    var camera;
    var labels;
    var labelsWithHeight;

    // This Unicode square block will more reliably cover the center pixel than an 'x' or a 'w' char.
    var solidBox = "\u25a0";

    beforeAll(function () {
      scene = createScene();
      camera = scene.camera;
    });

    afterAll(function () {
      scene.destroyForSpecs();
    });

    beforeEach(function () {
      scene.morphTo3D(0);

      camera.position = new Cartesian3(10.0, 0.0, 0.0);
      camera.direction = Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3());
      camera.up = Cartesian3.clone(Cartesian3.UNIT_Z);

      labels = new LabelCollection();
      scene.primitives.add(labels);
    });

    afterEach(function () {
      // labels are destroyed by removeAll().
      scene.primitives.removeAll();
    });

    it("has default values when adding a label", function () {
      var label = labels.add();
      expect(label.show).toEqual(true);
      expect(label.position).toEqual(Cartesian3.ZERO);
      expect(label.text).toEqual("");
      expect(label.font).toEqual("30px sans-serif");
      expect(label.fillColor).toEqual(Color.WHITE);
      expect(label.outlineColor).toEqual(Color.BLACK);
      expect(label.outlineWidth).toEqual(1);
      expect(label.showBackground).toEqual(false);
      expect(label.backgroundColor).toEqual(
        new Color(0.165, 0.165, 0.165, 0.8)
      );
      expect(label.backgroundPadding).toEqual(new Cartesian2(7, 5));
      expect(label.style).toEqual(LabelStyle.FILL);
      expect(label.pixelOffset).toEqual(Cartesian2.ZERO);
      expect(label.eyeOffset).toEqual(Cartesian3.ZERO);
      expect(label.heightReference).toEqual(HeightReference.NONE);
      expect(label.horizontalOrigin).toEqual(HorizontalOrigin.LEFT);
      expect(label.verticalOrigin).toEqual(VerticalOrigin.BASELINE);
      expect(label.scale).toEqual(1.0);
      expect(label.id).toBeUndefined();
      expect(label.translucencyByDistance).toBeUndefined();
      expect(label.pixelOffsetScaleByDistance).toBeUndefined();
      expect(label.scaleByDistance).toBeUndefined();
      expect(label.distanceDisplayCondition).toBeUndefined();
      expect(label.disableDepthTestDistance).toBeUndefined();
    });

    it("can add a label with specified values", function () {
      var show = false;
      var position = new Cartesian3(1.0, 2.0, 3.0);
      var text = "abc";
      var font = '24px "Open Sans"';
      var fillColor = {
        red: 2.0,
        green: 3.0,
        blue: 4.0,
        alpha: 1.0,
      };
      var outlineColor = {
        red: 3.0,
        green: 4.0,
        blue: 2.0,
        alpha: 1.0,
      };
      var outlineWidth = 2;

      var style = LabelStyle.FILL_AND_OUTLINE;
      var pixelOffset = new Cartesian2(4.0, 5.0);
      var eyeOffset = new Cartesian3(6.0, 7.0, 8.0);
      var horizontalOrigin = HorizontalOrigin.LEFT;
      var verticalOrigin = VerticalOrigin.BOTTOM;
      var scale = 2.0;
      var showBackground = true;
      var backgroundColor = Color.BLUE;
      var backgroundPadding = new Cartesian2(11, 12);
      var translucency = new NearFarScalar(1.0e4, 1.0, 1.0e6, 0.0);
      var pixelOffsetScale = new NearFarScalar(1.0e4, 1.0, 1.0e6, 0.0);
      var scaleByDistance = new NearFarScalar(1.0e4, 1.0, 1.0e6, 0.0);
      var distanceDisplayCondition = new DistanceDisplayCondition(10.0, 100.0);
      var disableDepthTestDistance = 10.0;
      var label = labels.add({
        show: show,
        position: position,
        text: text,
        font: font,
        fillColor: fillColor,
        outlineColor: outlineColor,
        outlineWidth: outlineWidth,
        style: style,
        showBackground: showBackground,
        backgroundColor: backgroundColor,
        backgroundPadding: backgroundPadding,
        pixelOffset: pixelOffset,
        eyeOffset: eyeOffset,
        horizontalOrigin: horizontalOrigin,
        verticalOrigin: verticalOrigin,
        scale: scale,
        id: "id",
        translucencyByDistance: translucency,
        pixelOffsetScaleByDistance: pixelOffsetScale,
        scaleByDistance: scaleByDistance,
        distanceDisplayCondition: distanceDisplayCondition,
        disableDepthTestDistance: disableDepthTestDistance,
      });

      expect(label.show).toEqual(show);
      expect(label.position).toEqual(position);
      expect(label.text).toEqual(text);
      expect(label.font).toEqual(font);
      expect(label.fillColor).toEqual(fillColor);
      expect(label.outlineColor).toEqual(outlineColor);
      expect(label.outlineWidth).toEqual(outlineWidth);
      expect(label.style).toEqual(style);
      expect(label.showBackground).toEqual(showBackground);
      expect(label.backgroundColor).toEqual(backgroundColor);
      expect(label.backgroundPadding).toEqual(backgroundPadding);
      expect(label.pixelOffset).toEqual(pixelOffset);
      expect(label.eyeOffset).toEqual(eyeOffset);
      expect(label.horizontalOrigin).toEqual(horizontalOrigin);
      expect(label.verticalOrigin).toEqual(verticalOrigin);
      expect(label.scale).toEqual(scale);
      expect(label.id).toEqual("id");
      expect(label.translucencyByDistance).toEqual(translucency);
      expect(label.pixelOffsetScaleByDistance).toEqual(pixelOffsetScale);
      expect(label.scaleByDistance).toEqual(scaleByDistance);
      expect(label.distanceDisplayCondition).toEqual(distanceDisplayCondition);
      expect(label.disableDepthTestDistance).toEqual(disableDepthTestDistance);
    });

    it("can specify font using units other than pixels", function () {
      var label = labels.add({
        font: '12pt "Open Sans"',
        text: "Hello there",
      });
      scene.renderForSpecs();

      var dimensions = label._glyphs[0].dimensions;
      expect(dimensions.height).toBeGreaterThan(0);
    });

    it("has zero labels when constructed", function () {
      expect(labels.length).toEqual(0);
    });

    it("can add a label", function () {
      var label = labels.add();

      expect(labels.length).toEqual(1);
      expect(labels.get(0)).toBe(label);
    });

    it("can remove the first label", function () {
      var one = labels.add();
      var two = labels.add();

      expect(labels.contains(one)).toEqual(true);
      expect(labels.contains(two)).toEqual(true);

      expect(labels.remove(one)).toEqual(true);

      expect(labels.contains(one)).toEqual(false);
      expect(labels.contains(two)).toEqual(true);
    });

    it("can remove the last label", function () {
      var one = labels.add();
      var two = labels.add();

      expect(labels.contains(one)).toEqual(true);
      expect(labels.contains(two)).toEqual(true);

      expect(labels.remove(two)).toEqual(true);

      expect(labels.contains(one)).toEqual(true);
      expect(labels.contains(two)).toEqual(false);
    });

    it("returns false when removing undefined", function () {
      labels.add();
      expect(labels.length).toEqual(1);
      expect(labels.remove(undefined)).toEqual(false);
      expect(labels.length).toEqual(1);
    });

    it("returns false when removing a previously removed label", function () {
      var label = labels.add();
      expect(labels.length).toEqual(1);
      expect(labels.remove(label)).toEqual(true);
      expect(labels.remove(label)).toEqual(false);
      expect(labels.length).toEqual(0);
    });

    it("is not destroyed", function () {
      expect(labels.isDestroyed()).toEqual(false);
    });

    it("can add and remove multiple labels", function () {
      var one = labels.add();
      var two = labels.add();
      var three = labels.add();

      expect(labels.remove(one)).toEqual(true);
      expect(labels.remove(two)).toEqual(true);

      expect(one.isDestroyed()).toEqual(true);
      expect(two.isDestroyed()).toEqual(true);
      expect(three.isDestroyed()).toEqual(false);

      expect(labels.contains(one)).toEqual(false);
      expect(labels.contains(two)).toEqual(false);
      expect(labels.contains(three)).toEqual(true);

      expect(labels.length).toEqual(1);
      expect(labels.get(0)).toBe(three);

      var four = labels.add();
      expect(labels.length).toEqual(2);
      expect(labels.get(0)).toBe(three);
      expect(labels.get(1)).toBe(four);
      expect(labels.contains(three)).toEqual(true);
      expect(labels.contains(four)).toEqual(true);
    });

    it("can remove all labels", function () {
      labels.add({
        position: new Cartesian3(1.0, 2.0, 3.0),
      });
      labels.add({
        position: new Cartesian3(4.0, 5.0, 6.0),
      });
      expect(labels.length).toEqual(2);

      labels.removeAll();
      expect(labels.length).toEqual(0);
    });

    it("can check if it contains a label", function () {
      var label = labels.add();

      expect(labels.contains(label)).toEqual(true);
    });

    it("returns false when checking if it contains a label it does not contain", function () {
      var label = labels.add();
      labels.remove(label);

      expect(labels.contains(label)).toEqual(false);
    });

    it("does not contain undefined", function () {
      expect(labels.contains(undefined)).toEqual(false);
    });

    it("does not contain random other objects", function () {
      expect(labels.contains({})).toEqual(false);
      expect(labels.contains(new Cartesian2())).toEqual(false);
    });

    it("does not render when constructed", function () {
      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("can render after modifying and removing a label", function () {
      var labelOne = labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });
      labels.add({
        position: Cartesian3.ZERO,
        text: "o",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });

      labelOne.scale = 2.0;
      labels.remove(labelOne);

      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("can render a label", function () {
      labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });
    });

    it("renders in multiple passes", function () {
      labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });
      camera.position = new Cartesian3(2.0, 0.0, 0.0);

      var frameState = scene.frameState;
      frameState.commandList.length = 0;
      labels.blendOption = BlendOption.OPAQUE_AND_TRANSLUCENT;
      labels.update(frameState);
      expect(frameState.commandList.length).toEqual(2);

      frameState.commandList.length = 0;
      labels.blendOption = BlendOption.OPAQUE;
      labels.update(frameState);
      expect(frameState.commandList.length).toEqual(1);

      frameState.commandList.length = 0;
      labels.blendOption = BlendOption.TRANSLUCENT;
      labels.update(frameState);
      expect(frameState.commandList.length).toEqual(1);
    });

    it("can render after adding a label", function () {
      labels.add({
        position: Cartesian3.ZERO,
        text: solidBox,
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(200);
        expect(rgba[1]).toBeGreaterThan(200);
        expect(rgba[2]).toBeGreaterThan(200);
      });

      labels.add({
        position: new Cartesian3(1.0, 0.0, 0.0), // Closer to camera
        text: solidBox,
        fillColor: {
          red: 1.0,
          green: 0.0,
          blue: 0.0,
          alpha: 1.0,
        },
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(200);
        expect(rgba[1]).toBeLessThan(10);
        expect(rgba[2]).toBeLessThan(10);
      });
    });

    it("can render after removing a label", function () {
      var label = labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });

      labels.remove(label);
      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("can render after removing and adding a label", function () {
      var label = labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });

      labels.remove(label);
      labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });
    });

    it("can render after removing all labels", function () {
      labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });

      labels.removeAll();
      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("can render after removing all labels and adding a label", function () {
      labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });

      labels.removeAll();
      labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });
    });

    it("can render a label background", function () {
      var label = labels.add({
        position: Cartesian3.ZERO,
        text: "_",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
        showBackground: true,
        backgroundColor: Color.BLUE,
      });

      expect(scene).toRender([0, 0, 255, 255]);

      labels.remove(label);
      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("does not render labels with show set to false", function () {
      var label = labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });

      label.show = false;
      expect(scene).toRender([0, 0, 0, 255]);

      label.show = true;
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });
    });

    it("does not render label background with show set to false", function () {
      var label = labels.add({
        position: Cartesian3.ZERO,
        text: "_",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
        showBackground: true,
        backgroundColor: Color.BLUE,
      });

      expect(scene).toRender([0, 0, 255, 255]);

      label.show = false;
      expect(scene).toRender([0, 0, 0, 255]);

      label.show = true;
      expect(scene).toRender([0, 0, 255, 255]);
    });

    it("does not render labels that are behind the viewer", function () {
      var label = labels.add({
        position: Cartesian3.ZERO, // in front of the camera
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });

      label.position = new Cartesian3(20.0, 0.0, 0.0); // Behind camera
      expect(scene).toRender([0, 0, 0, 255]);

      label.position = new Cartesian3(1.0, 0.0, 0.0); // Back in front of camera
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });
    });

    it("does not render labels with a scale of zero", function () {
      var label = labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      label.scale = 0.0;
      expect(scene).toRender([0, 0, 0, 255]);

      label.scale = 2.0;
      scene.render();
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });
    });

    it("renders label with translucencyByDistance", function () {
      labels.add({
        position: Cartesian3.ZERO,
        text: "x",
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
        translucencyByDistance: new NearFarScalar(2.0, 1.0, 4.0, 0.0),
      });

      camera.position = new Cartesian3(2.0, 0.0, 0.0);
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });

      camera.position = new Cartesian3(4.0, 0.0, 0.0);
      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("renders label with pixelOffsetScaleByDistance", function () {
      labels.add({
        position: Cartesian3.ZERO,
        pixelOffset: new Cartesian2(1.0, 0.0),
        text: solidBox,
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
        pixelOffsetScaleByDistance: new NearFarScalar(2.0, 0.0, 4.0, 1000.0),
      });

      camera.position = new Cartesian3(2.0, 0.0, 0.0);
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(10);
      });

      camera.position = new Cartesian3(4.0, 0.0, 0.0);
      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("renders label with scaleByDistance", function () {
      labels.add({
        position: Cartesian3.ZERO,
        text: solidBox,
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
        scaleByDistance: new NearFarScalar(2.0, 1.0, 4.0, 0.0),
      });

      camera.position = new Cartesian3(2.0, 0.0, 0.0);
      expect(scene).toRender([255, 255, 255, 255]);

      camera.position = new Cartesian3(4.0, 0.0, 0.0);
      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("renders label with distanceDisplayCondition", function () {
      labels.add({
        position: Cartesian3.ZERO,
        text: solidBox,
        distanceDisplayCondition: new DistanceDisplayCondition(10.0, 100.0),
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      camera.position = new Cartesian3(200.0, 0.0, 0.0);
      expect(scene).toRender([0, 0, 0, 255]);

      camera.position = new Cartesian3(50.0, 0.0, 0.0);
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(200);
        expect(rgba[1]).toBeGreaterThan(200);
        expect(rgba[2]).toBeGreaterThan(200);
      });

      camera.position = new Cartesian3(5.0, 0.0, 0.0);
      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("does not render label if show is false", function () {
      labels.add({
        position: Cartesian3.ZERO,
        text: solidBox,
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
        scaleByDistance: new NearFarScalar(2.0, 1.0, 4.0, 0.0),
      });

      camera.position = new Cartesian3(2.0, 0.0, 0.0);
      expect(scene).toRender([255, 255, 255, 255]);

      labels.show = false;
      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("throws new label with invalid distanceDisplayCondition (near >= far)", function () {
      var dc = new DistanceDisplayCondition(100.0, 10.0);
      expect(function () {
        labels.add({
          distanceDisplayCondition: dc,
        });
      }).toThrowDeveloperError();
    });

    it("throws distanceDisplayCondition with near >= far", function () {
      var l = labels.add();
      var dc = new DistanceDisplayCondition(100.0, 10.0);
      expect(function () {
        l.distanceDisplayCondition = dc;
      }).toThrowDeveloperError();
    });

    it("renders with disableDepthTestDistance", function () {
      var l = labels.add({
        position: new Cartesian3(-1.0, 0.0, 0.0),
        text: solidBox,
        fillColor: Color.LIME,
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });
      labels.add({
        position: Cartesian3.ZERO,
        text: solidBox,
        fillColor: Color.BLUE,
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).toRender([0, 0, 255, 255]);

      l.disableDepthTestDistance = Number.POSITIVE_INFINITY;
      expect(scene).toRender([0, 255, 0, 255]);
    });

    it("throws with new label with disableDepthTestDistance less than 0.0", function () {
      expect(function () {
        labels.add({
          disableDepthTestDistance: -1.0,
        });
      }).toThrowDeveloperError();
    });

    it("throws with disableDepthTestDistance set less than 0.0", function () {
      var l = labels.add();
      expect(function () {
        l.disableDepthTestDistance = -1.0;
      }).toThrowDeveloperError();
    });

    it("can pick a label", function () {
      var label = labels.add({
        position: Cartesian3.ZERO,
        text: solidBox,
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
        id: "id",
      });

      expect(scene).toPickAndCall(function (result) {
        expect(result.primitive).toEqual(label);
        expect(result.id).toEqual("id");
      });
    });

    it("can change pick id", function () {
      var label = labels.add({
        position: Cartesian3.ZERO,
        text: solidBox,
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
        id: "id",
      });

      expect(scene).toPickAndCall(function (result) {
        expect(result.primitive).toEqual(label);
        expect(result.id).toEqual("id");
      });

      label.id = "id2";

      expect(scene).toPickAndCall(function (result) {
        expect(result.primitive).toEqual(label);
        expect(result.id).toEqual("id2");
      });
    });

    it("does not pick a label with show set to false", function () {
      labels.add({
        show: false,
        position: Cartesian3.ZERO,
        text: solidBox,
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      expect(scene).notToPick();
    });

    it("picks a label using translucencyByDistance", function () {
      var label = labels.add({
        position: Cartesian3.ZERO,
        text: solidBox,
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      var translucency = new NearFarScalar(1.0, 0.9, 3.0e9, 0.8);
      label.translucencyByDistance = translucency;

      expect(scene).toPickPrimitive(label);

      translucency.nearValue = 0.0;
      translucency.farValue = 0.0;
      label.translucencyByDistance = translucency;

      expect(scene).notToPick();
    });

    it("picks a label using pixelOffsetScaleByDistance", function () {
      var label = labels.add({
        position: Cartesian3.ZERO,
        pixelOffset: new Cartesian2(0.0, 100.0),
        text: solidBox,
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
      });

      var pixelOffsetScale = new NearFarScalar(1.0, 0.0, 3.0e9, 0.0);
      label.pixelOffsetScaleByDistance = pixelOffsetScale;

      expect(scene).toPickPrimitive(label);

      pixelOffsetScale.nearValue = 10.0;
      pixelOffsetScale.farValue = 10.0;
      label.pixelOffsetScaleByDistance = pixelOffsetScale;

      expect(scene).notToPick();
    });

    it("throws when calling get without an index", function () {
      expect(function () {
        labels.get();
      }).toThrowDeveloperError();
    });

    it("should reuse canvases for letters, but only if other settings are the same", function () {
      labels.add({
        text: "a",
      });
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(1);

      labels.add({
        text: "a",
      });
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(1);

      labels.add({
        text: "abcd",
      });
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(4);

      labels.add({
        text: "abc",
      });
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(4);

      var label = labels.add({
        text: "de",
      });
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(5);

      var originalFont = label.font;
      label.font = '30px "Open Sans"';
      expect(label.font).not.toEqual(originalFont); // otherwise this test needs fixing.
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(7);

      // Changing the outline doesn't cause new glyphs to be generated.
      label.style = LabelStyle.OUTLINE;
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(7);

      // Changing fill color doesn't cause new glyphs to be generated.
      label.fillColor = new Color(1.0, 165.0 / 255.0, 0.0, 1.0);
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(7);

      // Changing outline color doesn't cause new glyphs to be generated.
      label.outlineColor = new Color(1.0, 1.0, 1.0, 1.0);
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(7);

      // vertical origin only affects glyph positions, not glyphs themselves.
      label.verticalOrigin = VerticalOrigin.CENTER;
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(7);
      label.verticalOrigin = VerticalOrigin.TOP;
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(7);

      //even though we're resetting to the original font, other properties used to create the id have changed
      label.font = originalFont;
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(9);

      //Changing thickness doesn't requires new glyphs
      label.outlineWidth = 3;
      scene.renderForSpecs();
      expect(Object.keys(labels._glyphTextureCache).length).toEqual(9);
    });

    it("should reuse billboards that are not needed any more", function () {
      var label = labels.add({
        text: "abc",
      });
      scene.renderForSpecs();
      expect(labels._billboardCollection.length).toEqual(3);
      expect(labels._spareBillboards.length).toEqual(0);

      label.text = "a";
      scene.renderForSpecs();
      expect(labels._billboardCollection.length).toEqual(3);
      expect(labels._spareBillboards.length).toEqual(2);

      label.text = "def";
      scene.renderForSpecs();
      expect(labels._billboardCollection.length).toEqual(3);
      expect(labels._spareBillboards.length).toEqual(0);
    });

    it("should not reuse background billboards that are not needed any more", function () {
      var label = labels.add({
        text: "abc",
        showBackground: true,
      });
      scene.renderForSpecs();
      expect(labels._backgroundBillboardCollection.length).toEqual(1);

      label.showBackground = false;
      scene.renderForSpecs();
      expect(labels._backgroundBillboardCollection.length).toEqual(0);

      label.showBackground = true;
      scene.renderForSpecs();
      expect(labels._backgroundBillboardCollection.length).toEqual(1);
    });

    describe(
      "Label",
      function () {
        it("can set properties after being added", function () {
          var label = labels.add();

          var show = false;
          var position = new Cartesian3(1.0, 2.0, 3.0);
          var text = "abc";
          var font = '24px "Open Sans"';
          var fillColor = {
            red: 2.0,
            green: 3.0,
            blue: 4.0,
            alpha: 1.0,
          };
          var outlineColor = {
            red: 3.0,
            green: 4.0,
            blue: 2.0,
            alpha: 1.0,
          };
          var outlineWidth = 2;
          var style = LabelStyle.FILL_AND_OUTLINE;
          var pixelOffset = new Cartesian2(4.0, 5.0);
          var eyeOffset = new Cartesian3(6.0, 7.0, 8.0);
          var horizontalOrigin = HorizontalOrigin.LEFT;
          var verticalOrigin = VerticalOrigin.BOTTOM;
          var scale = 2.0;
          var translucency = new NearFarScalar(1.0e4, 1.0, 1.0e6, 0.0);
          var pixelOffsetScale = new NearFarScalar(1.0e4, 1.0, 1.0e6, 0.0);
          var scaleByDistance = new NearFarScalar(1.0e4, 1.0, 1.0e6, 0.0);

          label.show = show;
          label.position = position;
          label.text = text;
          label.font = font;
          label.fillColor = fillColor;
          label.outlineColor = outlineColor;
          label.outlineWidth = outlineWidth;
          label.style = style;
          label.pixelOffset = pixelOffset;
          label.eyeOffset = eyeOffset;
          label.horizontalOrigin = horizontalOrigin;
          label.verticalOrigin = verticalOrigin;
          label.scale = scale;
          label.translucencyByDistance = translucency;
          label.pixelOffsetScaleByDistance = pixelOffsetScale;
          label.scaleByDistance = scaleByDistance;

          expect(label.show).toEqual(show);
          expect(label.position).toEqual(position);
          expect(label.text).toEqual(text);
          expect(label.font).toEqual(font);
          expect(label.fillColor).toEqual(fillColor);
          expect(label.outlineColor).toEqual(outlineColor);
          expect(label.outlineWidth).toEqual(outlineWidth);
          expect(label.style).toEqual(style);
          expect(label.pixelOffset).toEqual(pixelOffset);
          expect(label.eyeOffset).toEqual(eyeOffset);
          expect(label.horizontalOrigin).toEqual(horizontalOrigin);
          expect(label.verticalOrigin).toEqual(verticalOrigin);
          expect(label.scale).toEqual(scale);
          expect(label.translucencyByDistance).toEqual(translucency);
          expect(label.pixelOffsetScaleByDistance).toEqual(pixelOffsetScale);
          expect(label.scaleByDistance).toEqual(scaleByDistance);
        });

        it("is destroyed after being removed", function () {
          var label = labels.add();

          expect(label.isDestroyed()).toEqual(false);

          labels.remove(label);

          expect(label.isDestroyed()).toEqual(true);
        });

        it("throws after being removed", function () {
          var label = labels.add();
          labels.remove(label);
          expect(function () {
            label.equals(label);
          }).toThrowDeveloperError();
        });

        it("can compute screen space position", function () {
          labels.clampToPixel = false;
          var label = labels.add({
            text: "abc",
            position: Cartesian3.ZERO,
          });
          scene.renderForSpecs();
          expect(label.computeScreenSpacePosition(scene)).toEqualEpsilon(
            new Cartesian2(0.5, 0.5),
            CesiumMath.EPSILON1
          );
        });

        it("stores screen space position in a result", function () {
          labels.clampToPixel = false;
          var label = labels.add({
            text: "abc",
            position: Cartesian3.ZERO,
          });
          var result = new Cartesian2();
          scene.renderForSpecs();
          var actual = label.computeScreenSpacePosition(scene, result);
          expect(actual).toEqual(result);
          expect(result).toEqualEpsilon(
            new Cartesian2(0.5, 0.5),
            CesiumMath.EPSILON1
          );
        });

        it("can compute screen space position with pixelOffset", function () {
          labels.clampToPixel = false;
          var label = labels.add({
            text: "abc",
            position: Cartesian3.ZERO,
            pixelOffset: new Cartesian2(0.5, 0.5),
          });
          scene.renderForSpecs();
          expect(label.computeScreenSpacePosition(scene)).toEqualEpsilon(
            new Cartesian2(1.0, 1.0),
            CesiumMath.EPSILON1
          );
        });

        it("can compute screen space position with eyeOffset", function () {
          labels.clampToPixel = false;
          var label = labels.add({
            text: "abc",
            position: Cartesian3.ZERO,
            eyeOffset: new Cartesian3(1.0, 1.0, 0.0),
          });
          scene.renderForSpecs();
          expect(label.computeScreenSpacePosition(scene)).toEqualEpsilon(
            new Cartesian2(0.5, 0.5),
            CesiumMath.EPSILON1
          );
        });

        it("computes screen space bounding box", function () {
          var scale = 1.5;

          var label = labels.add({
            text: "abc",
            scale: scale,
          });
          scene.renderForSpecs();

          var bbox = Label.getScreenSpaceBoundingBox(label, Cartesian2.ZERO);
          expect(bbox.x).toBeDefined();
          expect(bbox.y).toBeDefined();
          expect(bbox.width).toBeGreaterThan(30);
          expect(bbox.width).toBeLessThan(200);
          expect(bbox.height).toBeGreaterThan(10);
          expect(bbox.height).toBeLessThan(50);
        });

        it("computes screen space bounding box with result", function () {
          var scale = 1.5;

          var label = labels.add({
            text: "abc",
            scale: scale,
          });
          scene.renderForSpecs();

          var result = new BoundingRectangle();
          var bbox = Label.getScreenSpaceBoundingBox(
            label,
            Cartesian2.ZERO,
            result
          );
          expect(bbox.x).toBeDefined();
          expect(bbox.y).toBeDefined();
          expect(bbox.width).toBeGreaterThan(30);
          expect(bbox.width).toBeLessThan(200);
          expect(bbox.height).toBeGreaterThan(10);
          expect(bbox.height).toBeLessThan(50);
          expect(bbox).toBe(result);
        });

        it("computes screen space bounding box with vertical origin center", function () {
          var scale = 1.5;

          var label = labels.add({
            text: "abc",
            scale: scale,
            verticalOrigin: VerticalOrigin.CENTER,
          });
          scene.renderForSpecs();

          var bbox = Label.getScreenSpaceBoundingBox(label, Cartesian2.ZERO);
          expect(bbox.y).toBeGreaterThan(bbox.height * -0.9);
          expect(bbox.y).toBeLessThan(bbox.height * -0.3);
        });

        it("computes screen space bounding box with vertical origin top", function () {
          var scale = 1.5;

          var label = labels.add({
            text: "abc",
            scale: scale,
            verticalOrigin: VerticalOrigin.TOP,
          });
          scene.renderForSpecs();

          var bbox = Label.getScreenSpaceBoundingBox(label, Cartesian2.ZERO);
          expect(bbox.y).toBeLessThan(5);
          expect(bbox.y).toBeGreaterThan(-5);
        });

        it("computes screen space bounding box with vertical origin baseline", function () {
          var scale = 1.5;

          var label = labels.add({
            text: "abc",
            scale: scale,
            verticalOrigin: VerticalOrigin.BASELINE,
          });
          scene.renderForSpecs();

          var bbox = Label.getScreenSpaceBoundingBox(label, Cartesian2.ZERO);
          expect(bbox.y).toBeLessThan(bbox.height * -0.8);
          expect(bbox.y).toBeGreaterThan(bbox.height * -1.2);
        });

        it("computes screen space bounding box with horizontal origin", function () {
          var scale = 1.5;

          var label = labels.add({
            text: "abc",
            scale: scale,
            horizontalOrigin: HorizontalOrigin.CENTER,
          });
          scene.renderForSpecs();

          var bbox = Label.getScreenSpaceBoundingBox(label, Cartesian2.ZERO);
          expect(bbox.x).toBeLessThan(bbox.width * -0.3);
          expect(bbox.x).toBeGreaterThan(bbox.width * -0.7);

          label.horizontalOrigin = HorizontalOrigin.RIGHT;
          scene.renderForSpecs();
          bbox = Label.getScreenSpaceBoundingBox(label, Cartesian2.ZERO);
          expect(bbox.x).toBeLessThan(bbox.width * -0.8);
          expect(bbox.x).toBeGreaterThan(bbox.width * -1.2);
        });

        it("computes screen space bounding box with padded background", function () {
          var scale = 1.5;

          var label = labels.add({
            text: "abc",
            scale: scale,
            showBackground: true,
            backgroundPadding: new Cartesian2(15, 10),
          });
          scene.renderForSpecs();

          var totalScale = label.scale * label._relativeSize;

          var backgroundBillboard = label._backgroundBillboard;
          var width = backgroundBillboard.width * totalScale;
          var height = backgroundBillboard.height * totalScale;
          var x = backgroundBillboard._translate.x;
          var y = -(backgroundBillboard._translate.y + height);

          var bbox = Label.getScreenSpaceBoundingBox(label, Cartesian2.ZERO);
          expect(bbox.x).toEqual(x);
          expect(bbox.y).toEqual(y);
          expect(bbox.width).toEqual(width);
          expect(bbox.height).toEqual(height);

          label.verticalOrigin = VerticalOrigin.CENTER;
          scene.renderForSpecs();
          y = -(backgroundBillboard._translate.y + height * 0.5);

          bbox = Label.getScreenSpaceBoundingBox(label, Cartesian2.ZERO);
          expect(bbox.x).toEqual(x);
          expect(bbox.y).toEqual(y);
          expect(bbox.width).toEqual(width);
          expect(bbox.height).toEqual(height);
        });

        it("can equal another label", function () {
          var label = labels.add({
            position: new Cartesian3(1.0, 2.0, 3.0),
            text: "equals",
          });
          var otherLabel = labels.add({
            position: new Cartesian3(1.0, 2.0, 3.0),
            text: "equals",
          });

          // This tests the `LabelCollection.equals` function itself, not simple equality.
          expect(label.equals(otherLabel)).toEqual(true);
        });

        it("can differ from another label", function () {
          var label = labels.add({
            position: new Cartesian3(1.0, 2.0, 3.0),
          });
          var otherLabel = labels.add({
            position: new Cartesian3(4.0, 5.0, 6.0),
          });

          // This tests the `LabelCollection.equals` function itself, not simple equality.
          expect(label.equals(otherLabel)).toEqual(false);
        });

        it("does not equal undefined", function () {
          // This tests the `LabelCollection.equals` function itself, not simple equality.
          var label = labels.add();
          expect(label.equals(undefined)).toEqual(false);
        });

        it("should have a number of glyphs equal to the number of characters", function () {
          var label = labels.add({
            text: "abc",
          });
          scene.renderForSpecs();
          expect(label._glyphs.length).toEqual(3);

          label.text = "abcd";
          scene.renderForSpecs();
          expect(label._glyphs.length).toEqual(4);

          label.text = "";
          scene.renderForSpecs();
          expect(label._glyphs.length).toEqual(0);

          label = labels.add();
          scene.renderForSpecs();
          expect(label._glyphs.length).toEqual(0);
        });

        it("does not create billboards for spaces", function () {
          var label = labels.add({
            text: "abc",
          });
          scene.renderForSpecs();
          expect(label._glyphs.length).toEqual(3);
          expect(labels._billboardCollection.length).toEqual(3);

          label.text = " ab c";
          scene.renderForSpecs();
          expect(label._glyphs.length).toEqual(5);
          expect(labels._billboardCollection.length).toEqual(3);
        });

        function getGlyphBillboardVertexTranslate(label, index) {
          return Cartesian2.clone(
            label._glyphs[index].billboard._translate,
            new Cartesian2()
          );
        }

        function getBackgroundBillboardVertexTranslate(label) {
          return Cartesian2.clone(
            label._backgroundBillboard._translate,
            new Cartesian2()
          );
        }

        it("sets billboard properties properly when they change on the label", function () {
          var position1 = new Cartesian3(1.0, 2.0, 3.0);
          var position2 = new Cartesian3(4.0, 5.0, 6.0);
          var pixelOffset1 = new Cartesian2(4.0, 5.0);
          var pixelOffset2 = new Cartesian2(6.0, 7.0);
          var eyeOffset1 = new Cartesian3(6.0, 7.0, 8.0);
          var eyeOffset2 = new Cartesian3(16.0, 17.0, 18.0);
          var verticalOrigin1 = VerticalOrigin.TOP;
          var verticalOrigin2 = VerticalOrigin.BASELINE;
          var scale1 = 2.0;
          var scale2 = 3.0;
          var id1 = "id1";
          var id2 = "id2";
          var translucency1 = new NearFarScalar(1.0e4, 1.0, 1.0e6, 0.0);
          var translucency2 = new NearFarScalar(1.1e4, 1.2, 1.3e6, 4.0);
          var pixelOffsetScale1 = new NearFarScalar(1.0e4, 1.0, 1.0e6, 0.0);
          var pixelOffsetScale2 = new NearFarScalar(1.5e4, 1.6, 1.7e6, 8.0);
          var scaleByDistance1 = new NearFarScalar(1.0e4, 1.0, 1.0e6, 0.0);
          var scaleByDistance2 = new NearFarScalar(1.5e4, 1.6, 1.7e6, 8.0);

          var label = labels.add({
            position: position1,
            text: "abc",
            pixelOffset: pixelOffset1,
            eyeOffset: eyeOffset1,
            verticalOrigin: verticalOrigin1,
            scale: scale1,
            id: id1,
            translucencyByDistance: translucency1,
            pixelOffsetScaleByDistance: pixelOffsetScale1,
            scaleByDistance: scaleByDistance1,
            showBackground: true,
          });

          scene.renderForSpecs();

          label.position = position2;
          label.text = "def";
          label.pixelOffset = pixelOffset2;
          label.eyeOffset = eyeOffset2;
          label.verticalOrigin = verticalOrigin2;
          label.scale = scale2;
          label.id = id2;
          label.translucencyByDistance = translucency2;
          label.pixelOffsetScaleByDistance = pixelOffsetScale2;
          label.scaleByDistance = scaleByDistance2;

          scene.renderForSpecs();

          for (var i = 0; i < label._glyphs.length; ++i) {
            var glyph = label._glyphs[i];
            var billboard = glyph.billboard;
            expect(billboard.show).toEqual(label.show);
            expect(billboard.position).toEqual(label.position);
            expect(billboard.eyeOffset).toEqual(label.eyeOffset);
            expect(billboard.pixelOffset).toEqual(label.pixelOffset);
            expect(billboard.verticalOrigin).toEqual(label.verticalOrigin);
            // glyph horizontal origin is always LEFT
            expect(billboard.scale).toEqual(label.scale * label._relativeSize);
            expect(billboard.id).toEqual(label.id);
            expect(billboard.translucencyByDistance).toEqual(
              label.translucencyByDistance
            );
            expect(billboard.pixelOffsetScaleByDistance).toEqual(
              label.pixelOffsetScaleByDistance
            );
            expect(billboard.scaleByDistance).toEqual(label.scaleByDistance);

            expect(billboard.pickPrimitive).toEqual(label);
          }
        });

        describe("sets individual billboard properties properly when they change on the label", function () {
          var label;
          beforeEach(function () {
            label = labels.add({
              position: new Cartesian3(1.0, 2.0, 3.0),
              text: "abc",
              pixelOffset: new Cartesian2(4.0, 5.0),
              eyeOffset: new Cartesian3(6.0, 7.0, 8.0),
              verticalOrigin: VerticalOrigin.TOP,
              scale: 2.0,
              id: "id1",
              translucencyByDistance: new NearFarScalar(1.0e4, 1.0, 1.0e6, 0.0),
              pixelOffsetScaleByDistance: new NearFarScalar(
                1.0e4,
                1.0,
                1.0e6,
                0.0
              ),
              scaleByDistance: new NearFarScalar(1.0e4, 1.0, 1.0e6, 0.0),
              showBackground: true,
            });
            scene.renderForSpecs();
          });

          function getGlyphBillboards() {
            return label._glyphs.map(function (glyph) {
              return glyph.billboard;
            });
          }

          it("position", function () {
            var newValue = new Cartesian3(4.0, 5.0, 6.0);
            expect(label.position).not.toEqual(newValue);
            label.position = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.position).toEqual(label.position);
            });
          });

          it("eyeOffset", function () {
            var newValue = new Cartesian3(16.0, 17.0, 18.0);
            expect(label.eyeOffset).not.toEqual(newValue);
            label.eyeOffset = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.eyeOffset).toEqual(label.eyeOffset);
            });
          });

          it("pixelOffset", function () {
            var newValue = new Cartesian3(16.0, 17.0, 18.0);
            expect(label.pixelOffset).not.toEqual(newValue);
            label.pixelOffset = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.pixelOffset).toEqual(label.pixelOffset);
            });
          });

          it("verticalOrigin", function () {
            var newValue = VerticalOrigin.BOTTOM;
            expect(label.verticalOrigin).not.toEqual(newValue);
            label.verticalOrigin = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.verticalOrigin).toEqual(label.verticalOrigin);
            });
          });

          // glyph horizontal origin is always LEFT

          it("scale", function () {
            var newValue = 3.0;
            expect(label.scale).not.toEqual(newValue);
            label.scale = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.scale).toEqual(label.totalScale);
            });
          });

          it("showBackground", function () {
            expect(label.showBackground).toEqual(true);
            label.showBackground = false;
            expect(label.showBackground).toEqual(false);
          });

          it("backgroundColor", function () {
            var newValue = Color.RED;
            expect(label.backgroundColor).not.toEqual(newValue);
            label.backgroundColor = newValue;
            expect(label.backgroundColor).toEqual(newValue);
          });

          it("backgroundPadding", function () {
            var newValue = new Cartesian2(8, 5);
            expect(label.backgroundPadding).not.toEqual(newValue);
            label.backgroundPadding = newValue;
            expect(label.backgroundPadding).toEqual(newValue);
          });

          it("id", function () {
            var newValue = "id2";
            expect(label.id).not.toEqual(newValue);
            label.id = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.id).toEqual(label.id);
            });
          });

          it("translucencyByDistance", function () {
            var newValue = new NearFarScalar(1.1e4, 1.2, 1.3e6, 4.0);
            expect(label.translucencyByDistance).not.toEqual(newValue);
            label.translucencyByDistance = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.translucencyByDistance).toEqual(
                label.translucencyByDistance
              );
            });
          });

          it("pixelOffsetScaleByDistance", function () {
            var newValue = new NearFarScalar(1.5e4, 1.6, 1.7e6, 8.0);
            expect(label.pixelOffsetScaleByDistance).not.toEqual(newValue);
            label.pixelOffsetScaleByDistance = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.pixelOffsetScaleByDistance).toEqual(
                label.pixelOffsetScaleByDistance
              );
            });
          });

          it("scaleByDistance", function () {
            var newValue = new NearFarScalar(1.5e4, 1.6, 1.7e6, 8.0);
            expect(label.scaleByDistance).not.toEqual(newValue);
            label.scaleByDistance = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.scaleByDistance).toEqual(label.scaleByDistance);
            });
          });

          it("translucencyByDistance to undefined", function () {
            var newValue;
            expect(label.translucencyByDistance).not.toEqual(newValue);
            label.translucencyByDistance = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.translucencyByDistance).toEqual(
                label.translucencyByDistance
              );
            });
          });

          it("pixelOffsetScaleByDistance to undefined", function () {
            var newValue;
            expect(label.pixelOffsetScaleByDistance).not.toEqual(newValue);
            label.pixelOffsetScaleByDistance = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.pixelOffsetScaleByDistance).toEqual(
                label.pixelOffsetScaleByDistance
              );
            });
          });

          it("scaleByDistance to undefined", function () {
            var newValue;
            expect(label.scaleByDistance).not.toEqual(newValue);
            label.scaleByDistance = newValue;
            scene.renderForSpecs();

            getGlyphBillboards().forEach(function (billboard) {
              expect(billboard.scaleByDistance).toEqual(label.scaleByDistance);
            });
          });

          it("clusterShow", function () {
            expect(label.clusterShow).toEqual(true);
            label.clusterShow = false;
            expect(label.clusterShow).toEqual(false);
          });
        });

        it("should set vertexTranslate of billboards correctly when vertical origin is changed", function () {
          var label = labels.add({
            text: "apl",
            font: '90px "Open Sans"',
            verticalOrigin: VerticalOrigin.CENTER,
          });
          scene.renderForSpecs();

          // store the offsets when vertically centered
          var offset0 = getGlyphBillboardVertexTranslate(label, 0);
          var offset1 = getGlyphBillboardVertexTranslate(label, 1);
          var offset2 = getGlyphBillboardVertexTranslate(label, 2);

          label.verticalOrigin = VerticalOrigin.TOP;
          scene.renderForSpecs();

          // Because changing the label's vertical origin also changes the vertical origin of each
          // individual glyph, it is not safe to assume anything about Y offsets being more or less.

          // X offset should be unchanged
          expect(getGlyphBillboardVertexTranslate(label, 0).x).toEqual(
            offset0.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).x).toEqual(
            offset1.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).x).toEqual(
            offset2.x
          );

          label.verticalOrigin = VerticalOrigin.BOTTOM;
          scene.renderForSpecs();

          // X offset should be unchanged
          expect(getGlyphBillboardVertexTranslate(label, 0).x).toEqual(
            offset0.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).x).toEqual(
            offset1.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).x).toEqual(
            offset2.x
          );
        });

        it("should set vertexTranslate of billboards correctly when horizontal origin is changed", function () {
          var label = labels.add({
            text: "apl",
            font: '90px "Open Sans"',
            horizontalOrigin: HorizontalOrigin.CENTER,
            showBackground: true,
          });
          scene.renderForSpecs();

          // store the offsets when horizontally centered
          var offset0 = getGlyphBillboardVertexTranslate(label, 0);
          var offset1 = getGlyphBillboardVertexTranslate(label, 1);
          var offset2 = getGlyphBillboardVertexTranslate(label, 2);
          var offsetBack = getBackgroundBillboardVertexTranslate(label);

          label.horizontalOrigin = HorizontalOrigin.LEFT;
          scene.renderForSpecs();

          // horizontal origin LEFT should increase X offset compared to CENTER
          expect(getGlyphBillboardVertexTranslate(label, 0).x).toBeGreaterThan(
            offset0.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).x).toBeGreaterThan(
            offset1.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).x).toBeGreaterThan(
            offset2.x
          );
          expect(
            getBackgroundBillboardVertexTranslate(label).x
          ).toBeGreaterThan(offsetBack.x);

          // Y offset should be unchanged
          expect(getGlyphBillboardVertexTranslate(label, 0).y).toEqual(
            offset0.y
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).y).toEqual(
            offset1.y
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).y).toEqual(
            offset2.y
          );
          expect(getBackgroundBillboardVertexTranslate(label).y).toEqual(
            offsetBack.y
          );

          label.horizontalOrigin = HorizontalOrigin.RIGHT;
          scene.renderForSpecs();

          // horizontal origin RIGHT should decrease X offset compared to CENTER
          expect(getGlyphBillboardVertexTranslate(label, 0).x).toBeLessThan(
            offset0.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).x).toBeLessThan(
            offset1.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).x).toBeLessThan(
            offset2.x
          );
          expect(getBackgroundBillboardVertexTranslate(label).x).toBeLessThan(
            offsetBack.x
          );

          // Y offset should be unchanged
          expect(getGlyphBillboardVertexTranslate(label, 0).y).toEqual(
            offset0.y
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).y).toEqual(
            offset1.y
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).y).toEqual(
            offset2.y
          );
          expect(getBackgroundBillboardVertexTranslate(label).y).toEqual(
            offsetBack.y
          );
        });

        it("should set vertexTranslate of billboards correctly when scale is changed", function () {
          var label = labels.add({
            text: "apl",
            font: '90px "Open Sans"',
            verticalOrigin: VerticalOrigin.CENTER,
            horizontalOrigin: HorizontalOrigin.CENTER,
          });
          scene.renderForSpecs();

          // store the offsets when vertically centered at scale 1
          var offset0 = getGlyphBillboardVertexTranslate(label, 0);
          var offset1 = getGlyphBillboardVertexTranslate(label, 1);
          var offset2 = getGlyphBillboardVertexTranslate(label, 2);

          label.scale = 2;
          scene.renderForSpecs();

          // scaling by 2 should double X and Y offset
          expect(getGlyphBillboardVertexTranslate(label, 0).x).toEqual(
            2 * offset0.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 0).y).toEqual(
            2 * offset0.y
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).x).toEqual(
            2 * offset1.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).y).toEqual(
            2 * offset1.y
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).x).toEqual(
            2 * offset2.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).y).toEqual(
            2 * offset2.y
          );

          // store the offsets when vertically centered at scale 2
          offset0 = getGlyphBillboardVertexTranslate(label, 0);
          offset1 = getGlyphBillboardVertexTranslate(label, 1);
          offset2 = getGlyphBillboardVertexTranslate(label, 2);

          // Because changing the label's vertical origin also changes the vertical origin of each
          // individual glyph, it is not safe to assume anything about Y offsets being more or less.

          label.horizontalOrigin = HorizontalOrigin.LEFT;
          scene.renderForSpecs();

          // horizontal origin LEFT should increase X offset compared to CENTER
          expect(getGlyphBillboardVertexTranslate(label, 0).x).toBeGreaterThan(
            offset0.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).x).toBeGreaterThan(
            offset1.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).x).toBeGreaterThan(
            offset2.x
          );

          // Y offset should be unchanged
          expect(getGlyphBillboardVertexTranslate(label, 0).y).toEqual(
            offset0.y
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).y).toEqual(
            offset1.y
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).y).toEqual(
            offset2.y
          );

          label.horizontalOrigin = HorizontalOrigin.RIGHT;
          scene.renderForSpecs();

          // horizontal origin RIGHT should decrease X offset compared to CENTER
          expect(getGlyphBillboardVertexTranslate(label, 0).x).toBeLessThan(
            offset0.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).x).toBeLessThan(
            offset1.x
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).x).toBeLessThan(
            offset2.x
          );

          // Y offset should be unchanged
          expect(getGlyphBillboardVertexTranslate(label, 0).y).toEqual(
            offset0.y
          );
          expect(getGlyphBillboardVertexTranslate(label, 1).y).toEqual(
            offset1.y
          );
          expect(getGlyphBillboardVertexTranslate(label, 2).y).toEqual(
            offset2.y
          );
        });

        it("label vertex translate should remain the same when pixel offset is changed", function () {
          var label = labels.add({
            text: "apl",
            font: '90px "Open Sans"',
          });
          scene.renderForSpecs();

          var offset0 = getGlyphBillboardVertexTranslate(label, 0);
          var offset1 = getGlyphBillboardVertexTranslate(label, 1);
          var offset2 = getGlyphBillboardVertexTranslate(label, 2);

          var xOffset = 20;
          var yOffset = -10;
          label.pixelOffset = new Cartesian2(xOffset, yOffset);
          scene.renderForSpecs();

          expect(getGlyphBillboardVertexTranslate(label, 0)).toEqual(offset0);
          expect(getGlyphBillboardVertexTranslate(label, 1)).toEqual(offset1);
          expect(getGlyphBillboardVertexTranslate(label, 2)).toEqual(offset2);

          expect(label.pixelOffset.x).toEqual(xOffset);
          expect(label.pixelOffset.y).toEqual(yOffset);
        });

        it("Correctly updates billboard position when height reference changes", function () {
          scene.globe = new Globe();
          var labelsWithScene = new LabelCollection({ scene: scene });
          scene.primitives.add(labelsWithScene);

          var position1 = new Cartesian3(1.0, 2.0, 3.0);
          var label = labelsWithScene.add({
            position: position1,
            text: "abc",
            heightReference: HeightReference.CLAMP_TO_GROUND,
          });

          scene.renderForSpecs();
          var glyph = label._glyphs[0];
          var billboard = glyph.billboard;
          expect(billboard.position).toEqual(label.position);

          label.heightReference = HeightReference.NONE;
          scene.renderForSpecs();

          expect(billboard.position).toEqual(label.position);
          scene.primitives.remove(labelsWithScene);
        });

        it("should set vertexTranslate of billboards correctly when font size changes", function () {
          var label = labels.add({
            text: "apl",
            font: '80px "Open Sans"',
            verticalOrigin: VerticalOrigin.TOP,
            horizontalOrigin: HorizontalOrigin.LEFT,
          });
          scene.renderForSpecs();

          var offset0 = getGlyphBillboardVertexTranslate(label, 0);
          var offset1 = getGlyphBillboardVertexTranslate(label, 1);
          var offset2 = getGlyphBillboardVertexTranslate(label, 2);

          label.font = '20px "Open Sans"';
          scene.renderForSpecs();

          // reducing font size should reduce absolute value of both X and Y offset

          expect(
            Math.abs(getGlyphBillboardVertexTranslate(label, 0).x)
          ).toBeLessThanOrEqualTo(Math.abs(offset0.x));
          expect(
            Math.abs(getGlyphBillboardVertexTranslate(label, 0).y)
          ).toBeLessThanOrEqualTo(Math.abs(offset0.y));
          expect(
            Math.abs(getGlyphBillboardVertexTranslate(label, 1).x)
          ).toBeLessThanOrEqualTo(Math.abs(offset1.x));
          expect(
            Math.abs(getGlyphBillboardVertexTranslate(label, 1).y)
          ).toBeLessThanOrEqualTo(Math.abs(offset1.y));
          expect(
            Math.abs(getGlyphBillboardVertexTranslate(label, 2).x)
          ).toBeLessThanOrEqualTo(Math.abs(offset2.x));
          expect(
            Math.abs(getGlyphBillboardVertexTranslate(label, 2).y)
          ).toBeLessThanOrEqualTo(Math.abs(offset2.y));
        });

        it("should have the same vertexTranslate of billboards whether values are set at construction or afterwards", function () {
          var text = "apl";
          var scale = 2.0;
          var font = '20px "Open Sans"';
          var verticalOrigin = VerticalOrigin.CENTER;
          var pixelOffset = new Cartesian2(10.0, 15.0);

          var one = labels.add({
            text: text,
            scale: scale,
            font: font,
            verticalOrigin: verticalOrigin,
            pixelOffset: pixelOffset,
          });
          scene.renderForSpecs();

          var two = labels.add();
          two.text = text;
          two.scale = scale;
          two.font = font;
          two.verticalOrigin = verticalOrigin;
          two.pixelOffset = pixelOffset;

          scene.renderForSpecs();

          expect(getGlyphBillboardVertexTranslate(one, 0)).toEqual(
            getGlyphBillboardVertexTranslate(two, 0)
          );
          expect(getGlyphBillboardVertexTranslate(one, 1)).toEqual(
            getGlyphBillboardVertexTranslate(two, 1)
          );
          expect(getGlyphBillboardVertexTranslate(one, 2)).toEqual(
            getGlyphBillboardVertexTranslate(two, 2)
          );
        });

        it("should not change vertexTranslate of billboards when position changes", function () {
          var label = labels.add({
            text: "apl",
          });
          scene.renderForSpecs();

          var offset0 = getGlyphBillboardVertexTranslate(label, 0);
          var offset1 = getGlyphBillboardVertexTranslate(label, 1);
          var offset2 = getGlyphBillboardVertexTranslate(label, 2);

          label.position = new Cartesian3(1.0, 1.0, 1.0);
          scene.renderForSpecs();

          expect(getGlyphBillboardVertexTranslate(label, 0)).toEqual(offset0);
          expect(getGlyphBillboardVertexTranslate(label, 1)).toEqual(offset1);
          expect(getGlyphBillboardVertexTranslate(label, 2)).toEqual(offset2);
        });

        it("should not change vertexTranslate of billboards when eye offset changes", function () {
          var label = labels.add({
            text: "apl",
          });
          scene.renderForSpecs();

          var offset0 = getGlyphBillboardVertexTranslate(label, 0);
          var offset1 = getGlyphBillboardVertexTranslate(label, 1);
          var offset2 = getGlyphBillboardVertexTranslate(label, 2);

          label.eyeOffset = new Cartesian3(10.0, 10.0, -10.0);
          scene.renderForSpecs();

          expect(getGlyphBillboardVertexTranslate(label, 0)).toEqual(offset0);
          expect(getGlyphBillboardVertexTranslate(label, 1)).toEqual(offset1);
          expect(getGlyphBillboardVertexTranslate(label, 2)).toEqual(offset2);
        });

        it("should not change label dimensions when scale changes", function () {
          var label = labels.add({
            text: "apl",
          });
          scene.renderForSpecs();

          var originalDimensions = label._glyphs[0].dimensions;

          label.scale = 3;
          scene.renderForSpecs();

          var dimensions = label._glyphs[0].dimensions;
          expect(dimensions.width).toEqual(originalDimensions.width);
          expect(dimensions.height).toEqual(originalDimensions.height);
          expect(dimensions.descent).toEqual(originalDimensions.descent);
        });

        it("should not change label dimensions when font size changes", function () {
          var label = labels.add({
            text: "apl",
            font: '90px "Open Sans"',
          });
          scene.renderForSpecs();

          var originalDimensions = label._glyphs[0].dimensions;

          label.font = '20px "Open Sans"';
          scene.renderForSpecs();

          var dimensions = label._glyphs[0].dimensions;
          expect(dimensions.width).toEqual(originalDimensions.width);
          expect(dimensions.height).toEqual(originalDimensions.height);
          expect(dimensions.descent).toEqual(originalDimensions.descent);
        });

        it("should increase label height and decrease width when adding newlines", function () {
          var label = labels.add({
            text: "apl apl apl",
          });
          scene.renderForSpecs();

          var originalBbox = Label.getScreenSpaceBoundingBox(
            label,
            Cartesian2.ZERO
          );

          label.text = "apl\napl\napl";
          scene.renderForSpecs();
          var newlinesBbox = Label.getScreenSpaceBoundingBox(
            label,
            Cartesian2.ZERO
          );

          expect(newlinesBbox.width).toBeLessThan(originalBbox.width);
          expect(newlinesBbox.height).toBeGreaterThan(originalBbox.height);
        });

        it("should not modify text when rightToLeft is false", function () {
          var text = "bla bla bla";
          var label = labels.add({
            text: text,
          });
          scene.renderForSpecs();

          expect(label.text).toEqual(text);
        });
      },
      "WebGL"
    );

    describe("right to left detection", function () {
      beforeAll(function () {
        Label.enableRightToLeftDetection = true;
      });

      afterAll(function () {
        Label.enableRightToLeftDetection = false;
      });

      it("should not modify text when rightToLeft is true and there are no RTL characters", function () {
        var text = "bla bla bla";
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(text);
      });

      it("should reverse text when there are only hebrew characters and rightToLeft is true", function () {
        var text = "שלום";
        var expectedText = "םולש";
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(text);
        expect(label._renderedText).toEqual(expectedText);
      });

      it("should reverse text when there are only arabic characters and rightToLeft is true", function () {
        var text = "مرحبا";
        var expectedText = "ابحرم";
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(text);
        expect(label._renderedText).toEqual(expectedText);
      });

      it("should reverse part of text when there is mix of right-to-left and other kind of characters and rightToLeft is true", function () {
        var text = 'Master (אדון): "Hello"\nתלמיד (student): "שלום"';
        var expectedText = 'Master (ןודא): "Hello"\n"םולש" :(student) דימלת';
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(text);
        expect(label._renderedText).toEqual(expectedText);
      });

      it("should reverse all text and replace brackets when there is right-to-left characters and rightToLeft is true", function () {
        var text = "משפט [מורכב] {עם} תווים <מיוחדים special>";
        var expectedText = "<special םידחוימ> םיוות {םע} [בכרומ] טפשמ";
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(text);
        expect(label._renderedText).toEqual(expectedText);
      });

      it("should reverse only text that detected as rtl text when it begin with non rtl characters when rightToLeft is true", function () {
        var text =
          "(interesting sentence with hebrew characters) שלום(עליך)חביבי.";
        var expectedText =
          "(interesting sentence with hebrew characters) יביבח(ךילע)םולש.";
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(text);
        expect(label._renderedText).toEqual(expectedText);
      });

      it("should not change nothing if it only non alphanumeric characters when rightToLeft is true", function () {
        var text = "([{- -}])";
        var expectedText = "([{- -}])";
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(expectedText);
      });

      it("detects characters in the range \\u05D0-\\u05EA", function () {
        var text = "\u05D1\u05D2";
        var expectedText = "\u05D2\u05D1";
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(text);
        expect(label._renderedText).toEqual(expectedText);
      });

      it("detects characters in the range \\u0600-\\u06FF", function () {
        var text = "\u0601\u0602";
        var expectedText = "\u0602\u0601";
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(text);
        expect(label._renderedText).toEqual(expectedText);
      });

      it("detects characters in the range \\u0750-\\u077F", function () {
        var text = "\u0751\u0752";
        var expectedText = "\u0752\u0751";
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(text);
        expect(label._renderedText).toEqual(expectedText);
      });

      it("detects characters in the range \\u08A0-\\u08FF", function () {
        var text = "\u08A1\u08A2";
        var expectedText = "\u08A2\u08A1";
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(text);
        expect(label._renderedText).toEqual(expectedText);
      });

      it("should reversing correctly non alphabetic characters", function () {
        var text = "A אב: ג\nאב: ג";
        var expectedText = "A ג :בא\nג :בא";
        var label = labels.add({
          text: text,
        });

        expect(label.text).toEqual(text);
        expect(label._renderedText).toEqual(expectedText);
      });
    });

    it("computes bounding sphere in 3D", function () {
      var one = labels.add({
        position: Cartesian3.fromDegrees(-50.0, -50.0, 0.0),
        text: "one",
      });
      var two = labels.add({
        position: Cartesian3.fromDegrees(-50.0, 50.0, 0.0),
        text: "two",
      });

      scene.renderForSpecs();
      var actual = scene.frameState.commandList[0].boundingVolume;

      var positions = [one.position, two.position];
      var expected = BoundingSphere.fromPoints(positions);
      expect(actual.center).toEqual(expected.center);
      expect(actual.radius).toEqual(expected.radius);
    });

    it("computes bounding sphere in Columbus view", function () {
      // Disable collision detection to allow controlled camera position,
      // hence predictable bounding sphere size
      var originalEnableCollisionDetection =
        scene.screenSpaceCameraController.enableCollisionDetection;
      scene.screenSpaceCameraController.enableCollisionDetection = false;

      var projection = scene.mapProjection;
      var ellipsoid = projection.ellipsoid;

      var one = labels.add({
        position: Cartesian3.fromDegrees(-50.0, -50.0, 0.0),
        text: "one",
      });
      var two = labels.add({
        position: Cartesian3.fromDegrees(-50.0, 50.0, 0.0),
        text: "two",
      });

      // Update scene state
      scene.morphToColumbusView(0);
      scene.renderForSpecs();
      var actual = scene.frameState.commandList[0].boundingVolume;

      var projectedPositions = [
        projection.project(ellipsoid.cartesianToCartographic(one.position)),
        projection.project(ellipsoid.cartesianToCartographic(two.position)),
      ];
      var expected = BoundingSphere.fromPoints(projectedPositions);
      expected.center = new Cartesian3(
        0.0,
        expected.center.x,
        expected.center.y
      );
      expect(actual.center).toEqualEpsilon(
        expected.center,
        CesiumMath.EPSILON8
      );
      expect(actual.radius).toBeGreaterThanOrEqualTo(expected.radius);
      scene.screenSpaceCameraController.enableCollisionDetection = originalEnableCollisionDetection;
    });

    it("computes bounding sphere in 2D", function () {
      var projection = scene.mapProjection;
      var ellipsoid = projection.ellipsoid;

      var one = labels.add({
        position: Cartesian3.fromDegrees(-50.0, -50.0),
        text: "one",
      });
      var two = labels.add({
        position: Cartesian3.fromDegrees(-50.0, 50.0),
        text: "two",
      });

      camera.setView({
        destination: Rectangle.fromDegrees(-60.0, -60.0, -40.0, 60.0),
      });

      scene.morphTo2D(0);
      scene.renderForSpecs();

      scene.renderForSpecs();
      var actual = scene.frameState.commandList[0].boundingVolume;

      var projectedPositions = [
        projection.project(ellipsoid.cartesianToCartographic(one.position)),
        projection.project(ellipsoid.cartesianToCartographic(two.position)),
      ];
      var expected = BoundingSphere.fromPoints(projectedPositions);
      expected.center = new Cartesian3(
        0.0,
        expected.center.x,
        expected.center.y
      );
      expect(actual.center).toEqualEpsilon(
        expected.center,
        CesiumMath.EPSILON8
      );
      expect(actual.radius).toBeGreaterThan(expected.radius);
    });

    it("Label.show throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.show = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.position throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.position = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.text throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.text = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.font throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.font = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.fillColor throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.fillColor = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.outlineColor throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.outlineColor = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.outlineWidth throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.outlineWidth = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.style throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.style = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.pixelOffset throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.pixelOffset = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.eyeOffset throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.eyeOffset = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.horizontalOrigin throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.horizontalOrigin = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.verticalOrigin throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.verticalOrigin = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.scale throws with undefined", function () {
      var label = labels.add();
      expect(function () {
        label.scale = undefined;
      }).toThrowDeveloperError();
    });

    it("Label.computeScreenSpacePosition throws with undefined scene", function () {
      var label = labels.add();
      expect(function () {
        label.computeScreenSpacePosition();
      }).toThrowDeveloperError();
    });

    it("Label.translucencyByDistance throws with nearDistance === farDistance", function () {
      var label = labels.add();
      var translucency = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
      expect(function () {
        label.translucencyByDistance = translucency;
      }).toThrowDeveloperError();
    });

    it("Label.pixelOffsetScaleByDistance throws with nearDistance === farDistance", function () {
      var label = labels.add();
      var pixelOffsetScale = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
      expect(function () {
        label.pixelOffsetScaleByDistance = pixelOffsetScale;
      }).toThrowDeveloperError();
    });

    it("Label.scaleByDistance throws with nearDistance === farDistance", function () {
      var label = labels.add();
      var scaleByDistance = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
      expect(function () {
        label.scaleByDistance = scaleByDistance;
      }).toThrowDeveloperError();
    });

    it("new label throws with invalid translucencyByDistance (nearDistance === farDistance)", function () {
      var translucency = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
      expect(function () {
        labels.add({
          translucencyByDistance: translucency,
        });
      }).toThrowDeveloperError();
    });

    it("new label throws with invalid pixelOffsetScaleByDistance (nearDistance === farDistance)", function () {
      var pixelOffsetScale = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
      expect(function () {
        labels.add({
          pixelOffsetScaleByDistance: pixelOffsetScale,
        });
      }).toThrowDeveloperError();
    });

    it("new label throws with invalid scaleByDistance (nearDistance === farDistance)", function () {
      var scaleByDistance = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
      expect(function () {
        labels.add({
          scaleByDistance: scaleByDistance,
        });
      }).toThrowDeveloperError();
    });

    it("Label.translucencyByDistance throws with nearDistance > farDistance", function () {
      var label = labels.add();
      var translucency = new NearFarScalar(1.0e9, 1.0, 1.0e5, 1.0);
      expect(function () {
        label.translucencyByDistance = translucency;
      }).toThrowDeveloperError();
    });

    it("Label.pixelOffsetScaleByDistance throws with nearDistance > farDistance", function () {
      var label = labels.add();
      var pixelOffsetScale = new NearFarScalar(1.0e9, 1.0, 1.0e5, 1.0);
      expect(function () {
        label.pixelOffsetScaleByDistance = pixelOffsetScale;
      }).toThrowDeveloperError();
    });

    it("Label.scaleByDistance throws with nearDistance > farDistance", function () {
      var label = labels.add();
      var scaleByDistance = new NearFarScalar(1.0e9, 1.0, 1.0e5, 1.0);
      expect(function () {
        label.scaleByDistance = scaleByDistance;
      }).toThrowDeveloperError();
    });

    it("destroys texture atlas when destroying", function () {
      labels.add({
        text: "a",
      });
      scene.renderForSpecs();

      var textureAtlas = labels._textureAtlas;
      expect(textureAtlas.isDestroyed()).toBe(false);

      scene.primitives.removeAll();

      expect(textureAtlas.isDestroyed()).toBe(true);
    });

    describe("height referenced labels", function () {
      beforeEach(function () {
        scene.globe = createGlobe();

        labelsWithHeight = new LabelCollection({
          scene: scene,
        });
        scene.primitives.add(labelsWithHeight);
      });

      it("explicitly constructs a label with height reference", function () {
        scene.globe = createGlobe();
        var l = labelsWithHeight.add({
          text: "test",
          heightReference: HeightReference.CLAMP_TO_GROUND,
        });

        expect(l.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND);
      });

      it("set label height reference property", function () {
        scene.globe = createGlobe();
        var l = labelsWithHeight.add({
          text: "test",
        });
        l.heightReference = HeightReference.CLAMP_TO_GROUND;

        expect(l.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND);
      });

      it("creating with a height reference creates a height update callback", function () {
        scene.globe = createGlobe();
        labelsWithHeight.add({
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
        });
        expect(scene.globe.callback).toBeDefined();
      });

      it("set height reference property creates a height update callback", function () {
        scene.globe = createGlobe();
        var l = labelsWithHeight.add({
          position: Cartesian3.fromDegrees(-72.0, 40.0),
        });
        l.heightReference = HeightReference.CLAMP_TO_GROUND;
        expect(scene.globe.callback).toBeDefined();
      });

      it("updates the callback when the height reference changes", function () {
        scene.globe = createGlobe();
        var l = labelsWithHeight.add({
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
        });
        expect(scene.globe.callback).toBeDefined();

        l.heightReference = HeightReference.RELATIVE_TO_GROUND;
        expect(scene.globe.removedCallback).toEqual(true);
        expect(scene.globe.callback).toBeDefined();

        scene.globe.removedCallback = false;
        l.heightReference = HeightReference.NONE;
        expect(scene.globe.removedCallback).toEqual(true);
        expect(scene.globe.callback).toBeUndefined();
      });

      it("changing the position updates the callback", function () {
        scene.globe = createGlobe();
        var l = labelsWithHeight.add({
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
        });
        expect(scene.globe.callback).toBeDefined();

        l.position = Cartesian3.fromDegrees(-73.0, 40.0);
        expect(scene.globe.removedCallback).toEqual(true);
        expect(scene.globe.callback).toBeDefined();
      });

      it("callback updates the position", function () {
        scene.globe = createGlobe();
        var l = labelsWithHeight.add({
          heightReference: HeightReference.CLAMP_TO_GROUND,
          position: Cartesian3.fromDegrees(-72.0, 40.0),
        });
        expect(scene.globe.callback).toBeDefined();

        var cartographic = scene.globe.ellipsoid.cartesianToCartographic(
          l._clampedPosition
        );
        expect(cartographic.height).toEqual(0.0);

        scene.globe.callback(Cartesian3.fromDegrees(-72.0, 40.0, 100.0));
        cartographic = scene.globe.ellipsoid.cartesianToCartographic(
          l._clampedPosition
        );
        expect(cartographic.height).toEqualEpsilon(100.0, CesiumMath.EPSILON9);
      });

      it("resets the clamped position when HeightReference.NONE", function () {
        scene.globe = createGlobe();
        spyOn(scene.camera, "update");
        var l = labelsWithHeight.add({
          heightReference: HeightReference.CLAMP_TO_GROUND,
          text: "t",
          position: Cartesian3.fromDegrees(-72.0, 40.0),
        });
        scene.renderForSpecs();
        expect(l._clampedPosition).toBeDefined();
        expect(l._glyphs[0].billboard._clampedPosition).toBeDefined();

        l.heightReference = HeightReference.NONE;
        expect(l._clampedPosition).toBeUndefined();
        expect(l._glyphs[0].billboard._clampedPosition).toBeUndefined();
      });

      it("clears the billboard height reference callback when the label is removed", function () {
        scene.globe = createGlobe();
        spyOn(scene.camera, "update");
        var l = labelsWithHeight.add({
          heightReference: HeightReference.CLAMP_TO_GROUND,
          text: "t",
          position: Cartesian3.fromDegrees(-72.0, 40.0),
        });
        scene.renderForSpecs();
        var billboard = l._glyphs[0].billboard;
        expect(billboard._removeCallbackFunc).toBeDefined();
        var spy = spyOn(billboard, "_removeCallbackFunc");
        labelsWithHeight.remove(l);
        expect(spy).toHaveBeenCalled();
        expect(
          labelsWithHeight._spareBillboards[0]._removeCallbackFunc
        ).toBeUndefined();
      });
    });
  },
  "WebGL"
);
