/* * This file is part of BlueMap, licensed under the MIT License (MIT). * * Copyright (c) Blue (Lukas Rieger) * 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 */ 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} */ 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} */ 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 */ 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} */ 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} */ 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"); }); }); } } }