diff --git a/examples/basic/main.js b/examples/basic/main.js index fab1359..57a075b 100644 --- a/examples/basic/main.js +++ b/examples/basic/main.js @@ -1,6 +1,12 @@ +// London +// var coords = [51.505, -0.09]; + +// Manhattan +var coords = [40.739940, -73.988801]; + var world = VIZI.world('world', { skybox: true -}).setView([51.505, -0.09]); +}).setView(coords); world._environment._skybox.setInclination(0.4); diff --git a/src/World.js b/src/World.js new file mode 100644 index 0000000..6993d20 --- /dev/null +++ b/src/World.js @@ -0,0 +1,310 @@ +import EventEmitter from 'eventemitter3'; +import extend from 'lodash.assign'; +import CRS from './geo/crs/index'; +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'; + +// 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 = { + crs: CRS.EPSG3857, + skybox: false + }; + + this.options = extend({}, defaults, options); + + this._layers = []; + this._controls = []; + + this._initContainer(domId); + this._initEngine(); + this._initEnvironment(); + this._initEvents(); + + this._pause = false; + + // Kick off the update and render loop + this._update(); + } + + _initContainer(domId) { + this._container = document.getElementById(domId); + } + + _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 + }).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(); + }); + + this.emit('preUpdate', delta); + this._engine.update(delta); + this.emit('postUpdate', delta); + } + + // 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 this.options.crs.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 this.options.crs.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 this.options.crs.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 this.options.crs.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 this.options.crs.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) { + layer._addToWorld(this); + + this._layers.push(layer); + + if (layer.isOutput()) { + // 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); + } + + this.emit('layerAdded', layer); + return this; + } + + // 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.isOutput()) { + this._engine._scene.remove(layer._object3D); + this._engine._domScene3D.remove(layer._domObject3D); + this._engine._domScene2D.remove(layer._domObject2D); + } + + this.emit('layerRemoved'); + return this; + } + + addControls(controls) { + controls._addToWorld(this); + + this._controls.push(controls); + + this.emit('controlsAdded', controls); + return 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 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; + + // TODO: Probably should clean the container too / remove the canvas + 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};