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',
        './BasicWorldWindowController',
        './render/DrawContext',
        './globe/EarthElevationModel',
        './util/FrameStatistics',
        './geom/Frustum',
        './globe/Globe',
        './globe/Globe2D',
        './util/GoToAnimator',
        './cache/GpuResourceCache',
        './geom/Line',
        './util/Logger',
        './navigate/LookAtNavigator',
        './geom/Matrix',
        './pick/PickedObjectList',
        './geom/Position',
        './geom/Rectangle',
        './geom/Sector',
        './shapes/SurfaceShape',
        './shapes/SurfaceShapeTileBuilder',
        './globe/Terrain',
        './geom/Vec2',
        './geom/Vec3',
        './util/WWMath'
    ],
    function (ArgumentError,
              BasicWorldWindowController,
              DrawContext,
              EarthElevationModel,
              FrameStatistics,
              Frustum,
              Globe,
              Globe2D,
              GoToAnimator,
              GpuResourceCache,
              Line,
              Logger,
              LookAtNavigator,
              Matrix,
              PickedObjectList,
              Position,
              Rectangle,
              Sector,
              SurfaceShape,
              SurfaceShapeTileBuilder,
              Terrain,
              Vec2,
              Vec3,
              WWMath) {
        "use strict";

        /**
         * Constructs a WorldWind window for an HTML canvas.
         * @alias WorldWindow
         * @constructor
         * @classdesc Represents a WorldWind window for an HTML canvas.
         * @param {String|HTMLCanvasElement} canvasElem The ID assigned to the HTML canvas in the document or the canvas
         * element itself.
         * @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 ID in the document, or if the
         * HTML canvas does not support WebGL.
         */
        var WorldWindow = function (canvasElem, elevationModel) {
            if (!(window.WebGLRenderingContext)) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "constructor",
                        "The specified canvas does not support WebGL."));
            }

            // Get the actual canvas element either directly or by ID.
            var canvas;
            if (canvasElem instanceof HTMLCanvasElement) {
                canvas = canvasElem;
            } else {
                // Attempt to get the HTML canvas with the specified ID.
                canvas = document.getElementById(canvasElem);

                if (!canvas) {
                    throw new ArgumentError(
                        Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "constructor",
                            "The specified canvas ID 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;

            // Internal. Intentionally not documented.
            this.scratchModelview = Matrix.fromIdentity();

            // Internal. Intentionally not documented.
            this.scratchProjection = Matrix.fromIdentity();

            /**
             * 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();

            /**
             * The controller used to manipulate the globe.
             * @type {WorldWindowController}
             * @default [BasicWorldWindowController]{@link BasicWorldWindowController}
             */
            this.worldWindowController = new BasicWorldWindowController(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;

            // Prevent the browser's default actions in response to mouse and touch events, which interfere with
            // navigation. Register these event listeners  before any others to ensure that they're called last.
            function preventDefaultListener(event) {
                event.preventDefault();
            }

            this.addEventListener("mousedown", preventDefaultListener);
            this.addEventListener("touchstart", preventDefaultListener);
            this.addEventListener("contextmenu", preventDefaultListener);
            this.addEventListener("wheel", preventDefaultListener);

            var thisWindow = this;

            // Redirect various UI interactions to the appropriate handler.
            function onGestureEvent(event) {
                thisWindow.onGestureEvent(event);
            }

            if (window.PointerEvent) {
                // Prevent the browser's default actions in response to pointer events which interfere with navigation.
                // This CSS style property is configured here to ensure that it's set for all applications.
                this.canvas.style.setProperty("touch-action", "none");

                this.addEventListener("pointerdown", onGestureEvent, false);
                window.addEventListener("pointermove", onGestureEvent, false); // get pointermove events outside event target
                window.addEventListener("pointercancel", onGestureEvent, false); // get pointercancel events outside event target
                window.addEventListener("pointerup", onGestureEvent, false); // get pointerup events outside event target
            } else {
                this.addEventListener("mousedown", onGestureEvent, false);
                window.addEventListener("mousemove", onGestureEvent, false); // get mousemove events outside event target
                window.addEventListener("mouseup", onGestureEvent, false); // get mouseup events outside event target
                this.addEventListener("touchstart", onGestureEvent, false);
                this.addEventListener("touchmove", onGestureEvent, false);
                this.addEventListener("touchend", onGestureEvent, false);
                this.addEventListener("touchcancel", onGestureEvent, false);
            }

            // Register wheel event listeners on the WorldWindow's canvas.
            this.addEventListener("wheel", function (event) {
                onGestureEvent(event);
            });

            // Set up to handle WebGL context lost events.
            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);
        };

        WorldWindow.prototype.onGestureEvent = function (event) {
            this.worldWindowController.onGestureEvent(event);
        };

        /**
         * 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.drawContext.pickRay = this.rayThroughScreenPoint(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.drawContext.pickRay = this.rayThroughScreenPoint(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.computeViewingTransform = function (projection, modelview) {
            if (!modelview) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "computeViewingTransform", "missingModelview"));
            }

            modelview.setToIdentity();
            this.worldWindowController.applyLimits();
            var globe = this.globe;
            var navigator = this.navigator;
            var lookAtPosition = new Position(navigator.lookAtLocation.latitude, navigator.lookAtLocation.longitude, 0);
            modelview.multiplyByLookAtModelview(lookAtPosition, navigator.range, navigator.heading, navigator.tilt, navigator.roll, globe);

            if (projection) {
                projection.setToIdentity();
                var globeRadius = WWMath.max(globe.equatorialRadius, globe.polarRadius),
                    eyePoint = modelview.extractEyePoint(new Vec3(0, 0, 0)),
                    eyePos = globe.computePositionFromPoint(eyePoint[0], eyePoint[1], eyePoint[2], new Position(0, 0, 0)),
                    eyeHorizon = WWMath.horizonDistanceForGlobeRadius(globeRadius, eyePos.altitude),
                    atmosphereHorizon = WWMath.horizonDistanceForGlobeRadius(globeRadius, 160000),
                    viewport = this.viewport;

                // Set the far clip distance to the smallest value that does not clip the atmosphere.
                // TODO adjust the clip plane distances based on the navigator's orientation - shorter distances when the
                // TODO horizon is not in view
                // TODO parameterize the object altitude for horizon distance
                var farDistance = eyeHorizon + atmosphereHorizon;
                if (farDistance < 1e3)
                    farDistance = 1e3;

                // Compute the near clip distance in order to achieve a desired depth resolution at the far clip distance.
                // This computed distance is limited such that it does not intersect the terrain when possible and is never
                // less than a predetermined minimum (usually one). The computed near distance automatically scales with the
                // resolution of the WebGL depth buffer.
                var nearDistance = WWMath.perspectiveNearDistanceForFarDistance(farDistance, 10, this.depthBits);

                // Prevent the near clip plane from intersecting the terrain.
                var distanceToSurface = eyePos.altitude - globe.elevationAtLocation(eyePos.latitude, eyePos.longitude);
                if (distanceToSurface > 0) {
                    var maxNearDistance = WWMath.perspectiveNearDistance(viewport.width, viewport.height, distanceToSurface);
                    if (nearDistance > maxNearDistance) {
                        nearDistance = maxNearDistance;
                    }
                }

                if (nearDistance < 1) {
                    nearDistance = 1;
                }

                // Compute the current projection matrix based on this navigator's perspective properties and the current
                // WebGL viewport.
                projection.setToPerspectiveProjection(viewport.width, viewport.height, nearDistance, farDistance);
            }
        };

        // Internal. Intentionally not documented.
        WorldWindow.prototype.computePixelMetrics = function (projection) {
            var projectionInv = Matrix.fromIdentity();
            projectionInv.invertMatrix(projection);

            // Compute the eye coordinate rectangles carved out of the frustum by the near and far clipping planes, and
            // the distance between those planes and the eye point along the -Z axis. The rectangles are determined by
            // transforming the bottom-left and top-right points of the frustum from clip coordinates to eye
            // coordinates.
            var nbl = new Vec3(-1, -1, -1),
                ntr = new Vec3(+1, +1, -1),
                fbl = new Vec3(-1, -1, +1),
                ftr = new Vec3(+1, +1, +1);
            // Convert each frustum corner from clip coordinates to eye coordinates by multiplying by the inverse
            // projection matrix.
            nbl.multiplyByMatrix(projectionInv);
            ntr.multiplyByMatrix(projectionInv);
            fbl.multiplyByMatrix(projectionInv);
            ftr.multiplyByMatrix(projectionInv);

            var nrRectWidth = WWMath.fabs(ntr[0] - nbl[0]),
                frRectWidth = WWMath.fabs(ftr[0] - fbl[0]),
                nrDistance = -nbl[2],
                frDistance = -fbl[2];

            // Compute the scale and offset used to determine the width of a pixel on a rectangle carved out of the
            // frustum at a distance along the -Z axis in eye coordinates. These values are found by computing the scale
            // and offset of a frustum rectangle at a given distance, then dividing each by the viewport width.
            var frustumWidthScale = (frRectWidth - nrRectWidth) / (frDistance - nrDistance),
                frustumWidthOffset = nrRectWidth - frustumWidthScale * nrDistance;

            return {
                pixelSizeFactor: frustumWidthScale / this.viewport.width,
                pixelSizeOffset: frustumWidthOffset / this.viewport.height
            };
        };

        /**
         * Computes the approximate size of a pixel at a specified distance from the eye point.
         * <p>
         * This method assumes rectangular pixels, where pixel coordinates denote
         * infinitely thin spaces between pixels. The units of the returned size are in model coordinates per pixel
         * (usually meters per pixel). This returns 0 if the specified distance is zero. The returned size is undefined
         * if the distance is less than zero.
         *
         * @param {Number} distance The distance from the eye point at which to determine pixel size, in model
         * coordinates.
         * @returns {Number} The approximate pixel size at the specified distance from the eye point, in model
         * coordinates per pixel.
         */
        WorldWindow.prototype.pixelSizeAtDistance = function (distance) {
            this.computeViewingTransform(this.scratchProjection, this.scratchModelview);
            var pixelMetrics = this.computePixelMetrics(this.scratchProjection);
            return pixelMetrics.pixelSizeFactor * distance + pixelMetrics.pixelSizeOffset;
        };

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

            this.computeViewingTransform(dc.projection, dc.modelview);
            dc.viewport = this.viewport;
            dc.eyePoint = dc.modelview.extractEyePoint(new Vec3(0, 0, 0));

            dc.modelviewProjection.setToIdentity();
            dc.modelviewProjection.setToMultiply(dc.projection, dc.modelview);

            var pixelMetrics = this.computePixelMetrics(dc.projection);
            dc.pixelSizeFactor = pixelMetrics.pixelSizeFactor;
            dc.pixelSizeOffset = pixelMetrics.pixelSizeOffset;

            // Compute the inverse of the modelview, projection, and modelview-projection matrices. The inverse matrices
            // are used to support operations on navigator state.
            var modelviewInv = Matrix.fromIdentity();
            modelviewInv.invertOrthonormalMatrix(dc.modelview);

            dc.modelviewNormalTransform = Matrix.fromIdentity().setToTransposeOfMatrix(modelviewInv.upper3By3());

            // Compute the frustum in model coordinates. Start by computing the frustum in eye coordinates from the
            // projection matrix, then transform this frustum to model coordinates by multiplying its planes by the
            // transpose of the modelview matrix. We use the transpose of the modelview matrix because planes are
            // transformed by the inverse transpose of a matrix, and we want to transform from eye coordinates to model
            // coordinates.
            var modelviewTranspose = Matrix.fromIdentity();
            modelviewTranspose.setToTransposeOfMatrix(dc.modelview);
            dc.frustumInModelCoordinates = Frustum.fromProjectionMatrix(dc.projection);
            dc.frustumInModelCoordinates.transformByMatrix(modelviewTranspose);
            dc.frustumInModelCoordinates.normalize();
        };

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

            var dc = this.drawContext;
            dc.reset();
            dc.globe = this.globe;
            dc.navigator = this.navigator;
            dc.layers = this.layers.slice();
            dc.layers.push(dc.screenCreditController);
            this.computeDrawContext();
            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();
                }
            } else {
                this.drawContext.surfaceShapeTileBuilder.clear();
                this.drawLayers(true);
                this.drawSurfaceRenderables();
                this.drawContext.surfaceShapeTileBuilder.doRender(this.drawContext);

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

        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.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.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.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.
                    }
                }
            }
            dc.currentLayer = null;
            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) {
            if (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 WorldWindow's
         * layer list.
         */
        WorldWindow.prototype.removeLayer = function (layer) {
            var index = this.indexOfLayer(layer);
            if (index >= 0) {
                this.layers.splice(index, 1);
            }
        };

        /**
         * Inserts a specified layer at a specified position in this WorldWindow.
         * @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. May be null or undefined, in which case this WorldWindow is not
         * modified.
         */
        WorldWindow.prototype.insertLayer = function (index, layer) {
            if (layer) {
                this.layers.splice(index, 0, layer);
            }
        };

        /**
         * Returns the index of a specified layer in this WorldWindow.
         * @param {Layer} layer The layer to search for.
         * @returns {Number} The index of the specified layer or -1 if it doesn't exist in this WorldWindow.
         */
        WorldWindow.prototype.indexOfLayer = function (layer) {
            return this.layers.indexOf(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);
                    }
                }
            }
        };

        /**
         * Computes a ray originating at the eyePoint and extending through the specified point in window
         * coordinates.
         * <p>
         * The specified point is understood to be in the window coordinate system of the WorldWindow, with the origin
         * in the top-left corner and axes that extend down and to the right from the origin point.
         * <p>
         * The results of this method are undefined if the specified point is outside of the WorldWindow's
         * bounds.
         *
         * @param {Vec2} point The window coordinates point to compute a ray for.
         * @returns {Line} A new Line initialized to the origin and direction of the computed ray, or null if the
         * ray could not be computed.
         */
        WorldWindow.prototype.rayThroughScreenPoint = function (point) {
            if (!point) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "rayThroughScreenPoint",
                    "missingPoint"));
            }

            // Convert the point's xy coordinates from window coordinates to WebGL screen coordinates.
            var screenPoint = new Vec3(point[0], this.viewport.height - point[1], 0),
                nearPoint = new Vec3(0, 0, 0),
                farPoint = new Vec3(0, 0, 0);

            this.computeViewingTransform(this.scratchProjection, this.scratchModelview);
            var modelviewProjection = Matrix.fromIdentity();
            modelviewProjection.setToMultiply(this.scratchProjection, this.scratchModelview);
            var modelviewProjectionInv = Matrix.fromIdentity();
            modelviewProjectionInv.invertMatrix(modelviewProjection);

            // Compute the model coordinate point on the near clip plane with the xy coordinates and depth 0.
            if (!modelviewProjectionInv.unProject(screenPoint, this.viewport, nearPoint)) {
                return null;
            }

            // Compute the model coordinate point on the far clip plane with the xy coordinates and depth 1.
            screenPoint[2] = 1;
            if (!modelviewProjectionInv.unProject(screenPoint, this.viewport, farPoint)) {
                return null;
            }

            var eyePoint = this.scratchModelview.extractEyePoint(new Vec3(0, 0, 0));

            // Compute a ray originating at the eye point and with direction pointing from the xy coordinate on the near
            // plane to the same xy coordinate on the far plane.
            var origin = new Vec3(eyePoint[0], eyePoint[1], eyePoint[2]),
                direction = new Vec3(farPoint[0], farPoint[1], farPoint[2]);

            direction.subtract(nearPoint);
            direction.normalize();

            return new Line(origin, direction);
        };

        return WorldWindow;
    }
);