BlueMap/BlueMapCommon/webapp/src/js/BlueMapApp.js

775 lines
28 KiB
JavaScript

/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import "./BlueMap";
import {MapViewer} from "./MapViewer";
import {MapControls} from "./controls/map/MapControls";
import {FreeFlightControls} from "./controls/freeflight/FreeFlightControls";
import {FileLoader, MathUtils, Vector3} from "three";
import {Map as BlueMapMap} from "./map/Map";
import {alert, animate, EasingFunctions, generateCacheHash} from "./util/Utils";
import {MainMenu} from "./MainMenu";
import {PopupMarker} from "./PopupMarker";
import {MarkerSet} from "./markers/MarkerSet";
import {getLocalStorage, round, setLocalStorage} from "./Utils";
import {i18n, setLanguage} from "../i18n";
import {PlayerMarkerManager} from "./markers/PlayerMarkerManager";
import {NormalMarkerManager} from "./markers/NormalMarkerManager";
import {reactive} from "vue";
export class BlueMapApp {
/**
* @param rootElement {Element}
*/
constructor(rootElement) {
this.events = rootElement;
this.mapViewer = new MapViewer(rootElement, this.events);
this.mapControls = new MapControls(this.mapViewer.renderer.domElement, rootElement);
this.freeFlightControls = new FreeFlightControls(this.mapViewer.renderer.domElement);
/** @type {PlayerMarkerManager} */
this.playerMarkerManager = null;
/** @type {NormalMarkerManager} */
this.markerFileManager = null;
/** @type {{
* version: string,
* useCookies: boolean,
* enableFreeFlight: boolean,
* defaultToFlatView: boolean,
* resolutionDefault: number,
* minZoomDistance: number,
* maxZoomDistance: number,
* hiresSliderMax: number,
* hiresSliderDefault: number,
* hiresSliderMin: number,
* lowresSliderMax: number,
* lowresSliderDefault: number,
* lowresSliderMin: number,
* startLocation: string,
* maps: string[],
* scripts: string[],
* styles: string[]
* }}
**/
this.settings = null;
this.savedUserSettings = new Map();
/** @type BlueMapMap[] */
this.maps = [];
/** @type Map<BlueMapMap> */
this.mapsMap = new Map();
this.lastCameraMove = 0;
this.dataUrl = "maps/";
this.mainMenu = reactive(new MainMenu());
this.appState = reactive({
controls: {
state: "perspective",
mouseSensitivity: 1,
showZoomButtons: true,
invertMouse: false,
enableFreeFlight: false,
pauseTileLoading: false
},
menu: this.mainMenu,
maps: [],
theme: null,
screenshot: {
clipboard: true
},
debug: false
});
// init
this.updateControlsSettings();
this.initGeneralEvents();
// popup on click
this.popupMarkerSet = new MarkerSet("bm-popup-set");
this.popupMarkerSet.data.toggleable = false;
this.popupMarker = new PopupMarker("bm-popup", this.appState, this.events);
this.popupMarkerSet.add(this.popupMarker);
this.mapViewer.markers.add(this.popupMarkerSet);
this.updateLoop = null;
this.hashUpdateTimeout = null;
this.viewAnimation = null;
}
/**
* @returns {Promise<void|never>}
*/
async load() {
let oldMaps = this.maps;
this.maps = [];
this.appState.maps.splice(0, this.appState.maps.length);
this.mapsMap.clear();
// load settings
await this.getSettings();
this.mapControls.minDistance = this.settings.minZoomDistance;
this.mapControls.maxDistance = this.settings.maxZoomDistance;
this.appState.controls.enableFreeFlight = this.settings.enableFreeFlight;
// load settings-styles
if (this.settings.styles) for (let styleUrl of this.settings.styles) {
let styleElement = document.createElement("link");
styleElement.rel = "stylesheet";
styleElement.href = styleUrl;
alert(this.events, "Loading style: " + styleUrl, "fine");
document.head.appendChild(styleElement);
}
// unload loaded maps
await this.mapViewer.switchMap(null);
oldMaps.forEach(map => map.dispose());
// load user settings
await this.loadUserSettings();
// load maps
this.maps = await this.loadMaps();
for (let map of this.maps) {
this.mapsMap.set(map.data.id, map);
this.appState.maps.push(map.data);
}
// switch to map
try {
if (!await this.loadPageAddress()) {
if (this.maps.length > 0) await this.switchMap(this.maps[0].data.id);
this.resetCamera();
}
} catch (e) {
console.error("Failed to load map!", e);
}
// map position address
window.addEventListener("hashchange", this.loadPageAddress);
this.events.addEventListener("bluemapCameraMoved", this.cameraMoved);
this.events.addEventListener("bluemapMapInteraction", this.mapInteraction);
// start app update loop
if(this.updateLoop) clearTimeout(this.updateLoop);
this.updateLoop = setTimeout(this.update, 1000);
// save user settings
this.saveUserSettings();
// load settings-scripts
if (this.settings.scripts) for (let scriptUrl of this.settings.scripts) {
let scriptElement = document.createElement("script");
scriptElement.src = scriptUrl;
alert(this.events, "Loading script: " + scriptUrl, "fine");
document.body.appendChild(scriptElement);
}
}
update = async () => {
await this.followPlayerMarkerWorld();
this.updateLoop = setTimeout(this.update, 1000);
}
async followPlayerMarkerWorld() {
/** @type {PlayerLike} */
let player = this.mapViewer.controlsManager.controls?.data.followingPlayer;
if (this.mapViewer.map && player) {
if (player.foreign){
let matchingMap = await this.findPlayerMap(player.playerUuid)
if (matchingMap) {
this.mainMenu.closeAll();
await this.switchMap(matchingMap.data.id, false);
let playerMarker = this.playerMarkerManager.getPlayerMarker(player.playerUuid);
if (playerMarker && this.mapViewer.controlsManager.controls.followPlayerMarker)
this.mapViewer.controlsManager.controls.followPlayerMarker(playerMarker);
} else {
if (this.mapViewer.controlsManager.controls.stopFollowingPlayerMarker)
this.mapViewer.controlsManager.controls.stopFollowingPlayerMarker();
}
}
}
}
async findPlayerMap(playerUuid) {
/** @type BlueMapMap */
let matchingMap = null;
// search for the map that contains the player
if (this.maps.length < 20) {
for (let map of this.maps) {
let playerData = await this.loadPlayerData(map);
if (!Array.isArray(playerData.players)) continue;
for (let p of playerData.players) {
if (p.uuid === playerUuid && !p.foreign) {
matchingMap = map;
break;
}
}
if (matchingMap) break;
}
}
return matchingMap;
}
/**
* @param mapId {String}
* @param resetCamera {boolean}
* @returns {Promise<void>}
*/
async switchMap(mapId, resetCamera = true) {
let map = this.mapsMap.get(mapId);
if (!map) return Promise.reject(`There is no map with the id "${mapId}" loaded!`);
if (this.playerMarkerManager) this.playerMarkerManager.dispose();
if (this.markerFileManager) this.markerFileManager.dispose();
await this.mapViewer.switchMap(map)
if (resetCamera) this.resetCamera();
this.updatePageAddress();
await Promise.all([
this.initPlayerMarkerManager(),
this.initMarkerFileManager()
]);
}
resetCamera() {
let map = this.mapViewer.map;
let controls = this.mapViewer.controlsManager;
if (map) {
controls.position.set(map.data.startPos.x, 0, map.data.startPos.z);
controls.distance = 1500;
controls.angle = 0;
controls.rotation = 0;
controls.tilt = 0;
controls.ortho = 0;
}
controls.controls = this.mapControls;
this.appState.controls.state = "perspective";
if (this.settings.defaultToFlatView) {
this.setFlatView();
}
this.updatePageAddress();
}
/**
* @returns Promise<BlueMapMap[]>
*/
async loadMaps() {
let settings = this.settings;
let maps = [];
// create maps
if (settings.maps !== undefined){
for (let mapId of settings.maps) {
let map = new BlueMapMap(mapId, this.dataUrl + mapId + "/", this.loadBlocker, this.mapViewer.events);
maps.push(map);
await map.loadSettings(this.mapViewer.tileCacheHash)
.catch(error => {
alert(this.events, `Failed to load settings for map '${map.data.id}':` + error, "warning");
});
}
}
// sort maps
maps.sort((map1, map2) => {
let sort = map1.data.sorting - map2.data.sorting;
if (isNaN(sort)) return 0;
return sort;
});
return maps;
}
async getSettings() {
if (!this.settings){
this.settings = await this.loadSettings();
}
return this.settings;
}
/**
* @returns {Promise<Object>}
*/
loadSettings() {
return new Promise((resolve, reject) => {
let loader = new FileLoader();
loader.setResponseType("json");
loader.load("settings.json?" + generateCacheHash(),
resolve,
() => {},
() => reject("Failed to load the settings.json!")
);
});
}
/**
* @param map {BlueMapMap}
* @returns {Promise<Object>}
*/
loadPlayerData(map) {
return new Promise((resolve, reject) => {
let loader = new FileLoader();
loader.setResponseType("json");
loader.load(map.data.dataUrl + "live/players.json?" + generateCacheHash(),
fileData => {
if (!fileData) reject(`Failed to parse '${this.fileUrl}'!`);
else resolve(fileData);
},
() => {},
() => reject(`Failed to load '${this.fileUrl}'!`)
)
});
}
initPlayerMarkerManager() {
if (this.playerMarkerManager)
this.playerMarkerManager.dispose()
const map = this.mapViewer.map;
if (!map) return;
this.playerMarkerManager = new PlayerMarkerManager(this.mapViewer.markers, map.data.dataUrl + "live/players.json", map.data.dataUrl + "assets/playerheads/", this.events);
this.playerMarkerManager.setAutoUpdateInterval(0);
return this.playerMarkerManager.update()
.then(() => {
this.playerMarkerManager.setAutoUpdateInterval(1000);
})
.catch(e => {
alert(this.events, e, "warning");
this.playerMarkerManager.dispose();
});
}
initMarkerFileManager() {
if (this.markerFileManager)
this.markerFileManager.dispose();
const map = this.mapViewer.map;
if (!map) return;
this.markerFileManager = new NormalMarkerManager(this.mapViewer.markers, map.data.dataUrl + "live/markers.json", this.events);
return this.markerFileManager.update()
.then(() => {
this.markerFileManager.setAutoUpdateInterval(1000 * 10);
})
.catch(e => {
alert(this.events, e, "warning");
this.markerFileManager.dispose();
});
}
updateControlsSettings() {
let mouseInvert = this.appState.controls.invertMouse ? -1 : 1;
this.freeFlightControls.mouseRotate.speedCapture = -1.5 * this.appState.controls.mouseSensitivity;
this.freeFlightControls.mouseAngle.speedCapture = -1.5 * this.appState.controls.mouseSensitivity * mouseInvert;
this.freeFlightControls.mouseRotate.speedRight = -2 * this.appState.controls.mouseSensitivity;
this.freeFlightControls.mouseAngle.speedRight = -2 * this.appState.controls.mouseSensitivity * mouseInvert;
}
initGeneralEvents() {
//close menu on fullscreen
document.addEventListener("fullscreenchange", evt => {
if (document.fullscreenElement) {
this.mainMenu.closeAll();
}
});
}
setPerspectiveView(transition = 0, minDistance = 5) {
if (!this.mapViewer.map) return;
if (this.viewAnimation) this.viewAnimation.cancel();
let cm = this.mapViewer.controlsManager;
cm.controls = null;
let startDistance = cm.distance;
let targetDistance = Math.max(5, minDistance, startDistance);
let startY = cm.position.y;
let targetY = MathUtils.lerp(this.mapViewer.map.terrainHeightAt(cm.position.x, cm.position.z) + 3, 0, targetDistance / 500);
let startAngle = cm.angle;
let targetAngle = Math.min(Math.PI / 2, startAngle, this.mapControls.getMaxPerspectiveAngleForDistance(targetDistance));
let startOrtho = cm.ortho;
let startTilt = cm.tilt;
this.viewAnimation = animate(p => {
let ep = EasingFunctions.easeInOutQuad(p);
cm.position.y = MathUtils.lerp(startY, targetY, ep);
cm.distance = MathUtils.lerp(startDistance, targetDistance, ep);
cm.angle = MathUtils.lerp(startAngle, targetAngle, ep);
cm.ortho = MathUtils.lerp(startOrtho, 0, p);
cm.tilt = MathUtils.lerp(startTilt, 0, ep);
}, transition, finished => {
this.mapControls.reset();
if (finished){
cm.controls = this.mapControls;
this.updatePageAddress();
}
});
this.appState.controls.state = "perspective";
}
setFlatView(transition = 0, minDistance = 5) {
if (!this.mapViewer.map) return;
if (this.viewAnimation) this.viewAnimation.cancel();
let cm = this.mapViewer.controlsManager;
cm.controls = null;
let startDistance = cm.distance;
let targetDistance = Math.max(5, minDistance, startDistance);
let startRotation = cm.rotation;
let startAngle = cm.angle;
let startOrtho = cm.ortho;
let startTilt = cm.tilt;
this.viewAnimation = animate(p => {
let ep = EasingFunctions.easeInOutQuad(p);
cm.distance = MathUtils.lerp(startDistance, targetDistance, ep);
cm.rotation = MathUtils.lerp(startRotation, 0, ep);
cm.angle = MathUtils.lerp(startAngle, 0, ep);
cm.ortho = MathUtils.lerp(startOrtho, 1, p);
cm.tilt = MathUtils.lerp(startTilt, 0, ep);
}, transition, finished => {
this.mapControls.reset();
if (finished){
cm.controls = this.mapControls;
this.updatePageAddress();
}
});
this.appState.controls.state = "flat";
}
setFreeFlight(transition = 0, targetY = undefined) {
if (!this.mapViewer.map) return;
if (!this.settings.enableFreeFlight) return this.setPerspectiveView(transition);
if (this.viewAnimation) this.viewAnimation.cancel();
let cm = this.mapViewer.controlsManager;
cm.controls = null;
let startDistance = cm.distance;
let startY = cm.position.y;
if (!targetY) targetY = this.mapViewer.map.terrainHeightAt(cm.position.x, cm.position.z) + 3 || startY;
let startAngle = cm.angle;
let targetAngle = Math.PI / 2;
let startOrtho = cm.ortho;
let startTilt = cm.tilt;
this.viewAnimation = animate(p => {
let ep = EasingFunctions.easeInOutQuad(p);
cm.position.y = MathUtils.lerp(startY, targetY, ep);
cm.distance = MathUtils.lerp(startDistance, 0, ep);
cm.angle = MathUtils.lerp(startAngle, targetAngle, ep);
cm.ortho = MathUtils.lerp(startOrtho, 0, Math.min(p * 2, 1));
cm.tilt = MathUtils.lerp(startTilt, 0, ep);
}, transition, finished => {
if (finished){
cm.controls = this.freeFlightControls;
this.updatePageAddress();
}
});
this.appState.controls.state = "free";
}
setDebug(debug) {
this.appState.debug = debug;
if (debug){
this.mapViewer.stats.showPanel(0);
} else {
this.mapViewer.stats.showPanel(-1);
}
}
setTheme(theme) {
this.appState.theme = theme;
if (theme === "light") {
this.mapViewer.rootElement.classList.remove("theme-dark");
this.mapViewer.rootElement.classList.remove("theme-contrast");
this.mapViewer.rootElement.classList.add("theme-light");
}
else if (theme === "dark") {
this.mapViewer.rootElement.classList.remove("theme-light");
this.mapViewer.rootElement.classList.remove("theme-contrast");
this.mapViewer.rootElement.classList.add("theme-dark");
}
else if (theme === "contrast") {
this.mapViewer.rootElement.classList.remove("theme-light");
this.mapViewer.rootElement.classList.remove("theme-dark");
this.mapViewer.rootElement.classList.add("theme-contrast");
}
else {
this.mapViewer.rootElement.classList.remove("theme-light");
this.mapViewer.rootElement.classList.remove("theme-dark");
this.mapViewer.rootElement.classList.remove("theme-contrast");
}
}
setScreenshotClipboard(clipboard) {
this.appState.screenshot.clipboard = clipboard;
}
async updateMap() {
try {
this.mapViewer.clearTileCache();
if (this.mapViewer.map) {
await this.switchMap(this.mapViewer.map.data.id, false);
}
this.saveUserSettings();
} catch (e) {
alert(this.events, e, "error");
}
}
resetSettings() {
this.saveUserSetting("resetSettings", true);
location.reload();
}
async loadUserSettings(){
if (!isNaN(this.settings.resolutionDefault)) this.mapViewer.data.superSampling = this.settings.resolutionDefault;
if (!isNaN(this.settings.hiresSliderDefault)) this.mapViewer.data.loadedHiresViewDistance = this.settings.hiresSliderDefault;
if (!isNaN(this.settings.lowresSliderDefault)) this.mapViewer.data.loadedLowresViewDistance = this.settings.lowresSliderDefault;
if (!this.settings.useCookies) return;
if (this.loadUserSetting("resetSettings", false)) {
alert(this.events, "Settings reset!", "info");
this.saveUserSettings();
return;
}
this.mapViewer.clearTileCache(this.loadUserSetting("tileCacheHash", this.mapViewer.tileCacheHash));
this.mapViewer.superSampling = this.loadUserSetting("superSampling", this.mapViewer.data.superSampling);
this.mapViewer.data.loadedHiresViewDistance = this.loadUserSetting("hiresViewDistance", this.mapViewer.data.loadedHiresViewDistance);
this.mapViewer.data.loadedLowresViewDistance = this.loadUserSetting("lowresViewDistance", this.mapViewer.data.loadedLowresViewDistance);
this.mapViewer.updateLoadedMapArea();
this.appState.controls.mouseSensitivity = this.loadUserSetting("mouseSensitivity", this.appState.controls.mouseSensitivity);
this.appState.controls.invertMouse = this.loadUserSetting("invertMouse", this.appState.controls.invertMouse);
this.appState.controls.pauseTileLoading = this.loadUserSetting("pauseTileLoading", this.appState.controls.pauseTileLoading);
this.appState.controls.showZoomButtons = this.loadUserSetting("showZoomButtons", this.appState.controls.showZoomButtons);
this.updateControlsSettings();
this.setTheme(this.loadUserSetting("theme", this.appState.theme));
this.setScreenshotClipboard(this.loadUserSetting("screenshotClipboard", this.appState.screenshot.clipboard));
await setLanguage(this.loadUserSetting("lang", i18n.locale.value));
this.setDebug(this.loadUserSetting("debug", this.appState.debug));
alert(this.events, "Settings loaded!", "info");
}
saveUserSettings() {
if (!this.settings.useCookies) return;
this.saveUserSetting("resetSettings", false);
this.saveUserSetting("tileCacheHash", this.mapViewer.tileCacheHash);
this.saveUserSetting("superSampling", this.mapViewer.data.superSampling);
this.saveUserSetting("hiresViewDistance", this.mapViewer.data.loadedHiresViewDistance);
this.saveUserSetting("lowresViewDistance", this.mapViewer.data.loadedLowresViewDistance);
this.saveUserSetting("mouseSensitivity", this.appState.controls.mouseSensitivity);
this.saveUserSetting("invertMouse", this.appState.controls.invertMouse);
this.saveUserSetting("pauseTileLoading", this.appState.controls.pauseTileLoading);
this.saveUserSetting("showZoomButtons", this.appState.controls.showZoomButtons);
this.saveUserSetting("theme", this.appState.theme);
this.saveUserSetting("screenshotClipboard", this.appState.screenshot.clipboard);
this.saveUserSetting("lang", i18n.locale.value);
this.saveUserSetting("debug", this.appState.debug);
alert(this.events, "Settings saved!", "info");
}
loadUserSetting(key, defaultValue){
let value = getLocalStorage("bluemap-" + key);
if (value === undefined) return defaultValue;
return value;
}
saveUserSetting(key, value){
if (this.savedUserSettings.get(key) !== value){
this.savedUserSettings.set(key, value);
setLocalStorage("bluemap-" + key, value);
}
}
cameraMoved = () => {
if (this.hashUpdateTimeout) clearTimeout(this.hashUpdateTimeout);
this.hashUpdateTimeout = setTimeout(this.updatePageAddress, 1500);
this.lastCameraMove = Date.now();
}
loadBlocker = async () => {
if (!this.appState.controls.pauseTileLoading) return;
let timeToWait;
do {
let timeSinceLastMove = Date.now() - this.lastCameraMove;
timeToWait = 250 - timeSinceLastMove;
if (timeToWait > 0) await this.sleep(timeToWait);
} while (timeToWait > 0);
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
updatePageAddress = () => {
let hash = "#";
if (this.mapViewer.map) {
hash += this.mapViewer.map.data.id;
let controls = this.mapViewer.controlsManager;
hash += ":" + round(controls.position.x, 0);
hash += ":" + round(controls.position.y, 0);
hash += ":" + round(controls.position.z, 0);
hash += ":" + round(controls.distance, 0);
hash += ":" + round(controls.rotation, 2);
hash += ":" + round(controls.angle, 2);
hash += ":" + round(controls.tilt, 2);
hash += ":" + round(controls.ortho, 0);
hash += ":" + this.appState.controls.state;
}
history.replaceState(undefined, undefined, hash);
document.title = i18n.t("pageTitle", {
map: this.mapViewer.map ? this.mapViewer.map.data.name : "?",
version: this.settings.version
});
}
loadPageAddress = async () => {
let hash = window.location.hash?.substring(1) || this.settings.startLocation || "";
let values = hash.split(":");
if (values.length !== 10) return false;
let controls = this.mapViewer.controlsManager;
controls.controls = null;
if (!this.mapViewer.map || this.mapViewer.map.data.id !== values[0]) {
try {
await this.switchMap(values[0]);
} catch (e) {
return false;
}
}
switch (values[9]) {
case "flat" : this.setFlatView(0); break;
case "free" : this.setFreeFlight(0, controls.position.y); break;
default : this.setPerspectiveView(0); break;
}
controls.position.x = parseFloat(values[1]);
controls.position.y = parseFloat(values[2]);
controls.position.z = parseFloat(values[3]);
controls.distance = parseFloat(values[4]);
controls.rotation = parseFloat(values[5]);
controls.angle = parseFloat(values[6]);
controls.tilt = parseFloat(values[7]);
controls.ortho = parseFloat(values[8]);
this.updatePageAddress();
this.mapViewer.updateLoadedMapArea();
return true;
}
mapInteraction = event => {
if (event.detail.data.doubleTap) {
let cm = this.mapViewer.controlsManager;
let pos = event.detail.hit?.point || event.detail.object?.getWorldPosition(new Vector3());
if (!pos) return;
let startDistance = cm.distance;
let targetDistance = Math.max(startDistance * 0.25, 5);
let startX = cm.position.x;
let targetX = pos.x;
let startZ = cm.position.z;
let targetZ = pos.z;
this.viewAnimation = animate(p => {
let ep = EasingFunctions.easeInOutQuad(p);
cm.distance = MathUtils.lerp(startDistance, targetDistance, ep);
cm.position.x = MathUtils.lerp(startX, targetX, ep);
cm.position.z = MathUtils.lerp(startZ, targetZ, ep);
}, 500);
}
}
takeScreenshot = () => {
let link = document.createElement("a");
link.download = "bluemap-screenshot.png";
link.href = this.mapViewer.renderer.domElement.toDataURL('image/png');
link.click();
if (this.appState.screenshot.clipboard) {
this.mapViewer.renderer.domElement.toBlob(blob => {
// eslint-disable-next-line no-undef
navigator.clipboard.write([new ClipboardItem({ ['image/png']: blob })]).catch(e => {
alert(this.events, "Failed to copy screenshot to clipboard: " + e, "error");
});
});
}
}
}