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;