ViewModels/TerriaViewer.js

/*
 *   A collection of additional viewer functionality independent
 *   of datasets
 */

"use strict";

/*global require,console*/
var BingMapsApi = require("terriajs-cesium/Source/Core/BingMapsApi").default;
var Cartesian3 = require("terriajs-cesium/Source/Core/Cartesian3").default;
//var Cesium = require('../Models/Cesium');
var CesiumMath = require("terriajs-cesium/Source/Core/Math").default;
var cesiumRequestAnimationFrame = require("terriajs-cesium/Source/Core/requestAnimationFrame")
  .default;
var clone = require("terriajs-cesium/Source/Core/clone").default;
var CesiumTerrainProvider = require("terriajs-cesium/Source/Core/CesiumTerrainProvider")
  .default;
//var CesiumWidget = require('terriajs-cesium/Source/Widgets/CesiumWidget/CesiumWidget');
var createCredit = require("../Map/createCredit");
var createWorldTerrain = require("terriajs-cesium/Source/Core/createWorldTerrain")
  .default;
var Credit = require("terriajs-cesium/Source/Core/Credit").default;
var CreditDisplay = require("terriajs-cesium/Source/Scene/CreditDisplay")
  .default;
var DrawExtentHelper = require("../Map/DrawExtentHelper");
var defaultValue = require("terriajs-cesium/Source/Core/defaultValue").default;
var defined = require("terriajs-cesium/Source/Core/defined").default;
var Ellipsoid = require("terriajs-cesium/Source/Core/Ellipsoid").default;
var EllipsoidTerrainProvider = require("terriajs-cesium/Source/Core/EllipsoidTerrainProvider")
  .default;
var EventHelper = require("terriajs-cesium/Source/Core/EventHelper").default;
var FeatureDetection = require("terriajs-cesium/Source/Core/FeatureDetection")
  .default;
var FrameRateMonitor = require("terriajs-cesium/Source/Scene/FrameRateMonitor")
  .default;
var getElement = require("terriajs-cesium/Source/Widgets/getElement").default;
var Ion = require("terriajs-cesium/Source/Core/Ion").default;
var KeyboardEventModifier = require("terriajs-cesium/Source/Core/KeyboardEventModifier")
  .default;
var knockout = require("terriajs-cesium/Source/ThirdParty/knockout").default;
var L = require("leaflet");
var Leaflet = require("../Models/Leaflet");
var LeafletDataSourceDisplay = require("../Map/LeafletDataSourceDisplay");
var LeafletVisualizer = require("../Map/LeafletVisualizer");
var Matrix3 = require("terriajs-cesium/Source/Core/Matrix3").default;
var Matrix4 = require("terriajs-cesium/Source/Core/Matrix4").default;
var runLater = require("../Core/runLater");
var ScreenSpaceEventType = require("terriajs-cesium/Source/Core/ScreenSpaceEventType")
  .default;
var SingleTileImageryProvider = require("terriajs-cesium/Source/Scene/SingleTileImageryProvider")
  .default;
var supportsWebGL = require("../Core/supportsWebGL");
var Transforms = require("terriajs-cesium/Source/Core/Transforms").default;
var Tween = require("terriajs-cesium/Source/ThirdParty/Tween").default;
var URI = require("urijs");
var ViewerMode = require("../Models/ViewerMode");
var NoViewer = require("../Models/NoViewer");
var when = require("terriajs-cesium/Source/ThirdParty/when").default;
import i18next from "i18next";

//use our own bing maps key
BingMapsApi.defaultKey = undefined;

// Make Cesium think pixelated image rendering is supported, always.
// As a result, Cesium will honor the devicePixelRatio even in browsers (IE) that don't
// support pixelated rendering.  This means the imagery may look slightly blurrier than
// in other browers, but that's better than rendering 4x the pixels in an already
// slow browser!
FeatureDetection.supportsImageRenderingPixelated = function() {
  return true;
};

// Don't let Cesium automatically add its logo. We'll do so manually when necessary.
CreditDisplay._cesiumCreditInitialized = true;

/**
 * The Terria map viewer, utilizing Cesium and Leaflet.
 * @param {Terria} terria The Terria instance.
 * @param {Object} options Object with the following properties:
 * @param {Object} [options.developerAttribution] Attribution for the map developer, displayed at the bottom of the map.  This is an
 *                 object with two properties, `text` and `link`.  `link` is optional and is the URL to open when the user
 *                 clicks on the attribution.
 * @param {String|TerrainProvider} [options.terrain] The terrain to use in the 3D view.  This may be a string, in which case it is loaded using
 *                                 `CesiumTerrainProvider`, or it may be a `TerrainProvider`.  If this property is undefined, STK World Terrain
 *                                 is used.
 * @param {Integer} [options.maximumLeafletZoomLevel] The maximum level to which to allow Leaflet to zoom to.
                    If this property is undefined, Leaflet defaults to level 18.
 */
var TerriaViewer = function(terria, options) {
  options = defaultValue(options, {});

  this._mapContainer = defaultValue(options.mapContainer, "cesiumContainer");
  this._uiContainer = defaultValue(options.uiContainer, "ui");
  this._developerAttribution = options.developerAttribution;
  this.maximumLeafletZoomLevel = options.maximumLeafletZoomLevel;

  var webGLSupport = supportsWebGL(); // true, false, or 'slow'
  this._slowWebGLAvailable = webGLSupport === "slow";
  this._useWebGL = webGLSupport === true;

  if (
    (terria.viewerMode === ViewerMode.CesiumTerrain ||
      terria.viewerMode === ViewerMode.CesiumEllipsoid) &&
    !this._useWebGL
  ) {
    if (this._slowWebGLAvailable) {
      terria.error.raiseEvent({
        title: i18next.t("terriaViewer.slowWebGLAvailableTitle"),
        message: i18next.t("terriaViewer.slowWebGLAvailableMessage", {
          appName: terria.appName
        })
      });
    } else {
      terria.error.raiseEvent({
        title: i18next.t("terriaViewer.slowWebGLAvailableTitle"),
        message: i18next.t("terriaViewer.slowWebGLAvailableMessageII", {
          appName: terria.appName,
          chrome:
            '<a href="http://www.google.com/chrome" target="_blank">' +
            i18next.t("models.terriaViewer.chrome") +
            "</a>",
          firefox:
            '<a href="http://www.mozilla.org/firefox" target="_blank">' +
            i18next.t("models.terriaViewer.firefox") +
            "</a>",
          safari:
            '<a href="https://www.apple.com/au/osx/how-to-upgrade/" target="_blank">' +
            i18next.t("models.terriaViewer.safari") +
            "</a>",
          internetExplorer:
            '<a href="http://www.microsoft.com/ie" target="_blank">' +
            i18next.t("models.terriaViewer.internetExplorer") +
            "</a>"
        })
      });
    }
    terria.viewerMode = ViewerMode.Leaflet;
  }

  //TODO: perf test to set environment

  this.terria = terria;

  var useCesium = terria.viewerMode !== ViewerMode.Leaflet;
  terria.analytics.logEvent(
    "startup",
    "initialViewer",
    useCesium ? "cesium" : "leaflet"
  );

  /** A terrain provider used when ViewerMode === ViewerMode.CesiumTerrain */
  this._bumpyTerrainProvider = undefined;

  /** The terrain provider currently being used - can be either a bumpy terrain provider or a smooth EllipsoidTerrainProvider */
  this._terrain = options.terrain;
  this._terrainProvider = undefined;

  if (defined(this._terrain) && this._terrain.length === 0) {
    this._terrain = undefined;
  }

  if (this._useWebGL) {
    initializeTerrainProvider(this);
  }

  this.selectViewer(useCesium);
  this.observeSubscriptions = [];

  this.observeSubscriptions.push(
    knockout.getObservable(this.terria, "viewerMode").subscribe(function() {
      changeViewer(this);
    }, this)
  );

  this._previousBaseMap = this.terria.baseMap;
  this.observeSubscriptions.push(
    knockout.getObservable(this.terria, "baseMap").subscribe(function() {
      changeBaseMap(this, this.terria.baseMap);
    }, this)
  );

  this.observeSubscriptions.push(
    knockout.getObservable(this.terria, "fogSettings").subscribe(function() {
      changeFogSettings(this);
    }, this)
  );

  this.observeSubscriptions.push(
    knockout.getObservable(this.terria, "selectBox").subscribe(function() {
      changeSelectBox(this);
    }, this)
  );

  this.observeSubscriptions.push(
    knockout
      .getObservable(this.terria, "baseMaximumScreenSpaceError")
      .subscribe(function() {
        changeMaximumScreenSpaceError(this);
      }, this)
  );

  this.observeSubscriptions.push(
    knockout
      .getObservable(this.terria, "useNativeResolution")
      .subscribe(function() {
        changeUseNativeResolution(this);
      }, this)
  );
};

TerriaViewer.create = function(terria, options) {
  return new TerriaViewer(terria, options);
};

function changeSelectBox(viewer) {
  var terria = viewer.terria;
  var selectBox = terria.selectBox;

  if (terria.viewerMode === ViewerMode.Leaflet) {
    if (selectBox) {
      terria.leaflet.map.dragBox.enable();
    } else {
      terria.leaflet.map.dragBox.disable();
    }
  } else {
    if (selectBox) {
      // Add and start a DrawExtentHelper - used by mapInteractionMode with drawRectangle
      viewer._enableSelectExtent(terria.cesium.viewer.scene, false);
      viewer.dragBox = new DrawExtentHelper(
        terria,
        terria.cesium.viewer.scene,
        function(ext) {
          var mapInteractionModeStack = this.terria.mapInteractionModeStack;
          if (
            defined(mapInteractionModeStack) &&
            mapInteractionModeStack.length > 0
          ) {
            if (
              mapInteractionModeStack[mapInteractionModeStack.length - 1]
                .drawRectangle
            ) {
              mapInteractionModeStack[
                mapInteractionModeStack.length - 1
              ].pickedFeatures = clone(ext, true);
            }
          }
        }.bind(viewer),
        KeyboardEventModifier.SHIFT
      );
      viewer.dragBox.start();
    } else {
      viewer.dragBox.destroy();
      viewer._enableSelectExtent(terria.cesium.viewer.scene, true);
    }
  }
}

function changeViewer(viewer) {
  var terria = viewer.terria;
  var newMode = terria.viewerMode;

  if (newMode === ViewerMode.Leaflet) {
    if (!terria.leaflet) {
      terria.analytics.logEvent("mapSettings", "switchViewer", "2D");
      viewer.selectViewer(false);
    }
  } else {
    if (!viewer._useWebGL) {
      terria.error.raiseEvent({
        title: i18next.t("terriaViewer.webGLNotSupportedOrSlowTitle"),
        message: i18next.t("terriaViewer.webGLNotSupportedOrSlowMessage", {
          appName: terria.appName,
          chrome:
            '<a href="http://www.google.com/chrome" target="_blank">' +
            i18next.t("models.terriaViewer.chrome") +
            "</a>",
          firefox:
            '<a href="http://www.mozilla.org/firefox" target="_blank">' +
            i18next.t("models.terriaViewer.firefox") +
            "</a>",
          safari:
            '<a href="https://www.apple.com/au/osx/how-to-upgrade/" target="_blank">' +
            i18next.t("models.terriaViewer.safari") +
            "</a>",
          internetExplorer:
            '<a href="http://www.microsoft.com/ie" target="_blank">' +
            i18next.t("models.terriaViewer.internetExplorer") +
            "</a>"
        })
      });

      terria.viewerMode = ViewerMode.Leaflet;
    } else {
      if (newMode === ViewerMode.CesiumTerrain) {
        terria.analytics.logEvent("mapSettings", "switchViewer", "3D");

        if (defined(terria.leaflet)) {
          viewer.selectViewer(true);
        } else {
          terria.cesium.scene.globe.terrainProvider =
            viewer._bumpyTerrainProvider;
          CreditDisplay._cesiumCredit = viewer._bumpyTerrainCredit;
        }
      } else if (newMode === ViewerMode.CesiumEllipsoid) {
        terria.analytics.logEvent("mapSettings", "switchViewer", "Smooth 3D");

        if (defined(terria.leaflet)) {
          viewer.selectViewer(true);
        } else {
          terria.cesium.scene.globe.terrainProvider = new EllipsoidTerrainProvider();
          CreditDisplay._cesiumCredit = undefined;
        }
      }
    }
  }
}

function changeBaseMap(viewer, newBaseMap) {
  if (defined(viewer._previousBaseMap)) {
    viewer._previousBaseMap._hide();
    viewer._previousBaseMap._disable();
  }

  if (defined(newBaseMap)) {
    when(newBaseMap.load()).then(function() {
      newBaseMap._enable();
      newBaseMap._show();
      if (defined(viewer.terria.currentViewer)) {
        viewer.terria.currentViewer.lowerToBottom(newBaseMap);
      }
    });
  }

  viewer._previousBaseMap = newBaseMap;

  if (defined(viewer.terria.currentViewer)) {
    viewer.terria.currentViewer.notifyRepaintRequired();
  }
}

function changeFogSettings(viewer) {
  if (defined(viewer.terria.fogSettings) && defined(viewer.terria.cesium)) {
    var fogSettings = viewer.terria.fogSettings;
    for (var settingKey in fogSettings) {
      if (viewer.terria.cesium.scene.fog.hasOwnProperty(settingKey)) {
        viewer.terria.cesium.scene.fog[settingKey] = fogSettings[settingKey];
      }
    }
  }
}

function changeUseNativeResolution(viewer) {
  if (defined(viewer.terria.cesium)) {
    const terria = viewer.terria;
    const useNativeResolution = terria.useNativeResolution;
    terria.cesium.viewer.useBrowserRecommendedResolution = !useNativeResolution;
    terria.currentViewer.notifyRepaintRequired();
  }
}

function changeMaximumScreenSpaceError(viewer) {
  if (defined(viewer.terria.cesium)) {
    const terria = viewer.terria;
    terria.cesium.viewer.scene.globe.maximumScreenSpaceError =
      viewer.terria.baseMaximumScreenSpaceError;

    terria.nowViewing.items
      .filter(item => item.type === "3d-tiles")
      .forEach(
        item =>
          item.syncMaximumScreenSpaceError && item.syncMaximumScreenSpaceError()
      );
  }
}

// -------------------------------------------
// Region Selection
// -------------------------------------------
TerriaViewer.prototype._enableSelectExtent = function(scene, bActive) {
  if (bActive) {
    var that = this;
    this.regionSelect = new DrawExtentHelper(that.terria, scene, function(ext) {
      if (ext) {
        that.terria.currentViewer.zoomTo(ext, 2.0);
      }
    });
    this.regionSelect.start();
  } else {
    this.regionSelect.destroy();
  }
};

TerriaViewer.prototype._createCesiumViewer = function(container, CesiumWidget) {
  var that = this;

  initializeTerrainProvider(this);
  var terrainProvider = that._terrainProvider;

  //An arbitrary base64 encoded image used to populate the placeholder SingleTileImageryProvider
  var img =
    "data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA \
AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO \
9TXL0Y4OHwAAAABJRU5ErkJggg==";

  var options = {
    dataSources: this.terria.dataSources,
    clock: this.terria.clock,
    terrainProvider: terrainProvider,
    imageryProvider: new SingleTileImageryProvider({ url: img }),
    scene3DOnly: true,
    useBrowserRecommendedResolution: !this.terria.useNativeResolution
  };

  // Workaround for Firefox bug with WebGL and printing:
  // https://bugzilla.mozilla.org/show_bug.cgi?id=976173
  if (FeatureDetection.isFirefox()) {
    options.contextOptions = { webgl: { preserveDrawingBuffer: true } };
  }

  //create CesiumViewer
  var viewer = new CesiumWidget(container, options);

  // Disable HDR lighting for better performance and to avoid changing imagery colors.
  viewer.scene.highDynamicRange = false;

  viewer.scene.imageryLayers.removeAll();

  //catch Cesium terrain provider down and switch to Ellipsoid
  terrainProvider.errorEvent.addEventListener(function(err) {
    console.log("Terrain provider error.  ", err.message);
    if (viewer.scene.terrainProvider instanceof CesiumTerrainProvider) {
      console.log("Switching to EllipsoidTerrainProvider.");
      that.terria.viewerMode = ViewerMode.CesiumEllipsoid;
      if (!defined(that.TerrainMessageViewed)) {
        that.terria.error.raiseEvent({
          title: "Terrain Server Not Responding",
          message:
            "\
The terrain server is not responding at the moment.  You can still use all the features of " +
            that.terria.appName +
            " \
but there will be no terrain detail in 3D mode.  We're sorry for the inconvenience.  Please try \
again later and the terrain server should be responding as expected.  If the issue persists, please contact \
us via email at " +
            that.terria.supportEmail +
            "."
        });
        that.TerrainMessageViewed = true;
      }
    }
  });

  if (defined(this._defaultTerriaCredit)) {
    var containerElement = getElement(container);
    var creditsElement =
      containerElement &&
      containerElement.getElementsByClassName("cesium-widget-credits")[0];
    var logoContainer =
      creditsElement &&
      creditsElement.getElementsByClassName("cesium-credit-logoContainer")[0];
    if (logoContainer) {
      creditsElement.insertBefore(
        this._defaultTerriaCredit.element,
        logoContainer
      );
    }
  }

  var scene = viewer.scene;

  scene.globe.depthTestAgainstTerrain = false;

  var d = this._getDisclaimer();
  if (d) {
    scene.frameState.creditDisplay.addDefaultCredit(d);
  }

  if (defined(this._developerAttribution)) {
    scene.frameState.creditDisplay.addDefaultCredit(
      createCredit(
        this._developerAttribution.text,
        this._developerAttribution.link
      )
    );
  }

  scene.frameState.creditDisplay.addDefaultCredit(
    new Credit(
      '<a href="http://cesiumjs.org" target="_blank" rel="noopener noreferrer">CESIUM</a>'
    )
  );

  var inputHandler = viewer.screenSpaceEventHandler;

  // Add double click zoom
  inputHandler.setInputAction(function(movement) {
    zoomIn(scene, movement.position);
  }, ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
  inputHandler.setInputAction(
    function(movement) {
      zoomOut(scene, movement.position);
    },
    ScreenSpaceEventType.LEFT_DOUBLE_CLICK,
    KeyboardEventModifier.SHIFT
  );

  return viewer;
};

TerriaViewer.prototype.destroy = function() {
  this.terria.beforeViewerChanged.raiseEvent();

  changeBaseMap(this, undefined);

  this.observeSubscriptions.forEach(subscription => subscription.dispose());

  if (defined(this.terria.cesium)) {
    this.destroyCesium();
  } else if (defined(this.terria.leaflet)) {
    //
    this.destroyLeaflet();
  }

  this.terria.currentViewer = new NoViewer(this.terria);
  this.terria.afterViewerChanged.raiseEvent();
};

TerriaViewer.prototype.destroyCesium = function() {
  const viewer = this.terria.cesium.viewer;

  this.terria.cesium.destroy();

  if (this.cesiumEventHelper) {
    this.cesiumEventHelper.removeAll();
    this.cesiumEventHelper = undefined;
  }

  this.dataSourceDisplay.destroy(); //

  this._enableSelectExtent(viewer.scene, false);

  var inputHandler = viewer.screenSpaceEventHandler;
  inputHandler.removeInputAction(ScreenSpaceEventType.MOUSE_MOVE);
  inputHandler.removeInputAction(ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
  inputHandler.removeInputAction(
    ScreenSpaceEventType.LEFT_DOUBLE_CLICK,
    KeyboardEventModifier.SHIFT
  );

  if (defined(this.monitor)) {
    this.monitor.destroy();
    this.monitor = undefined;
  }
  viewer.destroy();
  this.terria.cesium = undefined;
};

TerriaViewer.prototype.destroyLeaflet = function() {
  const map = this.terria.leaflet.map;
  this.terria.leaflet.destroy();

  if (this.leafletEventHelper) {
    this.leafletEventHelper.removeAll();
    this.leafletEventHelper = undefined;
  }

  this.dataSourceDisplay.destroy();
  map.remove();
  this.terria.leaflet = undefined;
};

TerriaViewer.prototype.selectViewer = function(cesium) {
  changeBaseMap(this, undefined);

  this.terria.beforeViewerChanged.raiseEvent();

  var that = this;

  var createViewerPromise = cesium ? this.selectCesium() : this.selectLeaflet();
  createViewerPromise.then(function() {
    that.terria.afterViewerChanged.raiseEvent();
    changeBaseMap(that, that.terria.baseMap);
  });
};

TerriaViewer.prototype.selectLeaflet = function() {
  var map, rect, eventHelper;

  //shut down existing cesium
  if (defined(this.terria.cesium)) {
    //get camera and timeline settings
    try {
      rect = this.terria.cesium.getCurrentExtent();
    } catch (e) {
      console.log("Using default screen extent", e.message);
      rect = this.terria.initialView;
    }

    this.destroyCesium();
  } else {
    rect = this.terria.initialView;
  }

  //create leaflet viewer
  map = L.map(this._mapContainer, {
    zoomControl: false,
    attributionControl: false,
    maxZoom: this.maximumLeafletZoomLevel,
    zoomSnap: 1, // Change to  0.2 for incremental zoom when Chrome fixes canvas scaling gaps
    preferCanvas: true,
    worldCopyJump: true
  }).setView([-28.5, 135], 5);

  map.attributionControl = L.control.attribution({
    position: "bottomleft"
  });
  map.addControl(map.attributionControl);

  map.screenSpaceEventHandler = {
    setInputAction: function() {},
    remoteInputAction: function() {}
  };
  map.destroy = function() {};

  var leaflet = new Leaflet(this.terria, map);

  if (!defined(this.leafletVisualizer)) {
    this.leafletVisualizer = new LeafletVisualizer();
  }

  const terriaLogo = this._defaultTerriaCredit
    ? this._defaultTerriaCredit.html
    : "";

  const creditParts = [
    this._getDisclaimer(),
    this._developerAttribution &&
      createCredit(
        this._developerAttribution.text,
        this._developerAttribution.link
      ),
    new Credit('<a target="_blank" href="http://leafletjs.com/">Leaflet</a>')
  ];

  map.attributionControl.setPrefix(
    terriaLogo +
      creditParts
        .filter(part => defined(part))
        .map(credit => credit.html)
        .join(" | ")
  );

  map.on("boxzoomend", function(e) {
    console.log(e.boxZoomBounds);
  });

  this.terria.leaflet = leaflet;
  this.terria.currentViewer = this.terria.leaflet;

  this.dataSourceDisplay = new LeafletDataSourceDisplay({
    scene: leaflet.scene,
    dataSourceCollection: this.terria.dataSources,
    visualizersCallback: this.leafletVisualizer.visualizersCallback
  });

  eventHelper = new EventHelper();

  var that = this;
  eventHelper.add(that.terria.clock.onTick, function(clock) {
    that.dataSourceDisplay.update(clock.currentTime);
  });

  this.terria.leaflet.dataSourceDisplay = this.dataSourceDisplay;

  this.leafletEventHelper = eventHelper;

  var ticker = function() {
    if (defined(that.terria.leaflet)) {
      that.terria.clock.tick();
      cesiumRequestAnimationFrame(ticker);
    }
  };

  ticker();

  this.terria.leaflet.zoomTo(rect, 0.0);
  return when();
};

TerriaViewer.prototype.selectCesium = function() {
  var deferred = when.defer();

  var that = this;
  require.ensure(
    [
      "terriajs-cesium/Source/Widgets/CesiumWidget/CesiumWidget",
      "terriajs-cesium/Source/DataSources/DataSourceDisplay",
      "../Models/Cesium"
    ],
    function() {
      var CesiumWidget = require("terriajs-cesium/Source/Widgets/CesiumWidget/CesiumWidget")
        .default;
      var DataSourceDisplay = require("terriajs-cesium/Source/DataSources/DataSourceDisplay")
        .default;
      var Cesium = require("../Models/Cesium");

      var viewer, rect, eventHelper;

      if (defined(that.terria.leaflet)) {
        rect = that.terria.leaflet.getCurrentExtent();

        that.destroyLeaflet();
      }

      //create Cesium viewer
      viewer = that._createCesiumViewer(that._mapContainer, CesiumWidget);

      that._enableSelectExtent(viewer.scene, true);

      that.terria.cesium = new Cesium(that.terria, viewer);
      that.terria.currentViewer = that.terria.cesium;

      changeFogSettings(that);

      //Simple monitor to start up and switch to 2D if seem to be stuck.
      if (!defined(that.checkedStartupPerformance)) {
        that.checkedStartupPerformance = true;
        var uri = new URI(window.location);
        var params = uri.search(true);
        var frameRate = defined(params.fps) ? params.fps : 5;

        that.monitor = new FrameRateMonitor({
          scene: viewer.scene,
          minimumFrameRateDuringWarmup: frameRate,
          minimumFrameRateAfterWarmup: 0,
          samplingWindow: 2
        });
        that.monitor.lowFrameRate.addEventListener(function() {
          if (!that.terria.cesium.stoppedRendering) {
            that.terria.error.raiseEvent({
              title: "Unusually Slow Performance Detected",
              message:
                "It appears that your system is capable of running " +
                that.terria.appName +
                " \
in 3D mode, but is having significant performance issues. \
We are automatically switching to 2D mode to help resolve this issue.  If you want to switch back to 3D mode you can select \
that option from the Maps button at the top of the screen."
            });
            runLater(function() {
              that.terria.viewerMode = ViewerMode.Leaflet;
            });
          }
        });
      }

      eventHelper = new EventHelper();

      eventHelper.add(that.terria.clock.onTick, function(clock) {
        that.dataSourceDisplay.update(clock.currentTime);
      });

      that.cesiumEventHelper = eventHelper;

      that.dataSourceDisplay = new DataSourceDisplay({
        scene: viewer.scene,
        dataSourceCollection: that.terria.dataSources
      });

      that.terria.cesium.dataSourceDisplay = that.dataSourceDisplay;

      if (defined(rect)) {
        that.terria.cesium.zoomTo(rect, 0.0);
      } else {
        that.terria.cesium.zoomTo(that.terria.initialView, 0.0);
      }

      deferred.resolve();
    },
    "3D"
  );

  return deferred.promise;
};

TerriaViewer.prototype._getDisclaimer = function() {
  var d = this.terria.configParameters.disclaimer;
  if (d) {
    return createCredit(d.text, d.url);
  } else {
    return null;
  }
};

// -------------------------------------------
// Camera management
// -------------------------------------------

function flyToPosition(scene, position, durationMilliseconds) {
  var camera = scene.camera;
  var startPosition = camera.position;
  var endPosition = position;

  durationMilliseconds = defaultValue(durationMilliseconds, 200);

  var initialEnuToFixed = Transforms.eastNorthUpToFixedFrame(
    startPosition,
    Ellipsoid.WGS84
  );

  var initialEnuToFixedRotation = new Matrix4();
  Matrix4.getRotation(initialEnuToFixed, initialEnuToFixedRotation);

  var initialFixedToEnuRotation = new Matrix3();
  Matrix3.transpose(initialEnuToFixedRotation, initialFixedToEnuRotation);

  var initialEnuUp = new Matrix3();
  Matrix3.multiplyByVector(initialFixedToEnuRotation, camera.up, initialEnuUp);

  var initialEnuRight = new Matrix3();
  Matrix3.multiplyByVector(
    initialFixedToEnuRotation,
    camera.right,
    initialEnuRight
  );

  var initialEnuDirection = new Matrix3();
  Matrix3.multiplyByVector(
    initialFixedToEnuRotation,
    camera.direction,
    initialEnuDirection
  );

  var controller = scene.screenSpaceCameraController;
  controller.enableInputs = false;

  scene.tweens.add({
    duration: durationMilliseconds / 1000.0,
    easingFunction: Tween.Easing.Sinusoidal.InOut,
    startObject: {
      time: 0.0
    },
    stopObject: {
      time: 1.0
    },
    update: function(value) {
      scene.camera.position.x = CesiumMath.lerp(
        startPosition.x,
        endPosition.x,
        value.time
      );
      scene.camera.position.y = CesiumMath.lerp(
        startPosition.y,
        endPosition.y,
        value.time
      );
      scene.camera.position.z = CesiumMath.lerp(
        startPosition.z,
        endPosition.z,
        value.time
      );

      var enuToFixed = Transforms.eastNorthUpToFixedFrame(
        camera.position,
        Ellipsoid.WGS84
      );

      var enuToFixedRotation = new Matrix3();
      Matrix4.getRotation(enuToFixed, enuToFixedRotation);

      camera.up = Matrix3.multiplyByVector(
        enuToFixedRotation,
        initialEnuUp,
        camera.up
      );
      camera.right = Matrix3.multiplyByVector(
        enuToFixedRotation,
        initialEnuRight,
        camera.right
      );
      camera.direction = Matrix3.multiplyByVector(
        enuToFixedRotation,
        initialEnuDirection,
        camera.direction
      );
    },
    complete: function() {
      controller.enableInputs = true;
    },
    cancel: function() {
      controller.enableInputs = true;
    }
  });
}

var destinationScratch = new Cartesian3();

function zoomCamera(scene, distFactor, pos) {
  var camera = scene.camera;
  var pickRay = camera.getPickRay(pos);
  var targetCartesian = scene.globe.pick(pickRay, scene);
  if (targetCartesian) {
    // Zoom to the picked latitude/longitude, at a distFactor multiple
    // of the height.
    var destination = Cartesian3.lerp(
      camera.position,
      targetCartesian,
      distFactor,
      destinationScratch
    );
    flyToPosition(scene, destination);
  }
}

function zoomIn(scene, pos) {
  zoomCamera(scene, 2.0 / 3.0, pos);
}
function zoomOut(scene, pos) {
  zoomCamera(scene, -2.0, pos);
}

function initializeTerrainProvider(terriaViewer) {
  if (defined(terriaViewer._terrainProvider)) {
    return;
  }

  if (terriaViewer.terria.configParameters.cesiumIonAccessToken) {
    Ion.defaultAccessToken =
      terriaViewer.terria.configParameters.cesiumIonAccessToken;
  }

  if (terriaViewer.terria.configParameters.useCesiumIonTerrain) {
    terriaViewer._bumpyTerrainProvider = createWorldTerrain();
    const logo = require("terriajs-cesium/Source/Assets/Images/ion-credit.png")
      .default;
    terriaViewer._bumpyTerrainCredit = new Credit(
      '<a href="https://cesium.com/" target="_blank" rel="noopener noreferrer"><img src="' +
        logo +
        '" title="Cesium ion"/></a>',
      true
    );
  } else if (
    typeof terriaViewer._terrain === "string" ||
    terriaViewer._terrain instanceof String
  ) {
    terriaViewer._bumpyTerrainProvider = new CesiumTerrainProvider({
      url: terriaViewer._terrain
    });
    terriaViewer._bumpyTerrainCredit = undefined;
  } else if (defined(terriaViewer._terrain)) {
    terriaViewer._bumpyTerrainProvider = terriaViewer._terrain;
    terriaViewer._bumpyTerrainCredit = undefined;
  } else {
    terriaViewer._bumpyTerrainProvider = new EllipsoidTerrainProvider();
    terriaViewer._bumpyTerrainCredit = undefined;
  }

  var terria = terriaViewer.terria;
  if (terria.viewerMode === ViewerMode.CesiumTerrain) {
    terriaViewer._terrainProvider = terriaViewer._bumpyTerrainProvider;
    CreditDisplay._cesiumCredit = terriaViewer._bumpyTerrainCredit;
  } else if (terria.viewerMode === ViewerMode.CesiumEllipsoid) {
    terriaViewer._terrainProvider = new EllipsoidTerrainProvider();
    CreditDisplay._cesiumCredit = undefined;
  }
  // add terria logo unless specified not to
  if (!terriaViewer.terria.configParameters.hideTerriaLogo) {
    const terriaLogo = require("../../wwwroot/images/terria-watermark.svg");
    terriaViewer._defaultTerriaCredit = new Credit(
      '<div id="terriaLogoWrapper"><a id="terriaLogo" href="https://terria.io/" target="_blank" rel="noopener noreferrer" ><img src="' +
        terriaLogo +
        '" title="Built with Terria"/></a></div>',
      true
    );
  } else {
    terriaViewer._defaultTerriaCredit = undefined;
  }
}

module.exports = TerriaViewer;