Source: layer/ViewControlsLayer.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 ViewControlsLayer
 */
define([
        '../geom/Angle',
        '../error/ArgumentError',
        '../layer/Layer',
        '../geom/Location',
        '../util/Logger',
        '../util/Offset',
        '../shapes/ScreenImage',
        '../geom/Vec2'
    ],
    function (Angle,
              ArgumentError,
              Layer,
              Location,
              Logger,
              Offset,
              ScreenImage,
              Vec2) {
        "use strict";

        /**
         * Constructs a view controls layer.
         * @alias ViewControlsLayer
         * @constructor
         * @augments {Layer}
         * @classdesc Displays and manages view controls.
         * @param {WorldWindow} worldWindow The WorldWindow associated with this layer.
         * This layer may not be associated with more than one WorldWindow. Each WorldWindow must have it's own
         * instance of this layer if each window is to have view controls.
         *
         * <p>
         *     Placement of the controls within the WorldWindow is defined by the
         *     [placement]{@link ViewControlsLayer#placement} and [alignment]{@link ViewControlsLayer#alignment}
         *     properties. The default values of these properties place the view controls at the lower left corner
         *     of the WorldWindow. The placement property specifies the overall position of the controls within the
         *     WorldWindow. The alignment property specifies the alignment of the controls collection relative to
         *     the placement position. Some common combinations are:
         *     <table>
         *         <tr>
         *             <th>Location</th>
         *             <th>Placement</th>
         *             <th>Alignment</th>
         *         </tr>
         *         <tr>
         *             <td>Bottom Left</td>
         *             <td>WorldWind.OFFSET_FRACTION, 0, WorldWind.OFFSET_FRACTION, 0</td>
         *             <td>WorldWind.OFFSET_FRACTION, 0, WorldWind.OFFSET_FRACTION, 0</td>
         *         </tr>
         *         <tr>
         *             <td>Top Right</td>
         *             <td>WorldWind.OFFSET_FRACTION, 1, WorldWind.OFFSET_FRACTION, 1</td>
         *             <td>WorldWind.OFFSET_FRACTION, 1, WorldWind.OFFSET_FRACTION, 1</td>
         *         </tr>
         *         <tr>
         *             <td>Top Left</td>
         *             <td>WorldWind.OFFSET_FRACTION, 0, WorldWind.OFFSET_FRACTION, 1</td>
         *             <td>WorldWind.OFFSET_FRACTION, 0, WorldWind.OFFSET_FRACTION, 1</td>
         *         </tr>
         *         <tr>
         *             <td>Bottom Center</td>
         *             <td>WorldWind.OFFSET_FRACTION, 0.5, WorldWind.OFFSET_FRACTION, 0</td>
         *             <td>WorldWind.OFFSET_FRACTION, 0.5, WorldWind.OFFSET_FRACTION, 0</td>
         *         </tr>
         *         <tr>
         *             <td>Southeast</td>
         *             <td>WorldWind.OFFSET_FRACTION, 1, WorldWind.OFFSET_FRACTION, 0.25</td>
         *             <td>WorldWind.OFFSET_FRACTION, 1, WorldWind.OFFSET_FRACTION, 0.5</td>
         *         </tr>
         *     </table>
         * </p>
         * @throws {ArgumentError} If the specified WorldWindow is null or undefined.
         */
        var ViewControlsLayer = function (worldWindow) {
            if (!worldWindow) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ViewControlsLayer", "constructor", "missingWorldWindow"));
            }

            Layer.call(this, "View Controls");

            /**
             * The WorldWindow associated with this layer.
             * @type {WorldWindow}
             * @readonly
             */
            this.wwd = worldWindow;

            /**
             * An {@link Offset} indicating where to place the controls on the screen.
             * @type {Offset}
             * @default The lower left corner of the window.
             */
            this.placement = new Offset(WorldWind.OFFSET_FRACTION, 0, WorldWind.OFFSET_FRACTION, 0);

            /**
             * An {@link Offset} indicating the alignment of the control collection relative to the
             * [placement position]{@link ViewControlsLayer#placement}. A value of
             * {WorldWind.FRACTION, 0, WorldWind.Fraction 0} places the bottom left corner of the control collection
             * at the placement position.
             * @type {Offset}
             * @default The lower left corner of the control collection.
             */
            this.alignment = new Offset(WorldWind.OFFSET_FRACTION, 0, WorldWind.OFFSET_FRACTION, 0);

            /**
             * The incremental vertical exaggeration to apply each cycle.
             * @type {Number}
             * @default 0.01
             */
            this.exaggerationIncrement = 0.01;

            /**
             * The incremental amount to increase or decrease the eye distance (for zoom) each cycle.
             * @type {Number}
             * @default 0.04 (4%)
             */
            this.zoomIncrement = 0.04;

            /**
             * The incremental amount to increase or decrease the heading each cycle, in degrees.
             * @type {Number}
             * @default 1.0
             */
            this.headingIncrement = 1.0;

            /**
             * The incremental amount to increase or decrease the tilt each cycle, in degrees.
             * @type {Number}
             */
            this.tiltIncrement = 1.0;

            /**
             * The incremental amount to narrow or widen the field of view each cycle, in degrees.
             * @type {Number}
             * @default 0.1
             */
            this.fieldOfViewIncrement = 0.1;

            /**
             * The scale factor governing the pan speed. Increased values cause faster panning.
             * @type {Number}
             * @default 0.001
             */
            this.panIncrement = 0.001;

            // These are documented in their property accessors below.
            this._inactiveOpacity = 0.5;
            this._activeOpacity = 1.0;

            // Set the screen and image offsets of each control to the lower left corner.
            var screenOffset = new Offset(WorldWind.OFFSET_PIXELS, 0, WorldWind.OFFSET_PIXELS, 0),
                imagePath = WorldWind.configuration.baseUrl + "images/view/";

            // These controls are all internal and intentionally not documented.
            this.panControl = new ScreenImage(screenOffset.clone(), imagePath + "view-pan-64x64.png");
            this.zoomInControl = new ScreenImage(screenOffset.clone(), imagePath + "view-zoom-in-32x32.png");
            this.zoomOutControl = new ScreenImage(screenOffset.clone(), imagePath + "view-zoom-out-32x32.png");
            this.headingLeftControl = new ScreenImage(screenOffset.clone(), imagePath + "view-heading-left-32x32.png");
            this.headingRightControl = new ScreenImage(screenOffset.clone(), imagePath + "view-heading-right-32x32.png");
            this.tiltUpControl = new ScreenImage(screenOffset.clone(), imagePath + "view-pitch-up-32x32.png");
            this.tiltDownControl = new ScreenImage(screenOffset.clone(), imagePath + "view-pitch-down-32x32.png");
            this.exaggerationUpControl = new ScreenImage(screenOffset.clone(), imagePath + "view-elevation-up-32x32.png");
            this.exaggerationDownControl = new ScreenImage(screenOffset.clone(), imagePath + "view-elevation-down-32x32.png");
            this.fovNarrowControl = new ScreenImage(screenOffset.clone(), imagePath + "view-fov-narrow-32x32.png");
            this.fovWideControl = new ScreenImage(screenOffset.clone(), imagePath + "view-fov-wide-32x32.png");

            // Disable the FOV controls by default.
            this.fovNarrowControl.enabled = false;
            this.fovWideControl.enabled = false;

            // Put the controls in an array so we can easily apply bulk operations.
            this.controls = [
                this.panControl,
                this.zoomInControl,
                this.zoomOutControl,
                this.headingLeftControl,
                this.headingRightControl,
                this.tiltUpControl,
                this.tiltDownControl,
                this.exaggerationUpControl,
                this.exaggerationDownControl,
                this.fovNarrowControl,
                this.fovWideControl
            ];

            // Set the default alignment and opacity for each control.
            for (var i = 0; i < this.controls.length; i++) {
                this.controls[i].imageOffset = screenOffset.clone();
                this.controls[i].opacity = this._inactiveOpacity;
                if (this.controls[i] === this.panControl) {
                    this.controls[i].size = 64;
                } else {
                    this.controls[i].size = 32;
                }
            }

            // Internal variable to keep track of pan control center for use during interaction.
            this.panControlCenter = new Vec2(0, 0);

            // No picking for this layer. It performs its own picking.
            this.pickEnabled = false;

            // Establish event handlers.
            this.wwd.worldWindowController.addGestureListener(this);
        };

        ViewControlsLayer.prototype = Object.create(Layer.prototype);

        Object.defineProperties(ViewControlsLayer.prototype, {
            /**
             * Indicates whether to display the pan control.
             * @type {Boolean}
             * @default true
             * @memberof ViewControlsLayer.prototype
             */
            showPanControl: {
                get: function () {
                    return this.panControl.enabled;
                },
                set: function (value) {
                    this.panControl.enabled = value;
                }
            },

            /**
             * Indicates whether to display the zoom control.
             * @type {Boolean}
             * @default true
             * @memberof ViewControlsLayer.prototype
             */
            showZoomControl: {
                get: function () {
                    return this.zoomInControl.enabled;
                },
                set: function (value) {
                    this.zoomInControl.enabled = value;
                    this.zoomOutControl.enabled = value;
                }
            },

            /**
             * Indicates whether to display the heading control.
             * @type {Boolean}
             * @default true
             * @memberof ViewControlsLayer.prototype
             */
            showHeadingControl: {
                get: function () {
                    return this.headingLeftControl.enabled;
                },
                set: function (value) {
                    this.headingLeftControl.enabled = value;
                    this.headingRightControl.enabled = value;
                }
            },

            /**
             * Indicates whether to display the tilt control.
             * @type {Boolean}
             * @default true
             * @memberof ViewControlsLayer.prototype
             */
            showTiltControl: {
                get: function () {
                    return this.tiltUpControl.enabled;
                },
                set: function (value) {
                    this.tiltUpControl.enabled = value;
                    this.tiltDownControl.enabled = value;
                }
            },

            /**
             * Indicates whether to display the vertical exaggeration control.
             * @type {Boolean}
             * @default true
             * @memberof ViewControlsLayer.prototype
             */
            showExaggerationControl: {
                get: function () {
                    return this.exaggerationUpControl.enabled;
                },
                set: function (value) {
                    this.exaggerationUpControl.enabled = value;
                    this.exaggerationDownControl.enabled = value;
                }
            },

            /**
             * Indicates whether to display the field of view control.
             * @type {Boolean}
             * @default false
             * @memberof ViewControlsLayer.prototype
             */
            showFieldOfViewControl: {
                get: function () {
                    return this.fovNarrowControl.enabled;
                },
                set: function (value) {
                    this.fovNarrowControl.enabled = value;
                    this.fovWideControl.enabled = value;
                }
            },

            /**
             * The opacity of the controls when they are not in use. The opacity should be a value between 0 and 1,
             * with 1 indicating fully opaque.
             * @type {Number}
             * @default 0.5
             * @memberof ViewControlsLayer.prototype
             */
            inactiveOpacity: {
                get: function () {
                    return this._inactiveOpacity;
                },
                set: function (value) {
                    this._inactiveOpacity = value;
                    for (var i = 0; i < this.controls.length; i++) {
                        this.controls[i].opacity = value;
                    }
                }
            },

            /**
             * The opacity of the controls when they are in use. The opacity should be a value between 0 and 1,
             * with 1 indicating fully opaque.
             * @type {Number}
             * @default 1
             * @memberof ViewControlsLayer.prototype
             */
            activeOpacity: {
                get: function () {
                    return this._activeOpacity;
                },
                set: function (value) {
                    this._activeOpacity = value;
                    for (var i = 0; i < this.controls.length; i++) {
                        this.controls[i].opacity = value;
                    }
                }
            }
        });

        // Documented in superclass.
        ViewControlsLayer.prototype.doRender = function (dc) {
            var controlPanelWidth = 0, controlPanelHeight = 64,
                panelOffset, screenOffset,
                x, y;

            this.inCurrentFrame = false; // to track whether any control is displayed this frame

            // Determine the dimensions of the control panel and whether any control is displayed.
            if (this.showPanControl) {
                controlPanelWidth += this.panControl.size;
                this.inCurrentFrame = true;
            }
            if (this.showZoomControl) {
                controlPanelWidth += this.zoomInControl.size;
                this.inCurrentFrame = true;
            }
            if (this.showHeadingControl) {
                controlPanelWidth += this.headingLeftControl.size;
                this.inCurrentFrame = true;
            }
            if (this.showTiltControl) {
                controlPanelWidth += this.tiltDownControl.size;
                this.inCurrentFrame = true;
            }
            if (this.showExaggerationControl) {
                controlPanelWidth += this.exaggerationDownControl.size;
                this.inCurrentFrame = true;
            }
            if (this.showFieldOfViewControl) {
                controlPanelWidth += this.fovNarrowControl.size;
                this.inCurrentFrame = true;
            }

            // Determine the lower-left corner position of the control collection.
            screenOffset = this.placement.offsetForSize(dc.viewport.width,
                dc.viewport.height);
            panelOffset = this.alignment.offsetForSize(controlPanelWidth, controlPanelHeight);
            x = screenOffset[0] - panelOffset[0];
            y = screenOffset[1] - panelOffset[1];

            // Determine the control positions and render the controls.

            if (this.showPanControl) {
                this.panControl.screenOffset.x = x;
                this.panControl.screenOffset.y = y;
                this.panControl.render(dc);
                this.panControlCenter[0] = x + this.panControl.size / 2;
                this.panControlCenter[1] = y + this.panControl.size / 2;
                x += this.panControl.size;
            }

            if (this.showZoomControl) {
                this.zoomOutControl.screenOffset.x = x;
                this.zoomOutControl.screenOffset.y = y;
                this.zoomInControl.screenOffset.x = x;
                this.zoomInControl.screenOffset.y = y + this.zoomOutControl.size;
                this.zoomOutControl.render(dc);
                this.zoomInControl.render(dc);
                x += this.zoomOutControl.size;
            }

            if (this.showHeadingControl) {
                this.headingRightControl.screenOffset.x = x;
                this.headingRightControl.screenOffset.y = y;
                this.headingLeftControl.screenOffset.x = x;
                this.headingLeftControl.screenOffset.y = y + this.headingLeftControl.size;
                this.headingRightControl.render(dc);
                this.headingLeftControl.render(dc);
                x += this.headingLeftControl.size;
            }

            if (this.showTiltControl) {
                this.tiltDownControl.screenOffset.x = x;
                this.tiltDownControl.screenOffset.y = y;
                this.tiltUpControl.screenOffset.x = x;
                this.tiltUpControl.screenOffset.y = y + this.tiltDownControl.size;
                this.tiltDownControl.render(dc);
                this.tiltUpControl.render(dc);
                x += this.tiltDownControl.size;
            }

            if (this.showExaggerationControl) {
                this.exaggerationDownControl.screenOffset.x = x;
                this.exaggerationDownControl.screenOffset.y = y;
                this.exaggerationUpControl.screenOffset.x = x;
                this.exaggerationUpControl.screenOffset.y = y + this.exaggerationDownControl.size;
                this.exaggerationUpControl.render(dc);
                this.exaggerationDownControl.render(dc);
                x += this.exaggerationDownControl.size;
            }

            if (this.showFieldOfViewControl) {
                this.fovNarrowControl.screenOffset.x = x;
                this.fovNarrowControl.screenOffset.y = y;
                this.fovWideControl.screenOffset.x = x;
                this.fovWideControl.screenOffset.y = y + this.fovNarrowControl.size;
                this.fovNarrowControl.render(dc);
                this.fovWideControl.render(dc);
            }
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.isPointerDown = function (e) {
            if (e.type === "pointerdown") {
                return true;
            }
            else if (e.type === "mousedown" && e.which === 1) {
                return true;
            }

            return false;
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.isPointerUp = function (e) {
            if (e.type === "pointerup") {
                return true;
            }
            else if (e.type === "mouseup" && e.which === 1) {
                return true;
            }

            return false;
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.isPointerMove = function (e) {
            if (e.type === "pointermove") {
                return true;
            }
            else if (e.type === "mousemove") {
                return true;
            }

            return false;
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.isTouchStart = function (e) {
            return e.type === "touchstart";
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.isTouchEnd = function (e) {
            return e.type === "touchend" || e.type === "touchcancel";
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.isTouchMove = function (e) {
            return e.type === "touchmove";
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.onGestureEvent = function (e) {
            var handled = false, topObject;

            if (!this.enabled) {
                return handled;
            }

            // Turn off any highlight. If a control is in use it will be highlighted later.
            if (this.highlightedControl) {
                this.highlight(this.highlightedControl, false);
                this.wwd.redraw();
            }

            var terminateActive = false;

            if (e.type && this.activeControl) {
                if (this.isPointerUp(e)) {
                    terminateActive = true;
                }
                else if (this.isTouchEnd(e) && this.isCurrentTouch(e)) {
                    terminateActive = true;
                }
                else {
                    topObject = this.pickControl(e);
                    if (this.activeControl !== topObject) {
                        terminateActive = true;
                    }
                }
            }

            // Terminate the active operation when the mouse button goes up or touches end.
            if (terminateActive) {
                this.activeControl = null;
                this.activeOperation = null;
                e.preventDefault();
            } else {
                // Perform the active operation, or determine it and then perform it.
                if (this.activeOperation) {
                    handled = this.activeOperation.call(this, e, null);
                    e.preventDefault();
                } else {
                    topObject = this.pickControl(e);
                    if (topObject) {
                        var operation = this.determineOperation(e, topObject);
                        if (operation) {
                            handled = operation.call(this, e, topObject);
                        }
                    }
                }

                // Determine and display the new highlight state.
                this.handleHighlight(e, topObject);
                this.wwd.redraw();
            }

            return handled;

        };

        // Intentionally not documented. Determines whether a picked object is a view control.
        ViewControlsLayer.prototype.isControl = function (controlCandidate) {
            for (var i = 0; i < this.controls.length; i++) {
                if (this.controls[i] == controlCandidate) {
                    return true;
                }
            }
            return false;
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.pickControl = function (e) {
            var cx, cy;

            if (e.changedTouches) {
                var touch = e.changedTouches.item(0);
                cx = touch.clientX;
                cy = touch.clientY;
            }
            else {
                cx = e.clientX;
                cy = e.clientY;
            }

            var pickPoint = this.wwd.canvasCoordinates(cx, cy);

            var x = pickPoint[0], y = this.wwd.canvas.height - pickPoint[1],
                control;

            // TODO: Performance optimizations
            for (var i = 0; i < this.controls.length; i++) {
                control = this.controls[i];

                if (control.enabled) {
                    if (x >= control.screenOffset.x && x <= (control.screenOffset.x + control.size)
                        && y >= control.screenOffset.y && y <= (control.screenOffset.y + control.size)) {
                        return control;
                    }
                }
            }

            return null;
        };

        // Intentionally not documented. Determines which operation to perform from the picked object.
        ViewControlsLayer.prototype.determineOperation = function (e, topObject) {
            var operation = null;

            if (topObject && (topObject instanceof ScreenImage)) {
                if (topObject === this.panControl) {
                    operation = this.handlePan;
                } else if (topObject === this.zoomInControl
                    || topObject === this.zoomOutControl) {
                    operation = this.handleZoom;
                } else if (topObject === this.headingLeftControl
                    || topObject === this.headingRightControl) {
                    operation = this.handleHeading;
                } else if (topObject === this.tiltUpControl
                    || topObject === this.tiltDownControl) {
                    operation = this.handleTilt;
                } else if (topObject === this.exaggerationUpControl
                    || topObject === this.exaggerationDownControl) {
                    operation = this.handleExaggeration;
                } else if (topObject === this.fovNarrowControl
                    || topObject === this.fovWideControl) {
                    operation = this.handleFov;
                }
            }

            return operation;
        };

        // Intentionally not documented. Determines whether an event represents the touch of the active operation.
        ViewControlsLayer.prototype.isCurrentTouch = function (e) {
            for (var i = 0; i < e.changedTouches.length; i++) {
                if (e.changedTouches.item(i).identifier === this.currentTouchId) {
                    return true;
                }
            }

            return false;
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.handlePan = function (e, control) {
            var handled = false;

            // Capture the current position.
            if (this.isPointerDown(e) || this.isPointerMove(e)) {
                this.currentEventPoint = this.wwd.canvasCoordinates(e.clientX, e.clientY);
            } else if (this.isTouchStart(e) || this.isTouchMove(e)) {
                var touch = e.changedTouches.item(0);
                this.currentEventPoint = this.wwd.canvasCoordinates(touch.clientX, touch.clientY);
            }

            // Start an operation on left button down or touch start.
            if (this.isPointerDown(e) || this.isTouchStart(e)) {
                this.activeControl = control;
                this.activeOperation = this.handlePan;
                e.preventDefault();

                if (this.isTouchStart(e)) {
                    this.currentTouchId = e.changedTouches.item(0).identifier; // capture the touch identifier
                }

                // This function is called by the timer to perform the operation.
                var thisLayer = this; // capture 'this' for use in the function
                var setLookAtLocation = function () {
                    if (thisLayer.activeControl) {
                        var dx = thisLayer.panControlCenter[0] - thisLayer.currentEventPoint[0],
                            dy = thisLayer.panControlCenter[1]
                                - (thisLayer.wwd.viewport.height - thisLayer.currentEventPoint[1]),
                            oldLat = thisLayer.wwd.navigator.lookAtLocation.latitude,
                            oldLon = thisLayer.wwd.navigator.lookAtLocation.longitude,
                            // Scale the increment by a constant and the relative distance of the eye to the surface.
                            scale = thisLayer.panIncrement
                                * (thisLayer.wwd.navigator.range / thisLayer.wwd.globe.radiusAt(oldLat, oldLon)),
                            heading = thisLayer.wwd.navigator.heading + (Math.atan2(dx, dy) * Angle.RADIANS_TO_DEGREES),
                            distance = scale * Math.sqrt(dx * dx + dy * dy);

                        Location.greatCircleLocation(thisLayer.wwd.navigator.lookAtLocation, heading, -distance,
                            thisLayer.wwd.navigator.lookAtLocation);
                        thisLayer.wwd.redraw();
                        setTimeout(setLookAtLocation, 50);
                    }
                };
                setTimeout(setLookAtLocation, 50);

                handled = true;
            }

            return handled;
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.handleZoom = function (e, control) {
            var handled = false;

            // Start an operation on left button down or touch start.
            if (this.isPointerDown(e) || this.isTouchStart(e)) {
                this.activeControl = control;
                this.activeOperation = this.handleZoom;
                e.preventDefault();

                if (this.isTouchStart(e)) {
                    this.currentTouchId = e.changedTouches.item(0).identifier; // capture the touch identifier
                }

                // This function is called by the timer to perform the operation.
                var thisLayer = this; // capture 'this' for use in the function
                var setRange = function () {
                    if (thisLayer.activeControl) {
                        if (thisLayer.activeControl === thisLayer.zoomInControl) {
                            thisLayer.wwd.navigator.range *= (1 - thisLayer.zoomIncrement);
                        } else if (thisLayer.activeControl === thisLayer.zoomOutControl) {
                            thisLayer.wwd.navigator.range *= (1 + thisLayer.zoomIncrement);
                        }
                        thisLayer.wwd.redraw();
                        setTimeout(setRange, 50);
                    }
                };
                setTimeout(setRange, 50);

                handled = true;
            }

            return handled;
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.handleHeading = function (e, control) {
            var handled = false;

            // Start an operation on left button down or touch start.
            if (this.isPointerDown(e) || this.isTouchStart(e)) {
                this.activeControl = control;
                this.activeOperation = this.handleHeading;
                e.preventDefault();

                if (this.isTouchStart(e)) {
                    this.currentTouchId = e.changedTouches.item(0).identifier; // capture the touch identifier
                }

                // This function is called by the timer to perform the operation.
                var thisLayer = this; // capture 'this' for use in the function
                var setRange = function () {
                    if (thisLayer.activeControl) {
                        if (thisLayer.activeControl === thisLayer.headingLeftControl) {
                            thisLayer.wwd.navigator.heading += thisLayer.headingIncrement;
                        } else if (thisLayer.activeControl === thisLayer.headingRightControl) {
                            thisLayer.wwd.navigator.heading -= thisLayer.headingIncrement;
                        }
                        thisLayer.wwd.redraw();
                        setTimeout(setRange, 50);
                    }
                };
                setTimeout(setRange, 50);
                handled = true;
            }

            return handled;
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.handleTilt = function (e, control) {
            var handled = false;

            // Start an operation on left button down or touch start.
            if (this.isPointerDown(e) || this.isTouchStart(e)) {
                this.activeControl = control;
                this.activeOperation = this.handleTilt;
                e.preventDefault();

                if (this.isTouchStart(e)) {
                    this.currentTouchId = e.changedTouches.item(0).identifier; // capture the touch identifier
                }

                // This function is called by the timer to perform the operation.
                var thisLayer = this; // capture 'this' for use in the function
                var setRange = function () {
                    if (thisLayer.activeControl) {
                        if (thisLayer.activeControl === thisLayer.tiltUpControl) {
                            thisLayer.wwd.navigator.tilt =
                                Math.max(0, thisLayer.wwd.navigator.tilt - thisLayer.tiltIncrement);
                        } else if (thisLayer.activeControl === thisLayer.tiltDownControl) {
                            thisLayer.wwd.navigator.tilt =
                                Math.min(90, thisLayer.wwd.navigator.tilt + thisLayer.tiltIncrement);
                        }
                        thisLayer.wwd.redraw();
                        setTimeout(setRange, 50);
                    }
                };
                setTimeout(setRange, 50);

                handled = true;
            }

            return handled;
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.handleExaggeration = function (e, control) {
            var handled = false;

            // Start an operation on left button down or touch start.
            if (this.isPointerDown(e) || this.isTouchStart(e)) {
                this.activeControl = control;
                this.activeOperation = this.handleExaggeration;
                e.preventDefault();

                if (this.isTouchStart(e)) {
                    this.currentTouchId = e.changedTouches.item(0).identifier; // capture the touch identifier
                }

                // This function is called by the timer to perform the operation.
                var thisLayer = this; // capture 'this' for use in the function
                var setExaggeration = function () {
                    if (thisLayer.activeControl) {
                        if (thisLayer.activeControl === thisLayer.exaggerationUpControl) {
                            thisLayer.wwd.verticalExaggeration += thisLayer.exaggerationIncrement;
                        } else if (thisLayer.activeControl === thisLayer.exaggerationDownControl) {
                            thisLayer.wwd.verticalExaggeration =
                                Math.max(1, thisLayer.wwd.verticalExaggeration - thisLayer.exaggerationIncrement);
                        }
                        thisLayer.wwd.redraw();
                        setTimeout(setExaggeration, 50);
                    }
                };
                setTimeout(setExaggeration, 50);

                handled = true;
            }

            return handled;
        };

        // Intentionally not documented.
        ViewControlsLayer.prototype.handleFov = function (e, control) {
            var handled = false;

            // Start an operation on left button down or touch start.
            if (this.isPointerDown(e) || this.isTouchStart(e)) {
                this.activeControl = control;
                this.activeOperation = this.handleFov;
                e.preventDefault();

                if (this.isTouchStart(e)) {
                    this.currentTouchId = e.changedTouches.item(0).identifier; // capture the touch identifier
                }

                // This function is called by the timer to perform the operation.
                var thisLayer = this; // capture 'this' for use in the function
                var setRange = function () {
                    if (thisLayer.activeControl) {
                        if (thisLayer.activeControl === thisLayer.fovWideControl) {
                            thisLayer.wwd.navigator.fieldOfView =
                                Math.max(90, thisLayer.wwd.navigator.fieldOfView + thisLayer.fieldOfViewIncrement);
                        } else if (thisLayer.activeControl === thisLayer.fovNarrowControl) {
                            thisLayer.wwd.navigator.fieldOfView =
                                Math.min(0, thisLayer.wwd.navigator.fieldOfView - thisLayer.fieldOfViewIncrement);
                        }
                        thisLayer.wwd.redraw();
                        setTimeout(setRange, 50);
                    }
                };
                setTimeout(setRange, 50);
                handled = true;
            }

            return handled;
        };

        // Intentionally not documented. Determines whether to highlight a control.
        ViewControlsLayer.prototype.handleHighlight = function (e, topObject) {
            if (this.activeControl) {
                // Highlight the active control.
                this.highlight(this.activeControl, true);
            } else if (topObject && this.isControl(topObject)) {
                // Highlight the control under the cursor or finger.
                this.highlight(topObject, true);
            }
        };

        // Intentionally not documented. Sets the highlight state of a control.
        ViewControlsLayer.prototype.highlight = function (control, tf) {
            control.opacity = tf ? this._activeOpacity : this._inactiveOpacity;

            if (tf) {
                this.highlightedControl = control;
            } else {
                this.highlightedControl = null;
            }
        };

        return ViewControlsLayer;
    });