Source: view/Controls.js

import WorldWind from 'webworldwind-esa';
import './Controls.css';

const ArgumentError = WorldWind.ArgumentError,
    Logger = WorldWind.Logger;

/**
 * Constructs a controls in the top right corner of the given div. Controls zoom, rotation, heading and exaggeration.
 * @alias Controls
 * @constructor
 * @param {Object} options
 * @param {WorldWindow} options.worldWindow The World Window associated with these controls.
 * @param {string} options.mapContainerId Id of the container to draw controls into.
 * @param {string}[options.classes] Optional parameter. List of classes that will be added to the top container for controls.
 *
 * @throws {ArgumentError} If the specified world window is null or undefined or if the Id of the container is null.
 */
class Controls {
    constructor(options) {
        if (!options.worldWindow) {
            throw new ArgumentError(
                Logger.logMessage(Logger.LEVEL_SEVERE, "Controls", "constructor", "missingWorldWindow"));
        }
        if (!options.mapContainerId) {
            throw new ArgumentError(
                Logger.logMessage(Logger.LEVEL_SEVERE, "Controls", "constructor", "missingTarget"));
        }

        if (options.mapContainerId) {
            this._mapContainer = document.querySelector('#' + options.mapContainerId);
        }

        this._classes = options.classes || ['bottom-right'];

        /**
         * The World Window associated with controls.
         * @type {WorldWindow}
         * @readonly
         */
        this.wwd = options.worldWindow;

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

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

        // Render icons
        this.buildIcons();

        // Establish event handlers.
        this.setupInteraction();
    }

    /**
     * Render icons into the document. All icons are represented as SVGs.
     * @private
     */
    buildIcons() {
        const html = `
        <div class="map-controls ${this._classes.join(' ')}">
            <div class="exaggerate-control control">
                <a href="#" class="exaggerate-plus-control">
                    <svg width="18px" height="18px" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1675 971q0 51-37 90l-75 75q-38 38-91 38-54 0-90-38l-294-293v704q0 52-37.5 84.5t-90.5 32.5h-128q-53 0-90.5-32.5t-37.5-84.5v-704l-294 293q-36 38-90 38t-90-38l-75-75q-38-38-38-90 0-53 38-91l651-651q35-37 90-37 54 0 91 37l651 651q37 39 37 91z"/></svg>
                </a>
                <a href="#" class="exaggerate-minus-control">
                    <svg width="18px" height="18px" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1675 832q0 53-37 90l-651 652q-39 37-91 37-53 0-90-37l-651-652q-38-36-38-90 0-53 38-91l74-75q39-37 91-37 53 0 90 37l294 294v-704q0-52 38-90t90-38h128q52 0 90 38t38 90v704l294-294q37-37 90-37 52 0 91 37l75 75q37 39 37 91z"/></svg>
                </a>
            </div>
            <div class="zoom-control control">
                <a href="#" class="zoom-plus-control">
                    <svg width="18px" height="18px" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1600 736v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z"/></svg>
                </a>
                <a href="#" class="zoom-minus-control">
                    <svg width="18px" height="18px" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1600 736v192q0 40-28 68t-68 28h-1216q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h1216q40 0 68 28t28 68z"/></svg>
                </a>
            </div>
            <div class="rotate-control control">
                <a href="#" class="rotate-right-control">
                    <svg width="18px" height="18px" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1664 256v448q0 26-19 45t-45 19h-448q-42 0-59-40-17-39 14-69l138-138q-148-137-349-137-104 0-198.5 40.5t-163.5 109.5-109.5 163.5-40.5 198.5 40.5 198.5 109.5 163.5 163.5 109.5 198.5 40.5q119 0 225-52t179-147q7-10 23-12 15 0 25 9l137 138q9 8 9.5 20.5t-7.5 22.5q-109 132-264 204.5t-327 72.5q-156 0-298-61t-245-164-164-245-61-298 61-298 164-245 245-164 298-61q147 0 284.5 55.5t244.5 156.5l130-129q29-31 70-14 39 17 39 59z"/></svg>
                </a>
                <a href="#" class="rotate-needle-control" style="transform: rotate(-45deg);">
                    <svg width="18px" height="18px" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1593 349l-640 1280q-17 35-57 35-5 0-15-2-22-5-35.5-22.5t-13.5-39.5v-576h-576q-22 0-39.5-13.5t-22.5-35.5 4-42 29-30l1280-640q13-7 29-7 27 0 45 19 15 14 18.5 34.5t-6.5 39.5z"/></svg>
                </a>
                <a href="#" class="rotate-left-control">
                    <svg width="18px" height="18px" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1664 896q0 156-61 298t-164 245-245 164-298 61q-172 0-327-72.5t-264-204.5q-7-10-6.5-22.5t8.5-20.5l137-138q10-9 25-9 16 2 23 12 73 95 179 147t225 52q104 0 198.5-40.5t163.5-109.5 109.5-163.5 40.5-198.5-40.5-198.5-109.5-163.5-163.5-109.5-198.5-40.5q-98 0-188 35.5t-160 101.5l137 138q31 30 14 69-17 40-59 40h-448q-26 0-45-19t-19-45v-448q0-42 40-59 39-17 69 14l130 129q107-101 244.5-156.5t284.5-55.5q156 0 298 61t245 164 164 245 61 298z"/></svg>
                </a>
            </div>
            <div class="tilt-control control">
                <a href="#" class="tilt-more-control">
                    <svg version="1.1" class="icon-tilt-more" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
             width="18px" height="18px" viewBox="0 0 511.625 511.627" xml:space="preserve">
        <g>
            <path d="M224.595,201.822h-93.576c-6.879,0-13.674,3.236-15.211,7.307l-24.902,66.025c-2.188,5.797,2.867,10.61,11.34,10.61
                h115.271c8.477,0,15.584-4.813,15.873-10.611l3.295-66.024C236.888,205.057,231.474,201.822,224.595,201.822z"/>
            <path d="M379.204,201.821l-93.574,0.001c-6.879,0-12.293,3.236-12.092,7.307l3.293,66.024c0.291,5.798,7.4,10.611,15.873,10.611
                h115.275c8.473,0,13.525-4.813,11.338-10.611l-24.902-66.024C392.879,205.057,386.081,201.821,379.204,201.821z"/>
            <path d="M419.61,319.696H295.565c-9.117,0-16.227,5.703-15.865,12.927l6.236,125.035c0.586,11.742,10.93,21.594,23.078,21.594
                h165.289c12.148,0,18.283-9.852,13.855-21.594l-47.164-125.035C438.272,325.398,428.727,319.696,419.61,319.696z"/>
            <path d="M214.657,319.696l-124.043,0.001c-9.121,0-18.662,5.702-21.387,12.926L22.065,457.657
                c-4.428,11.742,1.703,21.594,13.855,21.594h165.285c12.152,0,22.492-9.852,23.076-21.594l6.24-125.035
                C230.884,325.399,223.776,319.696,214.657,319.696z"/>
        </g>
        </svg>
                </a>
                <a href="#" class="tilt-less-control">
                    <svg version="1.1" class="icon-tilt-less" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
             width="18px" height="18px" viewBox="0 0 511.625 511.627" xml:space="preserve">
        <g>
            <path d="M210.81,37.038H55.599c-11.047,0-20,8.954-20,20v155.211c0,11.046,8.953,20,20,20H210.81c11.045,0,20-8.954,20-20V57.038
                C230.81,45.992,221.854,37.038,210.81,37.038z"/>
            <path d="M454.127,37.038H298.917c-11.047,0-20,8.954-20,20v155.211c0,11.046,8.953,20,20,20h155.211c11.045,0,20-8.954,20-20
                V57.038C474.127,45.992,465.172,37.038,454.127,37.038z"/>
            <path d="M454.127,280.369H298.917c-11.047,0-20,8.954-20,20V455.58c0,11.046,8.953,20,20,20h155.211c11.045,0,20-8.954,20-20
                V300.369C474.127,289.323,465.172,280.369,454.127,280.369z"/>
            <path d="M210.81,280.369H55.599c-11.047,0-20,8.954-20,20V455.58c0,11.046,8.953,20,20,20H210.81c11.045,0,20-8.954,20-20V300.369
                C230.81,289.323,221.854,280.369,210.81,280.369z"/>
        </g>
        </svg>
                </a>
            </div>
        </div>
        `;

        const controlContainer = document.createElement('div');
        controlContainer.innerHTML = html;
        this._mapContainer.append(controlContainer);
    }

    /**
     * Setup mousedown, mouseup and mouseleave Event Listeners.
     * @param htmlElement {HTMLElement} HTML element to attach the listeners to.
     * @param functionToCall {Function} Function to call when the event occurs
     * @private
     */
    setupForInteractions(htmlElement, functionToCall) {
        htmlElement.addEventListener('mousedown', functionToCall);
        htmlElement.addEventListener('mouseup', functionToCall);
        htmlElement.addEventListener('mouselave', functionToCall);
        htmlElement.addEventListener('touchstart', functionToCall);
        htmlElement.addEventListener('touchend', functionToCall);
        htmlElement.addEventListener('touchcancel', functionToCall);
    }

    /**
     * Sets listeners for mouse interactions with all the control elements.
     * @private
     */
    setupInteraction() {
        this.setupForInteractions(this._mapContainer.querySelector(".exaggerate-plus-control"),
            this.handleMouseEvent.bind(this, this.handleExaggeratePlus.bind(this)));

        this.setupForInteractions(this._mapContainer.querySelector(".exaggerate-minus-control"),
            this.handleMouseEvent.bind(this, this.handleExaggerateMinus.bind(this)));

        this.setupForInteractions(this._mapContainer.querySelector(".zoom-plus-control"),
            this.handleMouseEvent.bind(this, this.handleZoomIn.bind(this)));

        this.setupForInteractions(this._mapContainer.querySelector(".zoom-minus-control"),
            this.handleMouseEvent.bind(this, this.handleZoomOut.bind(this)));

        this.setupForInteractions(this._mapContainer.querySelector(".tilt-less-control"),
            this.handleMouseEvent.bind(this, this.handleTiltUp.bind(this)));

        this.setupForInteractions(this._mapContainer.querySelector(".tilt-more-control"),
            this.handleMouseEvent.bind(this, this.handleTiltDown.bind(this)));

        this.setupForInteractions(this._mapContainer.querySelector(".rotate-right-control"),
            this.handleMouseEvent.bind(this, this.handleHeadingRight.bind(this)));

        this.setupForInteractions(this._mapContainer.querySelector(".rotate-left-control"),
            this.handleMouseEvent.bind(this, this.handleHeadingLeft.bind(this)));


        this._mapContainer.querySelector(".rotate-needle-control").addEventListener('mouseup', this.handleHeadingReset.bind(this));
        this.wwd.addEventListener("mousemove", this.handleManualRedraw.bind(this));
    }

    /**
     * The operation continues as long as the button is pushed.
     * @private
     * @param operation {Function} Function to call as long as the operation doesn't end
     * @param e {Event} Event starting the mouse event.
     */
    handleMouseEvent(operation, e) {
        if (
            e.type &&
            (
                (e.type === "mouseup" && e.which === 1) ||
                e.type === "mouseleave" ||
                e.type === "touchend" ||
                e.type === "touchcancel"
            )
        ) {
            this.handleOperationEnd(e);
        } else {
            this.handleOperationStart(operation, e);
        }
    }

    /**
     * Handle the start of the operation and make sure it runs as long as the buttons is pushed.
     * @private
     * @param operation {Function} Function to call as long as the operation doesn't end
     * @param e {Event} Event starting the mouse event.
     */
    handleOperationStart(operation, e) {
        if ((e.type === "mousedown" && e.which === 1) || (e.type === "touchstart")) {
            this.activeOperation = operation;
            e.preventDefault();

            let runOperation = () => {
                if (this.activeOperation) {
                    operation.call(self);
                    setTimeout(runOperation, 50);
                }
            };
            setTimeout(runOperation, 50);
        }
    }

    /**
     * Stops the operation from further repeating.
     * @param e {Event} Event starting the mouse event.
     */
    handleOperationEnd(e) {
        this.activeOperation = null;
        e.preventDefault();
    }

    /**
     * Lessen the difference in height between the places.
     * @private
     */
    handleExaggeratePlus() {
        const wwd = this.wwd;
        wwd.verticalExaggeration += this.exaggerationIncrement;
        wwd.redraw();
    }

    /**
     * Exaggerates the difference in height between the places.
     * @private
     */
    handleExaggerateMinus() {
        const wwd = this.wwd;
        wwd.verticalExaggeration = Math.max(1, wwd.verticalExaggeration - this.exaggerationIncrement);
        wwd.redraw();
    }

    /**
     * Zoom in by given increment.
     * @private
     */
    handleZoomIn() {
        const wwd = this.wwd;
        wwd.navigator.range *= (1 - this.zoomIncrement);
        wwd.redraw();
    }

    /**
     * Zoom out by given increment.
     * @private
     */
    handleZoomOut() {
        const wwd = this.wwd;
        wwd.navigator.range *= (1 + this.zoomIncrement);
        wwd.redraw();
    }

    /**
     * Turn the globe to the right.
     * @private
     */
    handleHeadingRight() {
        const wwd = this.wwd;
        wwd.navigator.heading -= this.headingIncrement;
        wwd.redraw();
        this.redrawHeadingIndicator();
    }

    /**
     * Turn the globe to the left.
     * @private
     */
    handleHeadingLeft() {
        const wwd = this.wwd;
        wwd.navigator.heading += this.headingIncrement;
        wwd.redraw();
        this.redrawHeadingIndicator();
    }

    /**
     * Turn the globe to the up in the right direction.
     * @private
     */
    handleHeadingReset() {
        const wwd = this.wwd;
        let headingIncrement = 1.0;
        if (Math.abs(wwd.navigator.heading) > 60) {
            headingIncrement = 2.0;
        } else if (Math.abs(navigator.heading) > 120) {
            headingIncrement = 3.0;
        }
        if (wwd.navigator.heading > 0) {
            headingIncrement = -headingIncrement;
        }

        let runOperation = () => {
            if (Math.abs(wwd.navigator.heading) > Math.abs(headingIncrement)) {
                wwd.navigator.heading += headingIncrement;
                setTimeout(runOperation, 10);
            } else {
                wwd.navigator.heading = 0;
            }
            wwd.redraw();
            this.redrawHeadingIndicator();
        };
        setTimeout(runOperation, 10);
    }

    /**
     * Change the rotation of the icon representing the turn of the globe to the left or right.
     * @private
     */
    redrawHeadingIndicator() {
        const wwd = this.wwd;
        let initialAngle = 45;
        let currentHeading = wwd.navigator.heading;
        let rotateAngle = 0 - currentHeading - initialAngle;
        this._mapContainer.querySelector(".rotate-needle-control").style.transform = 'rotate(' + rotateAngle + 'deg)';
    }

    /**
     * Tilt the globe up.
     * @private
     */
    handleTiltUp() {
        const wwd = this.wwd;
        wwd.navigator.tilt = Math.max(0, wwd.navigator.tilt - this.tiltIncrement);
        wwd.redraw();
    }

    /**
     * Tilt the globe down.
     * @private
     */
    handleTiltDown() {
        const wwd = this.wwd;
        wwd.navigator.tilt = Math.min(90, wwd.navigator.tilt + this.tiltIncrement);
        wwd.redraw();
    }

    /**
     * When user changes heading by the mouse display it properly on the Control panel as well.
     * @param e {Event} Event on moving the mouse with right button clicked
     */
    handleManualRedraw(e) {
        if (e.which) {
            const wwd = this.wwd;

            // Redraw heading indicator
            this._lastHeading = this._lastHeading || 0;
            if (wwd.navigator.heading !== this._lastHeading) {
                this.redrawHeadingIndicator();
            }
            this._lastHeading = wwd.navigator.heading;
        }
    }
}

export default Controls;