Source: Widgets/Viewer/Viewer.js

/*global define*/
define([
        '../../Core/BoundingSphere',
        '../../Core/Cartesian3',
        '../../Core/defaultValue',
        '../../Core/defined',
        '../../Core/defineProperties',
        '../../Core/destroyObject',
        '../../Core/DeveloperError',
        '../../Core/EventHelper',
        '../../Core/Fullscreen',
        '../../Core/isArray',
        '../../Core/Matrix4',
        '../../Core/Rectangle',
        '../../Core/ScreenSpaceEventType',
        '../../DataSources/BoundingSphereState',
        '../../DataSources/ConstantPositionProperty',
        '../../DataSources/DataSourceCollection',
        '../../DataSources/DataSourceDisplay',
        '../../DataSources/Entity',
        '../../DataSources/EntityView',
        '../../DataSources/Property',
        '../../Scene/ImageryLayer',
        '../../Scene/SceneMode',
        '../../Scene/ShadowMode',
        '../../ThirdParty/knockout',
        '../../ThirdParty/when',
        '../Animation/Animation',
        '../Animation/AnimationViewModel',
        '../BaseLayerPicker/BaseLayerPicker',
        '../BaseLayerPicker/createDefaultImageryProviderViewModels',
        '../BaseLayerPicker/createDefaultTerrainProviderViewModels',
        '../CesiumWidget/CesiumWidget',
        '../ClockViewModel',
        '../FullscreenButton/FullscreenButton',
        '../Geocoder/Geocoder',
        '../getElement',
        '../HomeButton/HomeButton',
        '../InfoBox/InfoBox',
        '../NavigationHelpButton/NavigationHelpButton',
        '../SceneModePicker/SceneModePicker',
        '../SelectionIndicator/SelectionIndicator',
        '../subscribeAndEvaluate',
        '../Timeline/Timeline',
        '../VRButton/VRButton'
    ], function(
        BoundingSphere,
        Cartesian3,
        defaultValue,
        defined,
        defineProperties,
        destroyObject,
        DeveloperError,
        EventHelper,
        Fullscreen,
        isArray,
        Matrix4,
        Rectangle,
        ScreenSpaceEventType,
        BoundingSphereState,
        ConstantPositionProperty,
        DataSourceCollection,
        DataSourceDisplay,
        Entity,
        EntityView,
        Property,
        ImageryLayer,
        SceneMode,
        ShadowMode,
        knockout,
        when,
        Animation,
        AnimationViewModel,
        BaseLayerPicker,
        createDefaultImageryProviderViewModels,
        createDefaultTerrainProviderViewModels,
        CesiumWidget,
        ClockViewModel,
        FullscreenButton,
        Geocoder,
        getElement,
        HomeButton,
        InfoBox,
        NavigationHelpButton,
        SceneModePicker,
        SelectionIndicator,
        subscribeAndEvaluate,
        Timeline,
        VRButton) {
    'use strict';

    var boundingSphereScratch = new BoundingSphere();

    function onTimelineScrubfunction(e) {
        var clock = e.clock;
        clock.currentTime = e.timeJulian;
        clock.shouldAnimate = false;
    }

    function pickEntity(viewer, e) {
        var picked = viewer.scene.pick(e.position);
        if (defined(picked)) {
            var id = defaultValue(picked.id, picked.primitive.id);
            if (id instanceof Entity) {
                return id;
            }
        }

        // No regular entity picked.  Try picking features from imagery layers.
        if (defined(viewer.scene.globe)) {
            return pickImageryLayerFeature(viewer, e.position);
        }
    }

    function trackDataSourceClock(timeline, clock, dataSource) {
        if (defined(dataSource)) {
            var dataSourceClock = dataSource.clock;
            if (defined(dataSourceClock)) {
                dataSourceClock.getValue(clock);
                if (defined(timeline)) {
                    timeline.updateFromClock();
                    timeline.zoomTo(dataSourceClock.startTime, dataSourceClock.stopTime);
                }
            }
        }
    }

    var cartesian3Scratch = new Cartesian3();

    function pickImageryLayerFeature(viewer, windowPosition) {
        var scene = viewer.scene;
        var pickRay = scene.camera.getPickRay(windowPosition);
        var imageryLayerFeaturePromise = scene.imageryLayers.pickImageryLayerFeatures(pickRay, scene);
        if (!defined(imageryLayerFeaturePromise)) {
            return;
        }

        // Imagery layer feature picking is asynchronous, so put up a message while loading.
        var loadingMessage = new Entity({
            id : 'Loading...',
            description : 'Loading feature information...'
        });

        when(imageryLayerFeaturePromise, function(features) {
            // Has this async pick been superseded by a later one?
            if (viewer.selectedEntity !== loadingMessage) {
                return;
            }

            if (!defined(features) || features.length === 0) {
                viewer.selectedEntity = createNoFeaturesEntity();
                return;
            }

            // Select the first feature.
            var feature = features[0];

            var entity = new Entity({
                id : feature.name,
                description : feature.description
            });

            if (defined(feature.position)) {
                var ecfPosition = viewer.scene.globe.ellipsoid.cartographicToCartesian(feature.position, cartesian3Scratch);
                entity.position = new ConstantPositionProperty(ecfPosition);
            }

            viewer.selectedEntity = entity;
        }, function() {
            // Has this async pick been superseded by a later one?
            if (viewer.selectedEntity !== loadingMessage) {
                return;
            }
            viewer.selectedEntity = createNoFeaturesEntity();
        });

        return loadingMessage;
    }

    function createNoFeaturesEntity() {
        return new Entity({
            id : 'None',
            description : 'No features found.'
        });
    }

    function enableVRUI(viewer, enabled) {
        var geocoder = viewer._geocoder;
        var homeButton = viewer._homeButton;
        var sceneModePicker = viewer._sceneModePicker;
        var baseLayerPicker = viewer._baseLayerPicker;
        var animation = viewer._animation;
        var timeline = viewer._timeline;
        var fullscreenButton = viewer._fullscreenButton;
        var infoBox = viewer._infoBox;
        var selectionIndicator = viewer._selectionIndicator;

        var visibility = enabled ? 'hidden' : 'visible';

        if (defined(geocoder)) {
            geocoder.container.style.visibility = visibility;
        }
        if (defined(homeButton)) {
            homeButton.container.style.visibility = visibility;
        }
        if(defined(sceneModePicker)) {
            sceneModePicker.container.style.visibility = visibility;
        }
        if(defined(baseLayerPicker)) {
            baseLayerPicker.container.style.visibility = visibility;
        }
        if (defined(animation)) {
            animation.container.style.visibility = visibility;
        }
        if (defined(timeline)) {
            timeline.container.style.visibility = visibility;
        }
        if (defined(fullscreenButton) && fullscreenButton.viewModel.isFullscreenEnabled) {
            fullscreenButton.container.style.visibility = visibility;
        }
        if (defined(infoBox)) {
            infoBox.container.style.visibility = visibility;
        }
        if (defined(selectionIndicator)) {
            selectionIndicator.container.style.visibility = visibility;
        }

        if (viewer._container) {
            var right = enabled || !defined(fullscreenButton) ? 0 : fullscreenButton.container.clientWidth;
            viewer._vrButton.container.style.right = right + 'px';

            viewer.forceResize();
        }
    }

    /**
     * A base widget for building applications.  It composites all of the standard Cesium widgets into one reusable package.
     * The widget can always be extended by using mixins, which add functionality useful for a variety of applications.
     *
     * @alias Viewer
     * @constructor
     *
     * @param {Element|String} container The DOM element or ID that will contain the widget.
     * @param {Object} [options] Object with the following properties:
     * @param {Boolean} [options.animation=true] If set to false, the Animation widget will not be created.
     * @param {Boolean} [options.baseLayerPicker=true] If set to false, the BaseLayerPicker widget will not be created.
     * @param {Boolean} [options.fullscreenButton=true] If set to false, the FullscreenButton widget will not be created.
     * @param {Boolean} [options.vrButton=false] If set to true, the VRButton widget will be created.
     * @param {Boolean} [options.geocoder=true] If set to false, the Geocoder widget will not be created.
     * @param {Boolean} [options.homeButton=true] If set to false, the HomeButton widget will not be created.
     * @param {Boolean} [options.infoBox=true] If set to false, the InfoBox widget will not be created.
     * @param {Boolean} [options.sceneModePicker=true] If set to false, the SceneModePicker widget will not be created.
     * @param {Boolean} [options.selectionIndicator=true] If set to false, the SelectionIndicator widget will not be created.
     * @param {Boolean} [options.timeline=true] If set to false, the Timeline widget will not be created.
     * @param {Boolean} [options.navigationHelpButton=true] If set to false, the navigation help button will not be created.
     * @param {Boolean} [options.navigationInstructionsInitiallyVisible=true] True if the navigation instructions should initially be visible, or false if the should not be shown until the user explicitly clicks the button.
     * @param {Boolean} [options.scene3DOnly=false] When <code>true</code>, each geometry instance will only be rendered in 3D to save GPU memory.
     * @param {Clock} [options.clock=new Clock()] The clock to use to control current time.
     * @param {ProviderViewModel} [options.selectedImageryProviderViewModel] The view model for the current base imagery layer, if not supplied the first available base layer is used.  This value is only valid if options.baseLayerPicker is set to true.
     * @param {ProviderViewModel[]} [options.imageryProviderViewModels=createDefaultImageryProviderViewModels()] The array of ProviderViewModels to be selectable from the BaseLayerPicker.  This value is only valid if options.baseLayerPicker is set to true.
     * @param {ProviderViewModel} [options.selectedTerrainProviderViewModel] The view model for the current base terrain layer, if not supplied the first available base layer is used.  This value is only valid if options.baseLayerPicker is set to true.
     * @param {ProviderViewModel[]} [options.terrainProviderViewModels=createDefaultTerrainProviderViewModels()] The array of ProviderViewModels to be selectable from the BaseLayerPicker.  This value is only valid if options.baseLayerPicker is set to true.
     * @param {ImageryProvider} [options.imageryProvider=new BingMapsImageryProvider()] The imagery provider to use.  This value is only valid if options.baseLayerPicker is set to false.
     * @param {TerrainProvider} [options.terrainProvider=new EllipsoidTerrainProvider()] The terrain provider to use
     * @param {SkyBox} [options.skyBox] The skybox used to render the stars.  When <code>undefined</code>, the default stars are used.
     * @param {SkyAtmosphere} [options.skyAtmosphere] Blue sky, and the glow around the Earth's limb.  Set to <code>false</code> to turn it off.
     * @param {Element|String} [options.fullscreenElement=document.body] The element or id to be placed into fullscreen mode when the full screen button is pressed.
     * @param {Boolean} [options.useDefaultRenderLoop=true] True if this widget should control the render loop, false otherwise.
     * @param {Number} [options.targetFrameRate] The target frame rate when using the default render loop.
     * @param {Boolean} [options.showRenderLoopErrors=true] If true, this widget will automatically display an HTML panel to the user containing the error, if a render loop error occurs.
     * @param {Boolean} [options.automaticallyTrackDataSourceClocks=true] If true, this widget will automatically track the clock settings of newly added DataSources, updating if the DataSource's clock changes.  Set this to false if you want to configure the clock independently.
     * @param {Object} [options.contextOptions] Context and WebGL creation properties corresponding to <code>options</code> passed to {@link Scene}.
     * @param {SceneMode} [options.sceneMode=SceneMode.SCENE3D] The initial scene mode.
     * @param {MapProjection} [options.mapProjection=new GeographicProjection()] The map projection to use in 2D and Columbus View modes.
     * @param {Globe} [options.globe=new Globe(mapProjection.ellipsoid)] The globe to use in the scene.  If set to <code>false</code>, no globe will be added.
     * @param {Boolean} [options.orderIndependentTranslucency=true] If true and the configuration supports it, use order independent translucency.
     * @param {Element|String} [options.creditContainer] The DOM element or ID that will contain the {@link CreditDisplay}.  If not specified, the credits are added to the bottom of the widget itself.
     * @param {DataSourceCollection} [options.dataSources=new DataSourceCollection()] The collection of data sources visualized by the widget.  If this parameter is provided,
     *                               the instance is assumed to be owned by the caller and will not be destroyed when the viewer is destroyed.
     * @param {Number} [options.terrainExaggeration=1.0] A scalar used to exaggerate the terrain. Note that terrain exaggeration will not modify any other primitive as they are positioned relative to the ellipsoid.
     * @param {Boolean} [options.shadows=false] Determines if shadows are cast by the sun.
     * @param {ShadowMode} [options.terrainShadows=ShadowMode.RECEIVE_ONLY] Determines if the terrain casts or receives shadows from the sun.
     * @param {MapMode2D} [options.mapMode2D=MapMode2D.INFINITE_SCROLL] Determines if the 2D map is rotatable or can be scrolled infinitely in the horizontal direction.
     *
     * @exception {DeveloperError} Element with id "container" does not exist in the document.
     * @exception {DeveloperError} options.imageryProvider is not available when using the BaseLayerPicker widget, specify options.selectedImageryProviderViewModel instead.
     * @exception {DeveloperError} options.terrainProvider is not available when using the BaseLayerPicker widget, specify options.selectedTerrainProviderViewModel instead.
     * @exception {DeveloperError} options.selectedImageryProviderViewModel is not available when not using the BaseLayerPicker widget, specify options.imageryProvider instead.
     * @exception {DeveloperError} options.selectedTerrainProviderViewModel is not available when not using the BaseLayerPicker widget, specify options.terrainProvider instead.
     *
     * @see Animation
     * @see BaseLayerPicker
     * @see CesiumWidget
     * @see FullscreenButton
     * @see HomeButton
     * @see SceneModePicker
     * @see Timeline
     * @see viewerDragDropMixin
     *
     * @demo {@link http://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Hello%20World.html|Cesium Sandcastle Hello World Demo}
     *
     * @example
     * //Initialize the viewer widget with several custom options and mixins.
     * var viewer = new Cesium.Viewer('cesiumContainer', {
     *     //Start in Columbus Viewer
     *     sceneMode : Cesium.SceneMode.COLUMBUS_VIEW,
     *     //Use standard Cesium terrain
     *     terrainProvider : new Cesium.CesiumTerrainProvider({
     *         url : 'https://assets.agi.com/stk-terrain/world'
     *     }),
     *     //Hide the base layer picker
     *     baseLayerPicker : false,
     *     //Use OpenStreetMaps
     *     imageryProvider : Cesium.createOpenStreetMapImageryProvider({
     *         url : 'https://a.tile.openstreetmap.org/'
     *     }),
     *     // Use high-res stars downloaded from https://github.com/AnalyticalGraphicsInc/cesium-assets
     *     skyBox : new Cesium.SkyBox({
     *         sources : {
     *           positiveX : 'stars/TychoSkymapII.t3_08192x04096_80_px.jpg',
     *           negativeX : 'stars/TychoSkymapII.t3_08192x04096_80_mx.jpg',
     *           positiveY : 'stars/TychoSkymapII.t3_08192x04096_80_py.jpg',
     *           negativeY : 'stars/TychoSkymapII.t3_08192x04096_80_my.jpg',
     *           positiveZ : 'stars/TychoSkymapII.t3_08192x04096_80_pz.jpg',
     *           negativeZ : 'stars/TychoSkymapII.t3_08192x04096_80_mz.jpg'
     *         }
     *     }),
     *     // Show Columbus View map with Web Mercator projection
     *     mapProjection : new Cesium.WebMercatorProjection()
     * });
     *
     * //Add basic drag and drop functionality
     * viewer.extend(Cesium.viewerDragDropMixin);
     *
     * //Show a pop-up alert if we encounter an error when processing a dropped file
     * viewer.dropError.addEventListener(function(dropHandler, name, error) {
     *     console.log(error);
     *     window.alert(error);
     * });
     */
    function Viewer(container, options) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(container)) {
            throw new DeveloperError('container is required.');
        }
        //>>includeEnd('debug');

        container = getElement(container);
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        var createBaseLayerPicker = (!defined(options.globe) || options.globe !== false) &&
            (!defined(options.baseLayerPicker) || options.baseLayerPicker !== false);

        //>>includeStart('debug', pragmas.debug);
        // If using BaseLayerPicker, imageryProvider is an invalid option
        if (createBaseLayerPicker && defined(options.imageryProvider)) {
            throw new DeveloperError('options.imageryProvider is not available when using the BaseLayerPicker widget. \
Either specify options.selectedImageryProviderViewModel instead or set options.baseLayerPicker to false.');
        }

        // If not using BaseLayerPicker, selectedImageryProviderViewModel is an invalid option
        if (!createBaseLayerPicker && defined(options.selectedImageryProviderViewModel)) {
            throw new DeveloperError('options.selectedImageryProviderViewModel is not available when not using the BaseLayerPicker widget. \
Either specify options.imageryProvider instead or set options.baseLayerPicker to true.');
        }

        // If using BaseLayerPicker, terrainProvider is an invalid option
        if (createBaseLayerPicker && defined(options.terrainProvider)) {
            throw new DeveloperError('options.terrainProvider is not available when using the BaseLayerPicker widget. \
Either specify options.selectedTerrainProviderViewModel instead or set options.baseLayerPicker to false.');
        }

        // If not using BaseLayerPicker, selectedTerrainProviderViewModel is an invalid option
        if (!createBaseLayerPicker && defined(options.selectedTerrainProviderViewModel)) {
            throw new DeveloperError('options.selectedTerrainProviderViewModel is not available when not using the BaseLayerPicker widget. \
Either specify options.terrainProvider instead or set options.baseLayerPicker to true.');
        }
        //>>includeEnd('debug')

        var that = this;

        var viewerContainer = document.createElement('div');
        viewerContainer.className = 'cesium-viewer';
        container.appendChild(viewerContainer);

        // Cesium widget container
        var cesiumWidgetContainer = document.createElement('div');
        cesiumWidgetContainer.className = 'cesium-viewer-cesiumWidgetContainer';
        viewerContainer.appendChild(cesiumWidgetContainer);

        // Bottom container
        var bottomContainer = document.createElement('div');
        bottomContainer.className = 'cesium-viewer-bottom';

        viewerContainer.appendChild(bottomContainer);

        var scene3DOnly = defaultValue(options.scene3DOnly, false);

        // Cesium widget
        var cesiumWidget = new CesiumWidget(cesiumWidgetContainer, {
            terrainProvider : options.terrainProvider,
            imageryProvider : createBaseLayerPicker ? false : options.imageryProvider,
            clock : options.clock,
            skyBox : options.skyBox,
            skyAtmosphere : options.skyAtmosphere,
            sceneMode : options.sceneMode,
            mapProjection : options.mapProjection,
            globe : options.globe,
            orderIndependentTranslucency : options.orderIndependentTranslucency,
            contextOptions : options.contextOptions,
            useDefaultRenderLoop : options.useDefaultRenderLoop,
            targetFrameRate : options.targetFrameRate,
            showRenderLoopErrors : options.showRenderLoopErrors,
            creditContainer : defined(options.creditContainer) ? options.creditContainer : bottomContainer,
            scene3DOnly : scene3DOnly,
            terrainExaggeration : options.terrainExaggeration,
            shadows : options.shadows,
            terrainShadows : options.terrainShadows,
            mapMode2D : options.mapMode2D
        });

        var dataSourceCollection = options.dataSources;
        var destroyDataSourceCollection = false;
        if (!defined(dataSourceCollection)) {
            dataSourceCollection = new DataSourceCollection();
            destroyDataSourceCollection = true;
        }

        var dataSourceDisplay = new DataSourceDisplay({
            scene : cesiumWidget.scene,
            dataSourceCollection : dataSourceCollection
        });

        var clock = cesiumWidget.clock;
        var clockViewModel = new ClockViewModel(clock);
        var eventHelper = new EventHelper();

        eventHelper.add(clock.onTick, Viewer.prototype._onTick, this);
        eventHelper.add(cesiumWidget.scene.morphStart, Viewer.prototype._clearTrackedObject, this);

        // Selection Indicator
        var selectionIndicator;
        if (!defined(options.selectionIndicator) || options.selectionIndicator !== false) {
            var selectionIndicatorContainer = document.createElement('div');
            selectionIndicatorContainer.className = 'cesium-viewer-selectionIndicatorContainer';
            viewerContainer.appendChild(selectionIndicatorContainer);
            selectionIndicator = new SelectionIndicator(selectionIndicatorContainer, cesiumWidget.scene);
        }

        // Info Box
        var infoBox;
        if (!defined(options.infoBox) || options.infoBox !== false) {
            var infoBoxContainer = document.createElement('div');
            infoBoxContainer.className = 'cesium-viewer-infoBoxContainer';
            viewerContainer.appendChild(infoBoxContainer);
            infoBox = new InfoBox(infoBoxContainer);

            var infoBoxViewModel = infoBox.viewModel;
            eventHelper.add(infoBoxViewModel.cameraClicked, Viewer.prototype._onInfoBoxCameraClicked, this);
            eventHelper.add(infoBoxViewModel.closeClicked, Viewer.prototype._onInfoBoxClockClicked, this);
        }

        // Main Toolbar
        var toolbar = document.createElement('div');
        toolbar.className = 'cesium-viewer-toolbar';
        viewerContainer.appendChild(toolbar);

        // Geocoder
        var geocoder;
        if (!defined(options.geocoder) || options.geocoder !== false) {
            var geocoderContainer = document.createElement('div');
            geocoderContainer.className = 'cesium-viewer-geocoderContainer';
            toolbar.appendChild(geocoderContainer);
            geocoder = new Geocoder({
                container : geocoderContainer,
                scene : cesiumWidget.scene
            });
            // Subscribe to search so that we can clear the trackedEntity when it is clicked.
            eventHelper.add(geocoder.viewModel.search.beforeExecute, Viewer.prototype._clearObjects, this);
        }

        // HomeButton
        var homeButton;
        if (!defined(options.homeButton) || options.homeButton !== false) {
            homeButton = new HomeButton(toolbar, cesiumWidget.scene);
            if (defined(geocoder)) {
                eventHelper.add(homeButton.viewModel.command.afterExecute, function() {
                    var viewModel = geocoder.viewModel;
                    viewModel.searchText = '';
                    if (viewModel.isSearchInProgress) {
                        viewModel.search();
                    }
                });
            }
            // Subscribe to the home button beforeExecute event so that we can clear the trackedEntity.
            eventHelper.add(homeButton.viewModel.command.beforeExecute, Viewer.prototype._clearTrackedObject, this);
        }

        // SceneModePicker
        // By default, we silently disable the scene mode picker if scene3DOnly is true,
        // but if sceneModePicker is explicitly set to true, throw an error.
        //>>includeStart('debug', pragmas.debug);
        if ((options.sceneModePicker === true) && scene3DOnly) {
            throw new DeveloperError('options.sceneModePicker is not available when options.scene3DOnly is set to true.');
        }
        //>>includeEnd('debug');

        var sceneModePicker;
        if (!scene3DOnly && (!defined(options.sceneModePicker) || options.sceneModePicker !== false)) {
            sceneModePicker = new SceneModePicker(toolbar, cesiumWidget.scene);
        }

        // BaseLayerPicker
        var baseLayerPicker;
        var baseLayerPickerDropDown;
        if (createBaseLayerPicker) {
            var imageryProviderViewModels = defaultValue(options.imageryProviderViewModels, createDefaultImageryProviderViewModels());
            var terrainProviderViewModels = defaultValue(options.terrainProviderViewModels, createDefaultTerrainProviderViewModels());

            baseLayerPicker = new BaseLayerPicker(toolbar, {
                globe : cesiumWidget.scene.globe,
                imageryProviderViewModels : imageryProviderViewModels,
                selectedImageryProviderViewModel : options.selectedImageryProviderViewModel,
                terrainProviderViewModels : terrainProviderViewModels,
                selectedTerrainProviderViewModel : options.selectedTerrainProviderViewModel
            });

            //Grab the dropdown for resize code.
            var elements = toolbar.getElementsByClassName('cesium-baseLayerPicker-dropDown');
            baseLayerPickerDropDown = elements[0];
        }

        // Navigation Help Button
        var navigationHelpButton;
        if (!defined(options.navigationHelpButton) || options.navigationHelpButton !== false) {
            var showNavHelp = true;
            try {
                //window.localStorage is null if disabled in Firefox or undefined in browsers with implementation
                if (defined(window.localStorage)) {
                    var hasSeenNavHelp = window.localStorage.getItem('cesium-hasSeenNavHelp');
                    if (defined(hasSeenNavHelp) && Boolean(hasSeenNavHelp)) {
                        showNavHelp = false;
                    } else {
                        window.localStorage.setItem('cesium-hasSeenNavHelp', 'true');
                    }
                }
            } catch (e) {
                //Accessing window.localStorage throws if disabled in Chrome
                //window.localStorage.setItem throws if in Safari private browsing mode or in any browser if we are over quota.
            }
            navigationHelpButton = new NavigationHelpButton({
                container : toolbar,
                instructionsInitiallyVisible : defaultValue(options.navigationInstructionsInitiallyVisible, showNavHelp)
            });
        }

        // Animation
        var animation;
        if (!defined(options.animation) || options.animation !== false) {
            var animationContainer = document.createElement('div');
            animationContainer.className = 'cesium-viewer-animationContainer';
            viewerContainer.appendChild(animationContainer);
            animation = new Animation(animationContainer, new AnimationViewModel(clockViewModel));
        }

        // Timeline
        var timeline;
        if (!defined(options.timeline) || options.timeline !== false) {
            var timelineContainer = document.createElement('div');
            timelineContainer.className = 'cesium-viewer-timelineContainer';
            viewerContainer.appendChild(timelineContainer);
            timeline = new Timeline(timelineContainer, clock);
            timeline.addEventListener('settime', onTimelineScrubfunction, false);
            timeline.zoomTo(clock.startTime, clock.stopTime);
        }

        // Fullscreen
        var fullscreenButton;
        var fullscreenSubscription;
        if (!defined(options.fullscreenButton) || options.fullscreenButton !== false) {
            var fullscreenContainer = document.createElement('div');
            fullscreenContainer.className = 'cesium-viewer-fullscreenContainer';
            viewerContainer.appendChild(fullscreenContainer);
            fullscreenButton = new FullscreenButton(fullscreenContainer, options.fullscreenElement);

            //Subscribe to fullscreenButton.viewModel.isFullscreenEnabled so
            //that we can hide/show the button as well as size the timeline.
            fullscreenSubscription = subscribeAndEvaluate(fullscreenButton.viewModel, 'isFullscreenEnabled', function(isFullscreenEnabled) {
                fullscreenContainer.style.display = isFullscreenEnabled ? 'block' : 'none';
                if (defined(timeline)) {
                    timeline.container.style.right = fullscreenContainer.clientWidth + 'px';
                    timeline.resize();
                }
            });
        }

        // VR
        var vrButton;
        var vrSubscription;
        var vrModeSubscription;
        if (options.vrButton) {
            var vrContainer = document.createElement('div');
            vrContainer.className = 'cesium-viewer-vrContainer';
            viewerContainer.appendChild(vrContainer);
            vrButton = new VRButton(vrContainer, cesiumWidget.scene, options.fullScreenElement);

            vrSubscription = subscribeAndEvaluate(vrButton.viewModel, 'isVREnabled', function(isVREnabled) {
                vrContainer.style.display = isVREnabled ? 'block' : 'none';
                if (defined(fullscreenButton)) {
                    vrContainer.style.right = fullscreenContainer.clientWidth + 'px';
                }
                if (defined(timeline)) {
                    timeline.container.style.right = vrContainer.clientWidth + 'px';
                    timeline.resize();
                }
            });

            vrModeSubscription = subscribeAndEvaluate(vrButton.viewModel, 'isVRMode', function(isVRMode) {
                enableVRUI(that, isVRMode);
            });
        }

        //Assign all properties to this instance.  No "this" assignments should
        //take place above this line.
        this._baseLayerPickerDropDown = baseLayerPickerDropDown;
        this._fullscreenSubscription = fullscreenSubscription;
        this._vrSubscription = vrSubscription;
        this._vrModeSubscription = vrModeSubscription;
        this._dataSourceChangedListeners = {};
        this._automaticallyTrackDataSourceClocks = defaultValue(options.automaticallyTrackDataSourceClocks, true);
        this._container = container;
        this._bottomContainer = bottomContainer;
        this._element = viewerContainer;
        this._cesiumWidget = cesiumWidget;
        this._selectionIndicator = selectionIndicator;
        this._infoBox = infoBox;
        this._dataSourceCollection = dataSourceCollection;
        this._destroyDataSourceCollection = destroyDataSourceCollection;
        this._dataSourceDisplay = dataSourceDisplay;
        this._clockViewModel = clockViewModel;
        this._toolbar = toolbar;
        this._homeButton = homeButton;
        this._sceneModePicker = sceneModePicker;
        this._baseLayerPicker = baseLayerPicker;
        this._navigationHelpButton = navigationHelpButton;
        this._animation = animation;
        this._timeline = timeline;
        this._fullscreenButton = fullscreenButton;
        this._vrButton = vrButton;
        this._geocoder = geocoder;
        this._eventHelper = eventHelper;
        this._lastWidth = 0;
        this._lastHeight = 0;
        this._allowDataSourcesToSuspendAnimation = true;
        this._entityView = undefined;
        this._enableInfoOrSelection = defined(infoBox) || defined(selectionIndicator);
        this._clockTrackedDataSource = undefined;
        this._trackedEntity = undefined;
        this._needTrackedEntityUpdate = false;
        this._selectedEntity = undefined;
        this._clockTrackedDataSource = undefined;
        this._forceResize = false;
        this._zoomIsFlight = false;
        this._zoomTarget = undefined;
        this._zoomPromise = undefined;
        this._zoomOptions = undefined;

        knockout.track(this, ['_trackedEntity', '_selectedEntity', '_clockTrackedDataSource']);

        //Listen to data source events in order to track clock changes.
        eventHelper.add(dataSourceCollection.dataSourceAdded, Viewer.prototype._onDataSourceAdded, this);
        eventHelper.add(dataSourceCollection.dataSourceRemoved, Viewer.prototype._onDataSourceRemoved, this);

        // Prior to each render, check if anything needs to be resized.
        eventHelper.add(cesiumWidget.scene.preRender, Viewer.prototype.resize, this);
        eventHelper.add(cesiumWidget.scene.postRender, Viewer.prototype._postRender, this);

        // We need to subscribe to the data sources and collections so that we can clear the
        // tracked object when it is removed from the scene.
        // Subscribe to current data sources
        var dataSourceLength = dataSourceCollection.length;
        for (var i = 0; i < dataSourceLength; i++) {
            this._dataSourceAdded(dataSourceCollection, dataSourceCollection.get(i));
        }
        this._dataSourceAdded(undefined, dataSourceDisplay.defaultDataSource);

        // Hook up events so that we can subscribe to future sources.
        eventHelper.add(dataSourceCollection.dataSourceAdded, Viewer.prototype._dataSourceAdded, this);
        eventHelper.add(dataSourceCollection.dataSourceRemoved, Viewer.prototype._dataSourceRemoved, this);

        // Subscribe to left clicks and zoom to the picked object.
        function pickAndTrackObject(e) {
            var entity = pickEntity(that, e);
            if (defined(entity)) {
                //Only track the entity if it has a valid position at the current time.
                if (Property.getValueOrUndefined(entity.position, that.clock.currentTime)) {
                    that.trackedEntity = entity;
                } else {
                    that.zoomTo(entity);
                }
            }
        }

        function pickAndSelectObject(e) {
            that.selectedEntity = pickEntity(that, e);
        }

        cesiumWidget.screenSpaceEventHandler.setInputAction(pickAndSelectObject, ScreenSpaceEventType.LEFT_CLICK);
        cesiumWidget.screenSpaceEventHandler.setInputAction(pickAndTrackObject, ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
    }

    defineProperties(Viewer.prototype, {
        /**
         * Gets the parent container.
         * @memberof Viewer.prototype
         * @type {Element}
         * @readonly
         */
        container : {
            get : function() {
                return this._container;
            }
        },

        /**
         * Gets the DOM element for the area at the bottom of the window containing the
         * {@link CreditDisplay} and potentially other things.
         * @memberof Viewer.prototype
         * @type {Element}
         * @readonly
         */
        bottomContainer : {
            get : function() {
                return this._bottomContainer;
            }
        },

        /**
         * Gets the CesiumWidget.
         * @memberof Viewer.prototype
         * @type {CesiumWidget}
         * @readonly
         */
        cesiumWidget : {
            get : function() {
                return this._cesiumWidget;
            }
        },

        /**
         * Gets the selection indicator.
         * @memberof Viewer.prototype
         * @type {SelectionIndicator}
         * @readonly
         */
        selectionIndicator : {
            get : function() {
                return this._selectionIndicator;
            }
        },

        /**
         * Gets the info box.
         * @memberof Viewer.prototype
         * @type {InfoBox}
         * @readonly
         */
        infoBox : {
            get : function() {
                return this._infoBox;
            }
        },

        /**
         * Gets the Geocoder.
         * @memberof Viewer.prototype
         * @type {Geocoder}
         * @readonly
         */
        geocoder : {
            get : function() {
                return this._geocoder;
            }
        },

        /**
         * Gets the HomeButton.
         * @memberof Viewer.prototype
         * @type {HomeButton}
         * @readonly
         */
        homeButton : {
            get : function() {
                return this._homeButton;
            }
        },

        /**
         * Gets the SceneModePicker.
         * @memberof Viewer.prototype
         * @type {SceneModePicker}
         * @readonly
         */
        sceneModePicker : {
            get : function() {
                return this._sceneModePicker;
            }
        },

        /**
         * Gets the BaseLayerPicker.
         * @memberof Viewer.prototype
         * @type {BaseLayerPicker}
         * @readonly
         */
        baseLayerPicker : {
            get : function() {
                return this._baseLayerPicker;
            }
        },

        /**
         * Gets the NavigationHelpButton.
         * @memberof Viewer.prototype
         * @type {NavigationHelpButton}
         * @readonly
         */
        navigationHelpButton : {
            get : function() {
                return this._navigationHelpButton;
            }
        },

        /**
         * Gets the Animation widget.
         * @memberof Viewer.prototype
         * @type {Animation}
         * @readonly
         */
        animation : {
            get : function() {
                return this._animation;
            }
        },

        /**
         * Gets the Timeline widget.
         * @memberof Viewer.prototype
         * @type {Timeline}
         * @readonly
         */
        timeline : {
            get : function() {
                return this._timeline;
            }
        },

        /**
         * Gets the FullscreenButton.
         * @memberof Viewer.prototype
         * @type {FullscreenButton}
         * @readonly
         */
        fullscreenButton : {
            get : function() {
                return this._fullscreenButton;
            }
        },

        /**
         * Gets the VRButton.
         * @memberof Viewer.prototype
         * @type {VRButton}
         * @readonly
         */
        vrButton : {
            get : function() {
                return this._vrButton;
            }
        },

        /**
         * Gets the display used for {@link DataSource} visualization.
         * @memberof Viewer.prototype
         * @type {DataSourceDisplay}
         * @readonly
         */
        dataSourceDisplay : {
            get : function() {
                return this._dataSourceDisplay;
            }
        },

        /**
         * Gets the collection of entities not tied to a particular data source.
         * This is a shortcut to [dataSourceDisplay.defaultDataSource.entities]{@link Viewer#dataSourceDisplay}.
         * @memberof Viewer.prototype
         * @type {EntityCollection}
         * @readonly
         */
        entities : {
            get : function() {
                return this._dataSourceDisplay.defaultDataSource.entities;
            }
        },

        /**
         * Gets the set of {@link DataSource} instances to be visualized.
         * @memberof Viewer.prototype
         * @type {DataSourceCollection}
         * @readonly
         */
        dataSources : {
            get : function() {
                return this._dataSourceCollection;
            }
        },

        /**
         * Gets the canvas.
         * @memberof Viewer.prototype
         * @type {Canvas}
         * @readonly
         */
        canvas : {
            get : function() {
                return this._cesiumWidget.canvas;
            }
        },

        /**
         * Gets the Cesium logo element.
         * @memberof Viewer.prototype
         * @type {Element}
         * @readonly
         */
        cesiumLogo : {
            get : function() {
                return this._cesiumWidget.cesiumLogo;
            }
        },

        /**
         * Gets the scene.
         * @memberof Viewer.prototype
         * @type {Scene}
         * @readonly
         */
        scene : {
            get : function() {
                return this._cesiumWidget.scene;
            }
        },

        /**
         * Determines if shadows are cast by the sun.
         * @memberof Viewer.prototype
         * @type {Boolean}
         */
        shadows : {
            get : function() {
                return this.scene.shadowMap.enabled;
            },
            set : function(value) {
                this.scene.shadowMap.enabled = value;
            }
        },

        /**
         * Determines if the terrain casts or shadows from the sun.
         * @memberof Viewer.prototype
         * @type {ShadowMode}
         */
        terrainShadows : {
            get : function() {
                return this.scene.globe.shadows;
            },
            set : function(value) {
                this.scene.globe.shadows = value;
            }
        },

        /**
         * Get the scene's shadow map
         * @memberof Viewer.prototype
         * @type {ShadowMap}
         * @readonly
         */
        shadowMap : {
            get : function() {
                return this.scene.shadowMap;
            }
        },

        /**
         * Gets the collection of image layers that will be rendered on the globe.
         * @memberof Viewer.prototype
         *
         * @type {ImageryLayerCollection}
         * @readonly
         */
        imageryLayers : {
            get : function() {
                return this.scene.imageryLayers;
            }
        },

        /**
         * The terrain provider providing surface geometry for the globe.
         * @memberof Viewer.prototype
         *
         * @type {TerrainProvider}
         */
        terrainProvider : {
            get : function() {
                return this.scene.terrainProvider;
            },
            set : function(terrainProvider) {
                this.scene.terrainProvider = terrainProvider;
            }
        },

        /**
         * Gets the camera.
         * @memberof Viewer.prototype
         *
         * @type {Camera}
         * @readonly
         */
        camera : {
            get : function() {
                return this.scene.camera;
            }
        },

        /**
         * Gets the clock.
         * @memberof Viewer.prototype
         * @type {Clock}
         * @readonly
         */
        clock : {
            get : function() {
                return this._cesiumWidget.clock;
            }
        },

        /**
         * Gets the screen space event handler.
         * @memberof Viewer.prototype
         * @type {ScreenSpaceEventHandler}
         * @readonly
         */
        screenSpaceEventHandler : {
            get : function() {
                return this._cesiumWidget.screenSpaceEventHandler;
            }
        },

        /**
         * Gets or sets the target frame rate of the widget when <code>useDefaultRenderLoop</code>
         * is true. If undefined, the browser's {@link requestAnimationFrame} implementation
         * determines the frame rate.  If defined, this value must be greater than 0.  A value higher
         * than the underlying requestAnimationFrame implementation will have no effect.
         * @memberof Viewer.prototype
         *
         * @type {Number}
         */
        targetFrameRate : {
            get : function() {
                return this._cesiumWidget.targetFrameRate;
            },
            set : function(value) {
                this._cesiumWidget.targetFrameRate = value;
            }
        },

        /**
         * Gets or sets whether or not this widget should control the render loop.
         * If set to true the widget will use {@link requestAnimationFrame} to
         * perform rendering and resizing of the widget, as well as drive the
         * simulation clock. If set to false, you must manually call the
         * <code>resize</code>, <code>render</code> methods
         * as part of a custom render loop.  If an error occurs during rendering, {@link Scene}'s
         * <code>renderError</code> event will be raised and this property
         * will be set to false.  It must be set back to true to continue rendering
         * after the error.
         * @memberof Viewer.prototype
         *
         * @type {Boolean}
         */
        useDefaultRenderLoop : {
            get : function() {
                return this._cesiumWidget.useDefaultRenderLoop;
            },
            set : function(value) {
                this._cesiumWidget.useDefaultRenderLoop = value;
            }
        },

        /**
         * Gets or sets a scaling factor for rendering resolution.  Values less than 1.0 can improve
         * performance on less powerful devices while values greater than 1.0 will render at a higher
         * resolution and then scale down, resulting in improved visual fidelity.
         * For example, if the widget is laid out at a size of 640x480, setting this value to 0.5
         * will cause the scene to be rendered at 320x240 and then scaled up while setting
         * it to 2.0 will cause the scene to be rendered at 1280x960 and then scaled down.
         * @memberof Viewer.prototype
         *
         * @type {Number}
         * @default 1.0
         */
        resolutionScale : {
            get : function() {
                return this._cesiumWidget.resolutionScale;
            },
            set : function(value) {
                this._cesiumWidget.resolutionScale = value;
                this._forceResize = true;
            }
        },

        /**
         * Gets or sets whether or not data sources can temporarily pause
         * animation in order to avoid showing an incomplete picture to the user.
         * For example, if asynchronous primitives are being processed in the
         * background, the clock will not advance until the geometry is ready.
         *
         * @memberof Viewer.prototype
         *
         * @type {Boolean}
         */
        allowDataSourcesToSuspendAnimation : {
            get : function() {
                return this._allowDataSourcesToSuspendAnimation;
            },
            set : function(value) {
                this._allowDataSourcesToSuspendAnimation = value;
            }
        },

        /**
         * Gets or sets the Entity instance currently being tracked by the camera.
         * @memberof Viewer.prototype
         * @type {Entity}
         */
        trackedEntity : {
            get : function() {
                return this._trackedEntity;
            },
            set : function(value) {
                if (this._trackedEntity !== value) {
                    this._trackedEntity = value;

                    //Cancel any pending zoom
                    cancelZoom(this);

                    var scene = this.scene;
                    var sceneMode = scene.mode;

                    //Stop tracking
                    if (!defined(value) || !defined(value.position)) {
                        this._needTrackedEntityUpdate = false;
                        if (sceneMode === SceneMode.COLUMBUS_VIEW || sceneMode === SceneMode.SCENE2D) {
                            scene.screenSpaceCameraController.enableTranslate = true;
                        }

                        if (sceneMode === SceneMode.COLUMBUS_VIEW || sceneMode === SceneMode.SCENE3D) {
                            scene.screenSpaceCameraController.enableTilt = true;
                        }

                        this._entityView = undefined;
                        this.camera.lookAtTransform(Matrix4.IDENTITY);
                        return;
                    }

                    //We can't start tracking immediately, so we set a flag and start tracking
                    //when the bounding sphere is ready (most likely next frame).
                    this._needTrackedEntityUpdate = true;
                }
            }
        },
        /**
         * Gets or sets the object instance for which to display a selection indicator.
         * @memberof Viewer.prototype
         * @type {Entity}
         */
        selectedEntity : {
            get : function() {
                return this._selectedEntity;
            },
            set : function(value) {
                if (this._selectedEntity !== value) {
                    this._selectedEntity = value;
                    var selectionIndicatorViewModel = defined(this._selectionIndicator) ? this._selectionIndicator.viewModel : undefined;
                    if (defined(value)) {
                        if (defined(selectionIndicatorViewModel)) {
                            selectionIndicatorViewModel.animateAppear();
                        }
                    } else {
                        // Leave the info text in place here, it is needed during the exit animation.
                        if (defined(selectionIndicatorViewModel)) {
                            selectionIndicatorViewModel.animateDepart();
                        }
                    }
                }
            }
        },
        /**
         * Gets or sets the data source to track with the viewer's clock.
         * @type {DataSource}
         */
        clockTrackedDataSource : {
            get : function() {
                return this._clockTrackedDataSource;
            },
            set : function(value) {
                if (this._clockTrackedDataSource !== value) {
                    this._clockTrackedDataSource = value;
                    trackDataSourceClock(this._timeline, this.clock, value);
                }
            }
        }
    });

    /**
     * Extends the base viewer functionality with the provided mixin.
     * A mixin may add additional properties, functions, or other behavior
     * to the provided viewer instance.
     *
     * @param {Viewer~ViewerMixin} mixin The Viewer mixin to add to this instance.
     * @param {Object} [options] The options object to be passed to the mixin function.
     *
     * @see viewerDragDropMixin
     */
    Viewer.prototype.extend = function(mixin, options) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(mixin)) {
            throw new DeveloperError('mixin is required.');
        }
        //>>includeEnd('debug')

        mixin(this, options);
    };

    /**
     * Resizes the widget to match the container size.
     * This function is called automatically as needed unless
     * <code>useDefaultRenderLoop</code> is set to false.
     */
    Viewer.prototype.resize = function() {
        var cesiumWidget = this._cesiumWidget;
        var container = this._container;
        var width = container.clientWidth;
        var height = container.clientHeight;
        var animationExists = defined(this._animation);
        var timelineExists = defined(this._timeline);

        if (!this._forceResize && width === this._lastWidth && height === this._lastHeight) {
            return;
        }

        cesiumWidget.resize();
        this._forceResize = false;
        var panelMaxHeight = height - 125;
        var baseLayerPickerDropDown = this._baseLayerPickerDropDown;

        if (defined(baseLayerPickerDropDown)) {
            baseLayerPickerDropDown.style.maxHeight = panelMaxHeight + 'px';
        }

        if (defined(this._infoBox)) {
            this._infoBox.viewModel.maxHeight = panelMaxHeight;
        }

        var timeline = this._timeline;
        var animationContainer;
        var animationWidth = 0;
        var creditLeft = 0;
        var creditBottom = 0;

        if (animationExists && window.getComputedStyle(this._animation.container).visibility !== 'hidden') {
            var lastWidth = this._lastWidth;
            animationContainer = this._animation.container;
            if (width > 900) {
                animationWidth = 169;
                if (lastWidth <= 900) {
                    animationContainer.style.width = '169px';
                    animationContainer.style.height = '112px';
                    this._animation.resize();
                }
            } else if (width >= 600) {
                animationWidth = 136;
                if (lastWidth < 600 || lastWidth > 900) {
                    animationContainer.style.width = '136px';
                    animationContainer.style.height = '90px';
                    this._animation.resize();
                }
            } else {
                animationWidth = 106;
                if (lastWidth > 600 || lastWidth === 0) {
                    animationContainer.style.width = '106px';
                    animationContainer.style.height = '70px';
                    this._animation.resize();
                }
            }
            creditLeft = animationWidth + 5;
        }

        if (timelineExists && window.getComputedStyle(this._timeline.container).visibility !== 'hidden') {
            var fullscreenButton = this._fullscreenButton;
            var vrButton = this._vrButton;
            var timelineContainer = timeline.container;
            var timelineStyle = timelineContainer.style;

            creditBottom = timelineContainer.clientHeight + 3;
            timelineStyle.left = animationWidth + 'px';

            var pixels = 0;
            if (defined(fullscreenButton)) {
                pixels += fullscreenButton.container.clientWidth;
            }
            if (defined(vrButton)) {
                pixels += vrButton.container.clientWidth;
            }

            timelineStyle.right = pixels + 'px';
            timeline.resize();
        }

        this._bottomContainer.style.left = creditLeft + 'px';
        this._bottomContainer.style.bottom = creditBottom + 'px';

        this._lastWidth = width;
        this._lastHeight = height;
    };

    /**
     * This forces the widget to re-think its layout, including
     * widget sizes and credit placement.
     */
    Viewer.prototype.forceResize = function() {
        this._lastWidth = 0;
        this.resize();
    };

    /**
     * Renders the scene.  This function is called automatically
     * unless <code>useDefaultRenderLoop</code> is set to false;
     */
    Viewer.prototype.render = function() {
        this._cesiumWidget.render();
    };

    /**
     * @returns {Boolean} true if the object has been destroyed, false otherwise.
     */
    Viewer.prototype.isDestroyed = function() {
        return false;
    };

    /**
     * Destroys the widget.  Should be called if permanently
     * removing the widget from layout.
     */
    Viewer.prototype.destroy = function() {
        var i;

        this.screenSpaceEventHandler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK);
        this.screenSpaceEventHandler.removeInputAction(ScreenSpaceEventType.LEFT_DOUBLE_CLICK);

        // Unsubscribe from data sources
        var dataSources = this.dataSources;
        var dataSourceLength = dataSources.length;
        for (i = 0; i < dataSourceLength; i++) {
            this._dataSourceRemoved(dataSources, dataSources.get(i));
        }
        this._dataSourceRemoved(undefined, this._dataSourceDisplay.defaultDataSource);

        this._container.removeChild(this._element);
        this._element.removeChild(this._toolbar);

        this._eventHelper.removeAll();

        if (defined(this._geocoder)) {
            this._geocoder = this._geocoder.destroy();
        }

        if (defined(this._homeButton)) {
            this._homeButton = this._homeButton.destroy();
        }

        if (defined(this._sceneModePicker)) {
            this._sceneModePicker = this._sceneModePicker.destroy();
        }

        if (defined(this._baseLayerPicker)) {
            this._baseLayerPicker = this._baseLayerPicker.destroy();
        }

        if (defined(this._animation)) {
            this._element.removeChild(this._animation.container);
            this._animation = this._animation.destroy();
        }

        if (defined(this._timeline)) {
            this._timeline.removeEventListener('settime', onTimelineScrubfunction, false);
            this._element.removeChild(this._timeline.container);
            this._timeline = this._timeline.destroy();
        }

        if (defined(this._fullscreenButton)) {
            this._fullscreenSubscription.dispose();
            this._element.removeChild(this._fullscreenButton.container);
            this._fullscreenButton = this._fullscreenButton.destroy();
        }

        if (defined(this._vrButton)) {
            this._vrSubscription.dispose();
            this._vrModeSubscription.dispose();
            this._element.removeChild(this._vrButton.container);
            this._vrButton = this._vrButton.destroy();
        }

        if (defined(this._infoBox)) {
            this._element.removeChild(this._infoBox.container);
            this._infoBox = this._infoBox.destroy();
        }

        if (defined(this._selectionIndicator)) {
            this._element.removeChild(this._selectionIndicator.container);
            this._selectionIndicator = this._selectionIndicator.destroy();
        }

        this._clockViewModel = this._clockViewModel.destroy();
        this._dataSourceDisplay = this._dataSourceDisplay.destroy();
        this._cesiumWidget = this._cesiumWidget.destroy();

        if (this._destroyDataSourceCollection) {
            this._dataSourceCollection = this._dataSourceCollection.destroy();
        }

        return destroyObject(this);
    };

    /**
     * @private
     */
    Viewer.prototype._dataSourceAdded = function(dataSourceCollection, dataSource) {
        var entityCollection = dataSource.entities;
        entityCollection.collectionChanged.addEventListener(Viewer.prototype._onEntityCollectionChanged, this);
    };

    /**
     * @private
     */
    Viewer.prototype._dataSourceRemoved = function(dataSourceCollection, dataSource) {
        var entityCollection = dataSource.entities;
        entityCollection.collectionChanged.removeEventListener(Viewer.prototype._onEntityCollectionChanged, this);

        if (defined(this.trackedEntity)) {
            if (entityCollection.getById(this.trackedEntity.id) === this.trackedEntity) {
                this.trackedEntity = undefined;
            }
        }

        if (defined(this.selectedEntity)) {
            if (entityCollection.getById(this.selectedEntity.id) === this.selectedEntity) {
                this.selectedEntity = undefined;
            }
        }
    };

    /**
     * @private
     */
    Viewer.prototype._onTick = function(clock) {
        var time = clock.currentTime;

        var isUpdated = this._dataSourceDisplay.update(time);
        if (this._allowDataSourcesToSuspendAnimation) {
            this._clockViewModel.canAnimate = isUpdated;
        }

        var entityView = this._entityView;
        if (defined(entityView)) {
            var trackedEntity = this._trackedEntity;
            var trackedState = this._dataSourceDisplay.getBoundingSphere(trackedEntity, false, boundingSphereScratch);
            if (trackedState === BoundingSphereState.DONE) {
                entityView.update(time, boundingSphereScratch);
            }
        }

        var position;
        var enableCamera = false;
        var selectedEntity = this.selectedEntity;
        var showSelection = defined(selectedEntity) && this._enableInfoOrSelection;

        if (showSelection && selectedEntity.isShowing && selectedEntity.isAvailable(time)) {
            var state = this._dataSourceDisplay.getBoundingSphere(selectedEntity, true, boundingSphereScratch);
            if (state !== BoundingSphereState.FAILED) {
                position = boundingSphereScratch.center;
            } else if (defined(selectedEntity.position)) {
                position = selectedEntity.position.getValue(time, position);
            }
            enableCamera = defined(position);
        }

        var selectionIndicatorViewModel = defined(this._selectionIndicator) ? this._selectionIndicator.viewModel : undefined;
        if (defined(selectionIndicatorViewModel)) {
            selectionIndicatorViewModel.position = Cartesian3.clone(position, selectionIndicatorViewModel.position);
            selectionIndicatorViewModel.showSelection = showSelection && enableCamera;
            selectionIndicatorViewModel.update();
        }

        var infoBoxViewModel = defined(this._infoBox) ? this._infoBox.viewModel : undefined;
        if (defined(infoBoxViewModel)) {
            infoBoxViewModel.showInfo = showSelection;
            infoBoxViewModel.enableCamera = enableCamera;
            infoBoxViewModel.isCameraTracking = (this.trackedEntity === this.selectedEntity);

            if (showSelection) {
                infoBoxViewModel.titleText = defaultValue(selectedEntity.name, selectedEntity.id);
                infoBoxViewModel.description = Property.getValueOrDefault(selectedEntity.description, time, '');
            } else {
                infoBoxViewModel.titleText = '';
                infoBoxViewModel.description = '';
            }
        }
    };

    /**
     * @private
     */
    Viewer.prototype._onEntityCollectionChanged = function(collection, added, removed) {
        var length = removed.length;
        for (var i = 0; i < length; i++) {
            var removedObject = removed[i];
            if (this.trackedEntity === removedObject) {
                this.trackedEntity = undefined;
            }
            if (this.selectedEntity === removedObject) {
                this.selectedEntity = undefined;
            }
        }
    };

    /**
     * @private
     */
    Viewer.prototype._onInfoBoxCameraClicked = function(infoBoxViewModel) {
        if (infoBoxViewModel.isCameraTracking && (this.trackedEntity === this.selectedEntity)) {
            this.trackedEntity = undefined;
        } else {
            var selectedEntity = this.selectedEntity;
            var position = selectedEntity.position;
            if (defined(position)) {
                this.trackedEntity = this.selectedEntity;
            } else {
                this.zoomTo(this.selectedEntity);
            }
        }
    };

    /**
     * @private
     */
    Viewer.prototype._clearTrackedObject = function() {
        this.trackedEntity = undefined;
    };

    /**
     * @private
     */
    Viewer.prototype._onInfoBoxClockClicked = function(infoBoxViewModel) {
        this.selectedEntity = undefined;
    };

    /**
     * @private
     */
    Viewer.prototype._clearObjects = function() {
        this.trackedEntity = undefined;
        this.selectedEntity = undefined;
    };

    /**
     * @private
     */
    Viewer.prototype._onDataSourceChanged = function(dataSource) {
        if (this.clockTrackedDataSource === dataSource) {
            trackDataSourceClock(this.timeline, this.clock, dataSource);
        }
    };

    /**
     * @private
     */
    Viewer.prototype._onDataSourceAdded = function(dataSourceCollection, dataSource) {
        if (this._automaticallyTrackDataSourceClocks) {
            this.clockTrackedDataSource = dataSource;
        }
        var id = dataSource.entities.id;
        var removalFunc = this._eventHelper.add(dataSource.changedEvent, Viewer.prototype._onDataSourceChanged, this);
        this._dataSourceChangedListeners[id] = removalFunc;
    };

    /**
     * @private
     */
    Viewer.prototype._onDataSourceRemoved = function(dataSourceCollection, dataSource) {
        var resetClock = (this.clockTrackedDataSource === dataSource);
        var id = dataSource.entities.id;
        this._dataSourceChangedListeners[id]();
        this._dataSourceChangedListeners[id] = undefined;
        if (resetClock) {
            var numDataSources = dataSourceCollection.length;
            if (this._automaticallyTrackDataSourceClocks && numDataSources > 0) {
                this.clockTrackedDataSource = dataSourceCollection.get(numDataSources - 1);
            } else {
                this.clockTrackedDataSource = undefined;
            }
        }
    };

    /**
     * Asynchronously sets the camera to view the provided entity, entities, or data source.
     * If the data source is still in the process of loading or the visualization is otherwise still loading,
     * this method waits for the data to be ready before performing the zoom.
     *
     * <p>The offset is heading/pitch/range in the local east-north-up reference frame centered at the center of the bounding sphere.
     * The heading and the pitch angles are defined in the local east-north-up reference frame.
     * The heading is the angle from y axis and increasing towards the x axis. Pitch is the rotation from the xy-plane. Positive pitch
     * angles are above the plane. Negative pitch angles are below the plane. The range is the distance from the center. If the range is
     * zero, a range will be computed such that the whole bounding sphere is visible.</p>
     *
     * <p>In 2D, there must be a top down view. The camera will be placed above the target looking down. The height above the
     * target will be the range. The heading will be determined from the offset. If the heading cannot be
     * determined from the offset, the heading will be north.</p>
     *
     * @param {Entity|Entity[]|EntityCollection|DataSource|ImageryLayer|Promise.<Entity|Entity[]|EntityCollection|DataSource|ImageryLayer>} target The entity, array of entities, entity collection, data source or imagery layer to view. You can also pass a promise that resolves to one of the previously mentioned types.
     * @param {HeadingPitchRange} [offset] The offset from the center of the entity in the local east-north-up reference frame.
     * @returns {Promise.<Boolean>} A Promise that resolves to true if the zoom was successful or false if the entity is not currently visualized in the scene or the zoom was cancelled.
     */
    Viewer.prototype.zoomTo = function(target, offset) {
        return zoomToOrFly(this, target, offset, false);
    };

    /**
     * Flies the camera to the provided entity, entities, or data source.
     * If the data source is still in the process of loading or the visualization is otherwise still loading,
     * this method waits for the data to be ready before performing the flight.
     *
     * <p>The offset is heading/pitch/range in the local east-north-up reference frame centered at the center of the bounding sphere.
     * The heading and the pitch angles are defined in the local east-north-up reference frame.
     * The heading is the angle from y axis and increasing towards the x axis. Pitch is the rotation from the xy-plane. Positive pitch
     * angles are above the plane. Negative pitch angles are below the plane. The range is the distance from the center. If the range is
     * zero, a range will be computed such that the whole bounding sphere is visible.</p>
     *
     * <p>In 2D, there must be a top down view. The camera will be placed above the target looking down. The height above the
     * target will be the range. The heading will be determined from the offset. If the heading cannot be
     * determined from the offset, the heading will be north.</p>
     *
     * @param {Entity|Entity[]|EntityCollection|DataSource|ImageryLayer|Promise.<Entity|Entity[]|EntityCollection|DataSource|ImageryLayer>} target The entity, array of entities, entity collection, data source or imagery layer to view. You can also pass a promise that resolves to one of the previously mentioned types.
     * @param {Object} [options] Object with the following properties:
     * @param {Number} [options.duration=3.0] The duration of the flight in seconds.
     * @param {Number} [options.maximumHeight] The maximum height at the peak of the flight.
     * @param {HeadingPitchRange} [options.offset] The offset from the target in the local east-north-up reference frame centered at the target.
     * @returns {Promise.<Boolean>} A Promise that resolves to true if the flight was successful or false if the entity is not currently visualized in the scene or the flight was cancelled.
     */
    Viewer.prototype.flyTo = function(target, options) {
        return zoomToOrFly(this, target, options, true);
    };

    function zoomToOrFly(that, zoomTarget, options, isFlight) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(zoomTarget)) {
            throw new DeveloperError('zoomTarget is required.');
        }
        //>>includeEnd('debug');

        cancelZoom(that);

        //We can't actually perform the zoom until all visualization is ready and
        //bounding spheres have been computed.  Therefore we create and return
        //a deferred which will be resolved as part of the post-render step in the
        //frame that actually performs the zoom
        var zoomPromise = when.defer();
        that._zoomPromise = zoomPromise;
        that._zoomIsFlight = isFlight;
        that._zoomOptions = options;

        when(zoomTarget, function(zoomTarget) {
            //Only perform the zoom if it wasn't cancelled before the promise resolved.
            if (that._zoomPromise !== zoomPromise) {
                return;
            }

            //If the zoom target is a rectangular imagery in an ImageLayer
            if (zoomTarget instanceof ImageryLayer) {
                zoomTarget.getViewableRectangle().then(function(rectangle) {
                    //Only perform the zoom if it wasn't cancelled before the promise was resolved
                    if (that._zoomPromise === zoomPromise) {
                        that._zoomTarget = rectangle;
                    }
                });
                return;
            }

            //If the zoom target is a data source, and it's in the middle of loading, wait for it to finish loading.
            if (zoomTarget.isLoading && defined(zoomTarget.loadingEvent)) {
                var removeEvent = zoomTarget.loadingEvent.addEventListener(function() {
                    removeEvent();

                    //Only perform the zoom if it wasn't cancelled before the data source finished.
                    if (that._zoomPromise === zoomPromise) {
                        that._zoomTarget = zoomTarget.entities.values.slice(0);
                    }
                });
                return;
            }

            //Zoom target is already an array, just copy it and return.
            if (isArray(zoomTarget)) {
                that._zoomTarget = zoomTarget.slice(0);
                return;
            }

            //If zoomTarget is an EntityCollection, this will retrieve the array
            zoomTarget = defaultValue(zoomTarget.values, zoomTarget);

            //If zoomTarget is a DataSource, this will retrieve the array.
            if (defined(zoomTarget.entities)) {
                zoomTarget = zoomTarget.entities.values;
            }

            if (isArray(zoomTarget)) {
                that._zoomTarget = zoomTarget.slice(0);
            } else {
                //Single entity
                that._zoomTarget = [zoomTarget];
            }
        });

        return zoomPromise.promise;
    }

    function clearZoom(viewer) {
        viewer._zoomPromise = undefined;
        viewer._zoomTarget = undefined;
        viewer._zoomOptions = undefined;
    }

    function cancelZoom(viewer) {
        var zoomPromise = viewer._zoomPromise;
        if (defined(zoomPromise)) {
            clearZoom(viewer);
            zoomPromise.resolve(false);
        }
    }

    /**
     * @private
     */
    Viewer.prototype._postRender = function() {
        updateZoomTarget(this);
        updateTrackedEntity(this);
    };

    function updateZoomTarget(viewer) {
        var entities = viewer._zoomTarget;
        if (!defined(entities) || viewer.scene.mode === SceneMode.MORPHING) {
            return;
        }

        var scene = viewer.scene;
        var camera = scene.camera;
        var zoomPromise = viewer._zoomPromise;
        var zoomOptions = defaultValue(viewer._zoomOptions, {});

        //If zoomTarget was an ImageryLayer
        if (entities instanceof Rectangle) {
            var options = {
                destination : entities,
                duration : zoomOptions.duration,
                maximumHeight : zoomOptions.maximumHeight,
                complete : function() {
                    zoomPromise.resolve(true);
                },
                cancel : function() {
                    zoomPromise.resolve(false);
                }
            };

            if (viewer._zoomIsFlight) {
                camera.flyTo(options);
            } else {
                camera.setView(options);
                zoomPromise.resolve(true);
            }
            clearZoom(viewer);
            return;
        }

        var boundingSpheres = [];
        for (var i = 0, len = entities.length; i < len; i++) {
            var state = viewer._dataSourceDisplay.getBoundingSphere(entities[i], false, boundingSphereScratch);

            if (state === BoundingSphereState.PENDING) {
                return;
            } else if (state !== BoundingSphereState.FAILED) {
                boundingSpheres.push(BoundingSphere.clone(boundingSphereScratch));
            }
        }

        if (boundingSpheres.length === 0) {
            cancelZoom(viewer);
            return;
        }

        //Stop tracking the current entity.
        viewer.trackedEntity = undefined;

        var boundingSphere = BoundingSphere.fromBoundingSpheres(boundingSpheres);
        var controller = scene.screenSpaceCameraController;
        controller.minimumZoomDistance = Math.min(controller.minimumZoomDistance, boundingSphere.radius * 0.5);

        if (!viewer._zoomIsFlight) {
            camera.viewBoundingSphere(boundingSphere, viewer._zoomOptions);
            camera.lookAtTransform(Matrix4.IDENTITY);
            clearZoom(viewer);
            zoomPromise.resolve(true);
        } else {
            clearZoom(viewer);
            camera.flyToBoundingSphere(boundingSphere, {
                duration : zoomOptions.duration,
                maximumHeight : zoomOptions.maximumHeight,
                complete : function() {
                    zoomPromise.resolve(true);
                },
                cancel : function() {
                    zoomPromise.resolve(false);
                },
                offset : zoomOptions.offset
            });
        }
    }

    function updateTrackedEntity(viewer) {
        if (!viewer._needTrackedEntityUpdate) {
            return;
        }

        var trackedEntity = viewer._trackedEntity;
        var currentTime = viewer.clock.currentTime;

        //Verify we have a current position at this time. This is only triggered if a position
        //has become undefined after trackedEntity is set but before the boundingSphere has been
        //computed. In this case, we will track the entity once it comes back into existence.
        var currentPosition = Property.getValueOrUndefined(trackedEntity.position, currentTime);

        if (!defined(currentPosition)) {
            return;
        }

        var scene = viewer.scene;

        var state = viewer._dataSourceDisplay.getBoundingSphere(trackedEntity, false, boundingSphereScratch);
        if (state === BoundingSphereState.PENDING) {
            return;
        }

        var sceneMode = scene.mode;
        if (sceneMode === SceneMode.COLUMBUS_VIEW || sceneMode === SceneMode.SCENE2D) {
            scene.screenSpaceCameraController.enableTranslate = false;
        }

        if (sceneMode === SceneMode.COLUMBUS_VIEW || sceneMode === SceneMode.SCENE3D) {
            scene.screenSpaceCameraController.enableTilt = false;
        }

        var bs = state !== BoundingSphereState.FAILED ? boundingSphereScratch : undefined;
        viewer._entityView = new EntityView(trackedEntity, scene, scene.mapProjection.ellipsoid);
        viewer._entityView.update(currentTime, bs);
        viewer._needTrackedEntityUpdate = false;
    }

    /**
     * A function that augments a Viewer instance with additional functionality.
     * @callback Viewer~ViewerMixin
     * @param {Viewer} viewer The viewer instance.
     * @param {Object} options Options object to be passed to the mixin function.
     *
     * @see Viewer#extend
     */

    return Viewer;
});