import EventEmitter from 'eventemitter3'; import extend from 'lodash.assign'; import Geo from './geo/Geo'; import {point as Point} from './geo/Point'; import {latLon as LatLon} from './geo/LatLon'; import Engine from './engine/Engine'; import EnvironmentLayer from './layer/environment/EnvironmentLayer'; import Worker from './util/Worker'; // TODO: Make sure nothing is left behind in the heap after calling destroy() // Pretty much any event someone using ViziCities would need will be emitted or // proxied by World (eg. render events, etc) class World extends EventEmitter { constructor(domId, options) { super(); var defaults = { skybox: false, postProcessing: false }; this.options = extend({}, defaults, options); this._layers = []; this._controls = []; this._initContainer(domId); this._initAttribution(); this._initEngine(); this._initEnvironment().then(() => { this._initEvents(); this._pause = false; // Kick off the update and render loop this._update(); }); } createWorkers(maxWorkers, workerScript) { return Worker.createWorkers(maxWorkers, workerScript); } _initContainer(domId) { this._container = document.getElementById(domId); } _initAttribution() { var message = 'ViziCities | Attribution'; var element = document.createElement('div'); element.classList.add('vizicities-attribution'); var additionalElem = document.createElement('div'); additionalElem.id = 'attribution-container'; element.innerHTML = message; element.appendChild(additionalElem); this._container.appendChild(element); document.getElementById('show-attr').addEventListener('click', function(e) { e.currentTarget.parentNode.classList.toggle('is-visible'); }); } _initEngine() { this._engine = new Engine(this._container, this); // Engine events // // Consider proxying these through events on World for public access // this._engine.on('preRender', () => {}); // this._engine.on('postRender', () => {}); } _initEnvironment() { // Not sure if I want to keep this as a private API // // Makes sense to allow others to customise their environment so perhaps // add some method of disable / overriding the environment settings this._environment = new EnvironmentLayer({ skybox: this.options.skybox }); return this._environment.addTo(this); } _initEvents() { this.on('controlsMoveEnd', this._onControlsMoveEnd); } _onControlsMoveEnd(point) { var _point = Point(point.x, point.z); this._resetView(this.pointToLatLon(_point), _point); } // Reset world view _resetView(latlon, point) { this.emit('preResetView'); this._moveStart(); this._move(latlon, point); this._moveEnd(); this.emit('postResetView'); } _moveStart() { this.emit('moveStart'); } _move(latlon, point) { this._lastPosition = latlon; this.emit('move', latlon, point); } _moveEnd() { this.emit('moveEnd'); } _update() { if (this._pause) { return; } var delta = this._engine.clock.getDelta(); // Once _update is called it will run forever, for now window.requestAnimationFrame(this._update.bind(this)); // Update controls // this._controls.forEach(controls => { // controls.update(delta); // }); this.emit('preUpdate', delta); this._engine.update(delta); this.emit('postUpdate', delta); } _addAttribution(id, message) { var container = document.getElementById('attribution-container'); var span = document.createElement('p'); span.dataset.layer = id; span.innerHTML = message; container.appendChild(span); } _removeAttribution(id) { var elem = document.querySelectorAll('#attribution-container [data-layer="' + id + '"]')[0]; if (elem) { elem.remove(); } } // Set world view setView(latlon) { // Store initial geographic coordinate for the [0,0,0] world position // // The origin point doesn't move in three.js / 3D space so only set it once // here instead of every time _resetView is called // // If it was updated every time then coorindates would shift over time and // would be out of place / context with previously-placed points (0,0 would // refer to a different point each time) this._originLatlon = latlon; this._originPoint = this.project(latlon); this._resetView(latlon); return this; } // Return world geographic position getPosition() { return this._lastPosition; } // Transform geographic coordinate to world point // // This doesn't take into account the origin offset // // For example, this takes a geographic coordinate and returns a point // relative to the origin point of the projection (not the world) project(latlon) { return Geo.latLonToPoint(LatLon(latlon)); } // Transform world point to geographic coordinate // // This doesn't take into account the origin offset // // For example, this takes a point relative to the origin point of the // projection (not the world) and returns a geographic coordinate unproject(point) { return Geo.pointToLatLon(Point(point)); } // Takes into account the origin offset // // For example, this takes a geographic coordinate and returns a point // relative to the three.js / 3D origin (0,0) latLonToPoint(latlon) { var projectedPoint = this.project(LatLon(latlon)); return projectedPoint._subtract(this._originPoint); } // Takes into account the origin offset // // For example, this takes a point relative to the three.js / 3D origin (0,0) // and returns the exact geographic coordinate at that point pointToLatLon(point) { var projectedPoint = Point(point).add(this._originPoint); return this.unproject(projectedPoint); } // Return pointscale for a given geographic coordinate pointScale(latlon, accurate) { return Geo.pointScale(latlon, accurate); } // Convert from real meters to world units // // TODO: Would be nice not to have to pass in a pointscale here metresToWorld(metres, pointScale, zoom) { return Geo.metresToWorld(metres, pointScale, zoom); } // Convert from real meters to world units // // TODO: Would be nice not to have to pass in a pointscale here worldToMetres(worldUnits, pointScale, zoom) { return Geo.worldToMetres(worldUnits, pointScale, zoom); } // Unsure if it's a good idea to expose this here for components like // GridLayer to use (eg. to keep track of a frustum) getCamera() { return this._engine._camera; } addLayer(layer) { // Is is right to assume that there will always be some other layer // managing layers with output set to false? this._layers.push(layer); if (layer.isOutput() && layer.isOutputToScene()) { // Could move this into Layer but it'll do here for now this._engine._scene.add(layer._object3D); this._engine._domScene3D.add(layer._domObject3D); this._engine._domScene2D.add(layer._domObject2D); } return new Promise((resolve, reject) => { layer._addToWorld(this).then(() => { if (layer._options.attribution) { this._addAttribution(layer._options.id, layer._options.attribution); } // TODO: Consider moving this so it doesn't fire for layers that are // actually managed by a parent layer (eg. tiles) this.emit('layerAdded', layer); resolve(this); }).catch(reject); }); } // Remove layer from world and scene but don't destroy it entirely removeLayer(layer) { var layerIndex = this._layers.indexOf(layer); if (layerIndex > -1) { // Remove from this._layers this._layers.splice(layerIndex, 1); }; if (layer._options.attribution) { this._removeAttribution(layer._options.id); } if (layer.isOutput() && layer.isOutputToScene()) { this._engine._scene.remove(layer._object3D); this._engine._domScene3D.remove(layer._domObject3D); this._engine._domScene2D.remove(layer._domObject2D); } this.emit('layerRemoved'); return Promise.resolve(this); } addControls(controls) { controls._addToWorld(this); this._controls.push(controls); this.emit('controlsAdded', controls); return Promise.resolve(this); } // Remove controls from world but don't destroy them entirely removeControls(controls) { var controlsIndex = this._controls.indexOf(controlsIndex); if (controlsIndex > -1) { this._controls.splice(controlsIndex, 1); }; this.emit('controlsRemoved', controls); return Promise.resolve(this); } stop() { this._pause = true; } start() { this._pause = false; this._update(); } // Destroys the world(!) and removes it from the scene and memory // // TODO: World out why so much three.js stuff is left in the heap after this destroy() { this.stop(); // Remove listeners this.off('controlsMoveEnd', this._onControlsMoveEnd); var i; // Remove all controls var controls; for (i = this._controls.length - 1; i >= 0; i--) { controls = this._controls[0]; this.removeControls(controls); controls.destroy(); }; // Remove all layers var layer; for (i = this._layers.length - 1; i >= 0; i--) { layer = this._layers[0]; this.removeLayer(layer); layer.destroy(); }; // Environment layer is removed with the other layers this._environment = null; this._engine.destroy(); this._engine = null; // Clean the container / remove the canvas while (this._container.firstChild) { this._container.removeChild(this._container.firstChild); } this._container = null; } } export default World; var noNew = function(domId, options) { return new World(domId, options); }; // Initialise without requiring new keyword export {noNew as world};