BlueMapWeb/src/MapViewer.js

382 lines
10 KiB
JavaScript

import {
PerspectiveCamera,
WebGLRenderer,
Vector2, Raycaster, Layers, Scene
} from "three";
import {Map} from "./map/Map";
import {SkyboxScene} from "./skybox/SkyboxScene";
import {ControlsManager} from "./controls/ControlsManager";
import {MapControls} from "./controls/MapControls";
import Stats from "./util/Stats";
import {alert, dispatchEvent, elementOffset, htmlToElement} from "./util/Utils";
import {TileManager} from "./map/TileManager";
import {HIRES_VERTEX_SHADER} from "./map/hires/HiresVertexShader";
import {HIRES_FRAGMENT_SHADER} from "./map/hires/HiresFragmentShader";
import {LOWRES_VERTEX_SHADER} from "./map/lowres/LowresVertexShader";
import {LOWRES_FRAGMENT_SHADER} from "./map/lowres/LowresFragmentShader";
import {CombinedCamera} from "./util/CombinedCamera";
import {CSS2DRenderer} from "./util/CSS2DRenderer";
export class MapViewer {
static InteractionType = {
LEFTCLICK: 0,
RIGHTCLICK: 1
};
/**
* @param element {Element}
* @param dataUrl {string}
* @param liveApiUrl {string}
* @param events {EventTarget}
*/
constructor(element, dataUrl = "data/", liveApiUrl = "live/", events = element) {
Object.defineProperty( this, 'isMapViewer', { value: true } );
this.rootElement = element;
this.events = events;
this.dataUrl = dataUrl;
this.liveApiUrl = liveApiUrl;
this.stats = new Stats();
this.stats.hide();
this.superSamplingValue = 1;
this.loadedCenter = new Vector2(0, 0);
this.loadedHiresViewDistance = 200;
this.loadedLowresViewDistance = 2000;
// uniforms
this.uniforms = {
sunlightStrength: { value: 1 },
ambientLight: { value: 0 },
hiresTileMap: {
value: {
map: null,
size: TileManager.tileMapSize,
scale: new Vector2(1, 1),
translate: new Vector2(),
pos: new Vector2(),
}
}
};
// renderer
this.renderer = new WebGLRenderer({
antialias: true,
sortObjects: true,
preserveDrawingBuffer: true,
logarithmicDepthBuffer: true,
});
this.renderer.autoClear = false;
this.renderer.uniforms = this.uniforms;
// CSS2D renderer
this.css2dRenderer = new CSS2DRenderer();
this.skyboxScene = new SkyboxScene();
this.camera = new CombinedCamera(75, 1, 0.1, 10000, 0);
this.skyboxCamera = new PerspectiveCamera(75, 1, 0.1, 10000);
this.hammer = new Hammer.Manager(this.rootElement);
this.initializeHammer();
this.controlsManager = new ControlsManager(this, this.camera);
this.controlsManager.controls = new MapControls(this.rootElement, this.hammer, this.events);
this.raycaster = new Raycaster();
this.raycaster.layers.enableAll();
this.raycaster.params.Line2 = {threshold: 20}
/** @type {Map} */
this.map = null;
this.markerScene = new Scene();
this.lastFrame = 0;
// initialize
this.initializeRootElement();
// handle some events
window.addEventListener("resize", this.handleContainerResize);
// start render-loop
requestAnimationFrame(this.renderLoop);
}
initializeHammer() {
let touchTap = new Hammer.Tap({ event: 'tap', pointers: 1, taps: 1, threshold: 2 });
let touchMove = new Hammer.Pan({ event: 'move', direction: Hammer.DIRECTION_ALL, threshold: 0 });
let touchTilt = new Hammer.Pan({ event: 'tilt', direction: Hammer.DIRECTION_VERTICAL, pointers: 2, threshold: 0 });
let touchRotate = new Hammer.Rotate({ event: 'rotate', pointers: 2, threshold: 10 });
let touchZoom = new Hammer.Pinch({ event: 'zoom', pointers: 2, threshold: 0 });
touchTilt.recognizeWith(touchRotate);
touchTilt.recognizeWith(touchZoom);
touchRotate.recognizeWith(touchZoom);
this.hammer.add(touchTap);
this.hammer.add(touchMove);
this.hammer.add(touchTilt);
this.hammer.add(touchRotate);
this.hammer.add(touchZoom);
}
/**
* Initializes the root-element
*/
initializeRootElement() {
this.rootElement.innerHTML = "";
let outerDiv = htmlToElement(`<div style="position: relative; width: 100%; height: 100%; overflow: hidden;"></div>`);
this.rootElement.appendChild(outerDiv)
this.hammer.on('tap', event => {
let rootOffset = elementOffset(this.rootElement);
this.handleMapInteraction(new Vector2(
((event.center.x - rootOffset.top) / this.rootElement.clientWidth) * 2 - 1,
-((event.center.y - rootOffset.left) / this.rootElement.clientHeight) * 2 + 1
));
});
// 3d-canvas
outerDiv.appendChild(this.renderer.domElement);
// html-markers
this.css2dRenderer.domElement.style.position = 'absolute';
this.css2dRenderer.domElement.style.top = '0';
this.css2dRenderer.domElement.style.left = '0';
this.css2dRenderer.domElement.style.pointerEvents = 'none';
outerDiv.appendChild(this.css2dRenderer.domElement);
// performance monitor
outerDiv.appendChild(this.stats.dom);
this.handleContainerResize();
}
/**
* Updates the render-resolution and aspect ratio based on the size of the root-element
*/
handleContainerResize = () => {
this.renderer.setSize(this.rootElement.clientWidth, this.rootElement.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio * this.superSamplingValue);
this.css2dRenderer.setSize(this.rootElement.clientWidth, this.rootElement.clientHeight);
this.camera.aspect = this.rootElement.clientWidth / this.rootElement.clientHeight;
this.camera.updateProjectionMatrix();
};
/**
* @param screenPos {{x: number, y:number}}
* @param interactionType {number}
*/
handleMapInteraction(screenPos, interactionType = MapViewer.InteractionType.LEFTCLICK) {
if (this.map && this.map.isLoaded){
this.raycaster.setFromCamera(screenPos, this.camera);
let lowresLayer = new Layers();
lowresLayer.set(2);
// check marker interactions
let intersects = this.raycaster.intersectObjects([this.map.scene, this.markerScene], true);
let covered = false;
for (let i = 0; i < intersects.length; i++) {
if (intersects[i].object){
let object = intersects[i].object;
if (object.visible) {
if (!covered || (object.material && !object.material.depthTest)) {
if (object.onClick({
interactionType: interactionType,
intersection: intersects[i]
})) return;
covered = true;
} else if (!intersects[i].object.layers.test(lowresLayer)) {
covered = true;
}
}
}
}
}
}
updateLoadedMapArea = () => {
if (!this.map) return;
this.map.loadMapArea(this.loadedCenter.x, this.loadedCenter.y, this.loadedHiresViewDistance, this.loadedLowresViewDistance);
}
/**
* The render-loop to update and possibly render a new frame.
* @param now {number} the current time in milliseconds
*/
renderLoop = (now) => {
requestAnimationFrame(this.renderLoop);
// calculate delta time
if (this.lastFrame <= 0) { this.lastFrame = now; }
let delta = now - this.lastFrame;
this.lastFrame = now;
// update stats
this.stats.begin();
// update controls
if (this.map != null) {
this.controlsManager.update(delta, this.map);
this.controlsManager.updateCamera();
}
// render
this.render(delta);
// update stats
this.stats.update();
};
/**
* Renders a frame
* @param delta {number}
*/
render(delta) {
dispatchEvent(this.events, "bluemapRenderFrame", {
delta: delta,
});
//prepare camera
this.camera.updateProjectionMatrix();
this.skyboxCamera.rotation.copy(this.camera.rotation);
this.skyboxCamera.updateProjectionMatrix();
//render
this.renderer.clear();
this.renderer.render(this.skyboxScene, this.skyboxCamera);
this.renderer.clearDepth();
/*
Layers:
0 - always visible objects
1 - hires layer
2 - lowres layer
*/
if (this.map && this.map.isLoaded) {
//update uniforms
this.uniforms.hiresTileMap.value.pos.copy(this.map.hiresTileManager.centerTile);
this.camera.layers.set(2);
this.renderer.render(this.map.scene, this.camera);
this.renderer.clearDepth();
this.camera.layers.set(0);
if (this.controlsManager.distance < 2000) this.camera.layers.enable(1);
this.renderer.render(this.map.scene, this.camera);
//this.renderer.render(this.map.markerManager.objectMarkerScene, this.camera);
//this.css2dRenderer.render(this.map.markerManager.elementMarkerScene, this.camera);
}
// render markers
this.renderer.render(this.markerScene, this.camera);
this.css2dRenderer.render(this.markerScene, this.camera);
}
/**
* Changes / Sets the map that will be loaded and displayed
* @param map {Map}
*/
setMap(map = null) {
if (this.map && this.map.isMap) this.map.unload();
this.map = map;
if (this.map && this.map.isMap) {
return map.load(HIRES_VERTEX_SHADER, HIRES_FRAGMENT_SHADER, LOWRES_VERTEX_SHADER, LOWRES_FRAGMENT_SHADER, this.uniforms)
.then(() => {
this.skyboxScene.ambientLight = map.ambientLight;
this.skyboxScene.skyColor = map.skyColor;
this.uniforms.ambientLight.value = map.ambientLight;
this.uniforms.hiresTileMap.value.map = map.hiresTileManager.tileMap.texture;
this.uniforms.hiresTileMap.value.scale.set(map.hires.tileSize.x, map.hires.tileSize.z);
this.uniforms.hiresTileMap.value.translate.set(map.hires.translate.x, map.hires.translate.z);
setTimeout(this.updateLoadedMapArea);
dispatchEvent(this.events, "bluemapMapChanged", {
map: map
});
})
.catch(error => {
alert(this.events, error, "error");
});
} else {
return Promise.resolve();
}
}
/**
* @param centerX {number}
* @param centerZ {number}
* @param hiresViewDistance {number}
* @param lowresViewDistance {number}
*/
loadMapArea(centerX, centerZ, hiresViewDistance = -1, lowresViewDistance = -1) {
this.loadedCenter.set(centerX, centerZ);
if (hiresViewDistance >= 0) this.loadedHiresViewDistance = hiresViewDistance;
if (lowresViewDistance >= 0) this.loadedLowresViewDistance = lowresViewDistance;
this.updateLoadedMapArea();
}
/**
* @returns {number}
*/
get superSampling() {
return this.superSamplingValue;
}
/**
* @param value {number}
*/
set superSampling(value) {
this.superSamplingValue = value;
this.handleContainerResize();
}
// --------------------------
/**
* Applies a loaded settings-object (settings.json)
* @param settings {{maps: {}}}
*/
applySettings(settings) {
// reset maps
this.maps.forEach(map => map.dispose());
this.maps = [];
// create maps
if (settings.maps !== undefined){
for (let mapId in settings.maps) {
if (!settings.maps.hasOwnProperty(mapId)) continue;
let mapSettings = settings.maps[mapId];
if (mapSettings.enabled)
this.maps.push(new Map(mapId, this.dataUrl + mapId + "/", this.rootElement));
}
}
// sort maps
this.maps.sort((map1, map2) => {
let sort = settings.maps[map1.id].ordinal - settings.maps[map2.id].ordinal;
if (isNaN(sort)) return 0;
return sort;
});
}
}