Source: WorldWindow.js

/*
 * Copyright 2015-2017 WorldWind Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * @exports WorldWindow
 */
define([
        './error/ArgumentError',
        './render/DrawContext',
        './globe/EarthElevationModel',
        './util/FrameStatistics',
        './globe/Globe',
        './globe/Globe2D',
        './util/GoToAnimator',
        './cache/GpuResourceCache',
        './util/Logger',
        './navigate/LookAtNavigator',
        './navigate/NavigatorState',
        './pick/PickedObjectList',
        './geom/Rectangle',
        './geom/Sector',
        './shapes/SurfaceShape',
        './shapes/SurfaceShapeTileBuilder',
        './globe/Terrain',
        './geom/Vec2'],
    function (ArgumentError,
              DrawContext,
              EarthElevationModel,
              FrameStatistics,
              Globe,
              Globe2D,
              GoToAnimator,
              GpuResourceCache,
              Logger,
              LookAtNavigator,
              NavigatorState,
              PickedObjectList,
              Rectangle,
              Sector,
              SurfaceShape,
              SurfaceShapeTileBuilder,
              Terrain,
              Vec2) {
        "use strict";

        /**
         * Constructs a WorldWind window for an HTML canvas.
         * @alias WorldWindow
         * @constructor
         * @classdesc Represents a WorldWind window for an HTML canvas.
         * @param {String} canvasName The name assigned to the HTML canvas in the document.
         * @param {ElevationModel} elevationModel An optional argument indicating the elevation model to use for the World
         * Window. If missing or null, a default elevation model is used.
         * @throws {ArgumentError} If there is no HTML element with the specified name in the document, or if the
         * HTML canvas does not support WebGL.
         */
        var WorldWindow = function (canvasName, elevationModel) {
            if (!(window.WebGLRenderingContext)) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "constructor",
                        "The specified canvas does not support WebGL."));
            }

            // Attempt to get the HTML canvas with the specified name.
            var canvas = document.getElementById(canvasName);
            if (!canvas) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "constructor",
                        "The specified canvas name is not in the document."));
            }

            // Create the WebGL context associated with the HTML canvas.
            var gl = this.createContext(canvas);

            // Internal. Intentionally not documented.
            this.drawContext = new DrawContext(gl);

            // Internal. Intentionally not documented. Must be initialized before the navigator is created.
            this.eventListeners = {};

            // Internal. Intentionally not documented. Initially true in order to redraw at least once.
            this.redrawRequested = true;

            // Internal. Intentionally not documented.
            this.redrawRequestId = null;

            /**
             * The HTML canvas associated with this WorldWindow.
             * @type {HTMLElement}
             * @readonly
             */
            this.canvas = canvas;

            /**
             * The number of bits in the depth buffer associated with this WorldWindow.
             * @type {number}
             * @readonly
             */
            this.depthBits = gl.getParameter(gl.DEPTH_BITS);

            /**
             * The current viewport of this WorldWindow.
             * @type {Rectangle}
             * @readonly
             */
            this.viewport = new Rectangle(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);

            /**
             * The globe displayed.
             * @type {Globe}
             */
            this.globe = new Globe(elevationModel || new EarthElevationModel());

            /**
             * The layers to display in this WorldWindow.
             * This property is read-only. Use [addLayer]{@link WorldWindow#addLayer} or
             * [insertLayer]{@link WorldWindow#insertLayer} to add layers to this WorldWindow.
             * Use [removeLayer]{@link WorldWindow#removeLayer} to remove layers from this WorldWindow.
             * @type {Layer[]}
             * @readonly
             */
            this.layers = [];

            /**
             * The navigator used to manipulate the globe.
             * @type {LookAtNavigator}
             * @default [LookAtNavigator]{@link LookAtNavigator}
             */
            this.navigator = new LookAtNavigator(this);

            /**
             * The vertical exaggeration to apply to the terrain.
             * @type {Number}
             */
            this.verticalExaggeration = 1;

            /**
             * Indicates that picking will return all objects at the pick point, if any. The top-most object will have
             * its isOnTop flag set to true.
             * If deep picking is false, the default, only the top-most object is returned, plus
             * the picked-terrain object if the pick point is over the terrain.
             * @type {boolean}
             * @default false
             */
            this.deepPicking = false;

            /**
             * Indicates whether this WorldWindow should be configured for sub-surface rendering. If true, shapes
             * below the terrain can be seen when the terrain is made transparent. If false, sub-surface shapes are
             * not visible, however, performance is slightly increased.
             * @type {boolean}
             * @default false
             */
            this.subsurfaceMode = false;

            /**
             * The opacity to apply to terrain and surface shapes. This property is typically used when viewing
             * the sub-surface. It modifies the opacity of the terrain and surface shapes as a whole. It should be
             * a number between 0 and 1. It is compounded with the individual opacities of the image layers and
             * surface shapes on the terrain.
             * @type {Number}
             * @default 1
             */
            this.surfaceOpacity = 1;

            /**
             * Performance statistics for this WorldWindow.
             * @type {FrameStatistics}
             */
            this.frameStatistics = new FrameStatistics();

            /**
             * The {@link GoToAnimator} used by this WorldWindow to respond to its goTo method.
             * @type {GoToAnimator}
             */
            this.goToAnimator = new GoToAnimator(this);

            // Documented with its property accessor below.
            this._redrawCallbacks = [];

            // Documented with its property accessor below.
            this._orderedRenderingFilters = [
                function (dc) {
                    thisWindow.declutter(dc, 1);
                },
                function (dc) {
                    thisWindow.declutter(dc, 2);
                }
            ];

            // Intentionally not documented.
            this.pixelScale = 1;

            // Set up to handle WebGL context lost events.
            var thisWindow = this;

            function handleContextLost(event) {
                thisWindow.handleContextLost(event);
            }

            this.canvas.addEventListener("webglcontextlost", handleContextLost, false);

            // Set up to handle WebGL context restored events.
            function handleContextRestored(event) {
                thisWindow.handleContextRestored(event);
            }

            this.canvas.addEventListener("webglcontextrestored", handleContextRestored, false);

            // Set up to handle WebGL context events and WorldWind redraw request events. Imagery uses the canvas
            // redraw events because images are generally specific to the WebGL context associated with the canvas.
            // Elevation models use the global window redraw events because they can be shared among WorldWindows.
            function handleRedrawEvent(event) {
                thisWindow.handleRedrawEvent(event)
            }

            this.canvas.addEventListener(WorldWind.REDRAW_EVENT_TYPE, handleRedrawEvent, false);
            window.addEventListener(WorldWind.REDRAW_EVENT_TYPE, handleRedrawEvent, false);

            // Render to the WebGL context in an animation frame loop until the WebGL context is lost.
            this.animationFrameLoop();
        };

        Object.defineProperties(WorldWindow.prototype, {
            /**
             * An array of functions to call during ordered rendering prior to rendering the ordered renderables.
             * Each function is passed one argument, the current draw context. The function may modify the
             * ordered renderables in the draw context's ordered renderable list, which has been sorted from front
             * to back when the filter function is called. Ordered rendering filters are typically used to apply
             * decluttering. The default set of filter functions contains one function that declutters shapes with
             * declutter group ID of 1 ({@link GeographicText} by default) and one function that declutters shapes
             * with declutter group ID 2 ({@link Placemark} by default). Applications can add functions to this
             * array or remove them.
             * @type {Function[]}
             * @default [WorldWindow.declutter]{@link WorldWindow#declutter} with a group ID of 1
             * @readonly
             * @memberof WorldWindow.prototype
             */
            orderedRenderingFilters: {
                get: function () {
                    return this._orderedRenderingFilters;
                }
            },
            /**
             * The list of callbacks to call immediately before and immediately after performing a redraw. The callbacks
             * have two arguments: this WorldWindow and the redraw stage, e.g., <code style='white-space:nowrap'>redrawCallback(worldWindow, stage);</code>.
             * The stage will be either WorldWind.BEFORE_REDRAW or WorldWind.AFTER_REDRAW indicating whether the
             * callback has been called either immediately before or immediately after a redraw, respectively.
             * Applications may add functions to this array or remove them.
             * @type {Function[]}
             * @readonly
             * @memberof WorldWindow.prototype
             */
            redrawCallbacks: {
                get: function () {
                    return this._redrawCallbacks;
                }
            }
        });

        /**
         * Converts window coordinates to coordinates relative to this WorldWindow's canvas.
         * @param {Number} x The X coordinate to convert.
         * @param {Number} y The Y coordinate to convert.
         * @returns {Vec2} The converted coordinates.
         */
        WorldWindow.prototype.canvasCoordinates = function (x, y) {
            var bbox = this.canvas.getBoundingClientRect(),
                xc = x - (bbox.left + this.canvas.clientLeft),// * (this.canvas.width / bbox.width),
                yc = y - (bbox.top + this.canvas.clientTop);// * (this.canvas.height / bbox.height);

            return new Vec2(xc, yc);
        };

        /**
         * Registers an event listener for the specified event type on this WorldWindow's canvas. This function
         * delegates the processing of events to the WorldWindow's canvas. For details on this function and its
         * arguments, see the W3C [EventTarget]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
         * documentation.
         *
         * Registering event listeners using this function enables applications to prevent the WorldWindow's default
         * navigation behavior. To prevent default navigation behavior, call the [Event]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-Event}'s
         * preventDefault method from within an event listener for any events the navigator should not respond to.
         *
         * When an event occurs, this calls the registered event listeners in order of reverse registration. Since the
         * WorldWindow registers its navigator event listeners first, application event listeners are called before
         * navigator event listeners.
         *
         * @param type The event type to listen for.
         * @param listener The function to call when the event occurs.
         * @throws {ArgumentError} If any argument is null or undefined.
         */
        WorldWindow.prototype.addEventListener = function (type, listener) {
            if (!type) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "addEventListener", "missingType"));
            }

            if (!listener) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "addEventListener", "missingListener"));
            }

            var thisWorldWindow = this;
            var entry = this.eventListeners[type];
            if (!entry) {
                entry = {
                    listeners: [],
                    callback: function (event) { // calls listeners in reverse registration order
                        event.worldWindow = thisWorldWindow;
                        for (var i = 0, len = entry.listeners.length; i < len; i++) {
                            entry.listeners[i](event);
                        }
                    }
                };
                this.eventListeners[type] = entry;
            }

            var index = entry.listeners.indexOf(listener);
            if (index == -1) { // suppress duplicate listeners
                entry.listeners.splice(0, 0, listener); // insert the listener at the beginning of the list

                if (entry.listeners.length == 1) { // first listener added, add the event listener callback
                    this.canvas.addEventListener(type, entry.callback, false);
                }
            }
        };

        /**
         * Removes an event listener for the specified event type from this WorldWindow's canvas. The listener must be
         * the same object passed to addEventListener. Calling removeEventListener with arguments that do not identify a
         * currently registered listener has no effect.
         *
         * @param type Indicates the event type the listener registered for.
         * @param listener The listener to remove. Must be the same function object passed to addEventListener.
         * @throws {ArgumentError} If any argument is null or undefined.
         */
        WorldWindow.prototype.removeEventListener = function (type, listener) {
            if (!type) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "removeEventListener", "missingType"));
            }

            if (!listener) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "removeEventListener", "missingListener"));
            }

            var entry = this.eventListeners[type];
            if (!entry) {
                return; // no entry for the specified type
            }

            var index = entry.listeners.indexOf(listener);
            if (index != -1) {
                entry.listeners.splice(index, 1); // remove the listener from the list

                if (entry.listeners.length == 0) { // last listener removed, remove the event listener callback
                    this.canvas.removeEventListener(type, entry.callback, false);
                }
            }
        };

        /**
         * Causes this WorldWindow to redraw itself at the next available opportunity. The redraw occurs on the main
         * thread at a time of the browser's discretion. Applications should call redraw after changing the World
         * Window's state, but should not expect that change to be reflected on screen immediately after this function
         * returns. This is the preferred method for requesting a redraw of the WorldWindow.
         */
        WorldWindow.prototype.redraw = function () {
            this.redrawRequested = true; // redraw during the next animation frame
        };

        /**
         * Requests the WorldWind objects displayed at a specified screen-coordinate point.
         *
         * If the point intersects the terrain, the returned list contains an object identifying the associated geographic
         * position. This returns an empty list when nothing in the WorldWind scene intersects the specified point.
         *
         * @param pickPoint The point to examine in this WorldWindow's screen coordinates.
         * @returns {PickedObjectList} A list of picked WorldWind objects at the specified pick point.
         * @throws {ArgumentError} If the specified pick point is null or undefined.
         */
        WorldWindow.prototype.pick = function (pickPoint) {
            if (!pickPoint) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "pick", "missingPoint"));
            }

            // Suppress the picking operation and return an empty list when the WebGL context has been lost.
            if (this.drawContext.currentGlContext.isContextLost()) {
                return new PickedObjectList();
            }

            this.resize();
            this.resetDrawContext();
            this.drawContext.pickingMode = true;
            this.drawContext.pickPoint = pickPoint;
            this.drawFrame();

            return this.drawContext.objectsAtPickPoint;
        };

        /**
         * Requests the position of the WorldWind terrain at a specified screen-coordinate point. If the point
         * intersects the terrain, the returned list contains a single object identifying the associated geographic
         * position. Otherwise this returns an empty list.
         * @param pickPoint The point to examine in this WorldWindow's screen coordinates.
         * @returns {PickedObjectList} A list containing the picked WorldWind terrain position at the specified point,
         * or an empty list if the point does not intersect the terrain.
         * @throws {ArgumentError} If the specified pick point is null or undefined.
         */
        WorldWindow.prototype.pickTerrain = function (pickPoint) {
            if (!pickPoint) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "pickTerrain", "missingPoint"));
            }

            // Suppress the picking operation and return an empty list when the WebGL context has been lost.
            if (this.drawContext.currentGlContext.isContextLost()) {
                return new PickedObjectList();
            }

            this.resize();
            this.resetDrawContext();
            this.drawContext.pickingMode = true;
            this.drawContext.pickTerrainOnly = true;
            this.drawContext.pickPoint = pickPoint;
            this.drawFrame();

            return this.drawContext.objectsAtPickPoint;
        };

        /**
         * Requests the WorldWind objects displayed within a specified screen-coordinate region. This returns all
         * objects that intersect the specified region, regardless of whether or not an object is actually visible, and
         * marks objects that are visible as on top.
         * @param {Rectangle} rectangle The screen coordinate rectangle identifying the region to search.
         * @returns {PickedObjectList} A list of visible WorldWind objects within the specified region.
         * @throws {ArgumentError} If the specified rectangle is null or undefined.
         */
        WorldWindow.prototype.pickShapesInRegion = function (rectangle) {
            if (!rectangle) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "pickShapesInRegion", "missingRectangle"));
            }

            // Suppress the picking operation and return an empty list when the WebGL context has been lost.
            if (this.drawContext.currentGlContext.isContextLost()) {
                return new PickedObjectList();
            }

            this.resize();
            this.resetDrawContext();
            this.drawContext.pickingMode = true;
            this.drawContext.regionPicking = true;
            this.drawContext.pickRectangle =
                new Rectangle(rectangle.x, this.canvas.height - rectangle.y, rectangle.width, rectangle.height);
            this.drawFrame();

            return this.drawContext.objectsAtPickPoint;
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.createContext = function (canvas) {
            // Request a WebGL context with antialiasing is disabled. Antialiasing causes gaps to appear at the edges of
            // terrain tiles.
            var glAttrs = {antialias: false, stencil: true},
                gl = canvas.getContext("webgl", glAttrs);
            if (!gl) {
                gl = canvas.getContext("experimental-webgl", glAttrs);
            }

            var actualAttributes = gl.getContextAttributes();
            this.hasStencilBuffer = actualAttributes.stencil;

            // uncomment to debug WebGL
            //var gl = WebGLDebugUtils.makeDebugContext(this.canvas.getContext("webgl"),
            //        this.throwOnGLError,
            //        this.logAndValidate
            //);

            return gl;
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.handleContextLost = function (event) {
            Logger.log(Logger.LEVEL_INFO, "WebGL context event: " + event.statusMessage);
            // Inform WebGL that we handle context restoration, enabling the context restored event to be delivered.
            event.preventDefault();
            // Notify the draw context that the WebGL rendering context has been lost.
            this.drawContext.contextLost();
            // Stop the rendering animation frame loop, resuming only if the WebGL context is restored.
            window.cancelAnimationFrame(this.redrawRequestId);
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.handleContextRestored = function (event) {
            Logger.log(Logger.LEVEL_INFO, "WebGL context event: " + event.statusMessage);
            // Notify the draw context that the WebGL rendering context has been restored.
            this.drawContext.contextRestored();
            // Resume the rendering animation frame loop until the WebGL context is lost.
            this.redraw();
            this.animationFrameLoop();
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.handleRedrawEvent = function (event) {
            this.redraw(); // redraw in the next animation frame
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.animationFrameLoop = function () {
            // Render to the WebGL context as needed.
            this.redrawIfNeeded();

            // Continue the animation frame loop until the WebGL context is lost.
            var thisWindow = this;

            function animationFrameCallback() {
                thisWindow.animationFrameLoop();
            }

            this.redrawRequestId = window.requestAnimationFrame(animationFrameCallback);
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.redrawIfNeeded = function () {
            // Check if the drawing buffer needs to resize to match its screen size, which requires a redraw.
            this.resize();

            // Redraw the WebGL drawing buffer only when necessary.
            if (!this.redrawRequested) {
                return;
            }

            try {
                // Prepare to redraw and notify the redraw callbacks that a redraw is about to occur.
                this.redrawRequested = false;
                this.drawContext.previousRedrawTimestamp = this.drawContext.timestamp;
                this.callRedrawCallbacks(WorldWind.BEFORE_REDRAW);
                // Redraw the WebGL drawing buffer.
                this.resetDrawContext();
                this.drawFrame();
            } catch (e) {
                Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "redrawIfNeeded",
                    "Exception occurred during redrawing.\n" + e.toString());
            } finally {
                // Notify the redraw callbacks that a redraw has completed.
                this.callRedrawCallbacks(WorldWind.AFTER_REDRAW);
                // Handle rendering code redraw requests.
                if (this.drawContext.redrawRequested) {
                    this.redrawRequested = true;
                }
            }
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.resize = function () {
            var gl = this.drawContext.currentGlContext,
                width = gl.canvas.clientWidth * this.pixelScale,
                height = gl.canvas.clientHeight * this.pixelScale;

            if (gl.canvas.width != width ||
                gl.canvas.height != height) {

                // Make the canvas drawing buffer size match its screen size.
                gl.canvas.width = width;
                gl.canvas.height = height;

                // Set the WebGL viewport to match the canvas drawing buffer size.
                gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
                this.viewport = new Rectangle(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);

                // Cause this WorldWindow to redraw with the new size.
                this.redrawRequested = true;
            }
        };

        // Internal. Intentionally not documented.
        WorldWindow.prototype.resetDrawContext = function () {
            this.globe.offset = 0;

            var dc = this.drawContext;
            dc.reset();
            dc.globe = this.globe;
            dc.layers = this.layers;
            dc.navigatorState = this.navigator.currentState();
            dc.verticalExaggeration = this.verticalExaggeration;
            dc.surfaceOpacity = this.surfaceOpacity;
            dc.deepPicking = this.deepPicking;
            dc.frameStatistics = this.frameStatistics;
            dc.pixelScale = this.pixelScale;
            dc.update();
        };

        /* useful stuff to debug WebGL */
        /*
         function logGLCall(functionName, args) {
         console.log("gl." + functionName + "(" +
         WebGLDebugUtils.glFunctionArgsToString(functionName, args) + ")");
         };

         function validateNoneOfTheArgsAreUndefined(functionName, args) {
         for (var ii = 0; ii < args.length; ++ii) {
         if (args[ii] === undefined) {
         console.error("undefined passed to gl." + functionName + "(" +
         WebGLDebugUtils.glFunctionArgsToString(functionName, args) + ")");
         }
         }
         };

         WorldWindow.prototype.logAndValidate = function logAndValidate(functionName, args) {
         logGLCall(functionName, args);
         validateNoneOfTheArgsAreUndefined (functionName, args);
         };

         WorldWindow.prototype.throwOnGLError = function throwOnGLError(err, funcName, args) {
         throw WebGLDebugUtils.glEnumToString(err) + " was caused by call to: " + funcName;
         };
         */

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.drawFrame = function () {
            try {
                this.drawContext.frameStatistics.beginFrame();
                this.beginFrame();

                if (this.drawContext.globe.is2D() && this.drawContext.globe.continuous) {
                    this.do2DContiguousRepaint();
                } else {
                    this.doNormalRepaint();
                }

            } finally {
                this.endFrame();
                this.drawContext.frameStatistics.endFrame();
                //console.log(this.drawContext.frameStatistics.frameTime);
            }
        };

        WorldWindow.prototype.doNormalRepaint = function () {
            this.createTerrain();
            this.clearFrame();
            this.deferOrderedRendering = false;
            if (this.drawContext.pickingMode) {
                if (this.drawContext.makePickFrustum()) {
                    this.doPick();
                    this.resolvePick();
                }
            } else {
                this.doDraw();
                if (this.subsurfaceMode && this.hasStencilBuffer) {
                    this.redrawSurface();
                    this.drawScreenRenderables();
                }
            }
        };

        WorldWindow.prototype.do2DContiguousRepaint = function () {
            this.createTerrain2DContiguous();
            this.clearFrame();
            if (this.drawContext.pickingMode) {
                if (this.drawContext.makePickFrustum()) {
                    this.pick2DContiguous();
                    this.resolvePick();
                }
            } else {
                this.draw2DContiguous();
            }
        };

        WorldWindow.prototype.resolvePick = function () {
            if (this.drawContext.pickTerrainOnly) {
                this.resolveTerrainPick();
            } else if (this.drawContext.regionPicking) {
                this.resolveRegionPick();
            } else {
                this.resolveTopPick();
            }
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.beginFrame = function () {
            var gl = this.drawContext.currentGlContext;
            gl.enable(gl.BLEND);
            gl.enable(gl.CULL_FACE);
            gl.enable(gl.DEPTH_TEST);
            gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
            gl.depthFunc(gl.LEQUAL);

            if (this.drawContext.pickingMode) {
                this.drawContext.makePickFramebuffer();
                this.drawContext.bindFramebuffer(this.drawContext.pickFramebuffer);
            }
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.endFrame = function () {
            var gl = this.drawContext.currentGlContext;
            gl.disable(gl.BLEND);
            gl.disable(gl.CULL_FACE);
            gl.disable(gl.DEPTH_TEST);
            gl.blendFunc(gl.ONE, gl.ZERO);
            gl.depthFunc(gl.LESS);
            gl.clearColor(0, 0, 0, 1);

            this.drawContext.bindFramebuffer(null);
            this.drawContext.bindProgram(null);
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.clearFrame = function () {
            var dc = this.drawContext,
                gl = dc.currentGlContext;

            gl.clearColor(dc.clearColor.red, dc.clearColor.green, dc.clearColor.blue, dc.clearColor.alpha);
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.doDraw = function () {
            this.drawContext.renderShapes = true;

            if (this.subsurfaceMode && this.hasStencilBuffer) {
                // Draw the surface and collect the ordered renderables.
                this.drawContext.currentGlContext.disable(this.drawContext.currentGlContext.STENCIL_TEST);
                this.drawContext.surfaceShapeTileBuilder.clear();
                this.drawLayers(true);
                this.drawSurfaceRenderables();
                this.drawContext.surfaceShapeTileBuilder.doRender(this.drawContext);

                if (!this.deferOrderedRendering) {
                    // Clear the depth and stencil buffers prior to rendering the ordered renderables. This allows
                    // sub-surface renderables to be drawn beneath the terrain. Turn on stenciling to capture the
                    // fragments that ordered renderables draw. The terrain and surface shapes will be subsequently
                    // drawn again, and the stencil buffer will ensure that they are drawn only where they overlap
                    // the fragments drawn by the ordered renderables.
                    this.drawContext.currentGlContext.clear(
                        this.drawContext.currentGlContext.DEPTH_BUFFER_BIT | this.drawContext.currentGlContext.STENCIL_BUFFER_BIT);
                    this.drawContext.currentGlContext.enable(this.drawContext.currentGlContext.STENCIL_TEST);
                    this.drawContext.currentGlContext.stencilFunc(this.drawContext.currentGlContext.ALWAYS, 1, 1);
                    this.drawContext.currentGlContext.stencilOp(
                        this.drawContext.currentGlContext.REPLACE, this.drawContext.currentGlContext.REPLACE, this.drawContext.currentGlContext.REPLACE);
                    this.drawOrderedRenderables();

                    this.drawContext.screenCreditController.drawCredits(this.drawContext);
                }
            } else {
                this.drawContext.surfaceShapeTileBuilder.clear();
                this.drawLayers(true);
                this.drawSurfaceRenderables();
                this.drawContext.surfaceShapeTileBuilder.doRender(this.drawContext);

                if (!this.deferOrderedRendering) {
                    this.drawOrderedRenderables();
                    this.drawScreenRenderables();
                }

                this.drawContext.screenCreditController.drawCredits(this.drawContext);
            }
        };

        WorldWindow.prototype.redrawSurface = function () {
            // Draw the terrain and surface shapes but only where the current stencil buffer is non-zero.
            // The non-zero fragments are from drawing the ordered renderables previously.
            this.drawContext.currentGlContext.enable(this.drawContext.currentGlContext.STENCIL_TEST);
            this.drawContext.currentGlContext.stencilFunc(this.drawContext.currentGlContext.EQUAL, 1, 1);
            this.drawContext.currentGlContext.stencilOp(
                this.drawContext.currentGlContext.KEEP, this.drawContext.currentGlContext.KEEP, this.drawContext.currentGlContext.KEEP);
            this.drawContext.surfaceShapeTileBuilder.clear();
            this.drawLayers(false);
            this.drawSurfaceRenderables();
            this.drawContext.surfaceShapeTileBuilder.doRender(this.drawContext);
            this.drawContext.currentGlContext.disable(this.drawContext.currentGlContext.STENCIL_TEST);
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.doPick = function () {
            if (this.drawContext.terrain) {
                this.drawContext.terrain.pick(this.drawContext);
            }

            if (!this.drawContext.pickTerrainOnly) {
                if (this.subsurfaceMode && this.hasStencilBuffer) {
                    // Draw the surface and collect the ordered renderables.
                    this.drawContext.currentGlContext.disable(this.drawContext.currentGlContext.STENCIL_TEST);
                    this.drawContext.surfaceShapeTileBuilder.clear();
                    this.drawLayers(true);
                    this.drawSurfaceRenderables();
                    this.drawContext.surfaceShapeTileBuilder.doRender(this.drawContext);

                    if (!this.deferOrderedRendering) {
                        // Clear the depth and stencil buffers prior to rendering the ordered renderables. This allows
                        // sub-surface renderables to be drawn beneath the terrain. Turn on stenciling to capture the
                        // fragments that ordered renderables draw. The terrain and surface shapes will be subsequently
                        // drawn again, and the stencil buffer will ensure that they are drawn only where they overlap
                        // the fragments drawn by the ordered renderables.
                        this.drawContext.currentGlContext.clear(
                            this.drawContext.currentGlContext.DEPTH_BUFFER_BIT | this.drawContext.currentGlContext.STENCIL_BUFFER_BIT);
                        this.drawContext.currentGlContext.enable(this.drawContext.currentGlContext.STENCIL_TEST);
                        this.drawContext.currentGlContext.stencilFunc(this.drawContext.currentGlContext.ALWAYS, 1, 1);
                        this.drawContext.currentGlContext.stencilOp(
                            this.drawContext.currentGlContext.REPLACE, this.drawContext.currentGlContext.REPLACE, this.drawContext.currentGlContext.REPLACE);
                        this.drawOrderedRenderables();
                        this.drawContext.terrain.pick(this.drawContext);
                        this.drawScreenRenderables();
                    }
                } else {
                    this.drawContext.surfaceShapeTileBuilder.clear();

                    this.drawLayers(true);
                    this.drawSurfaceRenderables();

                    this.drawContext.surfaceShapeTileBuilder.doRender(this.drawContext);

                    if (!this.deferOrderedRendering) {
                        this.drawOrderedRenderables();
                        this.drawScreenRenderables();
                    }
                }
            }
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.createTerrain = function () {
            var dc = this.drawContext;
            dc.terrain = this.globe.tessellator.tessellate(dc);
            dc.frameStatistics.setTerrainTileCount(dc.terrain ? dc.terrain.surfaceGeometry.length : 0);
        };

        WorldWindow.prototype.makeCurrent = function (offset) {
            var dc = this.drawContext;
            dc.globe.offset = offset;
            dc.globeStateKey = dc.globe.stateKey;

            switch (offset) {
                case -1:
                    dc.terrain = this.terrainLeft;
                    break;

                case 0:
                    dc.terrain = this.terrainCenter;
                    break;

                case 1:
                    dc.terrain = this.terrainRight;
                    break;
            }
        };

        WorldWindow.prototype.createTerrain2DContiguous = function () {
            var dc = this.drawContext;

            this.terrainCenter = null;
            dc.globe.offset = 0;
            dc.globeStateKey = dc.globe.stateKey;
            if (dc.globe.intersectsFrustum(dc.navigatorState.frustumInModelCoordinates)) {
                this.terrainCenter = dc.globe.tessellator.tessellate(dc);
            }

            this.terrainRight = null;
            dc.globe.offset = 1;
            dc.globeStateKey = dc.globe.stateKey;
            if (dc.globe.intersectsFrustum(dc.navigatorState.frustumInModelCoordinates)) {
                this.terrainRight = dc.globe.tessellator.tessellate(dc);
            }

            this.terrainLeft = null;
            dc.globe.offset = -1;
            dc.globeStateKey = dc.globe.stateKey;
            if (dc.globe.intersectsFrustum(dc.navigatorState.frustumInModelCoordinates)) {
                this.terrainLeft = dc.globe.tessellator.tessellate(dc);
            }
        };

        WorldWindow.prototype.draw2DContiguous = function () {
            var drawing = "";

            if (this.terrainCenter) {
                drawing += " 0 ";
                this.makeCurrent(0);
                this.deferOrderedRendering = this.terrainLeft || this.terrainRight;
                this.doDraw();
            }

            if (this.terrainRight) {
                drawing += " 1 ";
                this.makeCurrent(1);
                this.deferOrderedRendering = this.terrainLeft || this.terrainLeft;
                this.doDraw();
            }

            this.deferOrderedRendering = false;

            if (this.terrainLeft) {
                drawing += " -1 ";
                this.makeCurrent(-1);
                this.doDraw();
            }
            //
            //console.log(drawing);

            if (this.subsurfaceMode && this.hasStencilBuffer) {
                this.deferOrderedRendering = true;

                if (this.terrainCenter) {
                    drawing += " 0 ";
                    this.makeCurrent(0);
                    this.redrawSurface();
                }

                if (this.terrainRight) {
                    drawing += " 1 ";
                    this.makeCurrent(1);
                    this.redrawSurface();
                }

                if (this.terrainLeft) {
                    drawing += " -1 ";
                    this.makeCurrent(-1);
                    this.redrawSurface();
                }
            }

            this.drawScreenRenderables();
        };

        WorldWindow.prototype.pick2DContiguous = function () {
            if (this.terrainCenter) {
                this.makeCurrent(0);
                this.deferOrderedRendering = this.terrainLeft || this.terrainRight;
                this.doPick();
            }

            if (this.terrainRight) {
                this.makeCurrent(1);
                this.deferOrderedRendering = this.terrainLeft || this.terrainLeft;
                this.doPick();
            }

            this.deferOrderedRendering = false;

            if (this.terrainLeft) {
                this.makeCurrent(-1);
                this.doPick();
            }
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.drawLayers = function (accumulateOrderedRenderables) {
            // Draw all the layers attached to this WorldWindow.

            var beginTime = Date.now(),
                dc = this.drawContext,
                layers = dc.layers,
                layer;

            dc.accumulateOrderedRenderables = accumulateOrderedRenderables;

            for (var i = 0, len = layers.length; i < len; i++) {
                layer = layers[i];
                if (layer) {
                    dc.currentLayer = layer;
                    try {
                        layer.render(dc);
                    } catch (e) {
                        Logger.log(Logger.LEVEL_SEVERE, "Error while rendering layer " + layer.displayName + ".\n"
                            + e.toString());
                        // Keep going. Render the rest of the layers.
                    }
                }
            }

            var now = Date.now();
            dc.frameStatistics.layerRenderingTime = now - beginTime;
        };

        /**
         * Adds a specified layer to the end of this WorldWindow.
         * @param {Layer} layer The layer to add. May be null or undefined, in which case this WorldWindow is not modified.
         */
        WorldWindow.prototype.addLayer = function (layer) {
            this.layers.push(layer);
        };

        /**
         * Removes the first instance of a specified layer from this WorldWindow.
         * @param {Layer} layer The layer to remove. May be null or undefined, in which case this WorldWindow is not
         * modified. This WorldWindow is also not modified if the specified layer does not exist in this world
         * window's layer list.
         */
        WorldWindow.prototype.removeLayer = function (layer) {
            if (!layer)
                return;

            var index = -1;
            for (var i = 0, len = this.layers.length; i < len; i++) {
                if (this.layers[i] == layer) {
                    index = i;
                    break;
                }
            }

            if (index >= 0) {
                this.layers.splice(index, 1);
            }
        };

        /**
         * Inserts a specified layer at a specified position in this WorldWindow's layer list.
         * @param {number} index The index at which to insert the layer. May be negative to specify the position
         * from the end of the array.
         * @param {Layer} layer The layer to insert. This WorldWindow's layer list is not changed if the specified
         * layer is null or undefined.
         */
        WorldWindow.prototype.insertLayer = function (index, layer) {
            if (layer) {
                this.layers.splice(index, 0, layer);
            }
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.drawSurfaceRenderables = function () {
            var dc = this.drawContext,
                sr;

            dc.reverseSurfaceRenderables();

            while (sr = dc.popSurfaceRenderable()) {
                try {
                    sr.renderSurface(dc);
                } catch (e) {
                    Logger.logMessage(Logger.LEVEL_WARNING, "WorldWindow", "drawSurfaceRenderables",
                        "Error while rendering a surface renderable.\n" + e.message);
                    // Keep going. Render the rest of the surface renderables.
                }
            }
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.drawOrderedRenderables = function () {
            var beginTime = Date.now(),
                dc = this.drawContext,
                or;

            dc.sortOrderedRenderables();

            if (this._orderedRenderingFilters) {
                for (var f = 0; f < this._orderedRenderingFilters.length; f++) {
                    this._orderedRenderingFilters[f](this.drawContext);
                }
            }

            dc.orderedRenderingMode = true;

            while (or = dc.popOrderedRenderable()) {
                try {
                    or.renderOrdered(dc);
                } catch (e) {
                    Logger.logMessage(Logger.LEVEL_WARNING, "WorldWindow", "drawOrderedRenderables",
                        "Error while rendering an ordered renderable.\n" + e.message);
                    // Keep going. Render the rest of the ordered renderables.
                }
            }

            dc.orderedRenderingMode = false;
            dc.frameStatistics.orderedRenderingTime = Date.now() - beginTime;
        };

        WorldWindow.prototype.drawScreenRenderables = function () {
            var dc = this.drawContext,
                or;

            while (or = dc.nextScreenRenderable()) {
                try {
                    or.renderOrdered(dc);
                } catch (e) {
                    Logger.logMessage(Logger.LEVEL_WARNING, "WorldWindow", "drawOrderedRenderables",
                        "Error while rendering a screen renderable.\n" + e.message);
                    // Keep going. Render the rest of the screen renderables.
                }
            }
        };

        // Internal function. Intentionally not documented.
        WorldWindow.prototype.resolveTopPick = function () {
            if (this.drawContext.objectsAtPickPoint.objects.length == 0) {
                return; // nothing picked; avoid calling readPickColor unnecessarily
            }

            // Make a last reading to determine what's on top.

            var pickedObjects = this.drawContext.objectsAtPickPoint,
                pickColor = this.drawContext.readPickColor(this.drawContext.pickPoint),
                topObject = null,
                terrainObject = null;

            if (pickColor) {
                // Find the picked object with the top color code and set its isOnTop flag.
                for (var i = 0, len = pickedObjects.objects.length; i < len; i++) {
                    var po = pickedObjects.objects[i];

                    if (po.isTerrain) {
                        terrainObject = po;
                    }

                    if (po.color.equals(pickColor)) {
                        po.isOnTop = true;
                        topObject = po;

                        if (terrainObject) {
                            break; // no need to search for more than the top object and the terrain object
                        }
                    }
                }

                // In single-pick mode provide only the top-most object and the terrain object, if any.
                if (!this.drawContext.deepPicking) {
                    pickedObjects.clear();
                    if (topObject) {
                        pickedObjects.add(topObject);
                    }
                    if (terrainObject && terrainObject != topObject) {
                        pickedObjects.add(terrainObject);
                    }
                }
            } else {
                pickedObjects.clear(); // nothing drawn at the pick point
            }
        };

        // Internal. Intentionally not documented.
        WorldWindow.prototype.resolveTerrainPick = function () {
            var pickedObjects = this.drawContext.objectsAtPickPoint,
                po;

            // Mark the first picked terrain object as "on top". The picked object list should contain only one entry
            // indicating the picked terrain object, but we iterate over the list contents anyway.
            for (var i = 0, len = pickedObjects.objects.length; i < len; i++) {
                po = pickedObjects.objects[i];
                if (po.isTerrain) {
                    po.isOnTop = true;
                    break;
                }
            }
        };

        // Internal. Intentionally not documented.
        WorldWindow.prototype.resolveRegionPick = function () {
            if (this.drawContext.objectsAtPickPoint.objects.length == 0) {
                return; // nothing picked; avoid calling readPickColors unnecessarily
            }

            // Mark every picked object with a color in the pick buffer as "on top".

            var pickedObjects = this.drawContext.objectsAtPickPoint,
                uniquePickColors = this.drawContext.readPickColors(this.drawContext.pickRectangle),
                po,
                color;

            for (var i = 0, len = pickedObjects.objects.length; i < len; i++) {
                po = pickedObjects.objects[i];
                if (!po) continue;
                var poColor = po.color.toByteString();
                color = uniquePickColors[poColor];
                if (color) {
                    po.isOnTop = true;
                } else if (po.userObject instanceof SurfaceShape) {
                    // SurfaceShapes ALWAYS get added to the pick list, since their rendering is deferred
                    // until the tile they are cached by is rendered. So a SurfaceShape may be in the pick list
                    // but is not seen in the pick rectangle.
                    //
                    // Remove the SurfaceShape that was not visible to the pick rectangle.
                    pickedObjects.objects.splice(i, 1);
                    i -= 1;
                }
            }
        };

        // Internal. Intentionally not documented.
        WorldWindow.prototype.callRedrawCallbacks = function (stage) {
            for (var i = 0, len = this._redrawCallbacks.length; i < len; i++) {
                try {
                    this._redrawCallbacks[i](this, stage);
                } catch (e) {
                    Logger.log(Logger.LEVEL_SEVERE, "Exception calling redraw callback.\n" + e.toString());
                    // Keep going. Execute the rest of the callbacks.
                }
            }
        };

        /**
         * Moves this WorldWindow's navigator to a specified location or position.
         * @param {Location | Position} position The location or position to move the navigator to. If this
         * argument contains an "altitude" property, as {@link Position} does, the end point of the navigation is
         * at the specified altitude. Otherwise the end point is at the current altitude of the navigator.
         *
         * This function uses this WorldWindow's {@link GoToAnimator} property to perform the move. That object's
         * properties can be specified by the application to modify its behavior during calls to this function.
         * It's cancel method can also be used to cancel the move initiated by this function.
         * @param {Function} completionCallback If not null or undefined, specifies a function to call when the
         * animation completes. The completion callback is called with a single argument, this animator.
         * @throws {ArgumentError} If the specified location or position is null or undefined.
         */
        WorldWindow.prototype.goTo = function (position, completionCallback) {
            this.goToAnimator.goTo(position, completionCallback);
        };

        /**
         * Declutters the current ordered renderables with a specified group ID. This function is not called by
         * applications directly. It's meant to be invoked as an ordered rendering filter in this WorldWindow's
         * [orderedRenderingFilters]{@link WorldWindow#orderedRenderingFilters} property.
         * <p>
         * The function operates by setting the target visibility of occluded shapes to 0 and unoccluded shapes to 1.
         * @param {DrawContext} dc The current draw context.
         * @param {Number} groupId The ID of the group to declutter. Must not be null, undefined or 0.
         * @throws {ArgumentError} If the specified group ID is null, undefined or 0.
         */
        WorldWindow.prototype.declutter = function (dc, groupId) {
            if (!groupId) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "declutter",
                        "Group ID is null, undefined or 0."));
            }

            // Collect all the declutterables in the specified group.
            var declutterables = [];
            for (var i = 0; i < dc.orderedRenderables.length; i++) {
                var orderedRenderable = dc.orderedRenderables[i].orderedRenderable;
                if (orderedRenderable.declutterGroup === groupId) {
                    declutterables.push(orderedRenderable);
                }
            }

            // Filter the declutterables by determining which are partially occluded. Since the ordered renderable
            // list was already sorted from front to back, the front-most will represent an entire occluded group.
            var rects = [];
            for (var j = 0; j < declutterables.length; j++) {
                var declutterable = declutterables[j],
                    screenBounds = declutterable.screenBounds;

                if (screenBounds && screenBounds.intersectsRectangles(rects)) {
                    declutterable.targetVisibility = 0;
                } else {
                    declutterable.targetVisibility = 1;
                    if (screenBounds) {
                        rects.push(screenBounds);
                    }
                }
            }
        };

        return WorldWindow;
    }
);