diff --git a/BlueMapCore/package.json b/BlueMapCore/package.json index 9900989d..89fb5510 100644 --- a/BlueMapCore/package.json +++ b/BlueMapCore/package.json @@ -29,6 +29,8 @@ "node-sass": "^4.13.0", "sass": "^1.24.4", "sass-loader": "^8.0.2", + "ts-loader": "^6.2.1", + "typescript": "^3.7.4", "webpack": "^4.41.5", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.10.1" diff --git a/BlueMapCore/src/main/webroot/js/libs/Controls.js b/BlueMapCore/src/main/webroot/js/libs/Controls.js new file mode 100644 index 00000000..7fd9748a --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/Controls.js @@ -0,0 +1,297 @@ +/* + * 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 Softwarevent. + * + * 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 SOFTWARevent. + */ +import $ from 'jquery'; +import { + Euler, + Raycaster, + Vector2, + Vector3, + MOUSE +} from 'three'; + +import { Vector2_ZERO } from './utils.js'; + +export default class Controls { + static KEYS = { + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + ORBIT: MOUSE.RIGHT, + MOVE: MOUSE.LEFT + }; + static STATES = { + NONE: -1, + ORBIT: 0, + MOVE: 1, + }; + + /** + * targetHeightScene and cameraHeightScene are scenes of objects that are checked via raycasting for a height for the target and the camera + */ + constructor(camera, element, heightScene) { + this.settings = { + zoom: { + min: 10, + max: 2000, + speed: 1.5, + smooth: 0.2, + }, + move: { + speed: 1.75, + smooth: 0.3, + smoothY: 0.075, + }, + tilt: { + max: Math.PI / 2.1, + speed: 1.5, + smooth: 0.3, + }, + rotate: { + speed: 1.5, + smooth: 0.3, + } + }; + + this.camera = camera; + this.element = element; + this.heightScene = heightScene; + this.minHeight = 0; + + this.raycaster = new Raycaster(); + this.rayDirection = new Vector3(0, -1, 0); + + this.resetPosition(); + + this.mouse = new Vector2(0, 0); + this.lastMouse = new Vector2(0, 0); + this.deltaMouse = new Vector2(0, 0); + + //variables used to calculate with (to prevent object creation every update) + this.orbitRot = new Euler(0, 0, 0, 'YXZ'); + this.cameraPosDelta = new Vector3(0, 0, 0); + this.moveDelta = new Vector2(0, 0); + + this.keyStates = {} + this.state = Controls.STATES.NONE; + + let canvas = $(this.element).find('canvas').get(0); + window.addEventListener('contextmenu', event => { + event.preventDefault(); + }, false); + canvas.addEventListener('mousedown', this.onMouseDown, false); + window.addEventListener('mousemove', this.onMouseMove, false); + window.addEventListener('mouseup', this.onMouseUp, false); + canvas.addEventListener('wheel', this.onMouseWheel, false); + window.addEventListener('keydown', this.onKeyDown, false); + window.addEventListener('keyup', this.onKeyUp, false); + + this.camera.position.set(0, 1000, 0); + this.camera.lookAt(this.position); + this.camera.updateProjectionMatrix(); + } + + resetPosition() { + this.position = new Vector3(0, 70, 0); + this.targetPosition = new Vector3(0, 70, 0); + + this.distance = 5000; + this.targetDistance = 1000; + + this.direction = 0; + this.targetDirection = 0; + + this.angle = 0; + this.targetAngle = 0; + } + + update() { + this.updateMouseMoves(); + + let changed = false; + + let zoomLerp = (this.distance - 100) / 200; + if (zoomLerp < 0) zoomLerp = 0; + if (zoomLerp > 1) zoomLerp = 1; + this.targetPosition.y = 300 * zoomLerp + this.minHeight * (1 - zoomLerp); + + this.position.x += (this.targetPosition.x - this.position.x) * this.settings.move.smooth; + this.position.y += (this.targetPosition.y - this.position.y) * this.settings.move.smoothY; + this.position.z += (this.targetPosition.z - this.position.z) * this.settings.move.smooth; + + this.distance += (this.targetDistance - this.distance) * this.settings.zoom.smooth; + + let deltaDir = (this.targetDirection - this.direction) * this.settings.rotate.smooth; + this.direction += deltaDir; + changed = changed || Math.abs(deltaDir) > 0.001; + + let max = Math.min(this.settings.tilt.max, this.settings.tilt.max - Math.pow(((this.distance - this.settings.zoom.min) / (this.settings.zoom.max - this.settings.zoom.min)) * Math.pow(this.settings.tilt.max, 4), 1/4)); + if (this.targetAngle > max) this.targetAngle = max; + if (this.targetAngle < 0.01) this.targetAngle = 0.001; + let deltaAngle = (this.targetAngle - this.angle) * this.settings.tilt.smooth; + this.angle += deltaAngle; + changed = changed || Math.abs(deltaAngle) > 0.001; + + let last = this.camera.position.x + this.camera.position.y + this.camera.position.z; + this.orbitRot.set(this.angle, this.direction, 0); + this.cameraPosDelta.set(0, this.distance, 0).applyEuler(this.orbitRot); + + this.camera.position.set(this.position.x + this.cameraPosDelta.x, this.position.y + this.cameraPosDelta.y, this.position.z + this.cameraPosDelta.z); + let move = last - (this.camera.position.x + this.camera.position.y + this.camera.position.z); + + changed = changed || Math.abs(move) > 0.001; + + if (changed) { + this.camera.lookAt(this.position); + this.camera.updateProjectionMatrix(); + + this.updateHeights(); + } + + return changed; + } + + updateHeights() { + //TODO: this can be performance-improved by only intersecting the correct tile? + + let rayStart = new Vector3(this.targetPosition.x, 300, this.targetPosition.z); + this.raycaster.set(rayStart, this.rayDirection); + this.raycaster.near = 1; + this.raycaster.far = 300; + let intersects = this.raycaster.intersectObjects(this.heightScene.children); + if (intersects.length > 0){ + this.minHeight = intersects[0].point.y; + //this.targetPosition.y = this.minHeight; + } else { + //this.targetPosition.y = 0; + } + + rayStart.set(this.camera.position.x, 300, this.camera.position.z); + this.raycaster.set(rayStart, this.rayDirection); + intersects.length = 0; + intersects = this.raycaster.intersectObjects(this.heightScene.children); + if (intersects.length > 0){ + if (intersects[0].point.y > this.minHeight){ + this.minHeight = intersects[0].point.y; + } + } + }; + + updateMouseMoves = () => { + this.deltaMouse.set(this.lastMouse.x - this.mouse.x, this.lastMouse.y - this.mouse.y); + + this.moveDelta.x = 0; + this.moveDelta.y = 0; + + if (this.keyStates[Controls.KEYS.UP]){ + this.moveDelta.y -= 20; + } + if (this.keyStates[Controls.KEYS.DOWN]){ + this.moveDelta.y += 20; + } + if (this.keyStates[Controls.KEYS.LEFT]){ + this.moveDelta.x -= 20; + } + if (this.keyStates[Controls.KEYS.RIGHT]){ + this.moveDelta.x += 20; + } + + if (this.state === Controls.STATES.MOVE) { + if (this.deltaMouse.x === 0 && this.deltaMouse.y === 0) return; + this.moveDelta.copy(this.deltaMouse); + } + + if (this.moveDelta.x !== 0 || this.moveDelta.y !== 0) { + this.moveDelta.rotateAround(Vector2_ZERO, -this.direction); + this.targetPosition.set( + this.targetPosition.x + (this.moveDelta.x * this.distance / this.element.clientHeight * this.settings.move.speed), + this.targetPosition.y, + this.targetPosition.z + (this.moveDelta.y * this.distance / this.element.clientHeight * this.settings.move.speed) + ); + } + + if (this.state === Controls.STATES.ORBIT) { + this.targetDirection += (this.deltaMouse.x / this.element.clientHeight * Math.PI); + this.targetAngle += (this.deltaMouse.y / this.element.clientHeight * Math.PI); + } + + this.lastMouse.copy(this.mouse); + }; + + onMouseWheel = event => { + if (event.deltaY > 0) { + this.targetDistance *= this.settings.zoom.speed; + } else if (event.deltaY < 0) { + this.targetDistance /= this.settings.zoom.speed; + } + + if (this.targetDistance < this.settings.zoom.min) this.targetDistance = this.settings.zoom.min; + if (this.targetDistance > this.settings.zoom.max) this.targetDistance = this.settings.zoom.max; + } + + onMouseMove = event => { + this.mouse.set(event.clientX, event.clientY); + + if (this.state !== Controls.STATES.NONE){ + event.preventDefault(); + } + } + + onMouseDown = event => { + if (this.state !== Controls.STATES.NONE) return; + + switch (event.button) { + case Controls.KEYS.MOVE : + this.state = Controls.STATES.MOVE; + event.preventDefault(); + break; + case Controls.KEYS.ORBIT : + this.state = Controls.STATES.ORBIT; + event.preventDefault(); + break; + } + } + + onMouseUp = event => { + if (this.state === Controls.STATES.NONE) return; + + switch (event.button) { + case Controls.KEYS.MOVE : + if (this.state === Controls.STATES.MOVE) this.state = Controls.STATES.NONE; + break; + case Controls.KEYS.ORBIT : + if (this.state === Controls.STATES.ORBIT) this.state = Controls.STATES.NONE; + break; + } + } + + onKeyDown = event => { + this.keyStates[event.keyCode] = true; + } + + onKeyUp = e => { + this.keyStates[event.keyCode] = false; + } +} diff --git a/BlueMapCore/src/main/webroot/js/libs/Tile.js b/BlueMapCore/src/main/webroot/js/libs/Tile.js new file mode 100644 index 00000000..6bcc7e24 --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/Tile.js @@ -0,0 +1,59 @@ +/* + * 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. + */ + +export default class Tile { + isLoading = false; + disposed = false; + model = null; + + constructor(scene, x, z) { + this.scene = scene; + this.x = x; + this.z = z; + } + + setModel(model) { + this.disposeModel(); + + if (model) { + this.model = model; + this.scene.add(model); + + //console.log("Added tile:", this.x, this.z); + } + } + + disposeModel() { + this.disposed = true; + + if (this.model) { + this.scene.remove(this.model); + this.model.geometry.dispose(); + delete this.model; + + //console.log("Removed tile:", this.x, this.z); + } + } +} diff --git a/BlueMapCore/src/main/webroot/js/libs/TileManager.js b/BlueMapCore/src/main/webroot/js/libs/TileManager.js new file mode 100644 index 00000000..7112564b --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/TileManager.js @@ -0,0 +1,183 @@ +/* + * 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 { Vector2 } from 'three'; + +import Tile from './Tile.js'; + +import { hashTile } from './utils.js'; + +export default class TileManager { + constructor(blueMap, viewDistance, tileLoader, scene, tileSize, position) { + this.blueMap = blueMap; + this.viewDistance = viewDistance; + this.tileLoader = tileLoader; + this.scene = scene; + this.tileSize = new Vector2(tileSize.x, tileSize.z); + + this.tile = new Vector2(position.x, position.z); + this.lastTile = this.tile.clone(); + + this.closed = false; + this.currentlyLoading = 0; + this.updateTimeout = null; + + this.tiles = {}; + } + + setPosition(center) { + this.tile.set(center.x, center.z).divide(this.tileSize).floor(); + + if (!this.tile.equals(this.lastTile) && !this.closed) { + this.update(); + this.lastTile.copy(this.tile); + } + } + + update() { + if (this.closed) return; + + //free a loader so if there was an error loading a tile we don"t get stuck forever with the blocked loading process + this.currentlyLoading--; + if (this.currentlyLoading < 0) this.currentlyLoading = 0; + + this.removeFarTiles(); + this.loadCloseTiles(); + } + + removeFarTiles() { + let keys = Object.keys(this.tiles); + for (let i = 0; i < keys.length; i++) { + if (!this.tiles.hasOwnProperty(keys[i])) continue; + + let tile = this.tiles[keys[i]]; + + let vd = this.viewDistance; + + if ( + tile.x + vd < this.tile.x || + tile.x - vd > this.tile.x || + tile.z + vd < this.tile.y || + tile.z - vd > this.tile.y + ) { + tile.disposeModel(); + delete this.tiles[keys[i]]; + } + } + } + + removeAllTiles() { + let keys = Object.keys(this.tiles); + for (let i = 0; i < keys.length; i++) { + if (!this.tiles.hasOwnProperty(keys[i])) continue; + + let tile = this.tiles[keys[i]]; + tile.disposeModel(); + delete this.tiles[keys[i]]; + } + } + + close() { + this.closed = true; + this.removeAllTiles(); + } + + loadCloseTiles() { + if (this.closed) return; + + if (this.currentlyLoading < 8) { + if (!this.loadNextTile()) return; + } + + if (this.updateTimeout) clearTimeout(this.updateTimeout); + this.updateTimeout = setTimeout(() => this.loadCloseTiles(), 0); + } + + loadNextTile() { + let x = 0; + let z = 0; + let d = 1; + let m = 1; + + while (m < this.viewDistance * 2) { + while (2 * x * d < m) { + if (this.tryLoadTile(this.tile.x + x, this.tile.y + z)) return true; + x = x + d; + } + while (2 * z * d < m) { + if (this.tryLoadTile(this.tile.x + x, this.tile.y + z)) return true; + z = z + d; + } + d = -1 * d; + m = m + 1; + } + + return false; + } + + tryLoadTile(x, z) { + if (this.closed) return; + + let tileHash = hashTile(x, z); + + let tile = this.tiles[tileHash]; + if (tile !== undefined) return false; + + tile = new Tile(this.scene, x, z); + tile.isLoading = true; + + this.currentlyLoading++; + + this.tiles[tileHash] = tile; + + this.tileLoader.call(this.blueMap, x, z, model => { + tile.isLoading = false; + + if (tile.disposed || this.closed) { + model.geometry.dispose(); + tile.disposeModel(); + delete this.tiles[tileHash]; + return; + } + + this.tiles[tileHash] = tile; + tile.setModel(model); + + this.blueMap.updateFrame = true; + + this.currentlyLoading--; + if (this.currentlyLoading < 0) this.currentlyLoading = 0; + }, error => { + tile.isLoading = false; + tile.disposeModel(); + + this.currentlyLoading--; + + //console.log("Failed to load tile: ", x, z); + }); + + return true; + } +} diff --git a/BlueMapCore/src/main/webroot/js/libs/bluemap.js b/BlueMapCore/src/main/webroot/js/libs/bluemap.js index 61d52685..a0972e0f 100644 --- a/BlueMapCore/src/main/webroot/js/libs/bluemap.js +++ b/BlueMapCore/src/main/webroot/js/libs/bluemap.js @@ -22,1202 +22,469 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -import $ from "jquery"; -import * as THREE from "three"; +import $ from 'jquery'; +import { + AmbientLight, + BackSide, + BufferGeometryLoader, + ClampToEdgeWrapping, + CubeGeometry, + DirectionalLight, + FileLoader, + FrontSide, + Mesh, + MeshBasicMaterial, + MeshLambertMaterial, + NormalBlending, + NearestFilter, + PerspectiveCamera, + Scene, + Texture, + TextureLoader, + VertexColors, + WebGLRenderer, +} from 'three'; -import GEAR from "../../assets/gear.svg"; -import COMPASS from "../../assets/compass.svg"; +import Compass from './modules/Compass.js'; +import Info from './modules/Info.js'; +import MapMenu from './modules/MapMenu.js'; +import Position from './modules/Position.js'; +import Settings from './modules/Settings.js'; -import SKYBOX_NORTH from "../../assets/skybox/north.png"; -import SKYBOX_SOUTH from "../../assets/skybox/south.png"; -import SKYBOX_EAST from "../../assets/skybox/east.png"; -import SKYBOX_WEST from "../../assets/skybox/west.png"; -import SKYBOX_UP from "../../assets/skybox/up.png"; -import SKYBOX_DOWN from "../../assets/skybox/down.png"; +import Controls from './Controls.js'; +import TileManager from './TileManager.js'; -const BlueMap = function (element, dataRoot) { - this.element = element; - this.dataRoot = dataRoot; +import { stringToImage, pathFromCoords } from './utils.js'; - this.loadingNoticeElement = $('
loading...
').appendTo($(this.element)); +import SKYBOX_NORTH from '../../assets/skybox/north.png'; +import SKYBOX_SOUTH from '../../assets/skybox/south.png'; +import SKYBOX_EAST from '../../assets/skybox/east.png'; +import SKYBOX_WEST from '../../assets/skybox/west.png'; +import SKYBOX_UP from '../../assets/skybox/up.png'; +import SKYBOX_DOWN from '../../assets/skybox/down.png'; - this.fileLoader = new THREE.FileLoader(); - this.blobLoader = new THREE.FileLoader(); - this.blobLoader.setResponseType("blob"); - this.bufferGeometryLoader = new THREE.BufferGeometryLoader(); +export default class BlueMap { + constructor(element, dataRoot) { + this.element = element; + this.dataRoot = dataRoot; - this.initStage(); - this.locationHash = ""; - this.controls = new BlueMap.Controls(this.camera, this.element, this.hiresScene); + this.loadingNoticeElement = $('
loading...
').appendTo($(this.element)); - this.loadSettings(function () { - this.lowresTileManager = new BlueMap.TileManager( + this.fileLoader = new FileLoader(); + this.blobLoader = new FileLoader(); + this.blobLoader.setResponseType('blob'); + this.bufferGeometryLoader = new BufferGeometryLoader(); + + this.initStage(); + this.locationHash = ''; + this.controls = new Controls(this.camera, this.element, this.hiresScene); + + this.loadSettings(() => { + this.lowresTileManager = new TileManager( + this, + this.settings[this.map]['lowres']['viewDistance'], + this.loadLowresTile, + this.lowresScene, + this.settings[this.map]['lowres']['tileSize'], + {x: 0, z: 0} + ); + + this.hiresTileManager = new TileManager( + this, + this.settings[this.map]['hires']['viewDistance'], + this.loadHiresTile, + this.hiresScene, + this.settings[this.map]['hires']['tileSize'], + {x: 0, z: 0} + ); + + this.loadHiresMaterial(() => { + this.loadLowresMaterial(() => { + this.initModules(); + this.start(); + }); + }); + }); + } + + initModules() { + this.modules = {}; + this.modules.compass = new Compass(this); + this.modules.position = new Position(this); + this.modules.mapMenu = new MapMenu(this); + this.modules.info = new Info(this); + this.modules.settings = new Settings(this); + } + + changeMap(map) { + this.hiresTileManager.close(); + this.lowresTileManager.close(); + + this.map = map; + this.controls.resetPosition(); + + this.lowresTileManager = new TileManager( this, - this.settings[this.map]["lowres"]["viewDistance"], + this.settings[this.map]['lowres']['viewDistance'], this.loadLowresTile, this.lowresScene, - this.settings[this.map]["lowres"]["tileSize"], + this.settings[this.map]['lowres']['tileSize'], {x: 0, z: 0} ); - this.hiresTileManager = new BlueMap.TileManager( + this.hiresTileManager = new TileManager( this, - this.settings[this.map]["hires"]["viewDistance"], + this.settings[this.map]['hires']['viewDistance'], this.loadHiresTile, this.hiresScene, - this.settings[this.map]["hires"]["tileSize"], + this.settings[this.map]['hires']['tileSize'], {x: 0, z: 0} ); - this.loadHiresMaterial(function () { - this.loadLowresMaterial(function () { - this.initModules(); - this.start(); - }); - }); - }); -}; + this.lowresTileManager.update(); + this.hiresTileManager.update(); -BlueMap.prototype.initModules = function () { - this.modules = {}; + document.dispatchEvent(new Event('bluemap-map-change')); + } - this.modules.compass = new BlueMap.Module.Compass(this); - this.modules.position = new BlueMap.Module.Position(this); - this.modules.mapMenu = new BlueMap.Module.MapMenu(this); - this.modules.info = new BlueMap.Module.Info(this); - this.modules.settings = new BlueMap.Module.Settings(this); -}; - -BlueMap.prototype.changeMap = function (map) { - this.hiresTileManager.close(); - this.lowresTileManager.close(); - - this.map = map; - this.controls.resetPosition(); - - this.lowresTileManager = new BlueMap.TileManager( - this, - this.settings[this.map]["lowres"]["viewDistance"], - this.loadLowresTile, - this.lowresScene, - this.settings[this.map]["lowres"]["tileSize"], - {x: 0, z: 0} - ); - - this.hiresTileManager = new BlueMap.TileManager( - this, - this.settings[this.map]["hires"]["viewDistance"], - this.loadHiresTile, - this.hiresScene, - this.settings[this.map]["hires"]["tileSize"], - {x: 0, z: 0} - ); - - this.lowresTileManager.update(); - this.hiresTileManager.update(); - - document.dispatchEvent(new Event("bluemap-map-change")); -}; - -BlueMap.prototype.loadLocationHash = function(){ - let hashVars = window.location.hash.substring(1).split(":"); - if (hashVars.length >= 1){ - if (this.settings[hashVars[0]] !== undefined && this.map !== hashVars[0]){ - this.changeMap(hashVars[0]); + loadLocationHash() { + let hashVars = window.location.hash.substring(1).split(':'); + if (hashVars.length >= 1){ + if (this.settings[hashVars[0]] !== undefined && this.map !== hashVars[0]){ + this.changeMap(hashVars[0]); + } } - } - if (hashVars.length >= 3){ - let x = parseInt(hashVars[1]); - let z = parseInt(hashVars[2]); - if (!isNaN(x) && !isNaN(z)){ - this.controls.targetPosition.x = x + 0.5; - this.controls.targetPosition.z = z + 0.5; + if (hashVars.length >= 3){ + let x = parseInt(hashVars[1]); + let z = parseInt(hashVars[2]); + if (!isNaN(x) && !isNaN(z)){ + this.controls.targetPosition.x = x + 0.5; + this.controls.targetPosition.z = z + 0.5; + } } - } - if (hashVars.length >= 6){ - let dir = parseFloat(hashVars[3]); - let dist = parseFloat(hashVars[4]); - let angle = parseFloat(hashVars[5]); - if (!isNaN(dir)) this.controls.targetDirection = dir; - if (!isNaN(dist)) this.controls.targetDistance = dist; - if (!isNaN(angle)) this.controls.targetAngle = angle; - this.controls.direction = this.controls.targetDirection; - this.controls.distance = this.controls.targetDistance; - this.controls.angle = this.controls.targetAngle; - this.controls.targetPosition.y = this.controls.minHeight; - this.controls.position.copy(this.controls.targetPosition); - } - if (hashVars.length >= 7){ - let height = parseInt(hashVars[6]); - if (!isNaN(height)){ - this.controls.minHeight = height; - this.controls.targetPosition.y = height; + if (hashVars.length >= 6){ + let dir = parseFloat(hashVars[3]); + let dist = parseFloat(hashVars[4]); + let angle = parseFloat(hashVars[5]); + if (!isNaN(dir)) this.controls.targetDirection = dir; + if (!isNaN(dist)) this.controls.targetDistance = dist; + if (!isNaN(angle)) this.controls.targetAngle = angle; + this.controls.direction = this.controls.targetDirection; + this.controls.distance = this.controls.targetDistance; + this.controls.angle = this.controls.targetAngle; + this.controls.targetPosition.y = this.controls.minHeight; this.controls.position.copy(this.controls.targetPosition); } - } -}; - -BlueMap.prototype.start = function () { - let scope = this; - - this.loadingNoticeElement.remove(); - - this.loadLocationHash(); - - $(window).on("hashchange", function(evt){ - if (scope.locationHash === window.location.hash) return; - scope.loadLocationHash(); - }); - - this.update(); - this.render(); - - this.lowresTileManager.update(); - this.hiresTileManager.update(); -}; - -BlueMap.prototype.update = function () { - let scope = this; - setTimeout(function () { - scope.update() - }, 1000); - - this.lowresTileManager.setPosition(this.controls.targetPosition); - this.hiresTileManager.setPosition(this.controls.targetPosition); - - this.locationHash = - "#" + this.map - + ":" + Math.floor(this.controls.targetPosition.x) - + ":" + Math.floor(this.controls.targetPosition.z) - + ":" + Math.round(this.controls.targetDirection * 100) / 100 - + ":" + Math.round(this.controls.targetDistance * 100) / 100 - + ":" + Math.ceil(this.controls.targetAngle * 100) / 100 - + ":" + Math.floor(this.controls.targetPosition.y); - history.replaceState(undefined, undefined, this.locationHash); -}; - -BlueMap.prototype.render = function () { - let scope = this; - requestAnimationFrame(function () { - scope.render() - }); - - if (this.controls.update()) this.updateFrame = true; - - if (!this.updateFrame) return; - this.updateFrame = false; - - document.dispatchEvent(new Event("bluemap-update-frame")); - - this.skyboxCamera.rotation.copy(this.camera.rotation); - this.skyboxCamera.updateProjectionMatrix(); - - - this.renderer.clear(); - this.renderer.render(this.skyboxScene, this.skyboxCamera, this.renderer.getRenderTarget(), false); - this.renderer.clearDepth(); - this.renderer.render(this.lowresScene, this.camera, this.renderer.getRenderTarget(), false); - if (this.camera.position.y < 400) { - this.renderer.clearDepth(); - this.renderer.render(this.hiresScene, this.camera, this.renderer.getRenderTarget(), false); - } -}; - -BlueMap.prototype.handleContainerResize = function () { - this.camera.aspect = this.element.clientWidth / this.element.clientHeight; - this.camera.updateProjectionMatrix(); - - this.skyboxCamera.aspect = this.element.clientWidth / this.element.clientHeight; - this.skyboxCamera.updateProjectionMatrix(); - - this.renderer.setSize(this.element.clientWidth * this.quality, this.element.clientHeight * this.quality); - $(this.renderer.domElement) - .css("width", this.element.clientWidth) - .css("height", this.element.clientHeight); - - this.updateFrame = true; -}; - -BlueMap.prototype.loadSettings = function (callback) { - let scope = this; - - this.fileLoader.load(this.dataRoot + "settings.json", function (settings) { - scope.settings = JSON.parse(settings); - - scope.maps = []; - for (let map in scope.settings){ - if (scope.settings.hasOwnProperty(map) && scope.settings[map].enabled){ - scope.maps.push(map); + if (hashVars.length >= 7){ + let height = parseInt(hashVars[6]); + if (!isNaN(height)){ + this.controls.minHeight = height; + this.controls.targetPosition.y = height; + this.controls.position.copy(this.controls.targetPosition); } } - - scope.maps.sort(function (map1, map2) { - var sort = scope.settings[map1].ordinal - scope.settings[map2].ordinal; - if (isNaN(sort)) return 0; - return sort; - }); - - scope.map = scope.maps[0]; - - callback.call(scope); - }); -}; - -BlueMap.prototype.initStage = function () { - let scope = this; - - this.updateFrame = true; - this.quality = 1; - - this.renderer = new THREE.WebGLRenderer({ - alpha: true, - antialias: true, - sortObjects: false, - preserveDrawingBuffer: true, - logarithmicDepthBuffer: true, - }); - this.renderer.autoClear = false; - - this.camera = new THREE.PerspectiveCamera(75, this.element.scrollWidth / this.element.scrollHeight, 0.1, 10000); - this.camera.updateProjectionMatrix(); - - this.skyboxCamera = this.camera.clone(); - this.skyboxCamera.updateProjectionMatrix(); - - this.skyboxScene = new THREE.Scene(); - this.skyboxScene.ambient = new THREE.AmbientLight(0xffffff, 1); - this.skyboxScene.add(this.skyboxScene.ambient); - this.skyboxScene.add(this.createSkybox()); - - this.lowresScene = new THREE.Scene(); - this.lowresScene.ambient = new THREE.AmbientLight(0xffffff, 0.6); - this.lowresScene.add(this.lowresScene.ambient); - this.lowresScene.sunLight = new THREE.DirectionalLight(0xccccbb, 0.7); - this.lowresScene.sunLight.position.set(1, 5, 3); - this.lowresScene.add(this.lowresScene.sunLight); - - this.hiresScene = new THREE.Scene(); - this.hiresScene.ambient = new THREE.AmbientLight(0xffffff, 1); - this.hiresScene.add(this.hiresScene.ambient); - this.hiresScene.sunLight = new THREE.DirectionalLight(0xccccbb, 0.2); - this.hiresScene.sunLight.position.set(1, 5, 3); - this.hiresScene.add(this.hiresScene.sunLight); - - this.element.append(this.renderer.domElement); - this.handleContainerResize(); - - $(window).resize(function () { - scope.handleContainerResize() - }); -}; - -BlueMap.prototype.createSkybox = function(){ - let geometry = new THREE.CubeGeometry(10, 10, 10); - let material = [ - new THREE.MeshBasicMaterial({ - map: new THREE.TextureLoader().load(SKYBOX_SOUTH), - side: THREE.BackSide - }), - new THREE.MeshBasicMaterial({ - map: new THREE.TextureLoader().load(SKYBOX_NORTH), - side: THREE.BackSide - }), - new THREE.MeshBasicMaterial({ - map: new THREE.TextureLoader().load(SKYBOX_UP), - side: THREE.BackSide - }), - new THREE.MeshBasicMaterial({ - map: new THREE.TextureLoader().load(SKYBOX_DOWN), - side: THREE.BackSide - }), - new THREE.MeshBasicMaterial({ - map: new THREE.TextureLoader().load(SKYBOX_EAST), - side: THREE.BackSide - }), - new THREE.MeshBasicMaterial({ - map: new THREE.TextureLoader().load(SKYBOX_WEST), - side: THREE.BackSide - }) - ]; - return new THREE.Mesh(geometry, material); -}; - -BlueMap.prototype.loadHiresMaterial = function (callback) { - let scope = this; - - this.fileLoader.load(this.dataRoot + "textures.json", function (textures) { - textures = JSON.parse(textures); - - let materials = []; - for (let i = 0; i < textures["textures"].length; i++) { - let t = textures["textures"][i]; - - let material = new THREE.MeshLambertMaterial({ - transparent: t["transparent"], - alphaTest: 0.01, - depthWrite: true, - depthTest: true, - blending: THREE.NormalBlending, - vertexColors: THREE.VertexColors, - side: THREE.FrontSide, - wireframe: false - }); - - let texture = new THREE.Texture(); - texture.image = BlueMap.utils.stringToImage(t["texture"]); - - texture.premultiplyAlpha = false; - texture.generateMipmaps = false; - texture.magFilter = THREE.NearestFilter; - texture.minFilter = THREE.NearestFilter; - texture.wrapS = THREE.ClampToEdgeWrapping; - texture.wrapT = THREE.ClampToEdgeWrapping; - texture.flipY = false; - texture.needsUpdate = true; - texture.flatShading = true; - - material.map = texture; - material.needsUpdate = true; - - materials[i] = material; - } - - scope.hiresMaterial = materials; - - callback.call(scope); - }); -}; - -BlueMap.prototype.loadLowresMaterial = function (callback) { - this.lowresMaterial = new THREE.MeshLambertMaterial({ - transparent: false, - depthWrite: true, - depthTest: true, - vertexColors: THREE.VertexColors, - side: THREE.FrontSide, - wireframe: false - }); - - callback.call(this); -}; - -BlueMap.prototype.loadHiresTile = function (tileX, tileZ, callback, onError) { - let scope = this; - - let path = this.dataRoot + this.map + "/hires/"; - path += BlueMap.utils.pathFromCoords(tileX, tileZ); - path += ".json"; - - - this.bufferGeometryLoader.load(path, function (geometry) { - let object = new THREE.Mesh(geometry, scope.hiresMaterial); - - let tileSize = scope.settings[scope.map]["hires"]["tileSize"]; - let translate = scope.settings[scope.map]["hires"]["translate"]; - let scale = scope.settings[scope.map]["hires"]["scale"]; - object.position.set(tileX * tileSize.x + translate.x, 0, tileZ * tileSize.z + translate.z); - object.scale.set(scale.x, 1, scale.z); - - callback.call(scope, object); - }, function () { - - }, function (error) { - onError.call(scope, error); - }); -}; - -BlueMap.prototype.loadLowresTile = function (tileX, tileZ, callback, onError) { - let scope = this; - - let path = this.dataRoot + this.map + "/lowres/"; - path += BlueMap.utils.pathFromCoords(tileX, tileZ); - path += ".json"; - - this.bufferGeometryLoader.load(path, function (geometry) { - let object = new THREE.Mesh(geometry, scope.lowresMaterial); - - let tileSize = scope.settings[scope.map]["lowres"]["tileSize"]; - let translate = scope.settings[scope.map]["lowres"]["translate"]; - let scale = scope.settings[scope.map]["lowres"]["scale"]; - object.position.set(tileX * tileSize.x + translate.x, 0, tileZ * tileSize.z + translate.z); - object.scale.set(scale.x, 1, scale.z); - - callback.call(scope, object); - }, function () { - - }, function (error) { - onError.call(scope, error); - }); -}; - - -// ###### UI ###### - -BlueMap.prototype.alert = function (content) { - let alertBox = $("#alert-box"); - if (alertBox.length === 0){ - alertBox = $('
').appendTo(this.element); } - let displayAlert = function(){ - let alert = $('").appendTo(alertBox); - alert.find(".alert-close-button").click(function(){ - alert.fadeOut(200, function(){ - alert.remove(); - }); + start() { + this.loadingNoticeElement.remove(); + + this.loadLocationHash(); + + $(window).on('hashchange', () => { + if (this.locationHash === window.location.hash) return; + this.loadLocationHash(); }); - alert.fadeIn(200); - }; - let oldAlerts = alertBox.find(".alert"); - if (oldAlerts.length > 0){ - alertBox.fadeOut(200, function () { - alertBox.html(""); - alertBox.show(); - displayAlert(); - }) - } else { - displayAlert(); - } -}; - -// ###### TileManager ###### -BlueMap.TileManager = function (blueMap, viewDistance, tileLoader, scene, tileSize, position) { - this.blueMap = blueMap; - this.viewDistance = viewDistance; - this.tileLoader = tileLoader; - this.scene = scene; - this.tileSize = new THREE.Vector2(tileSize.x, tileSize.z); - - this.tile = new THREE.Vector2(position.x, position.z); - this.lastTile = this.tile.clone(); - - this.closed = false; - this.currentlyLoading = 0; - this.updateTimeout = null; - - this.tiles = {}; -}; - -BlueMap.TileManager.prototype.setPosition = function (center) { - this.tile.set(center.x, center.z).divide(this.tileSize).floor(); - - if (!this.tile.equals(this.lastTile) && !this.closed) { this.update(); - this.lastTile.copy(this.tile); - } -}; + this.render(); -BlueMap.TileManager.prototype.update = function () { - if (this.closed) return; - - //free a loader so if there was an error loading a tile we don"t get stuck forever with the blocked loading process - this.currentlyLoading--; - if (this.currentlyLoading < 0) this.currentlyLoading = 0; - - this.removeFarTiles(); - this.loadCloseTiles(); -}; - -BlueMap.TileManager.prototype.removeFarTiles = function () { - let keys = Object.keys(this.tiles); - for (let i = 0; i < keys.length; i++) { - if (!this.tiles.hasOwnProperty(keys[i])) continue; - - let tile = this.tiles[keys[i]]; - - let vd = this.viewDistance; - - if ( - tile.x + vd < this.tile.x || - tile.x - vd > this.tile.x || - tile.z + vd < this.tile.y || - tile.z - vd > this.tile.y - ) { - tile.disposeModel(); - delete this.tiles[keys[i]]; - } - } -}; - -BlueMap.TileManager.prototype.removeAllTiles = function () { - let keys = Object.keys(this.tiles); - for (let i = 0; i < keys.length; i++) { - if (!this.tiles.hasOwnProperty(keys[i])) continue; - - let tile = this.tiles[keys[i]]; - tile.disposeModel(); - delete this.tiles[keys[i]]; - } -}; - -BlueMap.TileManager.prototype.close = function () { - this.closed = true; - this.removeAllTiles(); -}; - -BlueMap.TileManager.prototype.loadCloseTiles = function () { - if (this.closed) return; - - let scope = this; - - if (this.currentlyLoading < 8) { - if (!this.loadNextTile()) return; + this.lowresTileManager.update(); + this.hiresTileManager.update(); } - if (this.updateTimeout) clearTimeout(this.updateTimeout); - this.updateTimeout = setTimeout(function () { - scope.loadCloseTiles() - }, 0); -}; + update = () => { + setTimeout(this.update, 1000); -BlueMap.TileManager.prototype.loadNextTile = function () { - let x = 0; - let z = 0; - let d = 1; - let m = 1; + this.lowresTileManager.setPosition(this.controls.targetPosition); + this.hiresTileManager.setPosition(this.controls.targetPosition); - while (m < this.viewDistance * 2) { - while (2 * x * d < m) { - if (this.tryLoadTile(this.tile.x + x, this.tile.y + z)) return true; - x = x + d; - } - while (2 * z * d < m) { - if (this.tryLoadTile(this.tile.x + x, this.tile.y + z)) return true; - z = z + d; - } - d = -1 * d; - m = m + 1; - } - - return false; -}; - -BlueMap.TileManager.prototype.tryLoadTile = function (x, z) { - if (this.closed) return; - - let scope = this; - - let tileHash = BlueMap.utils.hashTile(x, z); - - let tile = this.tiles[tileHash]; - if (tile !== undefined) return false; - - tile = new BlueMap.Tile(this.scene, x, z); - tile.isLoading = true; - - this.currentlyLoading++; - - this.tiles[tileHash] = tile; - - this.tileLoader.call(this.blueMap, x, z, function (model) { - tile.isLoading = false; - - if (tile.disposed || scope.closed) { - model.geometry.dispose(); - tile.disposeModel(); - delete scope.tiles[tileHash]; - return; - } - - scope.tiles[tileHash] = tile; - tile.setModel(model); - - scope.blueMap.updateFrame = true; - - scope.currentlyLoading--; - if (scope.currentlyLoading < 0) scope.currentlyLoading = 0; - }, function (error) { - tile.isLoading = false; - tile.disposeModel(); - - scope.currentlyLoading--; - - //console.log("Failed to load tile: ", x, z); - }); - - return true; -}; - - -// ###### Tile ###### -BlueMap.Tile = function (scene, x, z) { - this.scene = scene; - this.x = x; - this.z = z; - - this.isLoading = false; - this.disposed = false; - - this.model = null; -}; - -BlueMap.Tile.prototype.setModel = function (model) { - this.disposeModel(); - - if (model) { - this.model = model; - this.scene.add(model); - - //console.log("Added tile:", this.x, this.z); - } -}; - -BlueMap.Tile.prototype.disposeModel = function () { - this.disposed = true; - - if (this.model) { - this.scene.remove(this.model); - this.model.geometry.dispose(); - delete this.model; - - //console.log("Removed tile:", this.x, this.z); - } -}; - - -// ###### Controls ###### - -/** - * targetHeightScene and cameraHeightScene are scenes of objects that are checked via raycasting for a height for the target and the camera - */ -BlueMap.Controls = function (camera, element, heightScene) { - let scope = this; - - this.settings = { - zoom: { - min: 10, - max: 2000, - speed: 1.5, - smooth: 0.2, - }, - move: { - speed: 1.75, - smooth: 0.3, - smoothY: 0.075, - }, - tilt: { - max: Math.PI / 2.1, - speed: 1.5, - smooth: 0.3, - }, - rotate: { - speed: 1.5, - smooth: 0.3, - } + this.locationHash = + '#' + this.map + + ':' + Math.floor(this.controls.targetPosition.x) + + ':' + Math.floor(this.controls.targetPosition.z) + + ':' + Math.round(this.controls.targetDirection * 100) / 100 + + ':' + Math.round(this.controls.targetDistance * 100) / 100 + + ':' + Math.ceil(this.controls.targetAngle * 100) / 100 + + ':' + Math.floor(this.controls.targetPosition.y); + history.replaceState(undefined, undefined, this.locationHash); }; - this.camera = camera; - this.element = element; - this.heightScene = heightScene; - this.minHeight = 0; + render = () => { + requestAnimationFrame(this.render); - this.raycaster = new THREE.Raycaster(); - this.rayDirection = new THREE.Vector3(0, -1, 0); + if (this.controls.update()) this.updateFrame = true; - this.resetPosition(); + if (!this.updateFrame) return; + this.updateFrame = false; - this.mouse = new THREE.Vector2(0, 0); - this.lastMouse = new THREE.Vector2(0, 0); - this.deltaMouse = new THREE.Vector2(0, 0); + document.dispatchEvent(new Event('bluemap-update-frame')); - //variables used to calculate with (to prevent object creation every update) - this.orbitRot = new THREE.Euler(0, 0, 0, "YXZ"); - this.cameraPosDelta = new THREE.Vector3(0, 0, 0); - this.moveDelta = new THREE.Vector2(0, 0); + this.skyboxCamera.rotation.copy(this.camera.rotation); + this.skyboxCamera.updateProjectionMatrix(); - this.keyStates = {}; + this.renderer.clear(); + this.renderer.render(this.skyboxScene, this.skyboxCamera, this.renderer.getRenderTarget(), false); + this.renderer.clearDepth(); + this.renderer.render(this.lowresScene, this.camera, this.renderer.getRenderTarget(), false); + if (this.camera.position.y < 400) { + this.renderer.clearDepth(); + this.renderer.render(this.hiresScene, this.camera, this.renderer.getRenderTarget(), false); + } + } - this.KEYS = { - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - ORBIT: THREE.MOUSE.RIGHT, - MOVE: THREE.MOUSE.LEFT - }; - this.STATES = { - NONE: -1, - ORBIT: 0, - MOVE: 1, - }; - - this.state = this.STATES.NONE; - - let canvas = $(this.element).find("canvas").get(0); - window.addEventListener("contextmenu", function (e) { - e.preventDefault(); - }, false); - canvas.addEventListener("mousedown", function (e) { - scope.onMouseDown(e); - }, false); - window.addEventListener("mousemove", function (e) { - scope.onMouseMove(e); - }, false); - window.addEventListener("mouseup", function (e) { - scope.onMouseUp(e); - }, false); - canvas.addEventListener("wheel", function (e) { - scope.onMouseWheel(e); - }, false); - window.addEventListener("keydown", function (e) { - scope.onKeyDown(e); - }, false); - window.addEventListener("keyup", function (e) { - scope.onKeyUp(e); - }, false); - - this.camera.position.set(0, 1000, 0); - this.camera.lookAt(this.position); - this.camera.updateProjectionMatrix(); -}; - -BlueMap.Controls.prototype.resetPosition = function () { - this.position = new THREE.Vector3(0, 70, 0); - this.targetPosition = new THREE.Vector3(0, 70, 0); - - this.distance = 5000; - this.targetDistance = 1000; - - this.direction = 0; - this.targetDirection = 0; - - this.angle = 0; - this.targetAngle = 0; -}; - -BlueMap.Controls.prototype.update = function () { - this.updateMouseMoves(); - - let changed = false; - - let zoomLerp = (this.distance - 100) / 200; - if (zoomLerp < 0) zoomLerp = 0; - if (zoomLerp > 1) zoomLerp = 1; - this.targetPosition.y = 300 * zoomLerp + this.minHeight * (1 - zoomLerp); - - this.position.x += (this.targetPosition.x - this.position.x) * this.settings.move.smooth; - this.position.y += (this.targetPosition.y - this.position.y) * this.settings.move.smoothY; - this.position.z += (this.targetPosition.z - this.position.z) * this.settings.move.smooth; - - this.distance += (this.targetDistance - this.distance) * this.settings.zoom.smooth; - - let deltaDir = (this.targetDirection - this.direction) * this.settings.rotate.smooth; - this.direction += deltaDir; - changed = changed || Math.abs(deltaDir) > 0.001; - - let max = Math.min(this.settings.tilt.max, this.settings.tilt.max - Math.pow(((this.distance - this.settings.zoom.min) / (this.settings.zoom.max - this.settings.zoom.min)) * Math.pow(this.settings.tilt.max, 4), 1/4)); - if (this.targetAngle > max) this.targetAngle = max; - if (this.targetAngle < 0.01) this.targetAngle = 0.001; - let deltaAngle = (this.targetAngle - this.angle) * this.settings.tilt.smooth; - this.angle += deltaAngle; - changed = changed || Math.abs(deltaAngle) > 0.001; - - let last = this.camera.position.x + this.camera.position.y + this.camera.position.z; - this.orbitRot.set(this.angle, this.direction, 0); - this.cameraPosDelta.set(0, this.distance, 0).applyEuler(this.orbitRot); - - this.camera.position.set(this.position.x + this.cameraPosDelta.x, this.position.y + this.cameraPosDelta.y, this.position.z + this.cameraPosDelta.z); - let move = last - (this.camera.position.x + this.camera.position.y + this.camera.position.z); - - changed = changed || Math.abs(move) > 0.001; - - if (changed) { - this.camera.lookAt(this.position); + handleContainerResize = () => { + this.camera.aspect = this.element.clientWidth / this.element.clientHeight; this.camera.updateProjectionMatrix(); - this.updateHeights(); + this.skyboxCamera.aspect = this.element.clientWidth / this.element.clientHeight; + this.skyboxCamera.updateProjectionMatrix(); + + this.renderer.setSize(this.element.clientWidth * this.quality, this.element.clientHeight * this.quality); + $(this.renderer.domElement) + .css('width', this.element.clientWidth) + .css('height', this.element.clientHeight); + + this.updateFrame = true; } - return changed; -}; + loadSettings(callback) { + let scope = this; -BlueMap.Controls.prototype.updateHeights = function(){ - //TODO: this can be performance-improved by only intersecting the correct tile? + this.fileLoader.load(this.dataRoot + 'settings.json', settings => { + scope.settings = JSON.parse(settings); - let rayStart = new THREE.Vector3(this.targetPosition.x, 300, this.targetPosition.z); - this.raycaster.set(rayStart, this.rayDirection); - this.raycaster.near = 1; - this.raycaster.far = 300; - let intersects = this.raycaster.intersectObjects(this.heightScene.children); - if (intersects.length > 0){ - this.minHeight = intersects[0].point.y; - //this.targetPosition.y = this.minHeight; - } else { - //this.targetPosition.y = 0; - } - - rayStart.set(this.camera.position.x, 300, this.camera.position.z); - this.raycaster.set(rayStart, this.rayDirection); - intersects.length = 0; - intersects = this.raycaster.intersectObjects(this.heightScene.children); - if (intersects.length > 0){ - if (intersects[0].point.y > this.minHeight){ - this.minHeight = intersects[0].point.y; + scope.maps = []; + for (let map in scope.settings) { + if (scope.settings.hasOwnProperty(map) && scope.settings[map].enabled){ + scope.maps.push(map); + } } - } -}; -BlueMap.Controls.prototype.updateMouseMoves = function (e) { - this.deltaMouse.set(this.lastMouse.x - this.mouse.x, this.lastMouse.y - this.mouse.y); + scope.maps.sort((map1, map2) => { + var sort = scope.settings[map1].ordinal - scope.settings[map2].ordinal; + if (isNaN(sort)) return 0; + return sort; + }); - this.moveDelta.x = 0; - this.moveDelta.y = 0; + scope.map = scope.maps[0]; - if (this.keyStates[this.KEYS.UP]){ - this.moveDelta.y -= 20; - } - if (this.keyStates[this.KEYS.DOWN]){ - this.moveDelta.y += 20; - } - if (this.keyStates[this.KEYS.LEFT]){ - this.moveDelta.x -= 20; - } - if (this.keyStates[this.KEYS.RIGHT]){ - this.moveDelta.x += 20; - } - - if (this.state === this.STATES.MOVE) { - if (this.deltaMouse.x === 0 && this.deltaMouse.y === 0) return; - this.moveDelta.copy(this.deltaMouse); - } - - if (this.moveDelta.x !== 0 || this.moveDelta.y !== 0) { - this.moveDelta.rotateAround(BlueMap.utils.Vector2.ZERO, -this.direction); - this.targetPosition.set( - this.targetPosition.x + (this.moveDelta.x * this.distance / this.element.clientHeight * this.settings.move.speed), - this.targetPosition.y, - this.targetPosition.z + (this.moveDelta.y * this.distance / this.element.clientHeight * this.settings.move.speed) - ); - } - - if (this.state === this.STATES.ORBIT) { - this.targetDirection += (this.deltaMouse.x / this.element.clientHeight * Math.PI); - this.targetAngle += (this.deltaMouse.y / this.element.clientHeight * Math.PI); - } - - this.lastMouse.copy(this.mouse); -}; - -BlueMap.Controls.prototype.onMouseWheel = function (e) { - if (e.deltaY > 0) { - this.targetDistance *= this.settings.zoom.speed; - } else if (e.deltaY < 0) { - this.targetDistance /= this.settings.zoom.speed; - } - - if (this.targetDistance < this.settings.zoom.min) this.targetDistance = this.settings.zoom.min; - if (this.targetDistance > this.settings.zoom.max) this.targetDistance = this.settings.zoom.max; -}; - -BlueMap.Controls.prototype.onMouseMove = function (e) { - this.mouse.set(e.clientX, e.clientY); - - if (this.state !== this.STATES.NONE){ - e.preventDefault(); - } -}; - -BlueMap.Controls.prototype.onMouseDown = function (e) { - if (this.state !== this.STATES.NONE) return; - - switch (e.button) { - case this.KEYS.MOVE : - this.state = this.STATES.MOVE; - e.preventDefault(); - break; - case this.KEYS.ORBIT : - this.state = this.STATES.ORBIT; - e.preventDefault(); - break; - } -}; - -BlueMap.Controls.prototype.onMouseUp = function (e) { - if (this.state === this.STATES.NONE) return; - - switch (e.button) { - case this.KEYS.MOVE : - if (this.state === this.STATES.MOVE) this.state = this.STATES.NONE; - break; - case this.KEYS.ORBIT : - if (this.state === this.STATES.ORBIT) this.state = this.STATES.NONE; - break; - } -}; - -BlueMap.Controls.prototype.onKeyDown = function (e) { - this.keyStates[e.keyCode] = true; -}; - -BlueMap.Controls.prototype.onKeyUp = function (e) { - this.keyStates[e.keyCode] = false; -}; - -// ###### Modules ###### -BlueMap.Module = { - getTopRightElement: function(blueMap){ - let element = $("#bluemap-topright"); - - if (element.length === 0){ - element = $('
').appendTo(blueMap.element); - } - - return element; - }, - - getTopLeftElement: function(blueMap){ - let element = $("#bluemap-topleft"); - - if (element.length === 0){ - element = $('
').appendTo(blueMap.element); - } - - return element; - } -}; - -// ###### Modules.MapMenu ###### -BlueMap.Module.MapMenu = function (blueMap) { - let scope = this; - - this.bluemap = blueMap; - let maps = this.bluemap.settings; - - $("#bluemap-mapmenu").remove(); - this.element = $('").appendTo(BlueMap.Module.getTopLeftElement(blueMap)); - - let dropdown = $('').appendTo(this.element); - this.maplist = $("
    ").appendTo(dropdown); - - for (let mapId in maps) { - if (!maps.hasOwnProperty(mapId)) continue; - if (!maps.enabled) continue; - - let map = maps[mapId]; - $('
  • ' + map.name + "
  • ").appendTo(this.maplist); - } - - this.maplist.find("li[map=" + this.bluemap.map + "]").hide(); - this.maplist.find("li[map]").click(function (e) { - let map = $(this).attr("map"); - scope.bluemap.changeMap(map); - }); - - $(document).on("bluemap-map-change", function(){ - scope.maplist.find("li").show(); - scope.maplist.find("li[map=" + scope.bluemap.map + "]").hide(); - - scope.element.find(".selection").html(scope.bluemap.settings[scope.bluemap.map].name); - }); -}; - -// ###### Modules.Compass ###### -BlueMap.Module.Compass = function (blueMap) { - let scope = this; - - this.blueMap = blueMap; - - $("#bluemap-compass").remove(); - this.element = $(`
    `).appendTo(BlueMap.Module.getTopLeftElement(blueMap)); - this.needle = $("#bluemap-compass-needle"); - - $(document).on("bluemap-update-frame", function (){ - scope.needle.css("transform", "rotate(" + scope.blueMap.controls.direction + "rad)"); - }); - - $(this.element).click(function(){ - scope.blueMap.controls.targetDirection = 0; - scope.blueMap.controls.direction = scope.blueMap.controls.direction % (Math.PI * 2); - if (scope.blueMap.controls.direction < -Math.PI) scope.blueMap.controls.direction += Math.PI * 2; - if (scope.blueMap.controls.direction > Math.PI) scope.blueMap.controls.direction -= Math.PI * 2; - }); -}; - -// ###### Modules.Position ###### -BlueMap.Module.Position = function (blueMap) { - let scope = this; - - this.blueMap = blueMap; - - let parent = BlueMap.Module.getTopLeftElement(blueMap); - - $(".bluemap-position").remove(); - this.elementX = $('
    0
    ').appendTo(parent); - //this.elementY = $('
    0
    ').appendTo(parent); - this.elementZ = $('
    0
    ').appendTo(parent); - - $(document).on("bluemap-update-frame", function (){ - scope.elementX.html(Math.floor(scope.blueMap.controls.targetPosition.x)); - //scope.elementY.html(scope.blueMap.controls.targetPosition.y === 0 ? "-" : Math.floor(scope.blueMap.controls.targetPosition.y)); - scope.elementZ.html(Math.floor(scope.blueMap.controls.targetPosition.z)); - }); -}; - -// ###### Modules.Settings ###### -BlueMap.Module.Settings = function (blueMap) { - let scope = this; - - this.blueMap = blueMap; - - let parent = BlueMap.Module.getTopRightElement(blueMap); - - $("#bluemap-settings").remove(); - this.elementMenu = $('').appendTo(parent); - this.elementSettings = $(`
    `).appendTo(parent); - this.elementSettings.click(function(){ - if (scope.elementMenu.css("display") === "none"){ - scope.elementSettings.addClass("active"); - } else { - scope.elementSettings.removeClass("active"); - } - - scope.elementMenu.animate({ - width: "toggle" - }, 200); - }); - - - /* Quality */ - - this.elementQuality = $( - '' - ).prependTo(this.elementMenu); - - this.elementQuality.find("li[quality]").click(function(){ - let desc = $(this).html(); - scope.blueMap.quality = parseFloat($(this).attr("quality")); - - scope.elementQuality.find("li").show(); - scope.elementQuality.find("li[quality=\"" + scope.blueMap.quality + "\"]").hide(); - - scope.elementQuality.find(".selection > span").html(desc); - - scope.blueMap.handleContainerResize(); - }); - - - /* Render Distance */ - - this.pctToRenderDistance = function ( value , defaultValue ) { - let max = defaultValue * 5; - if (max > 20) max = 20; - - return THREE.Math.mapLinear(value, 0, 100, 1, max); - }; - - this.renderDistanceToPct = function ( value , defaultValue ) { - let max = defaultValue * 5; - if (max > 20) max = 20; - - return THREE.Math.mapLinear(value, 1, max, 0, 100); - }; - - this.init = function(){ - scope.defaultHighRes = scope.blueMap.hiresTileManager.viewDistance; - scope.defaultLowRes = scope.blueMap.lowresTileManager.viewDistance; - - scope.elementRenderDistance.html( - 'View Distance: ' + scope.blueMap.hiresTileManager.viewDistance + '' + - '' - ); - - let slider = scope.elementRenderDistance.find("input"); - slider.on("change input", function(e){ - scope.blueMap.hiresTileManager.viewDistance = scope.pctToRenderDistance(parseFloat(slider.val()), scope.defaultHighRes); - scope.blueMap.lowresTileManager.viewDistance = scope.pctToRenderDistance(parseFloat(slider.val()), scope.defaultLowRes); - scope.elementRenderDistance.find(".selection > span").html(Math.round(scope.blueMap.hiresTileManager.viewDistance * 10) / 10); - - scope.blueMap.lowresTileManager.update(); - scope.blueMap.hiresTileManager.update(); + callback.call(scope); }); - }; - - this.elementRenderDistance = $( - '' - ).prependTo(this.elementMenu); - - scope.init(); - - $(document).on("bluemap-map-change", this.init); -}; - -// ###### Modules.Info ###### -BlueMap.Module.Info = function (blueMap) { - let scope = this; - - this.blueMap = blueMap; - - let parent = BlueMap.Module.getTopRightElement(blueMap); - - $("#bluemap-info").remove(); - this.elementInfo = $('
    ').appendTo(parent); - - this.elementInfo.click(function(){ - scope.blueMap.alert( - '

    Info

    ' + - 'Visit BlueMap on GitHub!
    ' + - 'BlueMap works best with Chrome.
    ' + - '

    Controls

    ' + - 'Leftclick-drag with your mouse or use the arrow-keys to navigate.
    ' + - 'Rightclick-drag with your mouse to rotate your view.
    ' + - 'Scroll to zoom.
    ' - ); - }); -}; - -// ###### Utils ###### -BlueMap.utils = {}; - -BlueMap.utils.stringToImage = function (string) { - let image = document.createElementNS("http://www.w3.org/1999/xhtml", "img"); - image.src = string; - return image; -}; - -BlueMap.utils.pathFromCoords = function (x, z) { - let path = "x"; - path += BlueMap.utils.splitNumberToPath(x); - - path += "z"; - path += BlueMap.utils.splitNumberToPath(z); - - path = path.substring(0, path.length - 1); - - return path; -}; - -BlueMap.utils.splitNumberToPath = function (num) { - let path = ""; - - if (num < 0) { - num = -num; - path += "-"; } - let s = num.toString(); + initStage() { + this.updateFrame = true; + this.quality = 1; - for (let i = 0; i < s.length; i++) { - path += s.charAt(i) + "/"; + this.renderer = new WebGLRenderer({ + alpha: true, + antialias: true, + sortObjects: false, + preserveDrawingBuffer: true, + logarithmicDepthBuffer: true, + }); + this.renderer.autoClear = false; + + this.camera = new PerspectiveCamera(75, this.element.scrollWidth / this.element.scrollHeight, 0.1, 10000); + this.camera.updateProjectionMatrix(); + + this.skyboxCamera = this.camera.clone(); + this.skyboxCamera.updateProjectionMatrix(); + + this.skyboxScene = new Scene(); + this.skyboxScene.ambient = new AmbientLight(0xffffff, 1); + this.skyboxScene.add(this.skyboxScene.ambient); + this.skyboxScene.add(this.createSkybox()); + + this.lowresScene = new Scene(); + this.lowresScene.ambient = new AmbientLight(0xffffff, 0.6); + this.lowresScene.add(this.lowresScene.ambient); + this.lowresScene.sunLight = new DirectionalLight(0xccccbb, 0.7); + this.lowresScene.sunLight.position.set(1, 5, 3); + this.lowresScene.add(this.lowresScene.sunLight); + + this.hiresScene = new Scene(); + this.hiresScene.ambient = new AmbientLight(0xffffff, 1); + this.hiresScene.add(this.hiresScene.ambient); + this.hiresScene.sunLight = new DirectionalLight(0xccccbb, 0.2); + this.hiresScene.sunLight.position.set(1, 5, 3); + this.hiresScene.add(this.hiresScene.sunLight); + + this.element.append(this.renderer.domElement); + this.handleContainerResize(); + + $(window).resize(this.handleContainerResize); } - return path; -}; + createSkybox() { + let geometry = new CubeGeometry(10, 10, 10); + let material = [ + new MeshBasicMaterial({ + map: new TextureLoader().load(SKYBOX_SOUTH), + side: BackSide + }), + new MeshBasicMaterial({ + map: new TextureLoader().load(SKYBOX_NORTH), + side: BackSide + }), + new MeshBasicMaterial({ + map: new TextureLoader().load(SKYBOX_UP), + side: BackSide + }), + new MeshBasicMaterial({ + map: new TextureLoader().load(SKYBOX_DOWN), + side: BackSide + }), + new MeshBasicMaterial({ + map: new TextureLoader().load(SKYBOX_EAST), + side: BackSide + }), + new MeshBasicMaterial({ + map: new TextureLoader().load(SKYBOX_WEST), + side: BackSide + }) + ]; + return new Mesh(geometry, material); + } -BlueMap.utils.hashTile = function (x, z) { - return "x" + x + "z" + z; -}; + loadHiresMaterial(callback) { + let scope = this; -BlueMap.utils.Vector2 = {}; -BlueMap.utils.Vector2.ZERO = new THREE.Vector2(0, 0); -BlueMap.utils.Vector3 = {}; -BlueMap.utils.Vector3.ZERO = new THREE.Vector3(0, 0); + this.fileLoader.load(this.dataRoot + 'textures.json', textures => { + textures = JSON.parse(textures); -export default BlueMap; + let materials = []; + for (let i = 0; i < textures['textures'].length; i++) { + let t = textures['textures'][i]; + + let material = new MeshLambertMaterial({ + transparent: t['transparent'], + alphaTest: 0.01, + depthWrite: true, + depthTest: true, + blending: NormalBlending, + vertexColors: VertexColors, + side: FrontSide, + wireframe: false + }); + + let texture = new Texture(); + texture.image = stringToImage(t['texture']); + + texture.premultiplyAlpha = false; + texture.generateMipmaps = false; + texture.magFilter = NearestFilter; + texture.minFilter = NearestFilter; + texture.wrapS = ClampToEdgeWrapping; + texture.wrapT = ClampToEdgeWrapping; + texture.flipY = false; + texture.needsUpdate = true; + texture.flatShading = true; + + material.map = texture; + material.needsUpdate = true; + + materials[i] = material; + } + + scope.hiresMaterial = materials; + + callback.call(scope); + }); + } + + loadLowresMaterial(callback) { + this.lowresMaterial = new MeshLambertMaterial({ + transparent: false, + depthWrite: true, + depthTest: true, + vertexColors: VertexColors, + side: FrontSide, + wireframe: false + }); + + callback.call(this); + } + + loadHiresTile(tileX, tileZ, callback, onError) { + let path = this.dataRoot + this.map + '/hires/'; + path += pathFromCoords(tileX, tileZ); + path += '.json'; + + this.bufferGeometryLoader.load(path, geometry => { + let object = new Mesh(geometry, this.hiresMaterial); + + let tileSize = this.settings[this.map]['hires']['tileSize']; + let translate = this.settings[this.map]['hires']['translate']; + let scale = this.settings[this.map]['hires']['scale']; + object.position.set(tileX * tileSize.x + translate.x, 0, tileZ * tileSize.z + translate.z); + object.scale.set(scale.x, 1, scale.z); + + callback.call(this, object); + }, () => { + + }, error => { + onError.call(this, error); + }); + } + + loadLowresTile(tileX, tileZ, callback, onError) { + let path = this.dataRoot + this.map + '/lowres/'; + path += pathFromCoords(tileX, tileZ); + path += '.json'; + + this.bufferGeometryLoader.load(path, geometry => { + let object = new Mesh(geometry, this.lowresMaterial); + + let tileSize = this.settings[this.map]['lowres']['tileSize']; + let translate = this.settings[this.map]['lowres']['translate']; + let scale = this.settings[this.map]['lowres']['scale']; + object.position.set(tileX * tileSize.x + translate.x, 0, tileZ * tileSize.z + translate.z); + object.scale.set(scale.x, 1, scale.z); + + callback.call(this, object); + }, () => { + + }, error => { + onError.call(this, error); + }); + } + + // ###### UI ###### + + alert(content) { + let alertBox = $('#alert-box'); + if (alertBox.length === 0){ + alertBox = $('
    ').appendTo(this.element); + } + + let displayAlert = () => { + let alert = $(``).appendTo(alertBox); + alert.find('.alert-close-button').click(() => { + alert.fadeOut(200, () => alert.remove()); + }); + alert.fadeIn(200); + }; + + let oldAlerts = alertBox.find('.alert'); + if (oldAlerts.length > 0){ + alertBox.fadeOut(200, () => { + alertBox.html(''); + alertBox.show(); + displayAlert(); + }); + } else { + displayAlert(); + } + } +} diff --git a/BlueMapCore/src/main/webroot/js/libs/modules/Compass.js b/BlueMapCore/src/main/webroot/js/libs/modules/Compass.js new file mode 100644 index 00000000..6e60cdc0 --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/modules/Compass.js @@ -0,0 +1,53 @@ +/* + * 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 $ from 'jquery'; + +import { getTopLeftElement } from './Module.js'; + +import COMPASS from '../../../assets/compass.svg'; + +export default class Compass { + constructor(blueMap) { + this.blueMap = blueMap; + + $('#bluemap-compass').remove(); + this.element = $(`
    `).appendTo(getTopLeftElement(blueMap)); + this.needle = $('#bluemap-compass-needle'); + + $(document).on('bluemap-update-frame', this.onBlueMapUpdateFrame); + $(this.element).click(this.onClick); + } + + onBlueMapUpdateFrame = () => { + this.needle.css('transform', `rotate(${this.blueMap.controls.direction}rad)`); + } + + onClick = () => { + this.blueMap.controls.targetDirection = 0; + this.blueMap.controls.direction = this.blueMap.controls.direction % (Math.PI * 2); + if (this.blueMap.controls.direction < -Math.PI) this.blueMap.controls.direction += Math.PI * 2; + if (this.blueMap.controls.direction > Math.PI) this.blueMap.controls.direction -= Math.PI * 2; + } +} diff --git a/BlueMapCore/src/main/webroot/js/libs/modules/Info.js b/BlueMapCore/src/main/webroot/js/libs/modules/Info.js new file mode 100644 index 00000000..19816d7c --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/modules/Info.js @@ -0,0 +1,49 @@ +/* + * 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 $ from 'jquery'; + +import { getTopRightElement } from './Module.js'; + +export default class Info { + constructor(blueMap) { + this.blueMap = blueMap; + const parent = getTopRightElement(blueMap); + $('#bluemap-info').remove(); + this.elementInfo = $('
    ').appendTo(parent); + this.elementInfo.click(this.onClick); + } + + onClick = () => { + this.blueMap.alert( + '

    Info

    ' + + 'Visit BlueMap on GitHub!
    ' + + 'BlueMap works best with Chrome.
    ' + + '

    Controls

    ' + + 'Leftclick-drag with your mouse or use the arrow-keys to navigate.
    ' + + 'Rightclick-drag with your mouse to rotate your view.
    ' + + 'Scroll to zoom.
    ' + ); + } +} diff --git a/BlueMapCore/src/main/webroot/js/libs/modules/MapMenu.js b/BlueMapCore/src/main/webroot/js/libs/modules/MapMenu.js new file mode 100644 index 00000000..3bf3fcfe --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/modules/MapMenu.js @@ -0,0 +1,63 @@ +/* + * 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 $ from 'jquery'; + +import { getTopLeftElement } from './Module.js'; + +export default class MapMenu { + constructor(blueMap) { + this.bluemap = blueMap; + const maps = this.bluemap.settings; + + $('#bluemap-mapmenu').remove(); + this.element = $(``).appendTo(getTopLeftElement(blueMap)); + + const dropdown = $('').appendTo(this.element); + this.maplist = $('
      ').appendTo(dropdown); + + for (let mapId in maps) { + if (!maps.hasOwnProperty(mapId)) continue; + if (!maps.enabled) continue; + + const map = maps[mapId]; + $(`
    • ${map.name}
    • `).appendTo(this.maplist); + } + + this.maplist.find('li[map=' + this.bluemap.map + ']').hide(); + this.maplist.find('li[map]').click(this.onMapClick); + $(document).on('bluemap-map-change', this.onBlueMapMapChange); + } + + onMapClick = event => { + const map = $(this).attr('map'); + this.bluemap.changeMap(map); + } + + onBlueMapMapChange = () => { + this.maplist.find('li').show(); + this.maplist.find('li[map=' + this.bluemap.map + ']').hide(); + this.element.find('.selection').html(this.bluemap.settings[this.bluemap.map].name); + } +} diff --git a/BlueMapCore/src/main/webroot/js/libs/modules/Module.js b/BlueMapCore/src/main/webroot/js/libs/modules/Module.js new file mode 100644 index 00000000..390c9812 --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/modules/Module.js @@ -0,0 +1,47 @@ +/* + * 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 $ from 'jquery'; + +// ###### Modules ###### + +export const getTopRightElement = blueMap => { + let element = $('#bluemap-topright'); + + if (element.length === 0){ + element = $('
      ').appendTo(blueMap.element); + } + + return element; +}; + +export const getTopLeftElement = blueMap => { + let element = $('#bluemap-topleft'); + + if (element.length === 0){ + element = $('
      ').appendTo(blueMap.element); + } + + return element; +}; diff --git a/BlueMapCore/src/main/webroot/js/libs/modules/Position.js b/BlueMapCore/src/main/webroot/js/libs/modules/Position.js new file mode 100644 index 00000000..95d2cda0 --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/modules/Position.js @@ -0,0 +1,47 @@ +/* + * 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 $ from 'jquery'; + +import { getTopLeftElement } from './Module.js'; + +export default class Position { + constructor(blueMap) { + this.blueMap = blueMap; + const parent = getTopLeftElement(blueMap); + + $('.bluemap-position').remove(); + this.elementX = $('
      0
      ').appendTo(parent); + //this.elementY = $('
      0
      ').appendTo(parent); + this.elementZ = $('
      0
      ').appendTo(parent); + + $(document).on('bluemap-update-frame', this.onBlueMapUpdateFrame); + } + + onBlueMapUpdateFrame = () => { + this.elementX.html(Math.floor(this.blueMap.controls.targetPosition.x)); + //this.elementY.html(this.blueMap.controls.targetPosition.y === 0 ? "-" : Math.floor(this.blueMap.controls.targetPosition.y)); + this.elementZ.html(Math.floor(this.blueMap.controls.targetPosition.z)); + } +} diff --git a/BlueMapCore/src/main/webroot/js/libs/modules/Settings.js b/BlueMapCore/src/main/webroot/js/libs/modules/Settings.js new file mode 100644 index 00000000..512f81fe --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/modules/Settings.js @@ -0,0 +1,122 @@ +/* + * 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 $ from 'jquery'; +import { Math as Math3 } from 'three'; + +import { getTopRightElement } from './Module.js'; + +import GEAR from '../../../assets/gear.svg'; + +export default class Settings { + constructor(blueMap) { + this.blueMap = blueMap; + const parent = getTopRightElement(blueMap); + + $('#bluemap-settings').remove(); + this.elementMenu = $('').appendTo(parent); + this.elementSettings = $(`
      `).appendTo(parent); + this.elementSettings.click(this.onSettingsClick); + + /* Quality */ + + this.elementQuality = $( + '' + ).prependTo(this.elementMenu); + + this.elementQuality.find('li[quality]').click(this.onQualityClick); + this.elementRenderDistance = $('').prependTo(this.elementMenu); + + this.init(); + + $(document).on('bluemap-map-change', this.init); + } + + init = () => { + this.defaultHighRes = this.blueMap.hiresTileManager.viewDistance; + this.defaultLowRes = this.blueMap.lowresTileManager.viewDistance; + + this.elementRenderDistance.html( + 'View Distance: ' + this.blueMap.hiresTileManager.viewDistance + '' + + '' + ); + + this.slider = this.elementRenderDistance.find('input'); + this.slider.on('change input', this.onViewDistanceSlider); + }; + + onViewDistanceSlider = () => { + this.blueMap.hiresTileManager.viewDistance = this.pctToRenderDistance(parseFloat(this.slider.val()), this.defaultHighRes); + this.blueMap.lowresTileManager.viewDistance = this.pctToRenderDistance(parseFloat(this.slider.val()), this.defaultLowRes); + this.elementRenderDistance.find('.selection > span').html(Math.round(this.blueMap.hiresTileManager.viewDistance * 10) / 10); + + this.blueMap.lowresTileManager.update(); + this.blueMap.hiresTileManager.update(); + }; + + onQualityClick = (event) => { + const target = event.target + const desc = $(target).html(); + this.blueMap.quality = parseFloat($(target).attr("quality")); + + this.elementQuality.find('li').show(); + this.elementQuality.find(`li[quality="${this.blueMap.quality}"]`).hide(); + + this.elementQuality.find('.selection > span').html(desc); + + this.blueMap.handleContainerResize(); + }; + + onSettingsClick = () => { + if (this.elementMenu.css('display') === 'none'){ + this.elementSettings.addClass('active'); + } else { + this.elementSettings.removeClass('active'); + } + + this.elementMenu.animate({ + width: 'toggle' + }, 200); + } + + pctToRenderDistance(value, defaultValue) { + let max = defaultValue * 5; + if (max > 20) max = 20; + + return Math3.mapLinear(value, 0, 100, 1, max); + } + + renderDistanceToPct(value, defaultValue) { + let max = defaultValue * 5; + if (max > 20) max = 20; + + return Math3.mapLinear(value, 1, max, 0, 100); + } +} diff --git a/BlueMapCore/src/main/webroot/js/libs/utils.js b/BlueMapCore/src/main/webroot/js/libs/utils.js new file mode 100644 index 00000000..97b71631 --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/utils.js @@ -0,0 +1,66 @@ +/* + * 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 { Vector2, Vector3 } from 'three'; + +export const stringToImage = string => { + let image = document.createElementNS("http://www.w3.org/1999/xhtml", "img"); + image.src = string; + return image; +}; + +export const pathFromCoords = (x, z) => { + let path = "x"; + path += splitNumberToPath(x); + + path += "z"; + path += splitNumberToPath(z); + + path = path.substring(0, path.length - 1); + + return path; +}; + +export const splitNumberToPath = num => { + let path = ""; + + if (num < 0) { + num = -num; + path += "-"; + } + + let s = num.toString(); + + for (let i = 0; i < s.length; i++) { + path += s.charAt(i) + "/"; + } + + return path; +}; + +export const hashTile = (x, z) => `x${x}z${z}`; + +export const Vector2_ZERO = new Vector2(0, 0); +export const Vector3_ZERO = new Vector3(0, 0, 0); diff --git a/BlueMapCore/src/main/webroot/js/site.js b/BlueMapCore/src/main/webroot/js/site.js index dfff18e7..86747393 100644 --- a/BlueMapCore/src/main/webroot/js/site.js +++ b/BlueMapCore/src/main/webroot/js/site.js @@ -1,11 +1,8 @@ -import $ from "jquery"; -import BlueMap from "./libs/bluemap.js"; +import $ from 'jquery'; +import BlueMap from './libs/BlueMap.js'; -import "../style/style.css" +import '../style/style.css'; -// global variable to enable access through browser console -var blueMap; - -$(document).ready(function () { - blueMap = new BlueMap($("#map-container")[0], "data/"); +$(document).ready(() => { + window.blueMap = new BlueMap($('#map-container')[0], 'data/'); }); diff --git a/BlueMapCore/tsconfig.json b/BlueMapCore/tsconfig.json new file mode 100644 index 00000000..f3fce1f4 --- /dev/null +++ b/BlueMapCore/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "sourceMap": true, + "module": "esnext", + "target": "es6", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "allowJs": true + }, + "include": [ "src" ] +} diff --git a/BlueMapCore/webpack.config.js b/BlueMapCore/webpack.config.js index d12f9f42..e2b68645 100644 --- a/BlueMapCore/webpack.config.js +++ b/BlueMapCore/webpack.config.js @@ -38,6 +38,12 @@ module.exports = { }, module: { rules: [ + // Transpile JavaScript source files using TypeScript engine + { + test: /\.(js|ts)$/, + include: /src/, + use: 'ts-loader', + }, // Just import normal css files { test: /\.css$/,