text: "use strict";
  
  /*global require*/
  var BoundingSphere = require("terriajs-cesium/Source/Core/BoundingSphere");
  var BoundingSphereState = require("terriajs-cesium/Source/DataSources/BoundingSphereState");
  var Cartesian2 = require("terriajs-cesium/Source/Core/Cartesian2");
  var Cartesian3 = require("terriajs-cesium/Source/Core/Cartesian3");
  var Cartographic = require("terriajs-cesium/Source/Core/Cartographic");
  var CesiumMath = require("terriajs-cesium/Source/Core/Math");
  var defaultValue = require("terriajs-cesium/Source/Core/defaultValue");
  var defined = require("terriajs-cesium/Source/Core/defined");
  var destroyObject = require("terriajs-cesium/Source/Core/destroyObject");
  var DeveloperError = require("terriajs-cesium/Source/Core/DeveloperError");
  var Ellipsoid = require("terriajs-cesium/Source/Core/Ellipsoid");
  var Entity = require("terriajs-cesium/Source/DataSources/Entity");
  var formatError = require("terriajs-cesium/Source/Core/formatError");
  var getTimestamp = require("terriajs-cesium/Source/Core/getTimestamp");
  var HeadingPitchRange = require("terriajs-cesium/Source/Core/HeadingPitchRange");
  var ImageryLayer = require("terriajs-cesium/Source/Scene/ImageryLayer");
  var JulianDate = require("terriajs-cesium/Source/Core/JulianDate");
  var knockout = require("terriajs-cesium/Source/ThirdParty/knockout");
  var loadWithXhr = require("../Core/loadWithXhr");
  var Matrix4 = require("terriajs-cesium/Source/Core/Matrix4");
  var Rectangle = require("terriajs-cesium/Source/Core/Rectangle");
  var sampleTerrain = require("terriajs-cesium/Source/Core/sampleTerrain");
  var SceneTransforms = require("terriajs-cesium/Source/Scene/SceneTransforms");
  var ScreenSpaceEventType = require("terriajs-cesium/Source/Core/ScreenSpaceEventType");
  var TaskProcessor = require("terriajs-cesium/Source/Core/TaskProcessor");
  var Transforms = require("terriajs-cesium/Source/Core/Transforms");
  var when = require("terriajs-cesium/Source/ThirdParty/when");
  var EventHelper = require("terriajs-cesium/Source/Core/EventHelper");
  var ImagerySplitDirection = require("terriajs-cesium/Source/Scene/ImagerySplitDirection");
  
  var CesiumSelectionIndicator = require("../Map/CesiumSelectionIndicator");
  var Feature = require("./Feature");
  var GlobeOrMap = require("./GlobeOrMap");
  var inherit = require("../Core/inherit");
  var pollToPromise = require("../Core/pollToPromise");
  var TerriaError = require("../Core/TerriaError");
  var PickedFeatures = require("../Map/PickedFeatures");
  var ViewerMode = require("./ViewerMode");
  
  /**
   * The Cesium viewer component
   *
   * @alias Cesium
   * @constructor
   * @extends GlobeOrMap
   *
   * @param {Terria} terria The Terria instance.
   * @param {Viewer} viewer The Cesium viewer instance.
   */
  var Cesium = function(terria, viewer) {
    GlobeOrMap.call(this, terria);
  
    /**
     * Gets or sets the Cesium {@link Viewer} instance.
     * @type {Viewer}
     */
    this.viewer = viewer;
  
    /**
     * Gets or sets the Cesium {@link Scene} instance.
     * @type {Scene}
     */
    this.scene = viewer.scene;
  
    /**
     * Gets or sets whether the viewer has stopped rendering since startup or last set to false.
     * @type {Boolean}
     */
    this.stoppedRendering = false;
  
    /**
     * Gets or sets whether to output info to the console when starting and stopping rendering loop.
     * @type {Boolean}
     */
    this.verboseRendering = false;
  
    /**
     * Gets or sets whether this viewer _can_ show a splitter.
     * @type {Boolean}
     */
    this.canShowSplitter = true;
  
    /**
     * Gets the {@link DataSourceDisplay} used to render a {@link DataSource}.
     * @type {DataSourceDisplay}
     */
    this.dataSourceDisplay = undefined;
  
    this._lastClockTime = new JulianDate(0, 0.0);
    this._lastCameraViewMatrix = new Matrix4();
    this._lastCameraMoveTime = 0;
  
    this._selectionIndicator = new CesiumSelectionIndicator(this);
  
    this._removePostRenderListener = this.scene.postRender.addEventListener(
      postRender.bind(undefined, this)
    );
    this._removeInfoBoxCloseListener = undefined;
    this._boundNotifyRepaintRequired = this.notifyRepaintRequired.bind(this);
  
    this._pauseMapInteractionCount = 0;
  
    this.scene.imagerySplitPosition = this.terria.splitPosition;
  
    this.supportsPolylinesOnTerrain = this.scene.context.depthTexture;
  
    // Handle left click by picking objects from the map.
    viewer.screenSpaceEventHandler.setInputAction(
      function(e) {
        this.pickFromScreenPosition(e.position);
      }.bind(this),
      ScreenSpaceEventType.LEFT_CLICK
    );
  
    // Force a repaint when the mouse moves or the window changes size.
    var canvas = this.viewer.canvas;
    canvas.addEventListener("mousemove", this._boundNotifyRepaintRequired, false);
    canvas.addEventListener("mousedown", this._boundNotifyRepaintRequired, false);
    canvas.addEventListener("mouseup", this._boundNotifyRepaintRequired, false);
    canvas.addEventListener(
      "touchstart",
      this._boundNotifyRepaintRequired,
      false
    );
    canvas.addEventListener("touchend", this._boundNotifyRepaintRequired, false);
    canvas.addEventListener("touchmove", this._boundNotifyRepaintRequired, false);
  
    if (defined(window.PointerEvent)) {
      canvas.addEventListener(
        "pointerdown",
        this._boundNotifyRepaintRequired,
        false
      );
      canvas.addEventListener(
        "pointerup",
        this._boundNotifyRepaintRequired,
        false
      );
      canvas.addEventListener(
        "pointermove",
        this._boundNotifyRepaintRequired,
        false
      );
    }
  
    // Detect available wheel event
    this._wheelEvent = undefined;
    if ("onwheel" in canvas) {
      // spec event type
      this._wheelEvent = "wheel";
    } else if (defined(document.onmousewheel)) {
      // legacy event type
      this._wheelEvent = "mousewheel";
    } else {
      // older Firefox
      this._wheelEvent = "DOMMouseScroll";
    }
  
    canvas.addEventListener(
      this._wheelEvent,
      this._boundNotifyRepaintRequired,
      false
    );
  
    window.addEventListener("resize", this._boundNotifyRepaintRequired, false);
  
    this._setViewerResolution = () => {
      // Force rendering in CSS pixel resolution instead of native device
      // resolution which is the default since cesium1.61.0
  
      // We only tweak if user hasn't specified to use useNativeResolution
      if (!this.terria.useNativeResolution) {
        this.viewer.resolutionScale = 1.0 / window.devicePixelRatio;
      }
    };
    this._setViewerResolution();
    window.addEventListener("resize", this._setViewerResolution, false);
  
    // Force a repaint when the feature info box is closed.  Cesium can't close its info box
    // when the clock is not ticking, for reasons that are not clear.
    if (defined(this.viewer.infoBox)) {
      this._removeInfoBoxCloseListener = this.viewer.infoBox.viewModel.closeClicked.addEventListener(
        this._boundNotifyRepaintRequired
      );
    }
  
    if (defined(this.viewer._clockViewModel)) {
      var clock = this.viewer._clockViewModel;
      this._shouldAnimateSubscription = knockout
        .getObservable(clock, "shouldAnimate")
        .subscribe(this._boundNotifyRepaintRequired);
      this._currentTimeSubscription = knockout
        .getObservable(clock, "currentTime")
        .subscribe(this._boundNotifyRepaintRequired);
    }
  
    if (defined(this.viewer.timeline)) {
      this.viewer.timeline.addEventListener(
        "settime",
        this._boundNotifyRepaintRequired,
        false
      );
    }
  
    this._selectedFeatureSubscription = knockout
      .getObservable(this.terria, "selectedFeature")
      .subscribe(function() {
        selectFeature(this);
      }, this);
  
    this._splitterPositionSubscription = knockout
      .getObservable(this.terria, "splitPosition")
      .subscribe(function() {
        if (this.scene) {
          this.scene.imagerySplitPosition = this.terria.splitPosition;
          this.notifyRepaintRequired();
        }
      }, this);
  
    this._showSplitterSubscription = knockout
      .getObservable(terria, "showSplitter")
      .subscribe(function() {
        this.updateAllItemsForSplitter();
      }, this);
  
    // Hacky way to force a repaint when an async load request completes
    var that = this;
    this._originalLoadWithXhr = loadWithXhr.load;
    loadWithXhr.load = function(
      url,
      responseType,
      method,
      data,
      headers,
      deferred,
      overrideMimeType,
      preferText,
      timeout
    ) {
      deferred.promise.always(that._boundNotifyRepaintRequired);
      that._originalLoadWithXhr(
        url,
        responseType,
        method,
        data,
        headers,
        deferred,
        overrideMimeType,
        preferText,
        timeout
      );
    };
  
    // Hacky way to force a repaint when a web worker sends something back.
    this._originalScheduleTask = TaskProcessor.prototype.scheduleTask;
    TaskProcessor.prototype.scheduleTask = function(
      parameters,
      transferableObjects
    ) {
      var result = that._originalScheduleTask.call(
        this,
        parameters,
        transferableObjects
      );
  
      if (!defined(this._originalWorkerMessageSinkRepaint)) {
        this._originalWorkerMessageSinkRepaint = this._worker.onmessage;
  
        var taskProcessor = this;
        this._worker.onmessage = function(event) {
          taskProcessor._originalWorkerMessageSinkRepaint(event);
  
          if (that.isDestroyed()) {
            taskProcessor._worker.onmessage =
              taskProcessor._originalWorkerMessageSinkRepaint;
            taskProcessor._originalWorkerMessageSinkRepaint = undefined;
          } else {
            that.notifyRepaintRequired();
          }
        };
      }
  
      return result;
    };
  
    this.eventHelper = new EventHelper();
  
    // If the render loop crashes, inform the user and then switch to 2D.
    this.eventHelper.add(
      this.scene.renderError,
      function(scene, error) {
        this.terria.error.raiseEvent(
          new TerriaError({
            sender: this,
            title: "Error rendering in 3D",
            message:
              "\
  <p>An error occurred while rendering in 3D.  This probably indicates a bug in " +
              terria.appName +
              " or an incompatibility with your system \
  or web browser.  We'll now switch you to 2D so that you can continue your work.  We would appreciate it if you report this \
  error by sending an email to <a href=\"mailto:" +
              terria.supportEmail +
              '">' +
              terria.supportEmail +
              "</a> with the \
  technical details below.  Thank you!</p><pre>" +
              formatError(error) +
              "</pre>"
          })
        );
  
        this.terria.viewerMode = ViewerMode.Leaflet;
      },
      this
    );
  
    this.eventHelper.add(
      this.scene.globe.tileLoadProgressEvent,
      this.updateTilesLoadingCount.bind(this)
    );
  
    selectFeature(this);
  };
  
  inherit(GlobeOrMap, Cesium);
  
  Cesium.prototype.destroy = function() {
    if (defined(this._selectionIndicator)) {
      this._selectionIndicator.destroy();
      this._selectionIndicator = undefined;
    }
  
    if (defined(this._removePostRenderListener)) {
      this._removePostRenderListener();
      this._removePostRenderListener = undefined;
    }
  
    if (defined(this._removeInfoBoxCloseListener)) {
      this._removeInfoBoxCloseListener();
    }
  
    if (defined(this._shouldAnimateSubscription)) {
      this._shouldAnimateSubscription.dispose();
      this._shouldAnimateSubscription = undefined;
    }
  
    if (defined(this._currentTimeSubscription)) {
      this._currentTimeSubscription.dispose();
      this._currentTimeSubscription = undefined;
    }
  
    if (defined(this.viewer.timeline)) {
      this.viewer.timeline.removeEventListener(
        "settime",
        this._boundNotifyRepaintRequired,
        false
      );
    }
  
    if (defined(this._selectedFeatureSubscription)) {
      this._selectedFeatureSubscription.dispose();
      this._selectedFeatureSubscription = undefined;
    }
  
    if (defined(this._splitterPositionSubscription)) {
      this._splitterPositionSubscription.dispose();
      this._splitterPositionSubscription = undefined;
    }
  
    if (defined(this._showSplitterSubscription)) {
      this._showSplitterSubscription.dispose();
      this._showSplitterSubscription = undefined;
    }
  
    this.viewer.canvas.removeEventListener(
      "mousemove",
      this._boundNotifyRepaintRequired,
      false
    );
    this.viewer.canvas.removeEventListener(
      "mousedown",
      this._boundNotifyRepaintRequired,
      false
    );
    this.viewer.canvas.removeEventListener(
      "mouseup",
      this._boundNotifyRepaintRequired,
      false
    );
    this.viewer.canvas.removeEventListener(
      "touchstart",
      this._boundNotifyRepaintRequired,
      false
    );
    this.viewer.canvas.removeEventListener(
      "touchend",
      this._boundNotifyRepaintRequired,
      false
    );
    this.viewer.canvas.removeEventListener(
      "touchmove",
      this._boundNotifyRepaintRequired,
      false
    );
  
    if (defined(window.PointerEvent)) {
      this.viewer.canvas.removeEventListener(
        "pointerdown",
        this._boundNotifyRepaintRequired,
        false
      );
      this.viewer.canvas.removeEventListener(
        "pointerup",
        this._boundNotifyRepaintRequired,
        false
      );
      this.viewer.canvas.removeEventListener(
        "pointermove",
        this._boundNotifyRepaintRequired,
        false
      );
    }
  
    this.viewer.canvas.removeEventListener(
      this._wheelEvent,
      this._boundNotifyRepaintRequired,
      false
    );
  
    window.removeEventListener("resize", this._boundNotifyRepaintRequired, false);
    window.removeEventListener("resize", this._setViewerResolution, false);
  
    loadWithXhr.load = this._originalLoadWithXhr;
    TaskProcessor.prototype.scheduleTask = this._originalScheduleTask;
  
    this.eventHelper.removeAll();
  
    GlobeOrMap.disposeCommonListeners(this);
  
    return destroyObject(this);
  };
  
  Cesium.prototype.isDestroyed = function() {
    return false;
  };
  
  var cartesian3Scratch = new Cartesian3();
  var enuToFixedScratch = new Matrix4();
  var southwestScratch = new Cartesian3();
  var southeastScratch = new Cartesian3();
  var northeastScratch = new Cartesian3();
  var northwestScratch = new Cartesian3();
  var southwestCartographicScratch = new Cartographic();
  var southeastCartographicScratch = new Cartographic();
  var northeastCartographicScratch = new Cartographic();
  var northwestCartographicScratch = new Cartographic();
  
  /**
   * Gets the current extent of the camera.  This may be approximate if the viewer does not have a strictly rectangular view.
   * @return {Rectangle} The current visible extent.
   */
  Cesium.prototype.getCurrentExtent = function() {
    var scene = this.scene;
    var camera = scene.camera;
  
    var width = scene.canvas.clientWidth;
    var height = scene.canvas.clientHeight;
  
    var centerOfScreen = new Cartesian2(width / 2.0, height / 2.0);
    var pickRay = scene.camera.getPickRay(centerOfScreen);
    var center = scene.globe.pick(pickRay, scene);
  
    if (!defined(center)) {
      // TODO: binary search to find the horizon point and use that as the center.
      return this.terria.homeView.rectangle;
    }
  
    var ellipsoid = this.scene.globe.ellipsoid;
  
    var fovy = scene.camera.frustum.fovy * 0.5;
    var fovx = Math.atan(Math.tan(fovy) * scene.camera.frustum.aspectRatio);
  
    var cameraOffset = Cartesian3.subtract(
      camera.positionWC,
      center,
      cartesian3Scratch
    );
    var cameraHeight = Cartesian3.magnitude(cameraOffset);
    var xDistance = cameraHeight * Math.tan(fovx);
    var yDistance = cameraHeight * Math.tan(fovy);
  
    var southwestEnu = new Cartesian3(-xDistance, -yDistance, 0.0);
    var southeastEnu = new Cartesian3(xDistance, -yDistance, 0.0);
    var northeastEnu = new Cartesian3(xDistance, yDistance, 0.0);
    var northwestEnu = new Cartesian3(-xDistance, yDistance, 0.0);
  
    var enuToFixed = Transforms.eastNorthUpToFixedFrame(
      center,
      ellipsoid,
      enuToFixedScratch
    );
    var southwest = Matrix4.multiplyByPoint(
      enuToFixed,
      southwestEnu,
      southwestScratch
    );
    var southeast = Matrix4.multiplyByPoint(
      enuToFixed,
      southeastEnu,
      southeastScratch
    );
    var northeast = Matrix4.multiplyByPoint(
      enuToFixed,
      northeastEnu,
      northeastScratch
    );
    var northwest = Matrix4.multiplyByPoint(
      enuToFixed,
      northwestEnu,
      northwestScratch
    );
  
    var southwestCartographic = ellipsoid.cartesianToCartographic(
      southwest,
      southwestCartographicScratch
    );
    var southeastCartographic = ellipsoid.cartesianToCartographic(
      southeast,
      southeastCartographicScratch
    );
    var northeastCartographic = ellipsoid.cartesianToCartographic(
      northeast,
      northeastCartographicScratch
    );
    var northwestCartographic = ellipsoid.cartesianToCartographic(
      northwest,
      northwestCartographicScratch
    );
  
    // Account for date-line wrapping
    if (southeastCartographic.longitude < southwestCartographic.longitude) {
      southeastCartographic.longitude += CesiumMath.TWO_PI;
    }
    if (northeastCartographic.longitude < northwestCartographic.longitude) {
      northeastCartographic.longitude += CesiumMath.TWO_PI;
    }
  
    var rect = new Rectangle(
      CesiumMath.convertLongitudeRange(
        Math.min(southwestCartographic.longitude, northwestCartographic.longitude)
      ),
      Math.min(southwestCartographic.latitude, southeastCartographic.latitude),
      CesiumMath.convertLongitudeRange(
        Math.max(northeastCartographic.longitude, southeastCartographic.longitude)
      ),
      Math.max(northeastCartographic.latitude, northwestCartographic.latitude)
    );
    rect.center = center;
    return rect;
  };
  
  /**
   * Gets the current container element.
   * @return {Element} The current container element.
   */
  Cesium.prototype.getContainer = function() {
    return this.viewer.container;
  };
  
  /**
   * Zooms to a specified camera view or extent with a smooth flight animation.
   *
   * @param {CameraView|Rectangle|DataSource|Cesium3DTileset} target The view, extent, DataSource, or Cesium3DTileset to which to zoom.
   * @param {Number} [flightDurationSeconds=3.0] The length of the flight animation in seconds.
   */
  Cesium.prototype.zoomTo = function(target, flightDurationSeconds) {
    if (!defined(target)) {
      throw new DeveloperError("target is required.");
    }
  
    flightDurationSeconds = defaultValue(flightDurationSeconds, 3.0);
  
    var that = this;
    that.lastTarget = target;
    return when()
      .then(function() {
        if (target instanceof Rectangle) {
          var camera = that.scene.camera;
  
          // Work out the destination that the camera would naturally fly to
          var destinationCartesian = camera.getRectangleCameraCoordinates(target);
          var destination = Ellipsoid.WGS84.cartesianToCartographic(
            destinationCartesian
          );
  
          var terrainProvider = that.scene.globe.terrainProvider;
          var level = 6; // A sufficiently coarse tile level that still has approximately accurate height
          var positions = [Rectangle.center(target)];
  
          // Perform an elevation query at the centre of the rectangle
          return sampleTerrain(terrainProvider, level, positions).then(function(
            results
          ) {
            if (that.lastTarget !== target) {
              return;
            }
            // Add terrain elevation to camera altitude
            var finalDestinationCartographic = {
              longitude: destination.longitude,
              latitude: destination.latitude,
              height: destination.height + results[0].height
            };
  
            var finalDestination = Ellipsoid.WGS84.cartographicToCartesian(
              finalDestinationCartographic
            );
  
            camera.flyTo({
              duration: flightDurationSeconds,
              destination: finalDestination
            });
          });
        } else if (defined(target.entities)) {
          // Zooming to a DataSource
          if (target.isLoading && defined(target.loadingEvent)) {
            var deferred = when.defer();
            var removeEvent = target.loadingEvent.addEventListener(function() {
              removeEvent();
              deferred.resolve();
            });
            return deferred.promise.then(function() {
              if (that.lastTarget !== target) {
                return;
              }
              return zoomToDataSource(that, target, flightDurationSeconds);
            });
          }
          return zoomToDataSource(that, target);
        } else if (defined(target.readyPromise)) {
          return target.readyPromise.then(function() {
            if (defined(target.boundingSphere) && that.lastTarget === target) {
              zoomToBoundingSphere(that, target, flightDurationSeconds);
            }
          });
        } else if (defined(target.boundingSphere)) {
          return zoomToBoundingSphere(that, target);
        } else if (defined(target.position)) {
          that.scene.camera.flyTo({
            duration: flightDurationSeconds,
            destination: target.position,
            orientation: {
              direction: target.direction,
              up: target.up
            }
          });
        } else {
          that.scene.camera.flyTo({
            duration: flightDurationSeconds,
            destination: target.rectangle
          });
        }
      })
      .then(function() {
        that.notifyRepaintRequired();
      });
  };
  
  var boundingSphereScratch = new BoundingSphere();
  
  function zoomToDataSource(cesium, target, flightDurationSeconds) {
    return pollToPromise(
      function() {
        var dataSourceDisplay = cesium.dataSourceDisplay;
        var entities = target.entities.values;
  
        var boundingSpheres = [];
        for (var i = 0, len = entities.length; i < len; i++) {
          var state = BoundingSphereState.PENDING;
          try {
            state = dataSourceDisplay.getBoundingSphere(
              entities[i],
              false,
              boundingSphereScratch
            );
          } catch (e) {}
  
          if (state === BoundingSphereState.PENDING) {
            return false;
          } else if (state !== BoundingSphereState.FAILED) {
            boundingSpheres.push(BoundingSphere.clone(boundingSphereScratch));
          }
        }
  
        var boundingSphere = BoundingSphere.fromBoundingSpheres(boundingSpheres);
        cesium.scene.camera.flyToBoundingSphere(boundingSphere, {
          duration: flightDurationSeconds
        });
        return true;
      },
      {
        pollInterval: 100,
        timeout: 5000
      }
    );
  }
  
  function zoomToBoundingSphere(cesium, target, flightDurationSeconds) {
    var boundingSphere = target.boundingSphere;
    var modelMatrix = target.modelMatrix;
    if (modelMatrix) {
      boundingSphere = BoundingSphere.transform(boundingSphere, modelMatrix);
    }
    cesium.scene.camera.flyToBoundingSphere(boundingSphere, {
      offset: new HeadingPitchRange(0.0, -0.5, boundingSphere.radius),
      duration: flightDurationSeconds
    });
  }
  
  /**
   * Captures a screenshot of the map.
   * @return {Promise} A promise that resolves to a data URL when the screenshot is ready.
   */
  Cesium.prototype.captureScreenshot = function() {
    var deferred = when.defer();
  
    var removeCallback = this.scene.postRender.addEventListener(function() {
      removeCallback();
      try {
        const cesiumCanvas = this.scene.canvas;
  
        // If we're using the splitter, draw the split position as a vertical white line.
        let canvas = cesiumCanvas;
        if (this.terria.showSplitter) {
          canvas = document.createElement("canvas");
          canvas.width = cesiumCanvas.width;
          canvas.height = cesiumCanvas.height;
  
          const context = canvas.getContext("2d");
          context.drawImage(cesiumCanvas, 0, 0);
  
          const x = this.terria.splitPosition * cesiumCanvas.width;
          context.strokeStyle = this.terria.baseMapContrastColor;
          context.beginPath();
          context.moveTo(x, 0);
          context.lineTo(x, cesiumCanvas.height);
          context.stroke();
        }
  
        deferred.resolve(canvas.toDataURL("image/png"));
      } catch (e) {
        deferred.reject(e);
      }
    }, this);
  
    this.scene.render(this.terria.clock.currentTime);
  
    return deferred.promise;
  };
  
  /**
   * Notifies the viewer that a repaint is required.
   */
  Cesium.prototype.notifyRepaintRequired = function() {
    if (this.verboseRendering && !this.viewer.useDefaultRenderLoop) {
      console.log("starting rendering @ " + getTimestamp());
    }
    this._lastCameraMoveTime = getTimestamp();
    this.viewer.useDefaultRenderLoop = true;
  };
  
  /**
   * Computes the screen position of a given world position.
   * @param  {Cartesian3} position The world position in Earth-centered Fixed coordinates.
   * @param  {Cartesian2} [result] The instance to which to copy the result.
   * @return {Cartesian2} The screen position, or undefined if the position is not on the screen.
   */
  Cesium.prototype.computePositionOnScreen = function(position, result) {
    return SceneTransforms.wgs84ToWindowCoordinates(this.scene, position, result);
  };
  
  /**
   * Adds an attribution to the globe.
   * @param {Credit} attribution The attribution to add.
   */
  Cesium.prototype.addAttribution = function(attribution) {
    if (attribution) {
      this.scene.frameState.creditDisplay.addDefaultCredit(attribution);
    }
  };
  
  /**
   * Removes an attribution from the globe.
   * @param {Credit} attribution The attribution to remove.
   */
  Cesium.prototype.removeAttribution = function(attribution) {
    if (attribution) {
      this.scene.frameState.creditDisplay.removeDefaultCredit(attribution);
    }
  };
  
  /**
   * Gets all attribution currently active on the globe or map.
   * @returns {String[]} The list of current attributions, as HTML strings.
   */
  Cesium.prototype.getAllAttribution = function() {
    const credits = this.scene.frameState.creditDisplay._currentFrameCredits.screenCredits.values.concat(
      this.scene.frameState.creditDisplay._currentFrameCredits.lightboxCredits
        .values
    );
    return credits.map(credit => credit.html);
  };
  
  /**
   * Updates the order of layers, moving layers where {@link CatalogItem#keepOnTop} is true to the top.
   */
  Cesium.prototype.updateLayerOrderToKeepOnTop = function() {
    // move alwaysOnTop layers to the top
    var items = this.terria.nowViewing.items;
    var scene = this.scene;
    for (var l = items.length - 1; l >= 0; l--) {
      if (items[l].imageryLayer && items[l].keepOnTop) {
        scene.imageryLayers.raiseToTop(items[l].imageryLayer);
      }
    }
  };
  
  Cesium.prototype.updateLayerOrderAfterReorder = function() {
    // because this Cesium model does the reordering via raise and lower, no action needed.
  };
  
  // useful for counting the number of items in composite and non-composite items
  function countNumberOfSubItems(item) {
    if (defined(item.items)) {
      return item.items.length;
    } else {
      return 1;
    }
  }
  
  /**
   * Raise an item's level in the viewer
   * This does not check that index is valid
   * @param {Number} index The index of the item to raise
   */
  Cesium.prototype.raise = function(index) {
    var items = this.terria.nowViewing.items;
    var item = items[index];
    var itemAbove = items[index - 1];
  
    if (!defined(itemAbove.items) && !defined(itemAbove.imageryLayer)) {
      return;
    }
  
    // Both item and itemAbove may either have a single imageryLayer, or be a composite item
    // Composite items have an items array of further items.
    // Define n as the number of subitems in ItemAbove (1 except for composites)
    // if item is a composite, then raise each subitem in item n times,
    // starting with the one at the top - which is the last one in the list
    // if item is not a composite, just raise the item n times directly.
  
    var n = countNumberOfSubItems(itemAbove);
    var i, j, subItem;
  
    if (defined(item.items)) {
      for (i = item.items.length - 1; i >= 0; --i) {
        subItem = item.items[i];
        if (defined(subItem.imageryLayer)) {
          for (j = 0; j < n; ++j) {
            this.scene.imageryLayers.raise(subItem.imageryLayer);
          }
        }
      }
    }
  
    if (!defined(item.imageryLayer)) {
      return;
    }
  
    for (j = 0; j < n; ++j) {
      this.scene.imageryLayers.raise(item.imageryLayer);
    }
  };
  
  /**
   * Lower an item's level in the viewer
   * This does not check that index is valid
   * @param {Number} index The index of the item to lower
   */
  Cesium.prototype.lower = function(index) {
    var items = this.terria.nowViewing.items;
    var item = items[index];
    var itemBelow = items[index + 1];
    if (!defined(itemBelow.items) && !defined(itemBelow.imageryLayer)) {
      return;
    }
  
    // same considerations as above, but lower composite subitems starting at the other end of the list
  
    var n = countNumberOfSubItems(itemBelow);
    var i, j, subItem;
  
    if (defined(item.items)) {
      for (i = 0; i < item.items.length; ++i) {
        subItem = item.items[i];
        if (defined(subItem.imageryLayer)) {
          for (j = 0; j < n; ++j) {
            this.scene.imageryLayers.lower(subItem.imageryLayer);
          }
        }
      }
    }
  
    if (!defined(item.imageryLayer)) {
      return;
    }
  
    for (j = 0; j < n; ++j) {
      this.scene.imageryLayers.lower(item.imageryLayer);
    }
  };
  
  /**
   * Lowers this imagery layer to the bottom, underneath all other layers.  If this item is not enabled or not shown,
   * this method does nothing.
   * @param {CatalogItem} item The item to lower to the bottom (usually a basemap)
   */
  Cesium.prototype.lowerToBottom = function(item) {
    if (defined(item.items)) {
      // the front item is at the end of the list.
      // so to preserve order of any subitems, send any subitems to the bottom in order from the front
      for (var i = item.items.length - 1; i >= 0; --i) {
        var subItem = item.items[i];
        this.lowerToBottom(subItem); // recursive
      }
    }
  
    if (!defined(item._imageryLayer)) {
      return;
    }
  
    this.terria.cesium.scene.imageryLayers.lowerToBottom(item._imageryLayer);
  };
  
  Cesium.prototype.adjustDisclaimer = function() {};
  
  /**
   * Picks features based off a latitude, longitude and (optionally) height.
   * @param {Object} latlng The position on the earth to pick.
   * @param {Object} imageryLayerCoords A map of imagery provider urls to the coords used to get features for those imagery
   *     providers - i.e. x, y, level
   * @param existingFeatures An optional list of existing features to concatenate the ones found from asynchronous picking to.
   */
  Cesium.prototype.pickFromLocation = function(
    latlng,
    imageryLayerCoords,
    existingFeatures
  ) {
    var pickPosition = this.scene.globe.ellipsoid.cartographicToCartesian(
      Cartographic.fromDegrees(latlng.lng, latlng.lat, latlng.height)
    );
    var pickPositionCartographic = Ellipsoid.WGS84.cartesianToCartographic(
      pickPosition
    );
  
    var promises = [];
    var imageryLayers = [];
    for (var i = this.scene.imageryLayers.length - 1; i >= 0; i--) {
      var imageryLayer = this.scene.imageryLayers.get(i);
      var imageryProvider = imageryLayer._imageryProvider;
  
      if (imageryProvider.url && imageryLayerCoords[imageryProvider.url]) {
        var coords = imageryLayerCoords[imageryProvider.url];
        promises.push(
          imageryProvider.pickFeatures(
            coords.x,
            coords.y,
            coords.level,
            pickPositionCartographic.longitude,
            pickPositionCartographic.latitude
          )
        );
        imageryLayers.push(imageryLayer);
      }
    }
  
    var result = this._buildPickedFeatures(
      imageryLayerCoords,
      pickPosition,
      existingFeatures,
      promises,
      imageryLayers,
      pickPositionCartographic.height
    );
  
    var mapInteractionModeStack = this.terria.mapInteractionModeStack;
    if (defined(mapInteractionModeStack) && mapInteractionModeStack.length > 0) {
      mapInteractionModeStack[
        mapInteractionModeStack.length - 1
      ].pickedFeatures = result;
    } else {
      this.terria.pickedFeatures = result;
    }
  };
  
  /**
   * Picks features based on coordinates relative to the Cesium window. Will draw a ray from the camera through the point
   * specified and set terria.pickedFeatures based on this.
   *
   * @param {Cartesian3} screenPosition The position on the screen.
   */
  Cesium.prototype.pickFromScreenPosition = function(screenPosition) {
    var pickRay = this.scene.camera.getPickRay(screenPosition);
    var pickPosition = this.scene.globe.pick(pickRay, this.scene);
    var pickPositionCartographic = Ellipsoid.WGS84.cartesianToCartographic(
      pickPosition
    );
  
    var vectorFeatures = this.pickVectorFeatures(screenPosition);
  
    var providerCoords = this._attachProviderCoordHooks();
    var pickRasterPromise = this.scene.imageryLayers.pickImageryLayerFeatures(
      pickRay,
      this.scene
    );
  
    var result = this._buildPickedFeatures(
      providerCoords,
      pickPosition,
      vectorFeatures,
      [pickRasterPromise],
      undefined,
      pickPositionCartographic.height
    );
    var mapInteractionModeStack = this.terria.mapInteractionModeStack;
    if (defined(mapInteractionModeStack) && mapInteractionModeStack.length > 0) {
      mapInteractionModeStack[
        mapInteractionModeStack.length - 1
      ].pickedFeatures = result;
    } else {
      this.terria.pickedFeatures = result;
    }
  };
  
  /**
   * Picks all *vector* features (e.g. GeoJSON) shown at a certain position on the screen, ignoring raster features
   * (e.g. WFS). Because all vector features are already in memory, this is synchronous.
   *
   * @param {Cartesian2} screenPosition position on the screen to look for features
   * @returns {Feature[]} The features found.
   */
  Cesium.prototype.pickVectorFeatures = function(screenPosition) {
    // Pick vector features
    var vectorFeatures = [];
    var pickedList = this.scene.drillPick(screenPosition);
    for (var i = 0; i < pickedList.length; ++i) {
      var picked = pickedList[i];
      var id = picked.id;
  
      if (
        id &&
        id.entityCollection &&
        id.entityCollection.owner &&
        id.entityCollection.owner.name === GlobeOrMap._featureHighlightName
      ) {
        continue;
      }
  
      if (!defined(id) && defined(picked.primitive)) {
        id = picked.primitive.id;
      }
      if (id instanceof Entity && vectorFeatures.indexOf(id) === -1) {
        var feature = Feature.fromEntityCollectionOrEntity(id);
        vectorFeatures.push(feature);
      } else if (
        picked.primitive &&
        picked.primitive._catalogItem &&
        picked.primitive._catalogItem.getFeaturesFromPickResult
      ) {
        var result = picked.primitive._catalogItem.getFeaturesFromPickResult(
          screenPosition,
          picked
        );
        if (result) {
          if (Array.isArray(result)) {
            vectorFeatures.push(...result);
          } else {
            vectorFeatures.push(result);
          }
        }
      }
    }
  
    return vectorFeatures;
  };
  
  /**
   * Hooks into the {@link ImageryProvider#pickFeatures} method of every imagery provider in the scene - when this method is
   * evaluated (usually as part of feature picking), it will record the tile coordinates used against the url of the
   * imagery provider in an object that is returned by this method. Hooks are removed immediately after being executed once.
   *
   * @returns {{x, y, level}} A map of urls to the coords used by the imagery provider when picking features. Will
   *     initially be empty but will be updated as the hooks are evaluated.
   * @private
   */
  Cesium.prototype._attachProviderCoordHooks = function() {
    var providerCoords = {};
  
    var pickFeaturesHook = function(
      imageryProvider,
      oldPick,
      x,
      y,
      level,
      longitude,
      latitude
    ) {
      var featuresPromise = oldPick.call(
        imageryProvider,
        x,
        y,
        level,
        longitude,
        latitude
      );
  
      // Use url to uniquely identify providers because what else can we do?
      if (imageryProvider.url) {
        providerCoords[imageryProvider.url] = {
          x: x,
          y: y,
          level: level
        };
      }
  
      imageryProvider.pickFeatures = oldPick;
      return featuresPromise;
    };
  
    for (var j = 0; j < this.scene.imageryLayers.length; j++) {
      var imageryProvider = this.scene.imageryLayers.get(j).imageryProvider;
      imageryProvider.pickFeatures = pickFeaturesHook.bind(
        undefined,
        imageryProvider,
        imageryProvider.pickFeatures
      );
    }
  
    return providerCoords;
  };
  
  /**
   * Builds a {@link PickedFeatures} object from a number of inputs.
   *
   * @param {{x, y, level}} providerCoords A map of imagery provider urls to the coords used to get features for that provider.
   * @param {Cartesian3} pickPosition The position in the 3D model that has been picked.
   * @param {Entity[]} existingFeatures Existing features - the results of feature promises will be appended to this.
   * @param {Promise[]} featurePromises Zero or more promises that each resolve to a list of {@link ImageryLayerFeatureInfo}s
   *     (usually there will be one promise per ImageryLayer. These will be combined as part of
   *     {@link PickedFeatures#allFeaturesAvailablePromise} and their results used to build the final
   *     {@link PickedFeatures#features} array.
   * @param {ImageryLayer[]} imageryLayers An array of ImageryLayers that should line up with the one passed as featurePromises.
   * @param {number} defaultHeight The height to use for feature position heights if none is available when picking.
   * @returns {PickedFeatures} A {@link PickedFeatures} object that is a combination of everything passed.
   * @private
   */
  Cesium.prototype._buildPickedFeatures = function(
    providerCoords,
    pickPosition,
    existingFeatures,
    featurePromises,
    imageryLayers,
    defaultHeight
  ) {
    var result = new PickedFeatures();
  
    result.providerCoords = providerCoords;
    result.pickPosition = pickPosition;
  
    result.allFeaturesAvailablePromise = when
      .all(featurePromises)
      .then(
        function(allFeatures) {
          result.isLoading = false;
  
          result.features = allFeatures.reduce(
            function(resultFeaturesSoFar, imageryLayerFeatures, i) {
              if (!defined(imageryLayerFeatures)) {
                return resultFeaturesSoFar;
              }
  
              var features = imageryLayerFeatures.map(
                function(feature) {
                  if (defined(imageryLayers)) {
                    feature.imageryLayer = imageryLayers[i];
                  }
  
                  if (!defined(feature.position)) {
                    feature.position = Ellipsoid.WGS84.cartesianToCartographic(
                      pickPosition
                    );
                  }
  
                  // If the picked feature does not have a height, use the height of the picked location.
                  // This at least avoids major parallax effects on the selection indicator.
                  if (
                    !defined(feature.position.height) ||
                    feature.position.height === 0.0
                  ) {
                    feature.position.height = defaultHeight;
                  }
                  return this._createFeatureFromImageryLayerFeature(feature);
                }.bind(this)
              );
  
              if (this.terria.showSplitter) {
                // Select only features from the same side or both sides of the splitter
                var screenPosition = this.computePositionOnScreen(
                  result.pickPosition
                );
                var pickedSide = this.terria.getSplitterSideForScreenPosition(
                  screenPosition
                );
  
                features = features.filter(function(feature) {
                  var splitDirection = feature.imageryLayer.splitDirection;
                  return (
                    splitDirection === pickedSide ||
                    splitDirection === ImagerySplitDirection.NONE
                  );
                });
              }
  
              return resultFeaturesSoFar.concat(features);
            }.bind(this),
            defaultValue(existingFeatures, [])
          );
        }.bind(this)
      )
      .otherwise(function() {
        result.isLoading = false;
        result.error = "An unknown error occurred while picking features.";
      });
  
    return result;
  };
  
  /**
   * Returns a new layer using a provided ImageryProvider, and adds it to the scene.
   * Note the optional parameters are a superset of the Leaflet version of this function, with one deletion (onProjectionError).
   *
   * @param {Object} options Options
   * @param {ImageryProvider} options.imageryProvider The imagery provider to create a new layer for.
   * @param {Number} [layerIndex] The index to add the layer at.  If omitted, the layer will added on top of all existing layers.
   * @param {Rectangle} [options.rectangle=imageryProvider.rectangle] The rectangle of the layer.  This rectangle
   *        can limit the visible portion of the imagery provider.
   * @param {Number|Function} [options.opacity=1.0] The alpha blending value of this layer, from 0.0 to 1.0.
   *                          This can either be a simple number or a function with the signature
   *                          <code>function(frameState, layer, x, y, level)</code>.  The function is passed the
   *                          current frame state, this layer, and the x, y, and level coordinates of the
   *                          imagery tile for which the alpha is required, and it is expected to return
   *                          the alpha value to use for the tile.
   * @param {Boolean} [options.clipToRectangle]
   * @param {Boolean} [options.treat403AsError]
   * @param {Boolean} [options.treat403AsError]
   * @param {Boolean} [options.ignoreUnknownTileErrors]
   * @param {Function} [options.onLoadError]
   * @returns {ImageryLayer} The newly created layer.
   */
  Cesium.prototype.addImageryProvider = function(options) {
    var scene = this.scene;
  
    var errorEvent = options.imageryProvider.errorEvent;
  
    if (defined(errorEvent)) {
      errorEvent.addEventListener(options.onLoadError);
    }
  
    var result = new ImageryLayer(options.imageryProvider, {
      show: false,
      alpha: options.opacity,
      rectangle: options.clipToRectangle ? options.rectangle : undefined,
      isRequired: options.isRequiredForRendering // TODO: This doesn't seem to be a valid option for ImageryLayer - remove (and upstream)?
    });
  
    // layerIndex is an optional parameter used when the imageryLayer corresponds to a CsvCatalogItem whose selected item has just changed
    // to ensure that the layer is re-added in the correct position
    scene.imageryLayers.add(result, options.layerIndex);
  
    this.updateLayerOrderToKeepOnTop();
  
    return result;
  };
  
  Cesium.prototype.removeImageryLayer = function(options) {
    var scene = this.scene;
    scene.imageryLayers.remove(options.layer);
  };
  
  Cesium.prototype.showImageryLayer = function(options) {
    options.layer.show = true;
  };
  
  Cesium.prototype.hideImageryLayer = function(options) {
    options.layer.show = false;
  };
  
  Cesium.prototype.isImageryLayerShown = function(options) {
    return options.layer.show;
  };
  
  Cesium.prototype.updateItemForSplitter = function(item) {
    if (!defined(item.splitDirection) || !defined(item.imageryLayer)) {
      return;
    }
  
    const terria = item.terria;
  
    if (terria.showSplitter) {
      item.imageryLayer.splitDirection = item.splitDirection;
    } else {
      item.imageryLayer.splitDirection = ImagerySplitDirection.NONE;
    }
  
    // Also update the next layer, if any.
    if (item._nextLayer) {
      item._nextLayer.splitDirection = item.imageryLayer.splitDirection;
    }
  
    this.notifyRepaintRequired();
  };
  
  Cesium.prototype.pauseMapInteraction = function() {
    ++this._pauseMapInteractionCount;
    if (this._pauseMapInteractionCount === 1) {
      this.scene.screenSpaceCameraController.enableInputs = false;
    }
  };
  
  Cesium.prototype.resumeMapInteraction = function() {
    --this._pauseMapInteractionCount;
    if (this._pauseMapInteractionCount === 0) {
      setTimeout(() => {
        if (this._pauseMapInteractionCount === 0) {
          this.scene.screenSpaceCameraController.enableInputs = true;
        }
      }, 0);
    }
  };
  
  function postRender(cesium, date) {
    // We can safely stop rendering when:
    //  - the camera position hasn't changed in over a second,
    //  - there are no tiles waiting to load, and
    //  - the clock is not animating
    //  - there are no tweens in progress
  
    var now = getTimestamp();
  
    var scene = cesium.scene;
  
    if (
      !Matrix4.equalsEpsilon(
        cesium._lastCameraViewMatrix,
        scene.camera.viewMatrix,
        1e-5
      )
    ) {
      cesium._lastCameraMoveTime = now;
    }
  
    var cameraMovedInLastSecond = now - cesium._lastCameraMoveTime < 1000;
  
    var surface = scene.globe._surface;
    var tilesWaiting =
      !surface._tileProvider.ready ||
      surface._tileLoadQueueHigh.length > 0 ||
      surface._tileLoadQueueMedium.length > 0 ||
      surface._tileLoadQueueLow.length > 0 ||
      surface._debug.tilesWaitingForChildren > 0;
  
    if (
      !cameraMovedInLastSecond &&
      !tilesWaiting &&
      !cesium.viewer.clock.shouldAnimate &&
      cesium.scene.tweens.length === 0
    ) {
      if (cesium.verboseRendering) {
        console.log("stopping rendering @ " + getTimestamp());
      }
      cesium.viewer.useDefaultRenderLoop = false;
      cesium.stoppedRendering = true;
    }
  
    Matrix4.clone(scene.camera.viewMatrix, cesium._lastCameraViewMatrix);
  
    var feature = cesium.terria.selectedFeature;
    if (defined(feature)) {
      var position;
  
      if (defined(cesium.dataSourceDisplay)) {
        var originalEntity = defined(feature.cesiumEntity)
          ? feature.cesiumEntity
          : feature;
        var state = cesium.dataSourceDisplay.getBoundingSphere(
          originalEntity,
          true,
          boundingSphereScratch
        );
  
        if (state === BoundingSphereState.DONE) {
          position = Cartesian3.clone(boundingSphereScratch.center);
        }
      }
  
      if (!defined(position) && defined(feature.position)) {
        position = feature.position.getValue(cesium.terria.clock.currentTime);
      }
  
      if (defined(position)) {
        cesium._selectionIndicator.position = position;
      }
    }
  
    cesium._selectionIndicator.update();
  }
  
  function selectFeature(cesium) {
    var feature = cesium.terria.selectedFeature;
  
    cesium._highlightFeature(feature);
  
    if (defined(feature) && defined(feature.position)) {
      cesium._selectionIndicator.position = feature.position.getValue(
        cesium.terria.clock.currentTime
      );
      cesium._selectionIndicator.animateAppear();
    } else {
      cesium._selectionIndicator.animateDepart();
    }
  
    cesium._selectionIndicator.update();
  }
  
  module.exports = Cesium;
  