├── .babelrc ├── examples ├── basic │ ├── main.css │ ├── index.html │ └── main.js ├── geojson │ ├── main.css │ ├── index.html │ └── main.js ├── interactive │ ├── main.css │ ├── index.html │ └── main.js ├── mta-routes │ ├── main.css │ ├── index.html │ └── main.js ├── all-the-things │ ├── main.css │ ├── index.html │ └── main.js ├── colour-by-height │ ├── main.css │ ├── index.html │ └── main.js ├── lots-of-features │ ├── main.css │ ├── index.html │ └── main.js └── vendor │ └── threex.rendererstats.js ├── .jscsrc ├── src ├── controls │ ├── index.js │ └── Controls.Orbit.js ├── engine │ ├── DOMScene2D.js │ ├── DOMScene3D.js │ ├── PickingScene.js │ ├── Scene.js │ ├── EffectComposer.js │ ├── Camera.js │ ├── DOMRenderer2D.js │ ├── DOMRenderer3D.js │ ├── PickingMaterial.js │ ├── PickingShader.js │ ├── Renderer.js │ ├── Engine.js │ └── Picking.js ├── util │ ├── index.js │ ├── wrapNum.js │ ├── extrudePolygon.js │ ├── Buffer.js │ └── GeoJSON.js ├── vizicities.css ├── layer │ ├── tile │ │ ├── TopoJSONTileLayer.js │ │ ├── TileCache.js │ │ ├── ImageTileLayerBaseMaterial.js │ │ ├── GeoJSONTileLayer.js │ │ ├── ImageTile.js │ │ ├── Tile.js │ │ ├── ImageTileLayer.js │ │ └── GeoJSONTile.js │ ├── TopoJSONLayer.js │ ├── LayerGroup.js │ ├── environment │ │ ├── EnvironmentLayer.js │ │ ├── Skybox.js │ │ └── Sky.js │ └── Layer.js ├── vendor │ ├── CopyShader.js │ ├── RenderPass.js │ ├── ShaderPass.js │ ├── VerticalTiltShiftShader.js │ ├── HorizontalTiltShiftShader.js │ ├── MaskPass.js │ ├── BoxHelper.js │ ├── FXAAShader.js │ ├── CSS2DRenderer.js │ ├── EffectComposer.js │ └── CSS3DRenderer.js ├── geo │ ├── Point.js │ ├── LatLon.js │ └── Geo.js ├── vizicities.js └── World.js ├── .eslintrc ├── test ├── unit │ ├── vizicities.js │ └── Geo.js ├── setup │ ├── browser.js │ ├── node.js │ └── setup.js ├── .eslintrc └── runner.html ├── .editorconfig ├── dist └── vizicities.css ├── .npmignore ├── LICENSE ├── .gitignore ├── package.json ├── README.md └── gulpfile.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "blacklist": ["useStrict"] 3 | } -------------------------------------------------------------------------------- /examples/basic/main.css: -------------------------------------------------------------------------------- 1 | * { margin: 0; padding: 0; } 2 | html, body { height: 100%; overflow: hidden;} 3 | 4 | #world { height: 100%; } 5 | -------------------------------------------------------------------------------- /examples/geojson/main.css: -------------------------------------------------------------------------------- 1 | * { margin: 0; padding: 0; } 2 | html, body { height: 100%; overflow: hidden;} 3 | 4 | #world { height: 100%; } 5 | -------------------------------------------------------------------------------- /examples/interactive/main.css: -------------------------------------------------------------------------------- 1 | * { margin: 0; padding: 0; } 2 | html, body { height: 100%; overflow: hidden;} 3 | 4 | #world { height: 100%; } 5 | -------------------------------------------------------------------------------- /examples/mta-routes/main.css: -------------------------------------------------------------------------------- 1 | * { margin: 0; padding: 0; } 2 | html, body { height: 100%; overflow: hidden;} 3 | 4 | #world { height: 100%; } 5 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "maximumLineLength": null, 4 | "esnext": true, 5 | "disallowSpacesInsideObjectBrackets": null 6 | } 7 | -------------------------------------------------------------------------------- /examples/all-the-things/main.css: -------------------------------------------------------------------------------- 1 | * { margin: 0; padding: 0; } 2 | html, body { height: 100%; overflow: hidden;} 3 | 4 | #world { height: 100%; } 5 | -------------------------------------------------------------------------------- /examples/colour-by-height/main.css: -------------------------------------------------------------------------------- 1 | * { margin: 0; padding: 0; } 2 | html, body { height: 100%; overflow: hidden;} 3 | 4 | #world { height: 100%; } 5 | -------------------------------------------------------------------------------- /examples/lots-of-features/main.css: -------------------------------------------------------------------------------- 1 | * { margin: 0; padding: 0; } 2 | html, body { height: 100%; overflow: hidden;} 3 | 4 | #world { height: 100%; } 5 | -------------------------------------------------------------------------------- /src/controls/index.js: -------------------------------------------------------------------------------- 1 | import Orbit, {orbit} from './Controls.Orbit'; 2 | 3 | const Controls = { 4 | Orbit: Orbit, 5 | orbit, orbit 6 | }; 7 | 8 | export default Controls; 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "strict": 0, 5 | "quotes": [2, "single"] 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/unit/vizicities.js: -------------------------------------------------------------------------------- 1 | import VIZI from '../../src/vizicities'; 2 | 3 | describe('VIZI', () => { 4 | describe('Version', () => { 5 | it('should exist', () => { 6 | expect(VIZI.version).to.exist; 7 | }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/engine/DOMScene2D.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | 3 | // This can be imported from anywhere and will still reference the same scene, 4 | // though there is a helper reference in Engine.scene 5 | 6 | export default (function() { 7 | var scene = new THREE.Scene(); 8 | return scene; 9 | })(); 10 | -------------------------------------------------------------------------------- /src/engine/DOMScene3D.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | 3 | // This can be imported from anywhere and will still reference the same scene, 4 | // though there is a helper reference in Engine.scene 5 | 6 | export default (function() { 7 | var scene = new THREE.Scene(); 8 | return scene; 9 | })(); 10 | -------------------------------------------------------------------------------- /test/setup/browser.js: -------------------------------------------------------------------------------- 1 | var config = require('../../package.json').babelBoilerplateOptions; 2 | 3 | window.mocha.setup('bdd'); 4 | window.onload = function() { 5 | window.mocha.checkLeaks(); 6 | window.mocha.globals(config.mochaGlobals); 7 | window.mocha.run(); 8 | require('./setup')(window); 9 | }; 10 | -------------------------------------------------------------------------------- /src/engine/PickingScene.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | 3 | // This can be imported from anywhere and will still reference the same scene, 4 | // though there is a helper reference in Engine.pickingScene 5 | 6 | export default (function() { 7 | var scene = new THREE.Scene(); 8 | return scene; 9 | })(); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true; 4 | 5 | [*] 6 | # Ensure there's no lingering whitespace 7 | trim_trailing_whitespace = true 8 | # Ensure a newline at the end of each file 9 | insert_final_newline = true 10 | 11 | [*.js] 12 | # Unix-style newlines 13 | end_of_line = lf 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 -------------------------------------------------------------------------------- /src/engine/Scene.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | 3 | // This can be imported from anywhere and will still reference the same scene, 4 | // though there is a helper reference in Engine.scene 5 | 6 | export default (function() { 7 | var scene = new THREE.Scene(); 8 | 9 | // TODO: Re-enable when this works with the skybox 10 | // scene.fog = new THREE.Fog(0xffffff, 1, 15000); 11 | return scene; 12 | })(); 13 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | // TODO: A lot of these utils don't need to be in separate, tiny files 2 | 3 | import wrapNum from './wrapNum'; 4 | import extrudePolygon from './extrudePolygon'; 5 | import GeoJSON from './GeoJSON'; 6 | import Buffer from './Buffer'; 7 | 8 | const Util = {}; 9 | 10 | Util.wrapNum = wrapNum; 11 | Util.extrudePolygon = extrudePolygon; 12 | Util.GeoJSON = GeoJSON; 13 | Util.Buffer = Buffer; 14 | 15 | export default Util; 16 | -------------------------------------------------------------------------------- /src/util/wrapNum.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Wrap the given number to lie within a certain range (eg. longitude) 3 | * 4 | * Based on: 5 | * https://github.com/Leaflet/Leaflet/blob/master/src/core/Util.js 6 | */ 7 | 8 | var wrapNum = function(x, range, includeMax) { 9 | var max = range[1]; 10 | var min = range[0]; 11 | var d = max - min; 12 | return x === max && includeMax ? x : ((x - min) % d + d) % d + min; 13 | }; 14 | 15 | export default wrapNum; 16 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "strict": 0, 5 | "quotes": [2, "single"], 6 | "no-unused-expressions": 0 7 | }, 8 | "env": { 9 | "browser": true, 10 | "node": true, 11 | "mocha": true 12 | }, 13 | "globals": { 14 | "spy": true, 15 | "stub": true, 16 | "mock": true, 17 | "useFakeTimers": true, 18 | "useFakeXMLHttpRequest": true, 19 | "useFakeServer": true, 20 | "expect": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/setup/node.js: -------------------------------------------------------------------------------- 1 | global.chai = require('chai'); 2 | global.sinon = require('sinon'); 3 | global.chai.use(require('sinon-chai')); 4 | 5 | require('babel-core/register'); 6 | require('./setup')(); 7 | 8 | /* 9 | Uncomment the following if your library uses features of the DOM, 10 | for example if writing a jQuery extension, and 11 | add 'simple-jsdom' to the `devDependencies` of your package.json 12 | */ 13 | // import simpleJSDom from 'simple-jsdom'; 14 | // simpleJSDom.install(); 15 | -------------------------------------------------------------------------------- /dist/vizicities.css: -------------------------------------------------------------------------------- 1 | .vizicities-attribution { 2 | background: rgba(255, 255, 255, 0.9); 3 | border-radius: 3px 0 0; 4 | bottom: 0; 5 | color: #666; 6 | font-family: Arial, Verdana, sans-serif; 7 | font-size: 11px; 8 | padding: 4px 7px; 9 | position: absolute; 10 | right: 0; 11 | z-index: 9998; 12 | } 13 | 14 | .vizicities-attribution a, .vizicities-attribution a:visited { 15 | color: #2bb2ed; 16 | text-decoration: none; 17 | } 18 | 19 | .vizicities-attribution a:hover { 20 | color: #2bb2ed; 21 | text-decoration: underline; 22 | } 23 | -------------------------------------------------------------------------------- /src/vizicities.css: -------------------------------------------------------------------------------- 1 | .vizicities-attribution { 2 | background: rgba(255, 255, 255, 0.9); 3 | border-radius: 3px 0 0; 4 | bottom: 0; 5 | color: #666; 6 | font-family: Arial, Verdana, sans-serif; 7 | font-size: 11px; 8 | padding: 4px 7px; 9 | position: absolute; 10 | right: 0; 11 | z-index: 9998; 12 | } 13 | 14 | .vizicities-attribution a, .vizicities-attribution a:visited { 15 | color: #2bb2ed; 16 | text-decoration: none; 17 | } 18 | 19 | .vizicities-attribution a:hover { 20 | color: #2bb2ed; 21 | text-decoration: underline; 22 | } 23 | -------------------------------------------------------------------------------- /src/layer/tile/TopoJSONTileLayer.js: -------------------------------------------------------------------------------- 1 | import GeoJSONTileLayer from './GeoJSONTileLayer'; 2 | import extend from 'lodash.assign'; 3 | 4 | class TopoJSONTileLayer extends GeoJSONTileLayer { 5 | constructor(path, options) { 6 | var defaults = { 7 | topojson: true 8 | }; 9 | 10 | options = extend({}, defaults, options); 11 | 12 | super(path, options); 13 | } 14 | } 15 | 16 | export default TopoJSONTileLayer; 17 | 18 | var noNew = function(path, options) { 19 | return new TopoJSONTileLayer(path, options); 20 | }; 21 | 22 | export {noNew as topoJSONTileLayer}; 23 | -------------------------------------------------------------------------------- /src/layer/TopoJSONLayer.js: -------------------------------------------------------------------------------- 1 | import GeoJSONLayer from './GeoJSONLayer'; 2 | import extend from 'lodash.assign'; 3 | 4 | class TopoJSONLayer extends GeoJSONLayer { 5 | constructor(topojson, options) { 6 | var defaults = { 7 | topojson: true 8 | }; 9 | 10 | options = extend({}, defaults, options); 11 | 12 | super(topojson, options); 13 | } 14 | } 15 | 16 | export default TopoJSONLayer; 17 | 18 | var noNew = function(topojson, options) { 19 | return new TopoJSONLayer(topojson, options); 20 | }; 21 | 22 | // Initialise without requiring new keyword 23 | export {noNew as topoJSONLayer}; 24 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Basic ViziCities Example 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/geojson/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GeoJSON ViziCities Example 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/mta-routes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MTA Routes ViziCities Example 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/all-the-things/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | All The Things ViziCities Example 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/lots-of-features/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lots of Features ViziCities Example 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/interactive/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Interactive ViziCities Example 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/colour-by-height/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Colour by Height ViziCities Example 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/engine/EffectComposer.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | import EffectComposer from '../vendor/EffectComposer'; 3 | 4 | export default function(renderer, container) { 5 | var composer = new EffectComposer(renderer); 6 | 7 | var updateSize = function() { 8 | // TODO: Re-enable this when perf issues can be solved 9 | // 10 | // Rendering double the resolution of the screen can be really slow 11 | // var pixelRatio = window.devicePixelRatio; 12 | var pixelRatio = 1; 13 | 14 | composer.setSize(container.clientWidth * pixelRatio, container.clientHeight * pixelRatio); 15 | }; 16 | 17 | window.addEventListener('resize', updateSize, false); 18 | updateSize(); 19 | 20 | return composer; 21 | }; 22 | -------------------------------------------------------------------------------- /src/engine/Camera.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | 3 | // This can only be accessed from Engine.camera if you want to reference the 4 | // same scene in multiple places 5 | 6 | // TODO: Ensure that FOV looks natural on all aspect ratios 7 | // http://stackoverflow.com/q/26655930/997339 8 | 9 | export default function(container) { 10 | var camera = new THREE.PerspectiveCamera(45, 1, 1, 2000000); 11 | camera.position.y = 4000; 12 | camera.position.z = 4000; 13 | 14 | var updateSize = function() { 15 | camera.aspect = container.clientWidth / container.clientHeight; 16 | camera.updateProjectionMatrix(); 17 | }; 18 | 19 | window.addEventListener('resize', updateSize, false); 20 | updateSize(); 21 | 22 | return camera; 23 | }; 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | bower_components 27 | coverage 28 | tmp 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | -------------------------------------------------------------------------------- /src/engine/DOMRenderer2D.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | import {CSS2DRenderer} from '../vendor/CSS2DRenderer'; 3 | import DOMScene2D from './DOMScene2D'; 4 | 5 | // This can only be accessed from Engine.renderer if you want to reference the 6 | // same scene in multiple places 7 | 8 | export default function(container) { 9 | var renderer = new CSS2DRenderer(); 10 | 11 | renderer.domElement.style.position = 'absolute'; 12 | renderer.domElement.style.top = 0; 13 | 14 | container.appendChild(renderer.domElement); 15 | 16 | var updateSize = function() { 17 | renderer.setSize(container.clientWidth, container.clientHeight); 18 | }; 19 | 20 | window.addEventListener('resize', updateSize, false); 21 | updateSize(); 22 | 23 | return renderer; 24 | }; 25 | -------------------------------------------------------------------------------- /src/engine/DOMRenderer3D.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | import {CSS3DRenderer} from '../vendor/CSS3DRenderer'; 3 | import DOMScene3D from './DOMScene3D'; 4 | 5 | // This can only be accessed from Engine.renderer if you want to reference the 6 | // same scene in multiple places 7 | 8 | export default function(container) { 9 | var renderer = new CSS3DRenderer(); 10 | 11 | renderer.domElement.style.position = 'absolute'; 12 | renderer.domElement.style.top = 0; 13 | 14 | container.appendChild(renderer.domElement); 15 | 16 | var updateSize = function() { 17 | renderer.setSize(container.clientWidth, container.clientHeight); 18 | }; 19 | 20 | window.addEventListener('resize', updateSize, false); 21 | updateSize(); 22 | 23 | return renderer; 24 | }; 25 | -------------------------------------------------------------------------------- /test/setup/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = function(root) { 2 | root = root ? root : this; 3 | root.expect = root.chai.expect; 4 | 5 | beforeEach(function() { 6 | // Using these globally-available Sinon features is preferrable, as they're 7 | // automatically restored for you in the subsequent `afterEach` 8 | root.sandbox = root.sinon.sandbox.create(); 9 | root.stub = root.sandbox.stub.bind(root.sandbox); 10 | root.spy = root.sandbox.spy.bind(root.sandbox); 11 | root.mock = root.sandbox.mock.bind(root.sandbox); 12 | root.useFakeTimers = root.sandbox.useFakeTimers.bind(root.sandbox); 13 | root.useFakeXMLHttpRequest = root.sandbox.useFakeXMLHttpRequest.bind(root.sandbox); 14 | root.useFakeServer = root.sandbox.useFakeServer.bind(root.sandbox); 15 | }); 16 | 17 | afterEach(function() { 18 | delete root.stub; 19 | delete root.spy; 20 | root.sandbox.restore(); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /examples/lots-of-features/main.js: -------------------------------------------------------------------------------- 1 | // London 2 | var coords = [51.505, -0.09]; 3 | 4 | var world = VIZI.world('world', { 5 | skybox: false, 6 | postProcessing: false 7 | }).setView(coords); 8 | 9 | // Add controls 10 | VIZI.Controls.orbit().addTo(world); 11 | 12 | // CartoDB basemap 13 | VIZI.imageTileLayer('http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', { 14 | attribution: '© OpenStreetMap contributors, © CartoDB' 15 | }).addTo(world); 16 | 17 | // Census boundary polygons 18 | VIZI.geoJSONLayer('https://cdn.rawgit.com/robhawkes/9a00cb9cfbd70174d856/raw/0d56960538909a844393f9f8e091608e3b978c7a/lsoa-simplified-merged-1.5%2525.geojson', { 19 | output: true, 20 | style: function(feature) { 21 | var colour = Math.random() * 0xffffff; 22 | 23 | return { 24 | color: colour, 25 | transparent: true, 26 | opacity: 0.4 27 | }; 28 | } 29 | }).addTo(world); 30 | -------------------------------------------------------------------------------- /src/vendor/CopyShader.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /* eslint-disable */ 3 | 4 | import THREE from 'three'; 5 | 6 | /** 7 | * @author alteredq / http://alteredqualia.com/ 8 | * 9 | * Full-screen textured quad shader 10 | */ 11 | 12 | var CopyShader = { 13 | 14 | uniforms: { 15 | 16 | "tDiffuse": { type: "t", value: null }, 17 | "opacity": { type: "f", value: 1.0 } 18 | 19 | }, 20 | 21 | vertexShader: [ 22 | 23 | "varying vec2 vUv;", 24 | 25 | "void main() {", 26 | 27 | "vUv = uv;", 28 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 29 | 30 | "}" 31 | 32 | ].join( "\n" ), 33 | 34 | fragmentShader: [ 35 | 36 | "uniform float opacity;", 37 | 38 | "uniform sampler2D tDiffuse;", 39 | 40 | "varying vec2 vUv;", 41 | 42 | "void main() {", 43 | 44 | "vec4 texel = texture2D( tDiffuse, vUv );", 45 | "gl_FragColor = opacity * texel;", 46 | 47 | "}" 48 | 49 | ].join( "\n" ) 50 | 51 | }; 52 | 53 | export default CopyShader; 54 | THREE.CopyShader = CopyShader; 55 | -------------------------------------------------------------------------------- /examples/geojson/main.js: -------------------------------------------------------------------------------- 1 | // Manhattan 2 | var coords = [40.722282152, -73.992919922]; 3 | 4 | var world = VIZI.world('world', { 5 | skybox: false, 6 | postProcessing: false 7 | }).setView(coords); 8 | 9 | // Add controls 10 | VIZI.Controls.orbit().addTo(world); 11 | 12 | // CartoDB basemap 13 | VIZI.imageTileLayer('http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', { 14 | attribution: '© OpenStreetMap contributors, © CartoDB' 15 | }).addTo(world); 16 | 17 | // Mapzen GeoJSON tile including points, linestrings and polygons 18 | VIZI.geoJSONLayer('http://vector.mapzen.com/osm/roads,pois,buildings/14/4824/6159.json', { 19 | output: true, 20 | style: { 21 | color: '#ff0000', 22 | lineColor: '#0000ff', 23 | lineRenderOrder: 1, 24 | pointColor: '#00cc00' 25 | }, 26 | pointGeometry: function(feature) { 27 | var geometry = new THREE.SphereGeometry(2, 16, 16); 28 | return geometry; 29 | } 30 | }).addTo(world); 31 | -------------------------------------------------------------------------------- /src/engine/PickingMaterial.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | import PickingShader from './PickingShader'; 3 | 4 | // FROM: https://github.com/brianxu/GPUPicker/blob/master/GPUPicker.js 5 | 6 | var PickingMaterial = function() { 7 | THREE.ShaderMaterial.call(this, { 8 | uniforms: { 9 | size: { 10 | type: 'f', 11 | value: 0.01, 12 | }, 13 | scale: { 14 | type: 'f', 15 | value: 400, 16 | } 17 | }, 18 | // attributes: ['position', 'id'], 19 | vertexShader: PickingShader.vertexShader, 20 | fragmentShader: PickingShader.fragmentShader 21 | }); 22 | 23 | this.linePadding = 2; 24 | }; 25 | 26 | PickingMaterial.prototype = Object.create(THREE.ShaderMaterial.prototype); 27 | 28 | PickingMaterial.prototype.constructor = PickingMaterial; 29 | 30 | PickingMaterial.prototype.setPointSize = function(size) { 31 | this.uniforms.size.value = size; 32 | }; 33 | 34 | PickingMaterial.prototype.setPointScale = function(scale) { 35 | this.uniforms.scale.value = scale; 36 | }; 37 | 38 | export default PickingMaterial; 39 | -------------------------------------------------------------------------------- /src/engine/PickingShader.js: -------------------------------------------------------------------------------- 1 | // FROM: https://github.com/brianxu/GPUPicker/blob/master/GPUPicker.js 2 | 3 | var PickingShader = { 4 | vertexShader: [ 5 | 'attribute float pickingId;', 6 | // '', 7 | // 'uniform float size;', 8 | // 'uniform float scale;', 9 | '', 10 | 'varying vec4 worldId;', 11 | '', 12 | 'void main() {', 13 | ' vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );', 14 | // ' gl_PointSize = size * ( scale / length( mvPosition.xyz ) );', 15 | ' vec3 a = fract(vec3(1.0/255.0, 1.0/(255.0*255.0), 1.0/(255.0*255.0*255.0)) * pickingId);', 16 | ' a -= a.xxy * vec3(0.0, 1.0/255.0, 1.0/255.0);', 17 | ' worldId = vec4(a,1);', 18 | ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', 19 | '}' 20 | ].join('\n'), 21 | 22 | fragmentShader: [ 23 | '#ifdef GL_ES\n', 24 | 'precision highp float;\n', 25 | '#endif\n', 26 | '', 27 | 'varying vec4 worldId;', 28 | '', 29 | 'void main() {', 30 | ' gl_FragColor = worldId;', 31 | '}' 32 | ].join('\n') 33 | }; 34 | 35 | export default PickingShader; 36 | -------------------------------------------------------------------------------- /examples/mta-routes/main.js: -------------------------------------------------------------------------------- 1 | // Manhattan 2 | var coords = [40.739940, -73.988801]; 3 | 4 | var world = VIZI.world('world', { 5 | skybox: false, 6 | postProcessing: false 7 | }).setView(coords); 8 | 9 | // Add controls 10 | VIZI.Controls.orbit().addTo(world); 11 | 12 | // CartoDB basemap 13 | VIZI.imageTileLayer('http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png', { 14 | attribution: '© OpenStreetMap contributors, © CartoDB' 15 | }).addTo(world); 16 | 17 | // MTA routes 18 | VIZI.geoJSONLayer('https://cdn.rawgit.com/robhawkes/0b08e6e60fd329bf2ef342c1122b9d43/raw/02954a741abd7d852c0cecb24a71252a74eac154/mta-routes-simplified.geojson', { 19 | output: true, 20 | interactive: false, 21 | style: function(feature) { 22 | var colour = (feature.properties.color) ? '#' + feature.properties.color : '#ffffff'; 23 | 24 | return { 25 | lineColor: colour, 26 | lineWidth: 1.5, 27 | lineRenderOrder: 2 28 | }; 29 | }, 30 | attribution: '© NYC MTA.' 31 | }).addTo(world); 32 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /src/layer/LayerGroup.js: -------------------------------------------------------------------------------- 1 | import Layer from './Layer'; 2 | import extend from 'lodash.assign'; 3 | 4 | class LayerGroup extends Layer { 5 | constructor(options) { 6 | var defaults = { 7 | output: false 8 | }; 9 | 10 | var _options = extend({}, defaults, options); 11 | 12 | super(_options); 13 | 14 | this._layers = []; 15 | } 16 | 17 | addLayer(layer) { 18 | this._layers.push(layer); 19 | this._world.addLayer(layer); 20 | } 21 | 22 | removeLayer(layer) { 23 | var layerIndex = this._layers.indexOf(layer); 24 | 25 | if (layerIndex > -1) { 26 | // Remove from this._layers 27 | this._layers.splice(layerIndex, 1); 28 | }; 29 | 30 | this._world.removeLayer(layer); 31 | } 32 | 33 | _onAdd(world) {} 34 | 35 | // Destroy the layers and remove them from the scene and memory 36 | destroy() { 37 | // TODO: Sometimes this is already null, find out why 38 | if (this._layers) { 39 | for (var i = 0; i < this._layers.length; i++) { 40 | this._layers[i].destroy(); 41 | } 42 | 43 | this._layers = null; 44 | } 45 | 46 | super.destroy(); 47 | } 48 | } 49 | 50 | export default LayerGroup; 51 | 52 | var noNew = function(options) { 53 | return new LayerGroup(options); 54 | }; 55 | 56 | export {noNew as layerGroup}; 57 | -------------------------------------------------------------------------------- /examples/basic/main.js: -------------------------------------------------------------------------------- 1 | // Manhattan 2 | var coords = [40.739940, -73.988801]; 3 | 4 | var world = VIZI.world('world', { 5 | skybox: false, 6 | postProcessing: false 7 | }).setView(coords); 8 | 9 | // Add controls 10 | VIZI.Controls.orbit().addTo(world); 11 | 12 | // CartoDB basemap 13 | VIZI.imageTileLayer('http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', { 14 | attribution: '© OpenStreetMap contributors, © CartoDB' 15 | }).addTo(world); 16 | 17 | // Buildings from Mapzen 18 | VIZI.topoJSONTileLayer('https://vector.mapzen.com/osm/buildings/{z}/{x}/{y}.topojson?api_key=vector-tiles-NT5Emiw', { 19 | interactive: false, 20 | style: function(feature) { 21 | var height; 22 | 23 | if (feature.properties.height) { 24 | height = feature.properties.height; 25 | } else { 26 | height = 10 + Math.random() * 10; 27 | } 28 | 29 | return { 30 | height: height 31 | }; 32 | }, 33 | filter: function(feature) { 34 | // Don't show points 35 | return feature.geometry.type !== 'Point'; 36 | }, 37 | attribution: '© OpenStreetMap contributors, Who\'s On First.' 38 | }).addTo(world); 39 | -------------------------------------------------------------------------------- /src/geo/Point.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Point is a helper class for ensuring consistent world positions. 3 | * 4 | * Based on: 5 | * https://github.com/Leaflet/Leaflet/blob/master/src/geo/Point.js 6 | */ 7 | 8 | class Point { 9 | constructor(x, y, round) { 10 | this.x = (round ? Math.round(x) : x); 11 | this.y = (round ? Math.round(y) : y); 12 | } 13 | 14 | clone() { 15 | return new Point(this.x, this.y); 16 | } 17 | 18 | // Non-destructive 19 | add(point) { 20 | return this.clone()._add(_point(point)); 21 | } 22 | 23 | // Destructive 24 | _add(point) { 25 | this.x += point.x; 26 | this.y += point.y; 27 | return this; 28 | } 29 | 30 | // Non-destructive 31 | subtract(point) { 32 | return this.clone()._subtract(_point(point)); 33 | } 34 | 35 | // Destructive 36 | _subtract(point) { 37 | this.x -= point.x; 38 | this.y -= point.y; 39 | return this; 40 | } 41 | } 42 | 43 | export default Point; 44 | 45 | // Accepts (point), ([x, y]) and (x, y, round) 46 | var _point = function(x, y, round) { 47 | if (x instanceof Point) { 48 | return x; 49 | } 50 | if (Array.isArray(x)) { 51 | return new Point(x[0], x[1]); 52 | } 53 | if (x === undefined || x === null) { 54 | return x; 55 | } 56 | return new Point(x, y, round); 57 | }; 58 | 59 | // Initialise without requiring new keyword 60 | export {_point as point}; 61 | -------------------------------------------------------------------------------- /src/layer/tile/TileCache.js: -------------------------------------------------------------------------------- 1 | import LRUCache from 'lru-cache'; 2 | 3 | // TODO: Make sure nothing is left behind in the heap after calling destroy() 4 | 5 | // This process is based on a similar approach taken by OpenWebGlobe 6 | // See: https://github.com/OpenWebGlobe/WebViewer/blob/master/source/core/globecache.js 7 | 8 | class TileCache { 9 | constructor(cacheLimit, onDestroyTile) { 10 | this._cache = LRUCache({ 11 | max: cacheLimit, 12 | dispose: (key, tile) => { 13 | onDestroyTile(tile); 14 | } 15 | }); 16 | } 17 | 18 | // Returns true if all specified tile providers are ready to be used 19 | // Otherwise, returns false 20 | isReady() { 21 | return false; 22 | } 23 | 24 | // Get a cached tile without requesting a new one 25 | getTile(quadcode) { 26 | return this._cache.get(quadcode); 27 | } 28 | 29 | // Add tile to cache 30 | setTile(quadcode, tile) { 31 | this._cache.set(quadcode, tile); 32 | } 33 | 34 | // Destroy the cache and remove it from memory 35 | // 36 | // TODO: Call destroy method on items in cache 37 | destroy() { 38 | this._cache.reset(); 39 | this._cache = null; 40 | } 41 | } 42 | 43 | export default TileCache; 44 | 45 | var noNew = function(cacheLimit, onDestroyTile) { 46 | return new TileCache(cacheLimit, onDestroyTile); 47 | }; 48 | 49 | // Initialise without requiring new keyword 50 | export {noNew as tileCache}; 51 | -------------------------------------------------------------------------------- /examples/interactive/main.js: -------------------------------------------------------------------------------- 1 | // London 2 | var coords = [51.5052, -0.0308]; 3 | 4 | var world = VIZI.world('world', { 5 | skybox: false, 6 | postProcessing: false 7 | }).setView(coords); 8 | 9 | // Add controls 10 | VIZI.Controls.orbit().addTo(world); 11 | 12 | // CartoDB basemap 13 | VIZI.imageTileLayer('http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', { 14 | attribution: '© OpenStreetMap contributors, © CartoDB' 15 | }).addTo(world); 16 | 17 | // Chroma scale for height-based colours 18 | var colourScale = chroma.scale('YlOrBr').domain([0,350]); 19 | 20 | // Census boundary polygons 21 | VIZI.geoJSONLayer('https://cdn.rawgit.com/robhawkes/5d6efd288b24e698783a/raw/dcf5ac06b40d7f0100cffd4af220865860e68b82/census.json', { 22 | output: true, 23 | interactive: true, 24 | style: function(feature) { 25 | var value = feature.properties.POPDEN; 26 | var colour = colourScale(value).hex(); 27 | 28 | return { 29 | color: colour 30 | }; 31 | }, 32 | onEachFeature: function(feature, layer) { 33 | layer.on('click', function(layer, point2d, point3d, intersects) { 34 | var id = layer.feature.properties.LAD11CD; 35 | var value = layer.feature.properties.POPDEN; 36 | 37 | console.log(id + ': ' + value, layer, point2d, point3d, intersects); 38 | }); 39 | } 40 | }).addTo(world); 41 | -------------------------------------------------------------------------------- /src/layer/tile/ImageTileLayerBaseMaterial.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | 3 | export default function(colour, skyboxTarget) { 4 | var canvas = document.createElement('canvas'); 5 | canvas.width = 1; 6 | canvas.height = 1; 7 | 8 | var context = canvas.getContext('2d'); 9 | context.fillStyle = colour; 10 | context.fillRect(0, 0, canvas.width, canvas.height); 11 | // context.strokeStyle = '#D0D0CF'; 12 | // context.strokeRect(0, 0, canvas.width, canvas.height); 13 | 14 | var texture = new THREE.Texture(canvas); 15 | 16 | // // Silky smooth images when tilted 17 | // texture.magFilter = THREE.LinearFilter; 18 | // texture.minFilter = THREE.LinearMipMapLinearFilter; 19 | // // 20 | // // // TODO: Set this to renderer.getMaxAnisotropy() / 4 21 | // texture.anisotropy = 4; 22 | 23 | // texture.wrapS = THREE.RepeatWrapping; 24 | // texture.wrapT = THREE.RepeatWrapping; 25 | // texture.repeat.set(segments, segments); 26 | 27 | texture.needsUpdate = true; 28 | 29 | var material; 30 | 31 | if (!skyboxTarget) { 32 | material = new THREE.MeshBasicMaterial({ 33 | map: texture, 34 | depthWrite: false 35 | }); 36 | } else { 37 | material = new THREE.MeshStandardMaterial({ 38 | map: texture, 39 | depthWrite: false 40 | }); 41 | material.roughness = 1; 42 | material.metalness = 0.1; 43 | material.envMap = skyboxTarget; 44 | } 45 | 46 | return material; 47 | }; 48 | -------------------------------------------------------------------------------- /src/vendor/RenderPass.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /* eslint-disable */ 3 | 4 | import THREE from 'three'; 5 | 6 | /** 7 | * @author alteredq / http://alteredqualia.com/ 8 | */ 9 | 10 | var RenderPass = function ( scene, camera, overrideMaterial, clearColor, clearAlpha ) { 11 | 12 | this.scene = scene; 13 | this.camera = camera; 14 | 15 | this.overrideMaterial = overrideMaterial; 16 | 17 | this.clearColor = clearColor; 18 | this.clearAlpha = ( clearAlpha !== undefined ) ? clearAlpha : 1; 19 | 20 | this.oldClearColor = new THREE.Color(); 21 | this.oldClearAlpha = 1; 22 | 23 | this.enabled = true; 24 | this.clear = true; 25 | this.needsSwap = false; 26 | 27 | }; 28 | 29 | RenderPass.prototype = { 30 | 31 | render: function ( renderer, writeBuffer, readBuffer, delta ) { 32 | 33 | this.scene.overrideMaterial = this.overrideMaterial; 34 | 35 | if ( this.clearColor ) { 36 | 37 | this.oldClearColor.copy( renderer.getClearColor() ); 38 | this.oldClearAlpha = renderer.getClearAlpha(); 39 | 40 | renderer.setClearColor( this.clearColor, this.clearAlpha ); 41 | 42 | } 43 | 44 | renderer.render( this.scene, this.camera, readBuffer, this.clear ); 45 | 46 | if ( this.clearColor ) { 47 | 48 | renderer.setClearColor( this.oldClearColor, this.oldClearAlpha ); 49 | 50 | } 51 | 52 | this.scene.overrideMaterial = null; 53 | 54 | } 55 | 56 | }; 57 | 58 | export default RenderPass; 59 | THREE.RenderPass = RenderPass; 60 | -------------------------------------------------------------------------------- /src/geo/LatLon.js: -------------------------------------------------------------------------------- 1 | /* 2 | * LatLon is a helper class for ensuring consistent geographic coordinates. 3 | * 4 | * Based on: 5 | * https://github.com/Leaflet/Leaflet/blob/master/src/geo/LatLng.js 6 | */ 7 | 8 | class LatLon { 9 | constructor(lat, lon, alt) { 10 | if (isNaN(lat) || isNaN(lon)) { 11 | throw new Error('Invalid LatLon object: (' + lat + ', ' + lon + ')'); 12 | } 13 | 14 | this.lat = +lat; 15 | this.lon = +lon; 16 | 17 | if (alt !== undefined) { 18 | this.alt = +alt; 19 | } 20 | } 21 | 22 | clone() { 23 | return new LatLon(this.lat, this.lon, this.alt); 24 | } 25 | } 26 | 27 | export default LatLon; 28 | 29 | // Accepts (LatLon), ([lat, lon, alt]), ([lat, lon]) and (lat, lon, alt) 30 | // Also converts between lng and lon 31 | var noNew = function(a, b, c) { 32 | if (a instanceof LatLon) { 33 | return a; 34 | } 35 | if (Array.isArray(a) && typeof a[0] !== 'object') { 36 | if (a.length === 3) { 37 | return new LatLon(a[0], a[1], a[2]); 38 | } 39 | if (a.length === 2) { 40 | return new LatLon(a[0], a[1]); 41 | } 42 | return null; 43 | } 44 | if (a === undefined || a === null) { 45 | return a; 46 | } 47 | if (typeof a === 'object' && 'lat' in a) { 48 | return new LatLon(a.lat, 'lng' in a ? a.lng : a.lon, a.alt); 49 | } 50 | if (b === undefined) { 51 | return null; 52 | } 53 | return new LatLon(a, b, c); 54 | }; 55 | 56 | // Initialise without requiring new keyword 57 | export {noNew as latLon}; 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, UrbanSim Inc. 2 | Copyright (c) 2013-2016, Robin Hawkes 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /examples/colour-by-height/main.js: -------------------------------------------------------------------------------- 1 | // Manhattan 2 | var coords = [40.739940, -73.988801]; 3 | 4 | var world = VIZI.world('world', { 5 | skybox: false, 6 | postProcessing: false 7 | }).setView(coords); 8 | 9 | // Add controls 10 | VIZI.Controls.orbit().addTo(world); 11 | 12 | // CartoDB basemap 13 | VIZI.imageTileLayer('http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', { 14 | attribution: '© OpenStreetMap contributors, © CartoDB' 15 | }).addTo(world); 16 | 17 | // Chroma scale for height-based colours 18 | var colourScale = chroma.scale('YlOrBr').domain([0,200]); 19 | 20 | // Buildings from Mapzen 21 | VIZI.topoJSONTileLayer('https://vector.mapzen.com/osm/buildings/{z}/{x}/{y}.topojson?api_key=vector-tiles-NT5Emiw', { 22 | interactive: false, 23 | style: function(feature) { 24 | var height; 25 | 26 | if (feature.properties.height) { 27 | height = feature.properties.height; 28 | } else { 29 | height = 10 + Math.random() * 10; 30 | } 31 | 32 | var colour = colourScale(height).hex(); 33 | 34 | return { 35 | color: colour, 36 | height: height 37 | }; 38 | }, 39 | filter: function(feature) { 40 | // Don't show points 41 | return feature.geometry.type !== 'Point'; 42 | }, 43 | attribution: '© OpenStreetMap contributors, Who\'s On First.' 44 | }).addTo(world); 45 | -------------------------------------------------------------------------------- /src/engine/Renderer.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | import Scene from './Scene'; 3 | 4 | // This can only be accessed from Engine.renderer if you want to reference the 5 | // same scene in multiple places 6 | 7 | export default function(container, antialias) { 8 | var renderer = new THREE.WebGLRenderer({ 9 | antialias: antialias 10 | }); 11 | 12 | // TODO: Re-enable when this works with the skybox 13 | // renderer.setClearColor(Scene.fog.color, 1); 14 | 15 | renderer.setClearColor(0xffffff, 1); 16 | 17 | // TODO: Re-enable this when perf issues can be solved 18 | // 19 | // Rendering double the resolution of the screen can be really slow 20 | // var pixelRatio = window.devicePixelRatio; 21 | var pixelRatio = 1; 22 | 23 | renderer.setPixelRatio(pixelRatio); 24 | 25 | // Gamma settings make things look nicer 26 | renderer.gammaInput = true; 27 | renderer.gammaOutput = true; 28 | 29 | renderer.shadowMap.enabled = true; 30 | 31 | // TODO: Work out which of the shadowmap types is best 32 | // https://github.com/mrdoob/three.js/blob/r56/src/Three.js#L107 33 | // renderer.shadowMap.type = THREE.PCFSoftShadowMap; 34 | 35 | // TODO: Check that leaving this as default (CullFrontFace) is right 36 | // renderer.shadowMap.cullFace = THREE.CullFaceBack; 37 | 38 | container.appendChild(renderer.domElement); 39 | 40 | var updateSize = function() { 41 | renderer.setSize(container.clientWidth, container.clientHeight); 42 | }; 43 | 44 | window.addEventListener('resize', updateSize, false); 45 | updateSize(); 46 | 47 | return renderer; 48 | }; 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | 3 | # Created by https://www.gitignore.io/api/node,osx,windows 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | node_modules 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | 41 | ### OSX ### 42 | .DS_Store 43 | .AppleDouble 44 | .LSOverride 45 | 46 | # Icon must end with two \r 47 | Icon 48 | 49 | # Thumbnails 50 | ._* 51 | 52 | # Files that might appear in the root of a volume 53 | .DocumentRevisions-V100 54 | .fseventsd 55 | .Spotlight-V100 56 | .TemporaryItems 57 | .Trashes 58 | .VolumeIcon.icns 59 | 60 | # Directories potentially created on remote AFP share 61 | .AppleDB 62 | .AppleDesktop 63 | Network Trash Folder 64 | Temporary Items 65 | .apdisk 66 | 67 | 68 | ### Windows ### 69 | # Windows image file caches 70 | Thumbs.db 71 | ehthumbs.db 72 | 73 | # Folder config file 74 | Desktop.ini 75 | 76 | # Recycle Bin used on file shares 77 | $RECYCLE.BIN/ 78 | 79 | # Windows Installer files 80 | *.cab 81 | *.msi 82 | *.msm 83 | *.msp 84 | 85 | # Windows shortcuts 86 | *.lnk 87 | -------------------------------------------------------------------------------- /src/vendor/ShaderPass.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /* eslint-disable */ 3 | 4 | import THREE from 'three'; 5 | 6 | /** 7 | * @author alteredq / http://alteredqualia.com/ 8 | */ 9 | 10 | var ShaderPass = function( shader, textureID ) { 11 | 12 | this.textureID = ( textureID !== undefined ) ? textureID : "tDiffuse"; 13 | 14 | if ( shader instanceof THREE.ShaderMaterial ) { 15 | 16 | this.uniforms = shader.uniforms; 17 | 18 | this.material = shader; 19 | 20 | } 21 | else if ( shader ) { 22 | 23 | this.uniforms = THREE.UniformsUtils.clone( shader.uniforms ); 24 | 25 | this.material = new THREE.ShaderMaterial( { 26 | 27 | defines: shader.defines || {}, 28 | uniforms: this.uniforms, 29 | vertexShader: shader.vertexShader, 30 | fragmentShader: shader.fragmentShader 31 | 32 | } ); 33 | 34 | } 35 | 36 | this.renderToScreen = false; 37 | 38 | this.enabled = true; 39 | this.needsSwap = true; 40 | this.clear = false; 41 | 42 | 43 | this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); 44 | this.scene = new THREE.Scene(); 45 | 46 | this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null ); 47 | this.scene.add( this.quad ); 48 | 49 | }; 50 | 51 | ShaderPass.prototype = { 52 | 53 | render: function( renderer, writeBuffer, readBuffer, delta ) { 54 | 55 | if ( this.uniforms[ this.textureID ] ) { 56 | 57 | this.uniforms[ this.textureID ].value = readBuffer; 58 | 59 | } 60 | 61 | this.quad.material = this.material; 62 | 63 | if ( this.renderToScreen ) { 64 | 65 | renderer.render( this.scene, this.camera ); 66 | 67 | } else { 68 | 69 | renderer.render( this.scene, this.camera, writeBuffer, this.clear ); 70 | 71 | } 72 | 73 | } 74 | 75 | }; 76 | 77 | export default ShaderPass; 78 | THREE.ShaderPass = ShaderPass; 79 | -------------------------------------------------------------------------------- /src/util/extrudePolygon.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Extrude a polygon given its vertices and triangulated faces 3 | * 4 | * Based on: 5 | * https://github.com/freeman-lab/extrude 6 | */ 7 | 8 | import extend from 'lodash.assign'; 9 | 10 | var extrudePolygon = function(points, faces, _options) { 11 | var defaults = { 12 | top: 1, 13 | bottom: 0, 14 | closed: true 15 | }; 16 | 17 | var options = extend({}, defaults, _options); 18 | 19 | var n = points.length; 20 | var positions; 21 | var cells; 22 | var topCells; 23 | var bottomCells; 24 | var sideCells; 25 | 26 | // If bottom and top values are identical then return the flat shape 27 | (options.top === options.bottom) ? flat() : full(); 28 | 29 | function flat() { 30 | positions = points.map(function(p) { return [p[0], options.top, p[1]]; }); 31 | cells = faces; 32 | topCells = faces; 33 | } 34 | 35 | function full() { 36 | positions = []; 37 | points.forEach(function(p) { positions.push([p[0], options.top, p[1]]); }); 38 | points.forEach(function(p) { positions.push([p[0], options.bottom, p[1]]); }); 39 | 40 | cells = []; 41 | for (var i = 0; i < n; i++) { 42 | if (i === (n - 1)) { 43 | cells.push([i + n, n, i]); 44 | cells.push([0, i, n]); 45 | } else { 46 | cells.push([i + n, i + n + 1, i]); 47 | cells.push([i + 1, i, i + n + 1]); 48 | } 49 | } 50 | 51 | sideCells = [].concat(cells); 52 | 53 | if (options.closed) { 54 | var top = faces; 55 | var bottom = top.map(function(p) { return p.map(function(v) { return v + n; }); }); 56 | bottom = bottom.map(function(p) { return [p[0], p[2], p[1]]; }); 57 | cells = cells.concat(top).concat(bottom); 58 | 59 | topCells = top; 60 | bottomCells = bottom; 61 | } 62 | } 63 | 64 | return { 65 | positions: positions, 66 | faces: cells, 67 | top: topCells, 68 | bottom: bottomCells, 69 | sides: sideCells 70 | }; 71 | }; 72 | 73 | export default extrudePolygon; 74 | -------------------------------------------------------------------------------- /src/vizicities.js: -------------------------------------------------------------------------------- 1 | import World, {world} from './World'; 2 | import Controls from './controls/index'; 3 | 4 | import Geo from './geo/Geo.js'; 5 | 6 | import Layer, {layer} from './layer/Layer'; 7 | import EnvironmentLayer, {environmentLayer} from './layer/environment/EnvironmentLayer'; 8 | import ImageTileLayer, {imageTileLayer} from './layer/tile/ImageTileLayer'; 9 | import GeoJSONTileLayer, {geoJSONTileLayer} from './layer/tile/GeoJSONTileLayer'; 10 | import TopoJSONTileLayer, {topoJSONTileLayer} from './layer/tile/TopoJSONTileLayer'; 11 | import GeoJSONLayer, {geoJSONLayer} from './layer/GeoJSONLayer'; 12 | import TopoJSONLayer, {topoJSONLayer} from './layer/TopoJSONLayer'; 13 | import PolygonLayer, {polygonLayer} from './layer/geometry/PolygonLayer'; 14 | import PolylineLayer, {polylineLayer} from './layer/geometry/PolylineLayer'; 15 | import PointLayer, {pointLayer} from './layer/geometry/PointLayer'; 16 | 17 | import Point, {point} from './geo/Point'; 18 | import LatLon, {latLon} from './geo/LatLon'; 19 | 20 | import PickingMaterial from './engine/PickingMaterial'; 21 | 22 | import Util from './util/index'; 23 | 24 | const VIZI = { 25 | version: '0.3', 26 | 27 | // Public API 28 | World: World, 29 | world: world, 30 | Controls: Controls, 31 | Geo: Geo, 32 | Layer: Layer, 33 | layer: layer, 34 | EnvironmentLayer: EnvironmentLayer, 35 | environmentLayer: environmentLayer, 36 | ImageTileLayer: ImageTileLayer, 37 | imageTileLayer: imageTileLayer, 38 | GeoJSONTileLayer: GeoJSONTileLayer, 39 | geoJSONTileLayer: geoJSONTileLayer, 40 | TopoJSONTileLayer: TopoJSONTileLayer, 41 | topoJSONTileLayer: topoJSONTileLayer, 42 | GeoJSONLayer: GeoJSONLayer, 43 | geoJSONLayer: geoJSONLayer, 44 | TopoJSONLayer: TopoJSONLayer, 45 | topoJSONLayer: topoJSONLayer, 46 | PolygonLayer: PolygonLayer, 47 | polygonLayer: polygonLayer, 48 | PolylineLayer: PolylineLayer, 49 | polylineLayer: polylineLayer, 50 | PointLayer: PointLayer, 51 | pointLayer: pointLayer, 52 | Point: Point, 53 | point: point, 54 | LatLon: LatLon, 55 | latLon: latLon, 56 | PickingMaterial: PickingMaterial, 57 | Util: Util 58 | }; 59 | 60 | export default VIZI; 61 | -------------------------------------------------------------------------------- /src/vendor/VerticalTiltShiftShader.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /* eslint-disable */ 3 | 4 | import THREE from 'three'; 5 | 6 | /** 7 | * @author alteredq / http://alteredqualia.com/ 8 | * 9 | * Simple fake tilt-shift effect, modulating two pass Gaussian blur (see above) by vertical position 10 | * 11 | * - 9 samples per pass 12 | * - standard deviation 2.7 13 | * - "h" and "v" parameters should be set to "1 / width" and "1 / height" 14 | * - "r" parameter control where "focused" horizontal line lies 15 | */ 16 | 17 | var VerticalTiltShiftShader = { 18 | 19 | uniforms: { 20 | 21 | "tDiffuse": { type: "t", value: null }, 22 | "v": { type: "f", value: 1.0 / 512.0 }, 23 | "r": { type: "f", value: 0.35 } 24 | 25 | }, 26 | 27 | vertexShader: [ 28 | 29 | "varying vec2 vUv;", 30 | 31 | "void main() {", 32 | 33 | "vUv = uv;", 34 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 35 | 36 | "}" 37 | 38 | ].join( "\n" ), 39 | 40 | fragmentShader: [ 41 | 42 | "uniform sampler2D tDiffuse;", 43 | "uniform float v;", 44 | "uniform float r;", 45 | 46 | "varying vec2 vUv;", 47 | 48 | "void main() {", 49 | 50 | "vec4 sum = vec4( 0.0 );", 51 | 52 | "float vv = v * abs( r - vUv.y );", 53 | 54 | "sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 4.0 * vv ) ) * 0.051;", 55 | "sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 3.0 * vv ) ) * 0.0918;", 56 | "sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 2.0 * vv ) ) * 0.12245;", 57 | "sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y - 1.0 * vv ) ) * 0.1531;", 58 | "sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y ) ) * 0.1633;", 59 | "sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 1.0 * vv ) ) * 0.1531;", 60 | "sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 2.0 * vv ) ) * 0.12245;", 61 | "sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 3.0 * vv ) ) * 0.0918;", 62 | "sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y + 4.0 * vv ) ) * 0.051;", 63 | 64 | "gl_FragColor = sum;", 65 | 66 | "}" 67 | 68 | ].join( "\n" ) 69 | 70 | }; 71 | 72 | export default VerticalTiltShiftShader; 73 | THREE.VerticalTiltShiftShader = VerticalTiltShiftShader; 74 | -------------------------------------------------------------------------------- /src/vendor/HorizontalTiltShiftShader.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /* eslint-disable */ 3 | 4 | import THREE from 'three'; 5 | 6 | /** 7 | * @author alteredq / http://alteredqualia.com/ 8 | * 9 | * Simple fake tilt-shift effect, modulating two pass Gaussian blur (see above) by vertical position 10 | * 11 | * - 9 samples per pass 12 | * - standard deviation 2.7 13 | * - "h" and "v" parameters should be set to "1 / width" and "1 / height" 14 | * - "r" parameter control where "focused" horizontal line lies 15 | */ 16 | 17 | var HorizontalTiltShiftShader = { 18 | 19 | uniforms: { 20 | 21 | "tDiffuse": { type: "t", value: null }, 22 | "h": { type: "f", value: 1.0 / 512.0 }, 23 | "r": { type: "f", value: 0.35 } 24 | 25 | }, 26 | 27 | vertexShader: [ 28 | 29 | "varying vec2 vUv;", 30 | 31 | "void main() {", 32 | 33 | "vUv = uv;", 34 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 35 | 36 | "}" 37 | 38 | ].join( "\n" ), 39 | 40 | fragmentShader: [ 41 | 42 | "uniform sampler2D tDiffuse;", 43 | "uniform float h;", 44 | "uniform float r;", 45 | 46 | "varying vec2 vUv;", 47 | 48 | "void main() {", 49 | 50 | "vec4 sum = vec4( 0.0 );", 51 | 52 | "float hh = h * abs( r - vUv.y );", 53 | 54 | "sum += texture2D( tDiffuse, vec2( vUv.x - 4.0 * hh, vUv.y ) ) * 0.051;", 55 | "sum += texture2D( tDiffuse, vec2( vUv.x - 3.0 * hh, vUv.y ) ) * 0.0918;", 56 | "sum += texture2D( tDiffuse, vec2( vUv.x - 2.0 * hh, vUv.y ) ) * 0.12245;", 57 | "sum += texture2D( tDiffuse, vec2( vUv.x - 1.0 * hh, vUv.y ) ) * 0.1531;", 58 | "sum += texture2D( tDiffuse, vec2( vUv.x, vUv.y ) ) * 0.1633;", 59 | "sum += texture2D( tDiffuse, vec2( vUv.x + 1.0 * hh, vUv.y ) ) * 0.1531;", 60 | "sum += texture2D( tDiffuse, vec2( vUv.x + 2.0 * hh, vUv.y ) ) * 0.12245;", 61 | "sum += texture2D( tDiffuse, vec2( vUv.x + 3.0 * hh, vUv.y ) ) * 0.0918;", 62 | "sum += texture2D( tDiffuse, vec2( vUv.x + 4.0 * hh, vUv.y ) ) * 0.051;", 63 | 64 | "gl_FragColor = sum;", 65 | 66 | "}" 67 | 68 | ].join( "\n" ) 69 | 70 | }; 71 | 72 | export default HorizontalTiltShiftShader; 73 | THREE.HorizontalTiltShiftShader = HorizontalTiltShiftShader; 74 | -------------------------------------------------------------------------------- /src/vendor/MaskPass.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /* eslint-disable */ 3 | 4 | import THREE from 'three'; 5 | 6 | /** 7 | * @author alteredq / http://alteredqualia.com/ 8 | */ 9 | 10 | var MaskPass = function ( scene, camera ) { 11 | 12 | this.scene = scene; 13 | this.camera = camera; 14 | 15 | this.enabled = true; 16 | this.clear = true; 17 | this.needsSwap = false; 18 | 19 | this.inverse = false; 20 | 21 | }; 22 | 23 | MaskPass.prototype = { 24 | 25 | render: function ( renderer, writeBuffer, readBuffer, delta ) { 26 | 27 | var context = renderer.context; 28 | 29 | // don't update color or depth 30 | 31 | context.colorMask( false, false, false, false ); 32 | context.depthMask( false ); 33 | 34 | // set up stencil 35 | 36 | var writeValue, clearValue; 37 | 38 | if ( this.inverse ) { 39 | 40 | writeValue = 0; 41 | clearValue = 1; 42 | 43 | } else { 44 | 45 | writeValue = 1; 46 | clearValue = 0; 47 | 48 | } 49 | 50 | context.enable( context.STENCIL_TEST ); 51 | context.stencilOp( context.REPLACE, context.REPLACE, context.REPLACE ); 52 | context.stencilFunc( context.ALWAYS, writeValue, 0xffffffff ); 53 | context.clearStencil( clearValue ); 54 | 55 | // draw into the stencil buffer 56 | 57 | renderer.render( this.scene, this.camera, readBuffer, this.clear ); 58 | renderer.render( this.scene, this.camera, writeBuffer, this.clear ); 59 | 60 | // re-enable update of color and depth 61 | 62 | context.colorMask( true, true, true, true ); 63 | context.depthMask( true ); 64 | 65 | // only render where stencil is set to 1 66 | 67 | context.stencilFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1 68 | context.stencilOp( context.KEEP, context.KEEP, context.KEEP ); 69 | 70 | } 71 | 72 | }; 73 | 74 | 75 | var ClearMaskPass = function () { 76 | 77 | this.enabled = true; 78 | 79 | }; 80 | 81 | ClearMaskPass.prototype = { 82 | 83 | render: function ( renderer, writeBuffer, readBuffer, delta ) { 84 | 85 | var context = renderer.context; 86 | 87 | context.disable( context.STENCIL_TEST ); 88 | 89 | } 90 | 91 | }; 92 | 93 | export default MaskPass; 94 | export {ClearMaskPass as ClearMaskPass}; 95 | 96 | THREE.MaskPass = MaskPass; 97 | THREE.ClearMaskPass = ClearMaskPass; 98 | -------------------------------------------------------------------------------- /src/vendor/BoxHelper.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /* eslint-disable */ 3 | 4 | import THREE from 'three'; 5 | 6 | /** 7 | * @author mrdoob / http://mrdoob.com/ 8 | */ 9 | 10 | BoxHelper = function ( object ) { 11 | 12 | var indices = new Uint16Array( [ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ] ); 13 | var positions = new Float32Array( 8 * 3 ); 14 | 15 | var geometry = new THREE.BufferGeometry(); 16 | geometry.setIndex( new THREE.BufferAttribute( indices, 1 ) ); 17 | geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) ); 18 | 19 | THREE.LineSegments.call( this, geometry, new THREE.LineBasicMaterial( { linewidth: 2, color: 0xff0000 } ) ); 20 | 21 | if ( object !== undefined ) { 22 | 23 | this.update( object ); 24 | 25 | } 26 | 27 | }; 28 | 29 | BoxHelper.prototype = Object.create( THREE.LineSegments.prototype ); 30 | BoxHelper.prototype.constructor = BoxHelper; 31 | 32 | BoxHelper.prototype.update = ( function () { 33 | 34 | var box = new THREE.Box3(); 35 | 36 | return function ( object ) { 37 | 38 | box.setFromObject( object ); 39 | 40 | if ( box.isEmpty() ) return; 41 | 42 | var min = box.min; 43 | var max = box.max; 44 | 45 | /* 46 | 5____4 47 | 1/___0/| 48 | | 6__|_7 49 | 2/___3/ 50 | 51 | 0: max.x, max.y, max.z 52 | 1: min.x, max.y, max.z 53 | 2: min.x, min.y, max.z 54 | 3: max.x, min.y, max.z 55 | 4: max.x, max.y, min.z 56 | 5: min.x, max.y, min.z 57 | 6: min.x, min.y, min.z 58 | 7: max.x, min.y, min.z 59 | */ 60 | 61 | var position = this.geometry.attributes.position; 62 | var array = position.array; 63 | 64 | array[ 0 ] = max.x; array[ 1 ] = max.y; array[ 2 ] = max.z; 65 | array[ 3 ] = min.x; array[ 4 ] = max.y; array[ 5 ] = max.z; 66 | array[ 6 ] = min.x; array[ 7 ] = min.y; array[ 8 ] = max.z; 67 | array[ 9 ] = max.x; array[ 10 ] = min.y; array[ 11 ] = max.z; 68 | array[ 12 ] = max.x; array[ 13 ] = max.y; array[ 14 ] = min.z; 69 | array[ 15 ] = min.x; array[ 16 ] = max.y; array[ 17 ] = min.z; 70 | array[ 18 ] = min.x; array[ 19 ] = min.y; array[ 20 ] = min.z; 71 | array[ 21 ] = max.x; array[ 22 ] = min.y; array[ 23 ] = min.z; 72 | 73 | position.needsUpdate = true; 74 | 75 | this.geometry.computeBoundingSphere(); 76 | 77 | }; 78 | 79 | } )(); 80 | 81 | export default BoxHelper; 82 | -------------------------------------------------------------------------------- /examples/vendor/threex.rendererstats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | * @author jetienne / http://jetienne.com/ 4 | */ 5 | /** @namespace */ 6 | var THREEx = THREEx || {} 7 | 8 | /** 9 | * provide info on THREE.WebGLRenderer 10 | * 11 | * @param {Object} renderer the renderer to update 12 | * @param {Object} Camera the camera to update 13 | */ 14 | THREEx.RendererStats = function (){ 15 | 16 | var msMin = 100; 17 | var msMax = 0; 18 | 19 | var container = document.createElement( 'div' ); 20 | container.style.cssText = 'width:80px;opacity:0.9;cursor:pointer'; 21 | 22 | var msDiv = document.createElement( 'div' ); 23 | msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#200;'; 24 | container.appendChild( msDiv ); 25 | 26 | var msText = document.createElement( 'div' ); 27 | msText.style.cssText = 'color:#f00;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; 28 | msText.innerHTML= 'WebGLRenderer'; 29 | msDiv.appendChild( msText ); 30 | 31 | var msTexts = []; 32 | var nLines = 9; 33 | for(var i = 0; i < nLines; i++){ 34 | msTexts[i] = document.createElement( 'div' ); 35 | msTexts[i].style.cssText = 'color:#f00;background-color:#311;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; 36 | msDiv.appendChild( msTexts[i] ); 37 | msTexts[i].innerHTML= '-'; 38 | } 39 | 40 | 41 | var lastTime = Date.now(); 42 | return { 43 | domElement: container, 44 | 45 | update: function(webGLRenderer){ 46 | // sanity check 47 | console.assert(webGLRenderer instanceof THREE.WebGLRenderer) 48 | 49 | // refresh only 30time per second 50 | if( Date.now() - lastTime < 1000/30 ) return; 51 | lastTime = Date.now() 52 | 53 | var i = 0; 54 | msTexts[i++].textContent = "== Memory ====="; 55 | msTexts[i++].textContent = "Programs: " + webGLRenderer.info.programs.length; 56 | msTexts[i++].textContent = "Geometries: "+webGLRenderer.info.memory.geometries; 57 | msTexts[i++].textContent = "Textures: " + webGLRenderer.info.memory.textures; 58 | 59 | msTexts[i++].textContent = "== Render ====="; 60 | msTexts[i++].textContent = "Calls: " + webGLRenderer.info.render.calls; 61 | msTexts[i++].textContent = "Vertices: " + webGLRenderer.info.render.vertices; 62 | msTexts[i++].textContent = "Faces: " + webGLRenderer.info.render.faces; 63 | msTexts[i++].textContent = "Points: " + webGLRenderer.info.render.points; 64 | } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /examples/all-the-things/main.js: -------------------------------------------------------------------------------- 1 | // London 2 | var coords = [51.505, -0.09]; 3 | 4 | var world = VIZI.world('world', { 5 | skybox: true, 6 | postProcessing: true 7 | }).setView(coords); 8 | 9 | // Set position of sun in sky 10 | world._environment._skybox.setInclination(0.3); 11 | 12 | // Add controls 13 | VIZI.Controls.orbit().addTo(world); 14 | 15 | // CartoDB basemap 16 | VIZI.imageTileLayer('http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', { 17 | attribution: '© OpenStreetMap contributors, © CartoDB' 18 | }).addTo(world); 19 | 20 | // Buildings and roads from Mapzen (polygons and linestrings) 21 | var topoJSONTileLayer = VIZI.topoJSONTileLayer('https://vector.mapzen.com/osm/buildings,roads/{z}/{x}/{y}.topojson?api_key=vector-tiles-NT5Emiw', { 22 | interactive: false, 23 | style: function(feature) { 24 | var height; 25 | 26 | if (feature.properties.height) { 27 | height = feature.properties.height; 28 | } else { 29 | height = 10 + Math.random() * 10; 30 | } 31 | 32 | return { 33 | height: height, 34 | lineColor: '#f7c616', 35 | lineWidth: 1, 36 | lineTransparent: true, 37 | lineOpacity: 0.2, 38 | lineBlending: THREE.AdditiveBlending, 39 | lineRenderOrder: 2 40 | }; 41 | }, 42 | filter: function(feature) { 43 | // Don't show points 44 | return feature.geometry.type !== 'Point'; 45 | }, 46 | attribution: '© OpenStreetMap contributors, Who\'s On First.' 47 | }).addTo(world); 48 | 49 | // London Underground lines 50 | VIZI.geoJSONLayer('https://rawgit.com/robhawkes/4acb9d6a6a5f00a377e2/raw/30ae704a44e10f2e13fb7e956e80c3b22e8e7e81/tfl_lines.json', { 51 | output: true, 52 | interactive: true, 53 | style: function(feature) { 54 | var colour = feature.properties.lines[0].colour || '#ffffff'; 55 | 56 | return { 57 | lineColor: colour, 58 | lineHeight: 20, 59 | lineWidth: 3, 60 | lineTransparent: true, 61 | lineOpacity: 0.5, 62 | lineBlending: THREE.AdditiveBlending, 63 | lineRenderOrder: 2 64 | }; 65 | }, 66 | onEachFeature: function(feature, layer) { 67 | layer.on('click', function(layer, point2d, point3d, intersects) { 68 | console.log(layer, point2d, point3d, intersects); 69 | }); 70 | }, 71 | attribution: '© Transport for London.' 72 | }).addTo(world); 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vizicities", 3 | "version": "0.3.0", 4 | "description": "A framework for 3D geospatial visualisation in the browser", 5 | "main": "dist/vizicities.js", 6 | "scripts": { 7 | "test": "gulp", 8 | "lint": "gulp lint", 9 | "test-browser": "gulp test-browser", 10 | "watch": "gulp watch", 11 | "build": "gulp build", 12 | "coverage": "gulp coverage" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/UDST/vizicities.git" 17 | }, 18 | "keywords": [ 19 | "mapping", 20 | "geography", 21 | "cities", 22 | "3d", 23 | "webgl", 24 | "three.js", 25 | "maps", 26 | "data", 27 | "visualisation", 28 | "geojson", 29 | "gis" 30 | ], 31 | "author": "ViziCities ", 32 | "license": "BSD-3-Clause", 33 | "bugs": { 34 | "url": "https://github.com/UDST/vizicities/issues" 35 | }, 36 | "homepage": "https://github.com/UDST/vizicities", 37 | "devDependencies": { 38 | "babel-core": "^5.2.17", 39 | "babel-eslint": "^6.0.4", 40 | "babel-loader": "^5.3.2", 41 | "chai": "^3.2.0", 42 | "del": "^1.1.1", 43 | "eslint": "^2.12.0", 44 | "glob": "^5.0.14", 45 | "gulp": "^3.8.10", 46 | "gulp-babel": "^5.0.0", 47 | "gulp-eslint": "^1.0.0", 48 | "gulp-filter": "^3.0.0", 49 | "gulp-istanbul": "^0.10.0", 50 | "gulp-jscs": "^3.0.0", 51 | "gulp-livereload": "^3.8.1", 52 | "gulp-load-plugins": "^0.10.0", 53 | "gulp-mocha": "^2.0.0", 54 | "gulp-plumber": "^1.0.1", 55 | "gulp-rename": "^1.2.0", 56 | "gulp-sourcemaps": "^1.3.0", 57 | "gulp-uglify": "^1.5.2", 58 | "gulp-util": "^3.0.6", 59 | "isparta": "^3.0.3", 60 | "json-loader": "^0.5.3", 61 | "mocha": "^2.1.0", 62 | "sinon": "^1.12.2", 63 | "sinon-chai": "^2.7.0", 64 | "vinyl-source-stream": "^1.0.0", 65 | "webpack": "^1.12.2", 66 | "webpack-stream": "^2.1.1" 67 | }, 68 | "babelBoilerplateOptions": { 69 | "entryFileName": "vizicities", 70 | "mainVarName": "VIZI", 71 | "mochaGlobals": [ 72 | "stub", 73 | "spy", 74 | "expect", 75 | "sandbox", 76 | "mock", 77 | "useFakeTimers", 78 | "useFakeXMLHttpRequest", 79 | "useFakeServer" 80 | ] 81 | }, 82 | "dependencies": { 83 | "earcut": "^2.0.8", 84 | "eventemitter3": "^1.1.1", 85 | "geojson-merge": "^0.1.0", 86 | "hammerjs": "^2.0.6", 87 | "lodash.assign": "^4.0.2", 88 | "lodash.throttle": "^4.0.0", 89 | "lru-cache": "^4.0.0", 90 | "reqwest": "^2.0.5", 91 | "three": "^0.74.0", 92 | "topojson": "^1.6.24", 93 | "xhr2": "^0.1.3" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/vendor/FXAAShader.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /* eslint-disable */ 3 | 4 | import THREE from 'three'; 5 | 6 | /** 7 | * @author alteredq / http://alteredqualia.com/ 8 | * @author davidedc / http://www.sketchpatch.net/ 9 | * 10 | * NVIDIA FXAA by Timothy Lottes 11 | * http://timothylottes.blogspot.com/2011/06/fxaa3-source-released.html 12 | * - WebGL port by @supereggbert 13 | * http://www.glge.org/demos/fxaa/ 14 | */ 15 | 16 | var FXAAShader = { 17 | 18 | uniforms: { 19 | 20 | "tDiffuse": { type: "t", value: null }, 21 | "resolution": { type: "v2", value: new THREE.Vector2( 1 / 1024, 1 / 512 ) } 22 | 23 | }, 24 | 25 | vertexShader: [ 26 | 27 | "void main() {", 28 | 29 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 30 | 31 | "}" 32 | 33 | ].join( "\n" ), 34 | 35 | fragmentShader: [ 36 | 37 | "uniform sampler2D tDiffuse;", 38 | "uniform vec2 resolution;", 39 | 40 | "#define FXAA_REDUCE_MIN (1.0/128.0)", 41 | "#define FXAA_REDUCE_MUL (1.0/8.0)", 42 | "#define FXAA_SPAN_MAX 8.0", 43 | 44 | "void main() {", 45 | 46 | "vec3 rgbNW = texture2D( tDiffuse, ( gl_FragCoord.xy + vec2( -1.0, -1.0 ) ) * resolution ).xyz;", 47 | "vec3 rgbNE = texture2D( tDiffuse, ( gl_FragCoord.xy + vec2( 1.0, -1.0 ) ) * resolution ).xyz;", 48 | "vec3 rgbSW = texture2D( tDiffuse, ( gl_FragCoord.xy + vec2( -1.0, 1.0 ) ) * resolution ).xyz;", 49 | "vec3 rgbSE = texture2D( tDiffuse, ( gl_FragCoord.xy + vec2( 1.0, 1.0 ) ) * resolution ).xyz;", 50 | "vec4 rgbaM = texture2D( tDiffuse, gl_FragCoord.xy * resolution );", 51 | "vec3 rgbM = rgbaM.xyz;", 52 | "vec3 luma = vec3( 0.299, 0.587, 0.114 );", 53 | 54 | "float lumaNW = dot( rgbNW, luma );", 55 | "float lumaNE = dot( rgbNE, luma );", 56 | "float lumaSW = dot( rgbSW, luma );", 57 | "float lumaSE = dot( rgbSE, luma );", 58 | "float lumaM = dot( rgbM, luma );", 59 | "float lumaMin = min( lumaM, min( min( lumaNW, lumaNE ), min( lumaSW, lumaSE ) ) );", 60 | "float lumaMax = max( lumaM, max( max( lumaNW, lumaNE) , max( lumaSW, lumaSE ) ) );", 61 | 62 | "vec2 dir;", 63 | "dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));", 64 | "dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE));", 65 | 66 | "float dirReduce = max( ( lumaNW + lumaNE + lumaSW + lumaSE ) * ( 0.25 * FXAA_REDUCE_MUL ), FXAA_REDUCE_MIN );", 67 | 68 | "float rcpDirMin = 1.0 / ( min( abs( dir.x ), abs( dir.y ) ) + dirReduce );", 69 | "dir = min( vec2( FXAA_SPAN_MAX, FXAA_SPAN_MAX),", 70 | "max( vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX),", 71 | "dir * rcpDirMin)) * resolution;", 72 | "vec4 rgbA = (1.0/2.0) * (", 73 | "texture2D(tDiffuse, gl_FragCoord.xy * resolution + dir * (1.0/3.0 - 0.5)) +", 74 | "texture2D(tDiffuse, gl_FragCoord.xy * resolution + dir * (2.0/3.0 - 0.5)));", 75 | "vec4 rgbB = rgbA * (1.0/2.0) + (1.0/4.0) * (", 76 | "texture2D(tDiffuse, gl_FragCoord.xy * resolution + dir * (0.0/3.0 - 0.5)) +", 77 | "texture2D(tDiffuse, gl_FragCoord.xy * resolution + dir * (3.0/3.0 - 0.5)));", 78 | "float lumaB = dot(rgbB, vec4(luma, 0.0));", 79 | 80 | "if ( ( lumaB < lumaMin ) || ( lumaB > lumaMax ) ) {", 81 | 82 | "gl_FragColor = rgbA;", 83 | 84 | "} else {", 85 | "gl_FragColor = rgbB;", 86 | 87 | "}", 88 | 89 | "}" 90 | 91 | ].join( "\n" ) 92 | 93 | }; 94 | 95 | export default FXAAShader; 96 | THREE.FXAAShader = FXAAShader; 97 | -------------------------------------------------------------------------------- /src/vendor/CSS2DRenderer.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /* eslint-disable */ 3 | 4 | /** 5 | * @author mrdoob / http://mrdoob.com/ 6 | */ 7 | 8 | import THREE from 'three'; 9 | 10 | var CSS2DObject = function ( element ) { 11 | 12 | THREE.Object3D.call( this ); 13 | 14 | this.element = element; 15 | this.element.style.position = 'absolute'; 16 | 17 | this.addEventListener( 'removed', function ( event ) { 18 | 19 | if ( this.element.parentNode !== null ) { 20 | 21 | this.element.parentNode.removeChild( this.element ); 22 | 23 | } 24 | 25 | } ); 26 | 27 | }; 28 | 29 | CSS2DObject.prototype = Object.create( THREE.Object3D.prototype ); 30 | CSS2DObject.prototype.constructor = CSS2DObject; 31 | 32 | // 33 | 34 | var CSS2DRenderer = function () { 35 | 36 | console.log( 'THREE.CSS2DRenderer', THREE.REVISION ); 37 | 38 | var _width, _height; 39 | var _widthHalf, _heightHalf; 40 | 41 | var vector = new THREE.Vector3(); 42 | var viewMatrix = new THREE.Matrix4(); 43 | var viewProjectionMatrix = new THREE.Matrix4(); 44 | 45 | var frustum = new THREE.Frustum(); 46 | 47 | var domElement = document.createElement( 'div' ); 48 | domElement.style.overflow = 'hidden'; 49 | 50 | this.domElement = domElement; 51 | 52 | this.setSize = function ( width, height ) { 53 | 54 | _width = width; 55 | _height = height; 56 | 57 | _widthHalf = _width / 2; 58 | _heightHalf = _height / 2; 59 | 60 | domElement.style.width = width + 'px'; 61 | domElement.style.height = height + 'px'; 62 | 63 | }; 64 | 65 | var renderObject = function ( object, camera ) { 66 | 67 | if ( object instanceof CSS2DObject ) { 68 | 69 | vector.setFromMatrixPosition( object.matrixWorld ); 70 | vector.applyProjection( viewProjectionMatrix ); 71 | 72 | var element = object.element; 73 | var style = 'translate(-50%,-50%) translate(' + ( vector.x * _widthHalf + _widthHalf ) + 'px,' + ( - vector.y * _heightHalf + _heightHalf ) + 'px)'; 74 | 75 | element.style.WebkitTransform = style; 76 | element.style.MozTransform = style; 77 | element.style.oTransform = style; 78 | element.style.transform = style; 79 | 80 | if ( element.parentNode !== domElement ) { 81 | 82 | domElement.appendChild( element ); 83 | 84 | } 85 | 86 | // Hide if outside view frustum 87 | if (!frustum.containsPoint(object.position)) { 88 | element.style.display = 'none'; 89 | } else { 90 | element.style.display = 'block'; 91 | } 92 | 93 | } 94 | 95 | for ( var i = 0, l = object.children.length; i < l; i ++ ) { 96 | 97 | renderObject( object.children[ i ], camera ); 98 | 99 | } 100 | 101 | }; 102 | 103 | this.render = function ( scene, camera ) { 104 | 105 | scene.updateMatrixWorld(); 106 | 107 | if ( camera.parent === null ) camera.updateMatrixWorld(); 108 | 109 | camera.matrixWorldInverse.getInverse( camera.matrixWorld ); 110 | 111 | viewMatrix.copy( camera.matrixWorldInverse.getInverse( camera.matrixWorld ) ); 112 | viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, viewMatrix ); 113 | 114 | frustum.setFromMatrix( new THREE.Matrix4().multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ) ); 115 | 116 | renderObject( scene, camera ); 117 | 118 | }; 119 | 120 | }; 121 | 122 | export {CSS2DObject as CSS2DObject}; 123 | export {CSS2DRenderer as CSS2DRenderer}; 124 | 125 | THREE.CSS2DObject = CSS2DObject; 126 | THREE.CSS2DRenderer = CSS2DRenderer; 127 | -------------------------------------------------------------------------------- /src/vendor/EffectComposer.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /* eslint-disable */ 3 | 4 | import THREE from 'three'; 5 | import CopyShader from './CopyShader'; 6 | import ShaderPass from './ShaderPass'; 7 | import MaskPass, {ClearMaskPass} from './MaskPass'; 8 | 9 | /** 10 | * @author alteredq / http://alteredqualia.com/ 11 | */ 12 | 13 | var EffectComposer = function ( renderer, renderTarget ) { 14 | 15 | this.renderer = renderer; 16 | 17 | if ( renderTarget === undefined ) { 18 | 19 | var pixelRatio = renderer.getPixelRatio(); 20 | 21 | var width = Math.floor( renderer.context.canvas.width / pixelRatio ) || 1; 22 | var height = Math.floor( renderer.context.canvas.height / pixelRatio ) || 1; 23 | var parameters = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, stencilBuffer: false }; 24 | 25 | renderTarget = new THREE.WebGLRenderTarget( width, height, parameters ); 26 | 27 | } 28 | 29 | this.renderTarget1 = renderTarget; 30 | this.renderTarget2 = renderTarget.clone(); 31 | 32 | this.writeBuffer = this.renderTarget1; 33 | this.readBuffer = this.renderTarget2; 34 | 35 | this.passes = []; 36 | 37 | if ( CopyShader === undefined ) 38 | console.error( "EffectComposer relies on THREE.CopyShader" ); 39 | 40 | this.copyPass = new ShaderPass( CopyShader ); 41 | 42 | }; 43 | 44 | EffectComposer.prototype = { 45 | 46 | swapBuffers: function() { 47 | 48 | var tmp = this.readBuffer; 49 | this.readBuffer = this.writeBuffer; 50 | this.writeBuffer = tmp; 51 | 52 | }, 53 | 54 | addPass: function ( pass ) { 55 | 56 | this.passes.push( pass ); 57 | 58 | }, 59 | 60 | insertPass: function ( pass, index ) { 61 | 62 | this.passes.splice( index, 0, pass ); 63 | 64 | }, 65 | 66 | render: function ( delta ) { 67 | 68 | this.writeBuffer = this.renderTarget1; 69 | this.readBuffer = this.renderTarget2; 70 | 71 | var maskActive = false; 72 | 73 | var pass, i, il = this.passes.length; 74 | 75 | for ( i = 0; i < il; i ++ ) { 76 | 77 | pass = this.passes[ i ]; 78 | 79 | if ( ! pass.enabled ) continue; 80 | 81 | pass.render( this.renderer, this.writeBuffer, this.readBuffer, delta, maskActive ); 82 | 83 | if ( pass.needsSwap ) { 84 | 85 | if ( maskActive ) { 86 | 87 | var context = this.renderer.context; 88 | 89 | context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff ); 90 | 91 | this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, delta ); 92 | 93 | context.stencilFunc( context.EQUAL, 1, 0xffffffff ); 94 | 95 | } 96 | 97 | this.swapBuffers(); 98 | 99 | } 100 | 101 | if ( pass instanceof MaskPass ) { 102 | 103 | maskActive = true; 104 | 105 | } else if ( pass instanceof ClearMaskPass ) { 106 | 107 | maskActive = false; 108 | 109 | } 110 | 111 | } 112 | 113 | }, 114 | 115 | reset: function ( renderTarget ) { 116 | 117 | if ( renderTarget === undefined ) { 118 | 119 | renderTarget = this.renderTarget1.clone(); 120 | 121 | var pixelRatio = this.renderer.getPixelRatio(); 122 | 123 | renderTarget.setSize( 124 | Math.floor( this.renderer.context.canvas.width / pixelRatio ), 125 | Math.floor( this.renderer.context.canvas.height / pixelRatio ) 126 | ); 127 | 128 | } 129 | 130 | this.renderTarget1.dispose(); 131 | this.renderTarget1 = renderTarget; 132 | this.renderTarget2.dispose(); 133 | this.renderTarget2 = renderTarget.clone(); 134 | 135 | this.writeBuffer = this.renderTarget1; 136 | this.readBuffer = this.renderTarget2; 137 | 138 | }, 139 | 140 | setSize: function ( width, height ) { 141 | 142 | this.renderTarget1.setSize( width, height ); 143 | this.renderTarget2.setSize( width, height ); 144 | 145 | } 146 | 147 | }; 148 | 149 | export default EffectComposer; 150 | THREE.EffectComposer = EffectComposer; 151 | -------------------------------------------------------------------------------- /src/layer/tile/GeoJSONTileLayer.js: -------------------------------------------------------------------------------- 1 | import TileLayer from './TileLayer'; 2 | import extend from 'lodash.assign'; 3 | import GeoJSONTile from './GeoJSONTile'; 4 | import throttle from 'lodash.throttle'; 5 | import THREE from 'three'; 6 | 7 | // TODO: Offer on-the-fly slicing of static, non-tile-based GeoJSON files into a 8 | // tile grid using geojson-vt 9 | // 10 | // See: https://github.com/mapbox/geojson-vt 11 | 12 | // TODO: Make sure nothing is left behind in the heap after calling destroy() 13 | 14 | // TODO: Consider pausing per-frame output during movement so there's little to 15 | // no jank caused by previous tiles still processing 16 | 17 | // This tile layer only updates the quadtree after world movement has occurred 18 | // 19 | // Tiles from previous quadtree updates are updated and outputted every frame 20 | // (or at least every frame, throttled to some amount) 21 | // 22 | // This is because the complexity of TopoJSON tiles requires a lot of processing 23 | // and so makes movement janky if updates occur every frame – only updating 24 | // after movement means frame drops are less obvious due to heavy processing 25 | // occurring while the view is generally stationary 26 | // 27 | // The downside is that until new tiles are requested and outputted you will 28 | // see blank spaces as you orbit and move around 29 | // 30 | // An added benefit is that it dramatically reduces the number of tiles being 31 | // requested over a period of time and the time it takes to go from request to 32 | // screen output 33 | // 34 | // It may be possible to perform these updates per-frame once Web Worker 35 | // processing is added 36 | 37 | class GeoJSONTileLayer extends TileLayer { 38 | constructor(path, options) { 39 | var defaults = { 40 | maxLOD: 14, 41 | distance: 30000 42 | }; 43 | 44 | options = extend({}, defaults, options); 45 | 46 | super(options); 47 | 48 | this._path = path; 49 | } 50 | 51 | _onAdd(world) { 52 | super._onAdd(world); 53 | 54 | // Trigger initial quadtree calculation on the next frame 55 | // 56 | // TODO: This is a hack to ensure the camera is all set up - a better 57 | // solution should be found 58 | setTimeout(() => { 59 | this._calculateLOD(); 60 | this._initEvents(); 61 | }, 0); 62 | } 63 | 64 | _initEvents() { 65 | // Run LOD calculations based on render calls 66 | // 67 | // Throttled to 1 LOD calculation per 100ms 68 | this._throttledWorldUpdate = throttle(this._onWorldUpdate, 100); 69 | 70 | this._world.on('preUpdate', this._throttledWorldUpdate, this); 71 | this._world.on('move', this._onWorldMove, this); 72 | this._world.on('controlsMove', this._onControlsMove, this); 73 | } 74 | 75 | // Update and output tiles each frame (throttled) 76 | _onWorldUpdate() { 77 | if (this._pauseOutput) { 78 | return; 79 | } 80 | 81 | this._outputTiles(); 82 | } 83 | 84 | // Update tiles grid after world move, but don't output them 85 | _onWorldMove(latlon, point) { 86 | this._pauseOutput = false; 87 | this._calculateLOD(); 88 | } 89 | 90 | // Pause updates during control movement for less visual jank 91 | _onControlsMove() { 92 | this._pauseOutput = true; 93 | } 94 | 95 | _createTile(quadcode, layer) { 96 | var options = {}; 97 | 98 | // if (this._options.filter) { 99 | // options.filter = this._options.filter; 100 | // } 101 | // 102 | // if (this._options.style) { 103 | // options.style = this._options.style; 104 | // } 105 | // 106 | // if (this._options.topojson) { 107 | // options.topojson = true; 108 | // } 109 | // 110 | // if (this._options.interactive) { 111 | // options.interactive = true; 112 | // } 113 | // 114 | // if (this._options.onClick) { 115 | // options.onClick = this._options.onClick; 116 | // } 117 | 118 | return new GeoJSONTile(quadcode, this._path, layer, this._options); 119 | } 120 | 121 | // Destroys the layer and removes it from the scene and memory 122 | destroy() { 123 | this._world.off('preUpdate', this._throttledWorldUpdate); 124 | this._world.off('move', this._onWorldMove); 125 | 126 | this._throttledWorldUpdate = null; 127 | 128 | // Run common destruction logic from parent 129 | super.destroy(); 130 | } 131 | } 132 | 133 | export default GeoJSONTileLayer; 134 | 135 | var noNew = function(path, options) { 136 | return new GeoJSONTileLayer(path, options); 137 | }; 138 | 139 | // Initialise without requiring new keyword 140 | export {noNew as geoJSONTileLayer}; 141 | -------------------------------------------------------------------------------- /src/layer/environment/EnvironmentLayer.js: -------------------------------------------------------------------------------- 1 | import Layer from '../Layer'; 2 | import extend from 'lodash.assign'; 3 | import THREE from 'three'; 4 | import Skybox from './Skybox'; 5 | 6 | // TODO: Make sure nothing is left behind in the heap after calling destroy() 7 | 8 | class EnvironmentLayer extends Layer { 9 | constructor(options) { 10 | var defaults = { 11 | skybox: false 12 | }; 13 | 14 | var _options = extend({}, defaults, options); 15 | 16 | super(_options); 17 | } 18 | 19 | _onAdd() { 20 | this._initLights(); 21 | 22 | if (this._options.skybox) { 23 | this._initSkybox(); 24 | } 25 | 26 | // this._initGrid(); 27 | } 28 | 29 | // Not fleshed out or thought through yet 30 | // 31 | // Lights could potentially be put it their own 'layer' to keep this class 32 | // much simpler and less messy 33 | _initLights() { 34 | // Position doesn't really matter (the angle is important), however it's 35 | // used here so the helpers look more natural. 36 | 37 | if (!this._options.skybox) { 38 | var directionalLight = new THREE.DirectionalLight(0xffffff, 1); 39 | directionalLight.position.x = 10000; 40 | directionalLight.position.y = 10000; 41 | directionalLight.position.z = 10000; 42 | 43 | // TODO: Get shadows working in non-PBR scenes 44 | 45 | // directionalLight.castShadow = true; 46 | // 47 | // var d = 100; 48 | // directionalLight.shadow.camera.left = -d; 49 | // directionalLight.shadow.camera.right = d; 50 | // directionalLight.shadow.camera.top = d; 51 | // directionalLight.shadow.camera.bottom = -d; 52 | // 53 | // directionalLight.shadow.camera.near = 10; 54 | // directionalLight.shadow.camera.far = 100; 55 | // 56 | // // TODO: Need to dial in on a good shadowmap size 57 | // directionalLight.shadow.mapSize.width = 2048; 58 | // directionalLight.shadow.mapSize.height = 2048; 59 | // 60 | // // directionalLight.shadowBias = -0.0010; 61 | // // directionalLight.shadow.darkness = 0.15; 62 | 63 | var directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.5); 64 | directionalLight2.position.x = -10000; 65 | directionalLight2.position.y = 10000; 66 | directionalLight2.position.z = 0; 67 | 68 | var directionalLight3 = new THREE.DirectionalLight(0xffffff, 0.5); 69 | directionalLight3.position.x = 10000; 70 | directionalLight3.position.y = 10000; 71 | directionalLight3.position.z = -10000; 72 | 73 | this.add(directionalLight); 74 | this.add(directionalLight2); 75 | this.add(directionalLight3); 76 | 77 | // var helper = new THREE.DirectionalLightHelper(directionalLight, 10); 78 | // var helper2 = new THREE.DirectionalLightHelper(directionalLight2, 10); 79 | // var helper3 = new THREE.DirectionalLightHelper(directionalLight3, 10); 80 | // 81 | // this.add(helper); 82 | // this.add(helper2); 83 | // this.add(helper3); 84 | } else { 85 | // Directional light that will be projected from the sun 86 | this._skyboxLight = new THREE.DirectionalLight(0xffffff, 1); 87 | 88 | this._skyboxLight.castShadow = true; 89 | 90 | var d = 10000; 91 | this._skyboxLight.shadow.camera.left = -d; 92 | this._skyboxLight.shadow.camera.right = d; 93 | this._skyboxLight.shadow.camera.top = d; 94 | this._skyboxLight.shadow.camera.bottom = -d; 95 | 96 | this._skyboxLight.shadow.camera.near = 10000; 97 | this._skyboxLight.shadow.camera.far = 70000; 98 | 99 | // TODO: Need to dial in on a good shadowmap size 100 | this._skyboxLight.shadow.mapSize.width = 2048; 101 | this._skyboxLight.shadow.mapSize.height = 2048; 102 | 103 | // this._skyboxLight.shadowBias = -0.0010; 104 | // this._skyboxLight.shadow.darkness = 0.15; 105 | 106 | // this._object3D.add(new THREE.CameraHelper(this._skyboxLight.shadow.camera)); 107 | 108 | this.add(this._skyboxLight); 109 | } 110 | } 111 | 112 | _initSkybox() { 113 | this._skybox = new Skybox(this._world, this._skyboxLight); 114 | this.add(this._skybox._mesh); 115 | } 116 | 117 | // Add grid helper for context during initial development 118 | _initGrid() { 119 | var size = 4000; 120 | var step = 100; 121 | 122 | var gridHelper = new THREE.GridHelper(size, step); 123 | this.add(gridHelper); 124 | } 125 | 126 | // Clean up environment 127 | destroy() { 128 | this._skyboxLight = null; 129 | 130 | this.remove(this._skybox._mesh); 131 | this._skybox.destroy(); 132 | this._skybox = null; 133 | 134 | super.destroy(); 135 | } 136 | } 137 | 138 | export default EnvironmentLayer; 139 | 140 | var noNew = function(options) { 141 | return new EnvironmentLayer(options); 142 | }; 143 | 144 | // Initialise without requiring new keyword 145 | export {noNew as environmentLayer}; 146 | -------------------------------------------------------------------------------- /src/layer/Layer.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import extend from 'lodash.assign'; 3 | import THREE from 'three'; 4 | import Scene from '../engine/Scene'; 5 | import {CSS3DObject} from '../vendor/CSS3DRenderer'; 6 | import {CSS2DObject} from '../vendor/CSS2DRenderer'; 7 | 8 | // TODO: Make sure nothing is left behind in the heap after calling destroy() 9 | 10 | // TODO: Need a single move method that handles moving all the various object 11 | // layers so that the DOM layers stay in sync with the 3D layer 12 | 13 | // TODO: Double check that objects within the _object3D Object3D parent are frustum 14 | // culled even if the layer position stays at the default (0,0,0) and the child 15 | // objects are positioned much further away 16 | // 17 | // Or does the layer being at (0,0,0) prevent the child objects from being 18 | // culled because the layer parent is effectively always in view even if the 19 | // child is actually out of camera 20 | 21 | class Layer extends EventEmitter { 22 | constructor(options) { 23 | super(); 24 | 25 | var defaults = { 26 | output: true, 27 | outputToScene: true 28 | }; 29 | 30 | this._options = extend({}, defaults, options); 31 | 32 | if (this.isOutput()) { 33 | this._object3D = new THREE.Object3D(); 34 | 35 | this._dom3D = document.createElement('div'); 36 | this._domObject3D = new CSS3DObject(this._dom3D); 37 | 38 | this._dom2D = document.createElement('div'); 39 | this._domObject2D = new CSS2DObject(this._dom2D); 40 | } 41 | } 42 | 43 | // Add THREE object directly to layer 44 | add(object) { 45 | this._object3D.add(object); 46 | } 47 | 48 | // Remove THREE object from to layer 49 | remove(object) { 50 | this._object3D.remove(object); 51 | } 52 | 53 | addDOM3D(object) { 54 | this._domObject3D.add(object); 55 | } 56 | 57 | removeDOM3D(object) { 58 | this._domObject3D.remove(object); 59 | } 60 | 61 | addDOM2D(object) { 62 | this._domObject2D.add(object); 63 | } 64 | 65 | removeDOM2D(object) { 66 | this._domObject2D.remove(object); 67 | } 68 | 69 | // Add layer to world instance and store world reference 70 | addTo(world) { 71 | world.addLayer(this); 72 | return this; 73 | } 74 | 75 | // Internal method called by World.addLayer to actually add the layer 76 | _addToWorld(world) { 77 | this._world = world; 78 | this._onAdd(world); 79 | this.emit('added'); 80 | } 81 | 82 | _onAdd(world) {} 83 | 84 | getPickingId() { 85 | if (this._world._engine._picking) { 86 | return this._world._engine._picking.getNextId(); 87 | } 88 | 89 | return false; 90 | } 91 | 92 | // TODO: Tidy this up and don't access so many private properties to work 93 | addToPicking(object) { 94 | if (!this._world._engine._picking) { 95 | return; 96 | } 97 | 98 | this._world._engine._picking.add(object); 99 | } 100 | 101 | removeFromPicking(object) { 102 | if (!this._world._engine._picking) { 103 | return; 104 | } 105 | 106 | this._world._engine._picking.remove(object); 107 | } 108 | 109 | isOutput() { 110 | return this._options.output; 111 | } 112 | 113 | isOutputToScene() { 114 | return this._options.outputToScene; 115 | } 116 | 117 | // Destroys the layer and removes it from the scene and memory 118 | destroy() { 119 | if (this._object3D && this._object3D.children) { 120 | // Remove everything else in the layer 121 | var child; 122 | for (var i = this._object3D.children.length - 1; i >= 0; i--) { 123 | child = this._object3D.children[i]; 124 | 125 | if (!child) { 126 | continue; 127 | } 128 | 129 | this.remove(child); 130 | 131 | if (child.geometry) { 132 | // Dispose of mesh and materials 133 | child.geometry.dispose(); 134 | child.geometry = null; 135 | } 136 | 137 | if (child.material) { 138 | if (child.material.map) { 139 | child.material.map.dispose(); 140 | child.material.map = null; 141 | } 142 | 143 | child.material.dispose(); 144 | child.material = null; 145 | } 146 | } 147 | } 148 | 149 | if (this._domObject3D && this._domObject3D.children) { 150 | // Remove everything else in the layer 151 | var child; 152 | for (var i = this._domObject3D.children.length - 1; i >= 0; i--) { 153 | child = this._domObject3D.children[i]; 154 | 155 | if (!child) { 156 | continue; 157 | } 158 | 159 | this.removeDOM3D(child); 160 | } 161 | } 162 | 163 | if (this._domObject2D && this._domObject2D.children) { 164 | // Remove everything else in the layer 165 | var child; 166 | for (var i = this._domObject2D.children.length - 1; i >= 0; i--) { 167 | child = this._domObject2D.children[i]; 168 | 169 | if (!child) { 170 | continue; 171 | } 172 | 173 | this.removeDOM2D(child); 174 | } 175 | } 176 | 177 | this._domObject3D = null; 178 | this._domObject2D = null; 179 | 180 | this._world = null; 181 | this._object3D = null; 182 | } 183 | } 184 | 185 | export default Layer; 186 | 187 | var noNew = function(options) { 188 | return new Layer(options); 189 | }; 190 | 191 | export {noNew as layer}; 192 | -------------------------------------------------------------------------------- /src/layer/tile/ImageTile.js: -------------------------------------------------------------------------------- 1 | import Tile from './Tile'; 2 | import BoxHelper from '../../vendor/BoxHelper'; 3 | import THREE from 'three'; 4 | 5 | // TODO: Make sure nothing is left behind in the heap after calling destroy() 6 | 7 | class ImageTile extends Tile { 8 | constructor(quadcode, path, layer) { 9 | super(quadcode, path, layer); 10 | } 11 | 12 | // Request data for the tile 13 | requestTileAsync() { 14 | // Making this asynchronous really speeds up the LOD framerate 15 | setTimeout(() => { 16 | if (!this._mesh) { 17 | this._mesh = this._createMesh(); 18 | this._requestTile(); 19 | } 20 | }, 0); 21 | } 22 | 23 | destroy() { 24 | // Cancel any pending requests 25 | this._abortRequest(); 26 | 27 | // Clear image reference 28 | this._image = null; 29 | 30 | super.destroy(); 31 | } 32 | 33 | _createMesh() { 34 | // Something went wrong and the tile 35 | // 36 | // Possibly removed by the cache before loaded 37 | if (!this._center) { 38 | return; 39 | } 40 | 41 | var mesh = new THREE.Object3D(); 42 | var geom = new THREE.PlaneBufferGeometry(this._side, this._side, 1); 43 | 44 | var material; 45 | if (!this._world._environment._skybox) { 46 | material = new THREE.MeshBasicMaterial({ 47 | depthWrite: false 48 | }); 49 | 50 | // var material = new THREE.MeshPhongMaterial({ 51 | // depthWrite: false 52 | // }); 53 | } else { 54 | // Other MeshStandardMaterial settings 55 | // 56 | // material.envMapIntensity will change the amount of colour reflected(?) 57 | // from the environment map – can be greater than 1 for more intensity 58 | 59 | material = new THREE.MeshStandardMaterial({ 60 | depthWrite: false 61 | }); 62 | material.roughness = 1; 63 | material.metalness = 0.1; 64 | material.envMap = this._world._environment._skybox.getRenderTarget(); 65 | } 66 | 67 | var localMesh = new THREE.Mesh(geom, material); 68 | localMesh.rotation.x = -90 * Math.PI / 180; 69 | 70 | localMesh.receiveShadow = true; 71 | 72 | mesh.add(localMesh); 73 | mesh.renderOrder = 0.1; 74 | 75 | mesh.position.x = this._center[0]; 76 | mesh.position.z = this._center[1]; 77 | 78 | // var box = new BoxHelper(localMesh); 79 | // mesh.add(box); 80 | // 81 | // mesh.add(this._createDebugMesh()); 82 | 83 | return mesh; 84 | } 85 | 86 | _createDebugMesh() { 87 | var canvas = document.createElement('canvas'); 88 | canvas.width = 256; 89 | canvas.height = 256; 90 | 91 | var context = canvas.getContext('2d'); 92 | context.font = 'Bold 20px Helvetica Neue, Verdana, Arial'; 93 | context.fillStyle = '#ff0000'; 94 | context.fillText(this._quadcode, 20, canvas.width / 2 - 5); 95 | context.fillText(this._tile.toString(), 20, canvas.width / 2 + 25); 96 | 97 | var texture = new THREE.Texture(canvas); 98 | 99 | // Silky smooth images when tilted 100 | texture.magFilter = THREE.LinearFilter; 101 | texture.minFilter = THREE.LinearMipMapLinearFilter; 102 | 103 | // TODO: Set this to renderer.getMaxAnisotropy() / 4 104 | texture.anisotropy = 4; 105 | 106 | texture.needsUpdate = true; 107 | 108 | var material = new THREE.MeshBasicMaterial({ 109 | map: texture, 110 | transparent: true, 111 | depthWrite: false 112 | }); 113 | 114 | var geom = new THREE.PlaneBufferGeometry(this._side, this._side, 1); 115 | var mesh = new THREE.Mesh(geom, material); 116 | 117 | mesh.rotation.x = -90 * Math.PI / 180; 118 | mesh.position.y = 0.1; 119 | 120 | return mesh; 121 | } 122 | 123 | _requestTile() { 124 | var urlParams = { 125 | x: this._tile[0], 126 | y: this._tile[1], 127 | z: this._tile[2] 128 | }; 129 | 130 | var url = this._getTileURL(urlParams); 131 | 132 | var image = document.createElement('img'); 133 | 134 | image.addEventListener('load', event => { 135 | var texture = new THREE.Texture(); 136 | 137 | texture.image = image; 138 | texture.needsUpdate = true; 139 | 140 | // Silky smooth images when tilted 141 | texture.magFilter = THREE.LinearFilter; 142 | texture.minFilter = THREE.LinearMipMapLinearFilter; 143 | 144 | // TODO: Set this to renderer.getMaxAnisotropy() / 4 145 | texture.anisotropy = 4; 146 | 147 | texture.needsUpdate = true; 148 | 149 | // Something went wrong and the tile or its material is missing 150 | // 151 | // Possibly removed by the cache before the image loaded 152 | if (!this._mesh || !this._mesh.children[0] || !this._mesh.children[0].material) { 153 | return; 154 | } 155 | 156 | this._mesh.children[0].material.map = texture; 157 | this._mesh.children[0].material.needsUpdate = true; 158 | 159 | this._texture = texture; 160 | this._ready = true; 161 | }, false); 162 | 163 | // image.addEventListener('progress', event => {}, false); 164 | // image.addEventListener('error', event => {}, false); 165 | 166 | image.crossOrigin = ''; 167 | 168 | // Load image 169 | image.src = url; 170 | 171 | this._image = image; 172 | } 173 | 174 | _abortRequest() { 175 | if (!this._image) { 176 | return; 177 | } 178 | 179 | this._image.src = ''; 180 | } 181 | } 182 | 183 | export default ImageTile; 184 | 185 | var noNew = function(quadcode, path, layer) { 186 | return new ImageTile(quadcode, path, layer); 187 | }; 188 | 189 | // Initialise without requiring new keyword 190 | export {noNew as imageTile}; 191 | -------------------------------------------------------------------------------- /src/layer/tile/Tile.js: -------------------------------------------------------------------------------- 1 | import {point as Point} from '../../geo/Point'; 2 | import {latLon as LatLon} from '../../geo/LatLon'; 3 | import THREE from 'three'; 4 | 5 | // TODO: Make sure nothing is left behind in the heap after calling destroy() 6 | 7 | // Manages a single tile and its layers 8 | 9 | var r2d = 180 / Math.PI; 10 | 11 | var tileURLRegex = /\{([szxy])\}/g; 12 | 13 | class Tile { 14 | constructor(quadcode, path, layer) { 15 | this._layer = layer; 16 | this._world = layer._world; 17 | this._quadcode = quadcode; 18 | this._path = path; 19 | 20 | this._ready = false; 21 | 22 | this._tile = this._quadcodeToTile(quadcode); 23 | 24 | // Bottom-left and top-right bounds in WGS84 coordinates 25 | this._boundsLatLon = this._tileBoundsWGS84(this._tile); 26 | 27 | // Bottom-left and top-right bounds in world coordinates 28 | this._boundsWorld = this._tileBoundsFromWGS84(this._boundsLatLon); 29 | 30 | // Tile center in world coordinates 31 | this._center = this._boundsToCenter(this._boundsWorld); 32 | 33 | // Tile center in projected coordinates 34 | this._centerLatlon = this._world.pointToLatLon(Point(this._center[0], this._center[1])); 35 | 36 | // Length of a tile side in world coorindates 37 | this._side = this._getSide(this._boundsWorld); 38 | 39 | // Point scale for tile (for unit conversion) 40 | this._pointScale = this._world.pointScale(this._centerLatlon); 41 | } 42 | 43 | // Returns true if the tile mesh and texture are ready to be used 44 | // Otherwise, returns false 45 | isReady() { 46 | return this._ready; 47 | } 48 | 49 | // Request data for the tile 50 | requestTileAsync() {} 51 | 52 | getQuadcode() { 53 | return this._quadcode; 54 | } 55 | 56 | getBounds() { 57 | return this._boundsWorld; 58 | } 59 | 60 | getCenter() { 61 | return this._center; 62 | } 63 | 64 | getSide() { 65 | return this._side; 66 | } 67 | 68 | getMesh() { 69 | return this._mesh; 70 | } 71 | 72 | getPickingMesh() { 73 | return this._pickingMesh; 74 | } 75 | 76 | // Destroys the tile and removes it from the layer and memory 77 | // 78 | // Ensure that this leaves no trace of the tile – no textures, no meshes, 79 | // nothing in memory or the GPU 80 | destroy() { 81 | // Delete reference to layer and world 82 | this._layer = null; 83 | this._world = null; 84 | 85 | // Delete location references 86 | this._boundsLatLon = null; 87 | this._boundsWorld = null; 88 | this._center = null; 89 | 90 | // Done if no mesh 91 | if (!this._mesh) { 92 | return; 93 | } 94 | 95 | if (this._mesh.children) { 96 | // Dispose of mesh and materials 97 | this._mesh.children.forEach(child => { 98 | child.geometry.dispose(); 99 | child.geometry = null; 100 | 101 | if (child.material.map) { 102 | child.material.map.dispose(); 103 | child.material.map = null; 104 | } 105 | 106 | child.material.dispose(); 107 | child.material = null; 108 | }); 109 | } else { 110 | this._mesh.geometry.dispose(); 111 | this._mesh.geometry = null; 112 | 113 | if (this._mesh.material.map) { 114 | this._mesh.material.map.dispose(); 115 | this._mesh.material.map = null; 116 | } 117 | 118 | this._mesh.material.dispose(); 119 | this._mesh.material = null; 120 | } 121 | } 122 | 123 | _createMesh() {} 124 | _createDebugMesh() {} 125 | 126 | _getTileURL(urlParams) { 127 | if (!urlParams.s) { 128 | // Default to a random choice of a, b or c 129 | urlParams.s = String.fromCharCode(97 + Math.floor(Math.random() * 3)); 130 | } 131 | 132 | tileURLRegex.lastIndex = 0; 133 | return this._path.replace(tileURLRegex, function(value, key) { 134 | // Replace with paramter, otherwise keep existing value 135 | return urlParams[key]; 136 | }); 137 | } 138 | 139 | // Convert from quadcode to TMS tile coordinates 140 | _quadcodeToTile(quadcode) { 141 | var x = 0; 142 | var y = 0; 143 | var z = quadcode.length; 144 | 145 | for (var i = z; i > 0; i--) { 146 | var mask = 1 << (i - 1); 147 | var q = +quadcode[z - i]; 148 | if (q === 1) { 149 | x |= mask; 150 | } 151 | if (q === 2) { 152 | y |= mask; 153 | } 154 | if (q === 3) { 155 | x |= mask; 156 | y |= mask; 157 | } 158 | } 159 | 160 | return [x, y, z]; 161 | } 162 | 163 | // Convert WGS84 tile bounds to world coordinates 164 | _tileBoundsFromWGS84(boundsWGS84) { 165 | var sw = this._layer._world.latLonToPoint(LatLon(boundsWGS84[1], boundsWGS84[0])); 166 | var ne = this._layer._world.latLonToPoint(LatLon(boundsWGS84[3], boundsWGS84[2])); 167 | 168 | return [sw.x, sw.y, ne.x, ne.y]; 169 | } 170 | 171 | // Get tile bounds in WGS84 coordinates 172 | _tileBoundsWGS84(tile) { 173 | var e = this._tile2lon(tile[0] + 1, tile[2]); 174 | var w = this._tile2lon(tile[0], tile[2]); 175 | var s = this._tile2lat(tile[1] + 1, tile[2]); 176 | var n = this._tile2lat(tile[1], tile[2]); 177 | return [w, s, e, n]; 178 | } 179 | 180 | _tile2lon(x, z) { 181 | return x / Math.pow(2, z) * 360 - 180; 182 | } 183 | 184 | _tile2lat(y, z) { 185 | var n = Math.PI - 2 * Math.PI * y / Math.pow(2, z); 186 | return r2d * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); 187 | } 188 | 189 | _boundsToCenter(bounds) { 190 | var x = bounds[0] + (bounds[2] - bounds[0]) / 2; 191 | var y = bounds[1] + (bounds[3] - bounds[1]) / 2; 192 | 193 | return [x, y]; 194 | } 195 | 196 | _getSide(bounds) { 197 | return (new THREE.Vector3(bounds[0], 0, bounds[3])).sub(new THREE.Vector3(bounds[0], 0, bounds[1])).length(); 198 | } 199 | } 200 | 201 | export default Tile; 202 | -------------------------------------------------------------------------------- /src/geo/Geo.js: -------------------------------------------------------------------------------- 1 | import {latLon as LatLon} from './LatLon'; 2 | import {point as Point} from './Point'; 3 | 4 | var Geo = {}; 5 | 6 | // Radius / WGS84 semi-major axis 7 | Geo.R = 6378137; 8 | Geo.MAX_LATITUDE = 85.0511287798; 9 | 10 | // WGS84 eccentricity 11 | Geo.ECC = 0.081819191; 12 | Geo.ECC2 = 0.081819191 * 0.081819191; 13 | 14 | Geo.project = function(latlon) { 15 | var d = Math.PI / 180; 16 | var max = Geo.MAX_LATITUDE; 17 | var lat = Math.max(Math.min(max, latlon.lat), -max); 18 | var sin = Math.sin(lat * d); 19 | 20 | return Point( 21 | Geo.R * latlon.lon * d, 22 | Geo.R * Math.log((1 + sin) / (1 - sin)) / 2 23 | ); 24 | }, 25 | 26 | Geo.unproject = function(point) { 27 | var d = 180 / Math.PI; 28 | 29 | return LatLon( 30 | (2 * Math.atan(Math.exp(point.y / Geo.R)) - (Math.PI / 2)) * d, 31 | point.x * d / Geo.R 32 | ); 33 | }; 34 | 35 | // Converts geo coords to pixel / WebGL ones 36 | // This just reverses the Y axis to match WebGL 37 | Geo.latLonToPoint = function(latlon) { 38 | var projected = Geo.project(latlon); 39 | projected.y *= -1; 40 | 41 | return projected; 42 | }; 43 | 44 | // Converts pixel / WebGL coords to geo coords 45 | // This just reverses the Y axis to match WebGL 46 | Geo.pointToLatLon = function(point) { 47 | var _point = Point(point.x, point.y * -1); 48 | return Geo.unproject(_point); 49 | }; 50 | 51 | // Scale factor for converting between real metres and projected metres 52 | // 53 | // projectedMetres = realMetres * pointScale 54 | // realMetres = projectedMetres / pointScale 55 | // 56 | // Accurate scale factor uses proper Web Mercator scaling 57 | // See pg.9: http://www.hydrometronics.com/downloads/Web%20Mercator%20-%20Non-Conformal,%20Non-Mercator%20(notes).pdf 58 | // See: http://jsfiddle.net/robhawkes/yws924cf/ 59 | Geo.pointScale = function(latlon, accurate) { 60 | var rad = Math.PI / 180; 61 | 62 | var k; 63 | 64 | if (!accurate) { 65 | k = 1 / Math.cos(latlon.lat * rad); 66 | 67 | // [scaleX, scaleY] 68 | return [k, k]; 69 | } else { 70 | var lat = latlon.lat * rad; 71 | var lon = latlon.lon * rad; 72 | 73 | var a = Geo.R; 74 | 75 | var sinLat = Math.sin(lat); 76 | var sinLat2 = sinLat * sinLat; 77 | 78 | var cosLat = Math.cos(lat); 79 | 80 | // Radius meridian 81 | var p = a * (1 - Geo.ECC2) / Math.pow(1 - Geo.ECC2 * sinLat2, 3 / 2); 82 | 83 | // Radius prime meridian 84 | var v = a / Math.sqrt(1 - Geo.ECC2 * sinLat2); 85 | 86 | // Scale N/S 87 | var h = (a / p) / cosLat; 88 | 89 | // Scale E/W 90 | k = (a / v) / cosLat; 91 | 92 | // [scaleX, scaleY] 93 | return [k, h]; 94 | } 95 | }; 96 | 97 | // Convert real metres to projected units 98 | // 99 | // Latitude scale is chosen because it fluctuates more than longitude 100 | Geo.metresToProjected = function(metres, pointScale) { 101 | return metres * pointScale[1]; 102 | }; 103 | 104 | // Convert projected units to real metres 105 | // 106 | // Latitude scale is chosen because it fluctuates more than longitude 107 | Geo.projectedToMetres = function(projectedUnits, pointScale) { 108 | return projectedUnits / pointScale[1]; 109 | }; 110 | 111 | // Convert real metres to a value in world (WebGL) units 112 | Geo.metresToWorld = function(metres, pointScale) { 113 | // Transform metres to projected metres using the latitude point scale 114 | // 115 | // Latitude scale is chosen because it fluctuates more than longitude 116 | var projectedMetres = Geo.metresToProjected(metres, pointScale); 117 | 118 | var scale = Geo.scale(); 119 | 120 | // Scale projected metres 121 | var scaledMetres = (scale * projectedMetres); 122 | 123 | return scaledMetres; 124 | }; 125 | 126 | // Convert world (WebGL) units to a value in real metres 127 | Geo.worldToMetres = function(worldUnits, pointScale) { 128 | var scale = Geo.scale(); 129 | 130 | var projectedUnits = worldUnits / scale; 131 | var realMetres = Geo.projectedToMetres(projectedUnits, pointScale); 132 | 133 | return realMetres; 134 | }; 135 | 136 | // If zoom is provided, returns the map width in pixels for a given zoom 137 | // Else, provides fixed scale value 138 | Geo.scale = function(zoom) { 139 | // If zoom is provided then return scale based on map tile zoom 140 | if (zoom >= 0) { 141 | return 256 * Math.pow(2, zoom); 142 | // Else, return fixed scale value to expand projected coordinates from 143 | // their 0 to 1 range into something more practical 144 | } else { 145 | return 1; 146 | } 147 | }; 148 | 149 | // Returns zoom level for a given scale value 150 | // This only works with a scale value that is based on map pixel width 151 | Geo.zoom = function(scale) { 152 | return Math.log(scale / 256) / Math.LN2; 153 | }; 154 | 155 | // Distance between two geographical points using spherical law of cosines 156 | // approximation or Haversine 157 | // 158 | // See: http://www.movable-type.co.uk/scripts/latlong.html 159 | Geo.distance = function(latlon1, latlon2, accurate) { 160 | var rad = Math.PI / 180; 161 | 162 | var lat1; 163 | var lat2; 164 | 165 | var a; 166 | 167 | if (!accurate) { 168 | lat1 = latlon1.lat * rad; 169 | lat2 = latlon2.lat * rad; 170 | 171 | a = Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlon2.lon - latlon1.lon) * rad); 172 | 173 | return Geo.R * Math.acos(Math.min(a, 1)); 174 | } else { 175 | lat1 = latlon1.lat * rad; 176 | lat2 = latlon2.lat * rad; 177 | 178 | var lon1 = latlon1.lon * rad; 179 | var lon2 = latlon2.lon * rad; 180 | 181 | var deltaLat = lat2 - lat1; 182 | var deltaLon = lon2 - lon1; 183 | 184 | var halfDeltaLat = deltaLat / 2; 185 | var halfDeltaLon = deltaLon / 2; 186 | 187 | a = Math.sin(halfDeltaLat) * Math.sin(halfDeltaLat) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(halfDeltaLon) * Math.sin(halfDeltaLon); 188 | 189 | var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 190 | 191 | return Geo.R * c; 192 | } 193 | }; 194 | 195 | Geo.bounds = (function() { 196 | var d = Geo.R * Math.PI; 197 | return [[-d, -d], [d, d]]; 198 | })(); 199 | 200 | export default Geo; 201 | -------------------------------------------------------------------------------- /src/engine/Engine.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import THREE from 'three'; 3 | import Scene from './Scene'; 4 | import DOMScene3D from './DOMScene3D'; 5 | import DOMScene2D from './DOMScene2D'; 6 | import Renderer from './Renderer'; 7 | import DOMRenderer3D from './DOMRenderer3D'; 8 | import DOMRenderer2D from './DOMRenderer2D'; 9 | import Camera from './Camera'; 10 | import Picking from './Picking'; 11 | import EffectComposer from './EffectComposer'; 12 | import RenderPass from '../vendor/RenderPass'; 13 | import ShaderPass from '../vendor/ShaderPass'; 14 | import CopyShader from '../vendor/CopyShader'; 15 | import HorizontalTiltShiftShader from '../vendor/HorizontalTiltShiftShader'; 16 | import VerticalTiltShiftShader from '../vendor/VerticalTiltShiftShader'; 17 | import FXAAShader from '../vendor/FXAAShader'; 18 | 19 | class Engine extends EventEmitter { 20 | constructor(container, world) { 21 | console.log('Init Engine'); 22 | 23 | super(); 24 | 25 | this._world = world; 26 | 27 | this._scene = Scene; 28 | this._domScene3D = DOMScene3D; 29 | this._domScene2D = DOMScene2D; 30 | 31 | var antialias = (this._world.options.postProcessing) ? false : true; 32 | this._renderer = Renderer(container, antialias); 33 | this._domRenderer3D = DOMRenderer3D(container); 34 | this._domRenderer2D = DOMRenderer2D(container); 35 | 36 | this._camera = Camera(container); 37 | 38 | this._container = container; 39 | 40 | // TODO: Make this optional 41 | this._picking = Picking(this._world, this._renderer, this._camera); 42 | 43 | this.clock = new THREE.Clock(); 44 | 45 | this._frustum = new THREE.Frustum(); 46 | 47 | if (this._world.options.postProcessing) { 48 | this._initPostProcessing(); 49 | } 50 | } 51 | 52 | // TODO: Set up composer to automatically resize on viewport change 53 | // TODO: Update passes that rely on width / height on resize 54 | // TODO: Merge default passes into a single shader / pass for performance 55 | _initPostProcessing() { 56 | var renderPass = new RenderPass(this._scene, this._camera); 57 | 58 | // TODO: Look at using @mattdesl's optimised FXAA shader 59 | // https://github.com/mattdesl/three-shader-fxaa 60 | var fxaaPass = new ShaderPass(FXAAShader); 61 | 62 | var hblurPass = new ShaderPass(HorizontalTiltShiftShader); 63 | var vblurPass = new ShaderPass(VerticalTiltShiftShader); 64 | var bluriness = 5; 65 | 66 | hblurPass.uniforms.r.value = vblurPass.uniforms.r.value = 0.6; 67 | 68 | var copyPass = new ShaderPass(CopyShader); 69 | copyPass.renderToScreen = true; 70 | 71 | this._composer = EffectComposer(this._renderer, this._container); 72 | 73 | this._composer.addPass(renderPass); 74 | this._composer.addPass(fxaaPass); 75 | this._composer.addPass(hblurPass); 76 | this._composer.addPass(vblurPass); 77 | this._composer.addPass(copyPass); 78 | 79 | var self = this; 80 | var updatePostProcessingSize = function() { 81 | var width = self._container.clientWidth; 82 | var height = self._container.clientHeight; 83 | 84 | // TODO: Re-enable this when perf issues can be solved 85 | // 86 | // Rendering double the resolution of the screen can be really slow 87 | // var pixelRatio = window.devicePixelRatio; 88 | var pixelRatio = 1; 89 | 90 | fxaaPass.uniforms.resolution.value.set(1 / (width * pixelRatio), 1 / (height * pixelRatio)); 91 | 92 | hblurPass.uniforms.h.value = bluriness / (width * pixelRatio); 93 | vblurPass.uniforms.v.value = bluriness / (height * pixelRatio); 94 | }; 95 | 96 | updatePostProcessingSize(); 97 | window.addEventListener('resize', updatePostProcessingSize, false); 98 | } 99 | 100 | update(delta) { 101 | this.emit('preRender'); 102 | 103 | if (this._world.options.postProcessing) { 104 | this._composer.render(delta); 105 | } else { 106 | this._renderer.render(this._scene, this._camera); 107 | } 108 | 109 | // Render picking scene 110 | // this._renderer.render(this._picking._pickingScene, this._camera); 111 | 112 | // Render DOM scenes 113 | this._domRenderer3D.render(this._domScene3D, this._camera); 114 | this._domRenderer2D.render(this._domScene2D, this._camera); 115 | 116 | this.emit('postRender'); 117 | } 118 | 119 | destroy() { 120 | // Remove any remaining objects from scene 121 | var child; 122 | for (var i = this._scene.children.length - 1; i >= 0; i--) { 123 | child = this._scene.children[i]; 124 | 125 | if (!child) { 126 | continue; 127 | } 128 | 129 | this._scene.remove(child); 130 | 131 | if (child.geometry) { 132 | // Dispose of mesh and materials 133 | child.geometry.dispose(); 134 | child.geometry = null; 135 | } 136 | 137 | if (child.material) { 138 | if (child.material.map) { 139 | child.material.map.dispose(); 140 | child.material.map = null; 141 | } 142 | 143 | child.material.dispose(); 144 | child.material = null; 145 | } 146 | }; 147 | 148 | for (var i = this._domScene3D.children.length - 1; i >= 0; i--) { 149 | child = this._domScene3D.children[i]; 150 | 151 | if (!child) { 152 | continue; 153 | } 154 | 155 | this._domScene3D.remove(child); 156 | }; 157 | 158 | for (var i = this._domScene2D.children.length - 1; i >= 0; i--) { 159 | child = this._domScene2D.children[i]; 160 | 161 | if (!child) { 162 | continue; 163 | } 164 | 165 | this._domScene2D.remove(child); 166 | }; 167 | 168 | this._picking.destroy(); 169 | this._picking = null; 170 | 171 | this._world = null; 172 | this._scene = null; 173 | this._domScene3D = null; 174 | this._domScene2D = null; 175 | 176 | this._composer = null; 177 | this._renderer = null; 178 | 179 | this._domRenderer3D = null; 180 | this._domRenderer2D = null; 181 | this._camera = null; 182 | this._clock = null; 183 | this._frustum = null; 184 | } 185 | } 186 | 187 | export default Engine; 188 | 189 | // // Initialise without requiring new keyword 190 | // export default function(container, world) { 191 | // return new Engine(container, world); 192 | // }; 193 | -------------------------------------------------------------------------------- /src/layer/tile/ImageTileLayer.js: -------------------------------------------------------------------------------- 1 | import TileLayer from './TileLayer'; 2 | import ImageTile from './ImageTile'; 3 | import ImageTileLayerBaseMaterial from './ImageTileLayerBaseMaterial'; 4 | import throttle from 'lodash.throttle'; 5 | import THREE from 'three'; 6 | import extend from 'lodash.assign'; 7 | 8 | // TODO: Make sure nothing is left behind in the heap after calling destroy() 9 | 10 | // DONE: Find a way to avoid the flashing caused by the gap between old tiles 11 | // being removed and the new tiles being ready for display 12 | // 13 | // DONE: Simplest first step for MVP would be to give each tile mesh the colour 14 | // of the basemap ground so it blends in a little more, or have a huge ground 15 | // plane underneath all the tiles that shows through between tile updates. 16 | // 17 | // Could keep the old tiles around until the new ones are ready, though they'd 18 | // probably need to be layered in a way so the old tiles don't overlap new ones, 19 | // which is similar to how Leaflet approaches this (it has 2 layers) 20 | // 21 | // Could keep the tile from the previous quadtree level visible until all 4 22 | // tiles at the new / current level have finished loading and are displayed. 23 | // Perhaps by keeping a map of tiles by quadcode and a boolean for each of the 24 | // child quadcodes showing whether they are loaded and in view. If all true then 25 | // remove the parent tile, otherwise keep it on a lower layer. 26 | 27 | // TODO: Load and display a base layer separate to the LOD grid that is at a low 28 | // resolution – used as a backup / background to fill in empty areas / distance 29 | 30 | // DONE: Fix the issue where some tiles just don't load, or at least the texture 31 | // never shows up – tends to happen if you quickly zoom in / out past it while 32 | // it's still loading, leaving a blank space 33 | 34 | // TODO: Optimise the request of many image tiles – look at how Leaflet and 35 | // OpenWebGlobe approach this (eg. batching, queues, etc) 36 | 37 | // TODO: Cancel pending tile requests if they get removed from view before they 38 | // reach a ready state (eg. cancel image requests, etc). Need to ensure that the 39 | // images are re-requested when the tile is next in scene (even if from cache) 40 | 41 | // TODO: Consider not performing an LOD calculation on every frame, instead only 42 | // on move end so panning, orbiting and zooming stays smooth. Otherwise it's 43 | // possible for performance to tank if you pan, orbit or zoom rapidly while all 44 | // the LOD calculations are being made and new tiles requested. 45 | // 46 | // Pending tiles should continue to be requested and output to the scene on each 47 | // frame, but no new LOD calculations should be made. 48 | 49 | // This tile layer both updates the quadtree and outputs tiles on every frame 50 | // (throttled to some amount) 51 | // 52 | // This is because the computational complexity of image tiles is generally low 53 | // and so there isn't much jank when running these calculations and outputs in 54 | // realtime 55 | // 56 | // The benefit to doing this is that the underlying map layer continues to 57 | // refresh and update during movement, which is an arguably better experience 58 | 59 | class ImageTileLayer extends TileLayer { 60 | constructor(path, options) { 61 | var defaults = { 62 | distance: 300000 63 | }; 64 | 65 | options = extend({}, defaults, options); 66 | 67 | super(options); 68 | 69 | this._path = path; 70 | } 71 | 72 | _onAdd(world) { 73 | super._onAdd(world); 74 | 75 | // Add base layer 76 | var geom = new THREE.PlaneBufferGeometry(2000000, 2000000, 1); 77 | 78 | var baseMaterial; 79 | if (this._world._environment._skybox) { 80 | baseMaterial = ImageTileLayerBaseMaterial('#f5f5f3', this._world._environment._skybox.getRenderTarget()); 81 | } else { 82 | baseMaterial = ImageTileLayerBaseMaterial('#f5f5f3'); 83 | } 84 | 85 | var mesh = new THREE.Mesh(geom, baseMaterial); 86 | mesh.renderOrder = 0; 87 | mesh.rotation.x = -90 * Math.PI / 180; 88 | 89 | // TODO: It might be overkill to receive a shadow on the base layer as it's 90 | // rarely seen (good to have if performance difference is negligible) 91 | mesh.receiveShadow = true; 92 | 93 | this._baseLayer = mesh; 94 | this.add(mesh); 95 | 96 | // Trigger initial quadtree calculation on the next frame 97 | // 98 | // TODO: This is a hack to ensure the camera is all set up - a better 99 | // solution should be found 100 | setTimeout(() => { 101 | this._calculateLOD(); 102 | this._initEvents(); 103 | }, 0); 104 | } 105 | 106 | _initEvents() { 107 | // Run LOD calculations based on render calls 108 | // 109 | // Throttled to 1 LOD calculation per 100ms 110 | this._throttledWorldUpdate = throttle(this._onWorldUpdate, 100); 111 | 112 | this._world.on('preUpdate', this._throttledWorldUpdate, this); 113 | this._world.on('move', this._onWorldMove, this); 114 | } 115 | 116 | _onWorldUpdate() { 117 | this._calculateLOD(); 118 | this._outputTiles(); 119 | } 120 | 121 | _onWorldMove(latlon, point) { 122 | this._moveBaseLayer(point); 123 | } 124 | 125 | _moveBaseLayer(point) { 126 | this._baseLayer.position.x = point.x; 127 | this._baseLayer.position.z = point.y; 128 | } 129 | 130 | _createTile(quadcode, layer) { 131 | return new ImageTile(quadcode, this._path, layer); 132 | } 133 | 134 | // Destroys the layer and removes it from the scene and memory 135 | destroy() { 136 | this._world.off('preUpdate', this._throttledWorldUpdate); 137 | this._world.off('move', this._onWorldMove); 138 | 139 | this._throttledWorldUpdate = null; 140 | 141 | // Dispose of mesh and materials 142 | this._baseLayer.geometry.dispose(); 143 | this._baseLayer.geometry = null; 144 | 145 | if (this._baseLayer.material.map) { 146 | this._baseLayer.material.map.dispose(); 147 | this._baseLayer.material.map = null; 148 | } 149 | 150 | this._baseLayer.material.dispose(); 151 | this._baseLayer.material = null; 152 | 153 | this._baseLayer = null; 154 | 155 | // Run common destruction logic from parent 156 | super.destroy(); 157 | } 158 | } 159 | 160 | export default ImageTileLayer; 161 | 162 | var noNew = function(path, options) { 163 | return new ImageTileLayer(path, options); 164 | }; 165 | 166 | // Initialise without requiring new keyword 167 | export {noNew as imageTileLayer}; 168 | -------------------------------------------------------------------------------- /src/layer/environment/Skybox.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | import Sky from './Sky'; 3 | import throttle from 'lodash.throttle'; 4 | 5 | // TODO: Make sure nothing is left behind in the heap after calling destroy() 6 | 7 | var cubemap = { 8 | vertexShader: [ 9 | 'varying vec3 vPosition;', 10 | 'void main() {', 11 | 'vPosition = position;', 12 | 'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', 13 | '}' 14 | ].join('\n'), 15 | 16 | fragmentShader: [ 17 | 'uniform samplerCube cubemap;', 18 | 'varying vec3 vPosition;', 19 | 20 | 'void main() {', 21 | 'gl_FragColor = textureCube(cubemap, normalize(vPosition));', 22 | '}' 23 | ].join('\n') 24 | }; 25 | 26 | class Skybox { 27 | constructor(world, light) { 28 | this._world = world; 29 | this._light = light; 30 | 31 | this._settings = { 32 | distance: 38000, 33 | turbidity: 10, 34 | reileigh: 2, 35 | mieCoefficient: 0.005, 36 | mieDirectionalG: 0.8, 37 | luminance: 1, 38 | // 0.48 is a cracking dusk / sunset 39 | // 0.4 is a beautiful early-morning / late-afternoon 40 | // 0.2 is a nice day time 41 | inclination: 0.48, // Elevation / inclination 42 | azimuth: 0.25, // Facing front 43 | }; 44 | 45 | this._initSkybox(); 46 | this._updateUniforms(); 47 | this._initEvents(); 48 | } 49 | 50 | _initEvents() { 51 | // Throttled to 1 per 100ms 52 | this._throttledWorldUpdate = throttle(this._update, 100); 53 | this._world.on('preUpdate', this._throttledWorldUpdate, this); 54 | } 55 | 56 | _initSkybox() { 57 | // Cube camera for skybox 58 | this._cubeCamera = new THREE.CubeCamera(1, 20000000, 128); 59 | 60 | // Cube material 61 | var cubeTarget = this._cubeCamera.renderTarget; 62 | 63 | // Add Sky Mesh 64 | this._sky = new Sky(); 65 | this._skyScene = new THREE.Scene(); 66 | this._skyScene.add(this._sky.mesh); 67 | 68 | // Add Sun Helper 69 | this._sunSphere = new THREE.Mesh( 70 | new THREE.SphereBufferGeometry(2000, 16, 8), 71 | new THREE.MeshBasicMaterial({ 72 | color: 0xffffff 73 | }) 74 | ); 75 | 76 | // TODO: This isn't actually visible because it's not added to the layer 77 | // this._sunSphere.visible = true; 78 | 79 | var skyboxUniforms = { 80 | cubemap: { type: 't', value: cubeTarget } 81 | }; 82 | 83 | var skyboxMat = new THREE.ShaderMaterial({ 84 | uniforms: skyboxUniforms, 85 | vertexShader: cubemap.vertexShader, 86 | fragmentShader: cubemap.fragmentShader, 87 | side: THREE.BackSide 88 | }); 89 | 90 | this._mesh = new THREE.Mesh(new THREE.BoxGeometry(1900000, 1900000, 1900000), skyboxMat); 91 | 92 | this._updateSkybox = true; 93 | } 94 | 95 | _updateUniforms() { 96 | var settings = this._settings; 97 | var uniforms = this._sky.uniforms; 98 | uniforms.turbidity.value = settings.turbidity; 99 | uniforms.reileigh.value = settings.reileigh; 100 | uniforms.luminance.value = settings.luminance; 101 | uniforms.mieCoefficient.value = settings.mieCoefficient; 102 | uniforms.mieDirectionalG.value = settings.mieDirectionalG; 103 | 104 | var theta = Math.PI * (settings.inclination - 0.5); 105 | var phi = 2 * Math.PI * (settings.azimuth - 0.5); 106 | 107 | this._sunSphere.position.x = settings.distance * Math.cos(phi); 108 | this._sunSphere.position.y = settings.distance * Math.sin(phi) * Math.sin(theta); 109 | this._sunSphere.position.z = settings.distance * Math.sin(phi) * Math.cos(theta); 110 | 111 | // Move directional light to sun position 112 | this._light.position.copy(this._sunSphere.position); 113 | 114 | this._sky.uniforms.sunPosition.value.copy(this._sunSphere.position); 115 | } 116 | 117 | _update(delta) { 118 | if (this._updateSkybox) { 119 | this._updateSkybox = false; 120 | } else { 121 | return; 122 | } 123 | 124 | // if (!this._angle) { 125 | // this._angle = 0; 126 | // } 127 | // 128 | // // Animate inclination 129 | // this._angle += Math.PI * delta; 130 | // this._settings.inclination = 0.5 * (Math.sin(this._angle) / 2 + 0.5); 131 | 132 | // Update light intensity depending on elevation of sun (day to night) 133 | this._light.intensity = 1 - 0.95 * (this._settings.inclination / 0.5); 134 | 135 | // // console.log(delta, this._angle, this._settings.inclination); 136 | // 137 | // TODO: Only do this when the uniforms have been changed 138 | this._updateUniforms(); 139 | 140 | // TODO: Only do this when the cubemap has actually changed 141 | this._cubeCamera.updateCubeMap(this._world._engine._renderer, this._skyScene); 142 | } 143 | 144 | getRenderTarget() { 145 | return this._cubeCamera.renderTarget; 146 | } 147 | 148 | setInclination(inclination) { 149 | this._settings.inclination = inclination; 150 | this._updateSkybox = true; 151 | } 152 | 153 | // Destroy the skybox and remove it from memory 154 | destroy() { 155 | this._world.off('preUpdate', this._throttledWorldUpdate); 156 | this._throttledWorldUpdate = null; 157 | 158 | this._world = null; 159 | this._light = null; 160 | 161 | this._cubeCamera = null; 162 | 163 | this._sky.mesh.geometry.dispose(); 164 | this._sky.mesh.geometry = null; 165 | 166 | if (this._sky.mesh.material.map) { 167 | this._sky.mesh.material.map.dispose(); 168 | this._sky.mesh.material.map = null; 169 | } 170 | 171 | this._sky.mesh.material.dispose(); 172 | this._sky.mesh.material = null; 173 | 174 | this._sky.mesh = null; 175 | this._sky = null; 176 | 177 | this._skyScene = null; 178 | 179 | this._sunSphere.geometry.dispose(); 180 | this._sunSphere.geometry = null; 181 | 182 | if (this._sunSphere.material.map) { 183 | this._sunSphere.material.map.dispose(); 184 | this._sunSphere.material.map = null; 185 | } 186 | 187 | this._sunSphere.material.dispose(); 188 | this._sunSphere.material = null; 189 | 190 | this._sunSphere = null; 191 | 192 | this._mesh.geometry.dispose(); 193 | this._mesh.geometry = null; 194 | 195 | if (this._mesh.material.map) { 196 | this._mesh.material.map.dispose(); 197 | this._mesh.material.map = null; 198 | } 199 | 200 | this._mesh.material.dispose(); 201 | this._mesh.material = null; 202 | } 203 | } 204 | 205 | export default Skybox; 206 | 207 | var noNew = function(world, light) { 208 | return new Skybox(world, light); 209 | }; 210 | 211 | // Initialise without requiring new keyword 212 | export {noNew as skybox}; 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ViziCities (0.3) 2 | 3 | A framework for 3D geospatial visualization in the browser 4 | 5 | [![](https://cloud.githubusercontent.com/assets/22612/16195132/1c0b2176-36f0-11e6-853b-3e93c04c4b17.gif)](http://vizicities.com/demos/all-the-things) 6 | 7 | 8 | ### Important links 9 | 10 | * [Examples](#examples) 11 | * [Getting started](#getting-started) 12 | * [Attribution](#using-vizicities-please-attribute-it) 13 | * [License](#license) 14 | 15 | 16 | ## Examples 17 | 18 | * [Basic example](https://github.com/UDST/vizicities/tree/master/examples/basic) (2D basemap and 3D buildings) ([demo](http://cdn.rawgit.com/UDST/vizicities/master/examples/basic/index.html)) 19 | * [Buildings coloured by height](https://github.com/UDST/vizicities/tree/master/examples/colour-by-height) ([demo](http://cdn.rawgit.com/UDST/vizicities/master/examples/colour-by-height/index.html)) 20 | * [Basic GeoJSON example](https://github.com/UDST/vizicities/tree/master/examples/geojson) (points, linestrings and polygons) ([demo](http://cdn.rawgit.com/UDST/vizicities/master/examples/geojson/index.html)) 21 | * [Interactivity](https://github.com/UDST/vizicities/tree/master/examples/interactive) (open console and click on features) ([demo](http://cdn.rawgit.com/UDST/vizicities/master/examples/interactive/index.html)) 22 | * [NYC MTA routes](https://github.com/UDST/vizicities/tree/master/examples/mta-routes) ([demo](http://cdn.rawgit.com/UDST/vizicities/master/examples/mta-routes/index.html)) 23 | * [Lots of GeoJSON](https://github.com/UDST/vizicities/tree/master/examples/lots-of-features) features ([demo](http://cdn.rawgit.com/UDST/vizicities/master/examples/lots-of-features/index.html)) 24 | * [All the things](https://github.com/UDST/vizicities/tree/master/examples/all-the-things) (will test even the best computers) ([demo](http://cdn.rawgit.com/UDST/vizicities/master/examples/all-the-things/index.html)) 25 | 26 | 27 | ## Main changes since 0.2 28 | 29 | * Re-written from the ground up 30 | * Complete overhaul of visual styling 31 | * Massive performance improvements across the board 32 | * Vastly simplified setup and API 33 | * Better management and cleanup of memory 34 | * Sophisticated quadtree-based grid system 35 | * Physically-based lighting and materials (when enabled) 36 | * Realistic day/night skybox (when enabled) 37 | * Shadows based on position of sun in sky (when enabled) 38 | * Built-in support for image-based tile endpoints 39 | * Built-in support for GeoJSON and TopoJSON tile endpoints 40 | * Built-in support for non-tiled GeoJSON and TopoJSON files 41 | * Click events on individual features (when enabled) 42 | * Internal caching of tile-based endpoints 43 | * Easier to extend and add new functionality 44 | * Easier to access and use general three.js features within ViziCities 45 | * Separation of three.js from the core ViziCities codebase 46 | 47 | 48 | ## Getting started 49 | 50 | The first step is to add the latest ViziCities distribution to your website: 51 | 52 | ```html 53 | 54 | 55 | ``` 56 | 57 | From there you will have access to the `VIZI` namespace which you can use to interact with and set up ViziCities. 58 | 59 | You'll also want to add a HTML element that you want to contain your ViziCities visualisation: 60 | 61 | ```html 62 |
63 | ``` 64 | 65 | It's worth adding some CSS to the page to size the ViziCities element correctly, in this case filling the entire page: 66 | 67 | ```css 68 | * { margin: 0; padding: 0; } 69 | html, body { height: 100%; overflow: hidden;} 70 | #vizicities { height: 100%; } 71 | ``` 72 | 73 | The next step is to set up an instance of the ViziCities `World` component and position it in Manhattan: 74 | 75 | ```javascript 76 | // Manhattan 77 | var coords = [40.739940, -73.988801]; 78 | var world = VIZI.world('vizicities').setView(coords); 79 | ``` 80 | 81 | The first argument is the ID of the HTML element that you want to use as a container for the ViziCities visualisation. 82 | 83 | Then add some controls: 84 | 85 | ```javascript 86 | VIZI.Controls.orbit().addTo(world); 87 | ``` 88 | 89 | And a 2D basemap using tiles from CartoDB: 90 | 91 | ```javascript 92 | VIZI.imageTileLayer('http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', { 93 | attribution: '© OpenStreetMap contributors, © CartoDB' 94 | }).addTo(world); 95 | ``` 96 | 97 | At this point you can take a look at your handywork and should be able to see a 2D map focussed on the Manhattan area. You can move around using the mouse. 98 | 99 | If you want to be a bit more adventurous then you can add 3D buildings using Mapzen vector tiles: 100 | 101 | ```javascript 102 | VIZI.topoJSONTileLayer('https://vector.mapzen.com/osm/buildings/{z}/{x}/{y}.topojson?api_key=vector-tiles-NT5Emiw', { 103 | interactive: false, 104 | style: function(feature) { 105 | var height; 106 | 107 | if (feature.properties.height) { 108 | height = feature.properties.height; 109 | } else { 110 | height = 10 + Math.random() * 10; 111 | } 112 | 113 | return { 114 | height: height 115 | }; 116 | }, 117 | filter: function(feature) { 118 | // Don't show points 119 | return feature.geometry.type !== 'Point'; 120 | }, 121 | attribution: '© OpenStreetMap contributors, Who\'s On First.' 122 | }).addTo(world); 123 | ``` 124 | 125 | Refresh the page and you'll see 3D buildings appear on top of the 2D basemap. 126 | 127 | [Take a look at the various examples](https://github.com/UDST/vizicities/tree/master/examples) to see some more complex uses of ViziCities. 128 | 129 | 130 | ## Using ViziCities? Please attribute it 131 | 132 | While we love giving you free and open access to the code for ViziCities, we also appreciate getting some recognition for all the hard work that's gone into it. A small attribution is built into ViziCities and, while possible to remove, we'd really appreciate it if you left it in. 133 | 134 | If you absolutely have to remove the attribution then please get in touch and we can work something out. 135 | 136 | 137 | ## Consultancy work 138 | 139 | Want to use ViziCities but don't want to customise it yourself? Or perhaps you have an idea that might benefit from ViziCities but aren't sure how to make it a reality? We offer consultancy related to ViziCities projects and would love to see how we can help you. 140 | 141 | Interested? [Get in touch with us](mailto:vizicities@urbansim.com) and let's get talking. 142 | 143 | 144 | ## Contact us 145 | 146 | Want to share an interesting use of ViziCities, or perhaps just have a question about it? You can communicate with the ViziCities team via email ([vizicities@urbansim.com](mailto:vizicities@urbansim.com)) and Twitter ([@ViziCities](http://twitter.com/ViziCities)). 147 | 148 | 149 | ## License 150 | 151 | ViziCities is copyright [UrbanSim Inc.](http://www.urbansim.com/) and uses the fair and simple BSD-3 license. Want to see it in full? No problem, [you can read it here](https://github.com/UDST/vizicities/blob/master/LICENSE). 152 | -------------------------------------------------------------------------------- /src/engine/Picking.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three'; 2 | import {point as Point} from '../geo/Point'; 3 | import PickingScene from './PickingScene'; 4 | 5 | // TODO: Look into a way of setting this up without passing in a renderer and 6 | // camera from the engine 7 | 8 | // TODO: Add a basic indicator on or around the mouse pointer when it is over 9 | // something pickable / clickable 10 | // 11 | // A simple transparent disc or ring at the mouse point should work to start, or 12 | // even just changing the cursor to the CSS 'pointer' style 13 | // 14 | // Probably want this on mousemove with a throttled update as not to spam the 15 | // picking method 16 | // 17 | // Relies upon the picking method not redrawing the scene every call due to 18 | // the way TileLayer invalidates the picking scene 19 | 20 | var nextId = 1; 21 | 22 | class Picking { 23 | constructor(world, renderer, camera) { 24 | this._world = world; 25 | this._renderer = renderer; 26 | this._camera = camera; 27 | 28 | this._raycaster = new THREE.Raycaster(); 29 | 30 | // TODO: Match this with the line width used in the picking layers 31 | this._raycaster.linePrecision = 3; 32 | 33 | this._pickingScene = PickingScene; 34 | this._pickingTexture = new THREE.WebGLRenderTarget(); 35 | this._pickingTexture.texture.minFilter = THREE.LinearFilter; 36 | this._pickingTexture.texture.generateMipmaps = false; 37 | 38 | this._nextId = 1; 39 | 40 | this._resizeTexture(); 41 | this._initEvents(); 42 | } 43 | 44 | _initEvents() { 45 | this._resizeHandler = this._resizeTexture.bind(this); 46 | window.addEventListener('resize', this._resizeHandler, false); 47 | 48 | this._mouseUpHandler = this._onMouseUp.bind(this); 49 | this._world._container.addEventListener('mouseup', this._mouseUpHandler, false); 50 | 51 | this._world.on('move', this._onWorldMove, this); 52 | } 53 | 54 | _onMouseUp(event) { 55 | // Only react to main button click 56 | if (event.button !== 0) { 57 | return; 58 | } 59 | 60 | var point = Point(event.clientX, event.clientY); 61 | 62 | var normalisedPoint = Point(0, 0); 63 | normalisedPoint.x = (point.x / this._width) * 2 - 1; 64 | normalisedPoint.y = -(point.y / this._height) * 2 + 1; 65 | 66 | this._pick(point, normalisedPoint); 67 | } 68 | 69 | _onWorldMove() { 70 | this._needUpdate = true; 71 | } 72 | 73 | // TODO: Ensure this doesn't get out of sync issue with the renderer resize 74 | _resizeTexture() { 75 | var size = this._renderer.getSize(); 76 | 77 | this._width = size.width; 78 | this._height = size.height; 79 | 80 | this._pickingTexture.setSize(this._width, this._height); 81 | this._pixelBuffer = new Uint8Array(4 * this._width * this._height); 82 | 83 | this._needUpdate = true; 84 | } 85 | 86 | // TODO: Make this only re-draw the scene if both an update is needed and the 87 | // camera has moved since the last update 88 | // 89 | // Otherwise it re-draws the scene on every click due to the way LOD updates 90 | // work in TileLayer – spamming this.add() and this.remove() 91 | // 92 | // TODO: Pause updates during map move / orbit / zoom as this is unlikely to 93 | // be a point in time where the user cares for picking functionality 94 | _update() { 95 | if (this._needUpdate) { 96 | var texture = this._pickingTexture; 97 | 98 | this._renderer.render(this._pickingScene, this._camera, this._pickingTexture); 99 | 100 | // Read the rendering texture 101 | this._renderer.readRenderTargetPixels(texture, 0, 0, texture.width, texture.height, this._pixelBuffer); 102 | 103 | this._needUpdate = false; 104 | } 105 | } 106 | 107 | _pick(point, normalisedPoint) { 108 | this._update(); 109 | 110 | var index = point.x + (this._pickingTexture.height - point.y) * this._pickingTexture.width; 111 | 112 | // Interpret the pixel as an ID 113 | var id = (this._pixelBuffer[index * 4 + 2] * 255 * 255) + (this._pixelBuffer[index * 4 + 1] * 255) + (this._pixelBuffer[index * 4 + 0]); 114 | 115 | // Skip if ID is 16646655 (white) as the background returns this 116 | if (id === 16646655) { 117 | return; 118 | } 119 | 120 | this._raycaster.setFromCamera(normalisedPoint, this._camera); 121 | 122 | // Perform ray intersection on picking scene 123 | // 124 | // TODO: Only perform intersection test on the relevant picking mesh 125 | var intersects = this._raycaster.intersectObjects(this._pickingScene.children, true); 126 | 127 | var _point2d = point.clone(); 128 | 129 | var _point3d; 130 | if (intersects.length > 0) { 131 | _point3d = intersects[0].point.clone(); 132 | } 133 | 134 | // Pass along as much data as possible for now until we know more about how 135 | // people use the picking API and what the returned data should be 136 | // 137 | // TODO: Look into the leak potential for passing so much by reference here 138 | this._world.emit('pick', id, _point2d, _point3d, intersects); 139 | this._world.emit('pick-' + id, _point2d, _point3d, intersects); 140 | } 141 | 142 | // Add mesh to picking scene 143 | // 144 | // Picking ID should already be added as an attribute 145 | add(mesh) { 146 | this._pickingScene.add(mesh); 147 | this._needUpdate = true; 148 | } 149 | 150 | // Remove mesh from picking scene 151 | remove(mesh) { 152 | this._pickingScene.remove(mesh); 153 | this._needUpdate = true; 154 | } 155 | 156 | // Returns next ID to use for picking 157 | getNextId() { 158 | return nextId++; 159 | } 160 | 161 | destroy() { 162 | // TODO: Find a way to properly remove these listeners as they stay 163 | // active at the moment 164 | window.removeEventListener('resize', this._resizeHandler, false); 165 | this._world._container.removeEventListener('mouseup', this._mouseUpHandler, false); 166 | 167 | this._world.off('move', this._onWorldMove); 168 | 169 | if (this._pickingScene.children) { 170 | // Remove everything else in the layer 171 | var child; 172 | for (var i = this._pickingScene.children.length - 1; i >= 0; i--) { 173 | child = this._pickingScene.children[i]; 174 | 175 | if (!child) { 176 | continue; 177 | } 178 | 179 | this._pickingScene.remove(child); 180 | 181 | // Probably not a good idea to dispose of geometry due to it being 182 | // shared with the non-picking scene 183 | // if (child.geometry) { 184 | // // Dispose of mesh and materials 185 | // child.geometry.dispose(); 186 | // child.geometry = null; 187 | // } 188 | 189 | if (child.material) { 190 | if (child.material.map) { 191 | child.material.map.dispose(); 192 | child.material.map = null; 193 | } 194 | 195 | child.material.dispose(); 196 | child.material = null; 197 | } 198 | } 199 | } 200 | 201 | this._pickingScene = null; 202 | this._pickingTexture = null; 203 | this._pixelBuffer = null; 204 | 205 | this._world = null; 206 | this._renderer = null; 207 | this._camera = null; 208 | } 209 | } 210 | 211 | // Initialise without requiring new keyword 212 | export default function(world, renderer, camera) { 213 | return new Picking(world, renderer, camera); 214 | }; 215 | -------------------------------------------------------------------------------- /src/vendor/CSS3DRenderer.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /* eslint-disable */ 3 | 4 | /** 5 | * Based on http://www.emagix.net/academic/mscs-project/item/camera-sync-with-css3-and-webgl-threejs 6 | * @author mrdoob / http://mrdoob.com/ 7 | */ 8 | 9 | import THREE from 'three'; 10 | 11 | var CSS3DObject = function ( element ) { 12 | 13 | THREE.Object3D.call( this ); 14 | 15 | this.element = element; 16 | this.element.style.position = 'absolute'; 17 | 18 | this.addEventListener( 'removed', function ( event ) { 19 | 20 | if ( this.element.parentNode !== null ) { 21 | 22 | this.element.parentNode.removeChild( this.element ); 23 | 24 | } 25 | 26 | } ); 27 | 28 | }; 29 | 30 | CSS3DObject.prototype = Object.create( THREE.Object3D.prototype ); 31 | CSS3DObject.prototype.constructor = CSS3DObject; 32 | 33 | var CSS3DSprite = function ( element ) { 34 | 35 | CSS3DObject.call( this, element ); 36 | 37 | }; 38 | 39 | CSS3DSprite.prototype = Object.create( CSS3DObject.prototype ); 40 | CSS3DSprite.prototype.constructor = CSS3DSprite; 41 | 42 | // 43 | 44 | var CSS3DRenderer = function () { 45 | 46 | console.log( 'THREE.CSS3DRenderer', THREE.REVISION ); 47 | 48 | var _width, _height; 49 | var _widthHalf, _heightHalf; 50 | 51 | var matrix = new THREE.Matrix4(); 52 | 53 | var cache = { 54 | camera: { fov: 0, style: '' }, 55 | objects: {} 56 | }; 57 | 58 | var domElement = document.createElement( 'div' ); 59 | domElement.style.overflow = 'hidden'; 60 | 61 | domElement.style.WebkitTransformStyle = 'preserve-3d'; 62 | domElement.style.MozTransformStyle = 'preserve-3d'; 63 | domElement.style.oTransformStyle = 'preserve-3d'; 64 | domElement.style.transformStyle = 'preserve-3d'; 65 | 66 | this.domElement = domElement; 67 | 68 | var cameraElement = document.createElement( 'div' ); 69 | 70 | cameraElement.style.WebkitTransformStyle = 'preserve-3d'; 71 | cameraElement.style.MozTransformStyle = 'preserve-3d'; 72 | cameraElement.style.oTransformStyle = 'preserve-3d'; 73 | cameraElement.style.transformStyle = 'preserve-3d'; 74 | 75 | domElement.appendChild( cameraElement ); 76 | 77 | this.setClearColor = function () {}; 78 | 79 | this.getSize = function() { 80 | 81 | return { 82 | width: _width, 83 | height: _height 84 | }; 85 | 86 | }; 87 | 88 | this.setSize = function ( width, height ) { 89 | 90 | _width = width; 91 | _height = height; 92 | 93 | _widthHalf = _width / 2; 94 | _heightHalf = _height / 2; 95 | 96 | domElement.style.width = width + 'px'; 97 | domElement.style.height = height + 'px'; 98 | 99 | cameraElement.style.width = width + 'px'; 100 | cameraElement.style.height = height + 'px'; 101 | 102 | }; 103 | 104 | var epsilon = function ( value ) { 105 | 106 | return Math.abs( value ) < Number.EPSILON ? 0 : value; 107 | 108 | }; 109 | 110 | var getCameraCSSMatrix = function ( matrix ) { 111 | 112 | var elements = matrix.elements; 113 | 114 | return 'matrix3d(' + 115 | epsilon( elements[ 0 ] ) + ',' + 116 | epsilon( - elements[ 1 ] ) + ',' + 117 | epsilon( elements[ 2 ] ) + ',' + 118 | epsilon( elements[ 3 ] ) + ',' + 119 | epsilon( elements[ 4 ] ) + ',' + 120 | epsilon( - elements[ 5 ] ) + ',' + 121 | epsilon( elements[ 6 ] ) + ',' + 122 | epsilon( elements[ 7 ] ) + ',' + 123 | epsilon( elements[ 8 ] ) + ',' + 124 | epsilon( - elements[ 9 ] ) + ',' + 125 | epsilon( elements[ 10 ] ) + ',' + 126 | epsilon( elements[ 11 ] ) + ',' + 127 | epsilon( elements[ 12 ] ) + ',' + 128 | epsilon( - elements[ 13 ] ) + ',' + 129 | epsilon( elements[ 14 ] ) + ',' + 130 | epsilon( elements[ 15 ] ) + 131 | ')'; 132 | 133 | }; 134 | 135 | var getObjectCSSMatrix = function ( matrix ) { 136 | 137 | var elements = matrix.elements; 138 | 139 | return 'translate3d(-50%,-50%,0) matrix3d(' + 140 | epsilon( elements[ 0 ] ) + ',' + 141 | epsilon( elements[ 1 ] ) + ',' + 142 | epsilon( elements[ 2 ] ) + ',' + 143 | epsilon( elements[ 3 ] ) + ',' + 144 | epsilon( - elements[ 4 ] ) + ',' + 145 | epsilon( - elements[ 5 ] ) + ',' + 146 | epsilon( - elements[ 6 ] ) + ',' + 147 | epsilon( - elements[ 7 ] ) + ',' + 148 | epsilon( elements[ 8 ] ) + ',' + 149 | epsilon( elements[ 9 ] ) + ',' + 150 | epsilon( elements[ 10 ] ) + ',' + 151 | epsilon( elements[ 11 ] ) + ',' + 152 | epsilon( elements[ 12 ] ) + ',' + 153 | epsilon( elements[ 13 ] ) + ',' + 154 | epsilon( elements[ 14 ] ) + ',' + 155 | epsilon( elements[ 15 ] ) + 156 | ')'; 157 | 158 | }; 159 | 160 | var renderObject = function ( object, camera ) { 161 | 162 | if ( object instanceof CSS3DObject ) { 163 | 164 | var style; 165 | 166 | if ( object instanceof CSS3DSprite ) { 167 | 168 | // http://swiftcoder.wordpress.com/2008/11/25/constructing-a-billboard-matrix/ 169 | 170 | matrix.copy( camera.matrixWorldInverse ); 171 | matrix.transpose(); 172 | matrix.copyPosition( object.matrixWorld ); 173 | matrix.scale( object.scale ); 174 | 175 | matrix.elements[ 3 ] = 0; 176 | matrix.elements[ 7 ] = 0; 177 | matrix.elements[ 11 ] = 0; 178 | matrix.elements[ 15 ] = 1; 179 | 180 | style = getObjectCSSMatrix( matrix ); 181 | 182 | } else { 183 | 184 | style = getObjectCSSMatrix( object.matrixWorld ); 185 | 186 | } 187 | 188 | var element = object.element; 189 | var cachedStyle = cache.objects[ object.id ]; 190 | 191 | if ( cachedStyle === undefined || cachedStyle !== style ) { 192 | 193 | element.style.WebkitTransform = style; 194 | element.style.MozTransform = style; 195 | element.style.oTransform = style; 196 | element.style.transform = style; 197 | 198 | cache.objects[ object.id ] = style; 199 | 200 | } 201 | 202 | if ( element.parentNode !== cameraElement ) { 203 | 204 | cameraElement.appendChild( element ); 205 | 206 | } 207 | 208 | } 209 | 210 | for ( var i = 0, l = object.children.length; i < l; i ++ ) { 211 | 212 | renderObject( object.children[ i ], camera ); 213 | 214 | } 215 | 216 | }; 217 | 218 | this.render = function ( scene, camera ) { 219 | 220 | var fov = 0.5 / Math.tan( THREE.Math.degToRad( camera.fov * 0.5 ) ) * _height; 221 | 222 | if ( cache.camera.fov !== fov ) { 223 | 224 | domElement.style.WebkitPerspective = fov + 'px'; 225 | domElement.style.MozPerspective = fov + 'px'; 226 | domElement.style.oPerspective = fov + 'px'; 227 | domElement.style.perspective = fov + 'px'; 228 | 229 | cache.camera.fov = fov; 230 | 231 | } 232 | 233 | scene.updateMatrixWorld(); 234 | 235 | if ( camera.parent === null ) camera.updateMatrixWorld(); 236 | 237 | camera.matrixWorldInverse.getInverse( camera.matrixWorld ); 238 | 239 | var style = 'translate3d(0,0,' + fov + 'px)' + getCameraCSSMatrix( camera.matrixWorldInverse ) + 240 | ' translate3d(' + _widthHalf + 'px,' + _heightHalf + 'px, 0)'; 241 | 242 | if ( cache.camera.style !== style ) { 243 | 244 | cameraElement.style.WebkitTransform = style; 245 | cameraElement.style.MozTransform = style; 246 | cameraElement.style.oTransform = style; 247 | cameraElement.style.transform = style; 248 | 249 | cache.camera.style = style; 250 | 251 | } 252 | 253 | renderObject( scene, camera ); 254 | 255 | }; 256 | 257 | }; 258 | 259 | export {CSS3DObject as CSS3DObject}; 260 | export {CSS3DSprite as CSS3DSprite}; 261 | export {CSS3DRenderer as CSS3DRenderer}; 262 | 263 | THREE.CSS3DObject = CSS3DObject; 264 | THREE.CSS3DSprite = CSS3DSprite; 265 | THREE.CSS3DRenderer = CSS3DRenderer; 266 | -------------------------------------------------------------------------------- /src/util/Buffer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * BufferGeometry helpers 3 | */ 4 | 5 | import THREE from 'three'; 6 | 7 | var Buffer = (function() { 8 | // Merge multiple attribute objects into a single attribute object 9 | // 10 | // Attribute objects must all use the same attribute keys 11 | var mergeAttributes = function(attributes) { 12 | var lengths = {}; 13 | 14 | // Find array lengths 15 | attributes.forEach(_attributes => { 16 | for (var k in _attributes) { 17 | if (!lengths[k]) { 18 | lengths[k] = 0; 19 | } 20 | 21 | lengths[k] += _attributes[k].length; 22 | } 23 | }); 24 | 25 | var mergedAttributes = {}; 26 | 27 | // Set up arrays to merge into 28 | for (var k in lengths) { 29 | mergedAttributes[k] = new Float32Array(lengths[k]); 30 | } 31 | 32 | var lastLengths = {}; 33 | 34 | attributes.forEach(_attributes => { 35 | for (var k in _attributes) { 36 | if (!lastLengths[k]) { 37 | lastLengths[k] = 0; 38 | } 39 | 40 | mergedAttributes[k].set(_attributes[k], lastLengths[k]); 41 | 42 | lastLengths[k] += _attributes[k].length; 43 | } 44 | }); 45 | 46 | return mergedAttributes; 47 | }; 48 | 49 | var createLineGeometry = function(lines, offset) { 50 | var geometry = new THREE.BufferGeometry(); 51 | 52 | var vertices = new Float32Array(lines.verticesCount * 3); 53 | var colours = new Float32Array(lines.verticesCount * 3); 54 | 55 | var pickingIds; 56 | if (lines.pickingIds) { 57 | // One component per vertex (1) 58 | pickingIds = new Float32Array(lines.verticesCount); 59 | } 60 | 61 | var _vertices; 62 | var _colour; 63 | var _pickingId; 64 | 65 | var lastIndex = 0; 66 | 67 | for (var i = 0; i < lines.vertices.length; i++) { 68 | _vertices = lines.vertices[i]; 69 | _colour = lines.colours[i]; 70 | 71 | if (pickingIds) { 72 | _pickingId = lines.pickingIds[i]; 73 | } 74 | 75 | for (var j = 0; j < _vertices.length; j++) { 76 | var ax = _vertices[j][0] + offset.x; 77 | var ay = _vertices[j][1]; 78 | var az = _vertices[j][2] + offset.y; 79 | 80 | var c1 = _colour[j]; 81 | 82 | vertices[lastIndex * 3 + 0] = ax; 83 | vertices[lastIndex * 3 + 1] = ay; 84 | vertices[lastIndex * 3 + 2] = az; 85 | 86 | colours[lastIndex * 3 + 0] = c1[0]; 87 | colours[lastIndex * 3 + 1] = c1[1]; 88 | colours[lastIndex * 3 + 2] = c1[2]; 89 | 90 | if (pickingIds) { 91 | pickingIds[lastIndex] = _pickingId; 92 | } 93 | 94 | lastIndex++; 95 | } 96 | } 97 | 98 | // itemSize = 3 because there are 3 values (components) per vertex 99 | geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3)); 100 | geometry.addAttribute('color', new THREE.BufferAttribute(colours, 3)); 101 | 102 | if (pickingIds) { 103 | geometry.addAttribute('pickingId', new THREE.BufferAttribute(pickingIds, 1)); 104 | } 105 | 106 | geometry.computeBoundingBox(); 107 | 108 | return geometry; 109 | }; 110 | 111 | // TODO: Make picking IDs optional 112 | var createGeometry = function(attributes, offset) { 113 | var geometry = new THREE.BufferGeometry(); 114 | 115 | // Three components per vertex per face (3 x 3 = 9) 116 | var vertices = new Float32Array(attributes.facesCount * 9); 117 | var normals = new Float32Array(attributes.facesCount * 9); 118 | var colours = new Float32Array(attributes.facesCount * 9); 119 | 120 | var pickingIds; 121 | if (attributes.pickingIds) { 122 | // One component per vertex per face (1 x 3 = 3) 123 | pickingIds = new Float32Array(attributes.facesCount * 3); 124 | } 125 | 126 | var pA = new THREE.Vector3(); 127 | var pB = new THREE.Vector3(); 128 | var pC = new THREE.Vector3(); 129 | 130 | var cb = new THREE.Vector3(); 131 | var ab = new THREE.Vector3(); 132 | 133 | var index; 134 | var _faces; 135 | var _vertices; 136 | var _colour; 137 | var _pickingId; 138 | var lastIndex = 0; 139 | for (var i = 0; i < attributes.faces.length; i++) { 140 | _faces = attributes.faces[i]; 141 | _vertices = attributes.vertices[i]; 142 | _colour = attributes.colours[i]; 143 | 144 | if (pickingIds) { 145 | _pickingId = attributes.pickingIds[i]; 146 | } 147 | 148 | for (var j = 0; j < _faces.length; j++) { 149 | // Array of vertex indexes for the face 150 | index = _faces[j][0]; 151 | 152 | var ax = _vertices[index][0] + offset.x; 153 | var ay = _vertices[index][1]; 154 | var az = _vertices[index][2] + offset.y; 155 | 156 | var c1 = _colour[j][0]; 157 | 158 | index = _faces[j][1]; 159 | 160 | var bx = _vertices[index][0] + offset.x; 161 | var by = _vertices[index][1]; 162 | var bz = _vertices[index][2] + offset.y; 163 | 164 | var c2 = _colour[j][1]; 165 | 166 | index = _faces[j][2]; 167 | 168 | var cx = _vertices[index][0] + offset.x; 169 | var cy = _vertices[index][1]; 170 | var cz = _vertices[index][2] + offset.y; 171 | 172 | var c3 = _colour[j][2]; 173 | 174 | // Flat face normals 175 | // From: http://threejs.org/examples/webgl_buffergeometry.html 176 | pA.set(ax, ay, az); 177 | pB.set(bx, by, bz); 178 | pC.set(cx, cy, cz); 179 | 180 | cb.subVectors(pC, pB); 181 | ab.subVectors(pA, pB); 182 | cb.cross(ab); 183 | 184 | cb.normalize(); 185 | 186 | var nx = cb.x; 187 | var ny = cb.y; 188 | var nz = cb.z; 189 | 190 | vertices[lastIndex * 9 + 0] = ax; 191 | vertices[lastIndex * 9 + 1] = ay; 192 | vertices[lastIndex * 9 + 2] = az; 193 | 194 | normals[lastIndex * 9 + 0] = nx; 195 | normals[lastIndex * 9 + 1] = ny; 196 | normals[lastIndex * 9 + 2] = nz; 197 | 198 | colours[lastIndex * 9 + 0] = c1[0]; 199 | colours[lastIndex * 9 + 1] = c1[1]; 200 | colours[lastIndex * 9 + 2] = c1[2]; 201 | 202 | vertices[lastIndex * 9 + 3] = bx; 203 | vertices[lastIndex * 9 + 4] = by; 204 | vertices[lastIndex * 9 + 5] = bz; 205 | 206 | normals[lastIndex * 9 + 3] = nx; 207 | normals[lastIndex * 9 + 4] = ny; 208 | normals[lastIndex * 9 + 5] = nz; 209 | 210 | colours[lastIndex * 9 + 3] = c2[0]; 211 | colours[lastIndex * 9 + 4] = c2[1]; 212 | colours[lastIndex * 9 + 5] = c2[2]; 213 | 214 | vertices[lastIndex * 9 + 6] = cx; 215 | vertices[lastIndex * 9 + 7] = cy; 216 | vertices[lastIndex * 9 + 8] = cz; 217 | 218 | normals[lastIndex * 9 + 6] = nx; 219 | normals[lastIndex * 9 + 7] = ny; 220 | normals[lastIndex * 9 + 8] = nz; 221 | 222 | colours[lastIndex * 9 + 6] = c3[0]; 223 | colours[lastIndex * 9 + 7] = c3[1]; 224 | colours[lastIndex * 9 + 8] = c3[2]; 225 | 226 | if (pickingIds) { 227 | pickingIds[lastIndex * 3 + 0] = _pickingId; 228 | pickingIds[lastIndex * 3 + 1] = _pickingId; 229 | pickingIds[lastIndex * 3 + 2] = _pickingId; 230 | } 231 | 232 | lastIndex++; 233 | } 234 | } 235 | 236 | // itemSize = 3 because there are 3 values (components) per vertex 237 | geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3)); 238 | geometry.addAttribute('normal', new THREE.BufferAttribute(normals, 3)); 239 | geometry.addAttribute('color', new THREE.BufferAttribute(colours, 3)); 240 | 241 | if (pickingIds) { 242 | geometry.addAttribute('pickingId', new THREE.BufferAttribute(pickingIds, 1)); 243 | } 244 | 245 | geometry.computeBoundingBox(); 246 | 247 | return geometry; 248 | }; 249 | 250 | return { 251 | mergeAttributes: mergeAttributes, 252 | createLineGeometry: createLineGeometry, 253 | createGeometry: createGeometry 254 | }; 255 | })(); 256 | 257 | export default Buffer; 258 | -------------------------------------------------------------------------------- /src/controls/Controls.Orbit.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import THREE from 'three'; 3 | import OrbitControls from '../vendor/OrbitControls'; 4 | import TweenLite from 'TweenLite'; 5 | 6 | // Prevent animation from pausing when tab is inactive 7 | TweenLite.lagSmoothing(0); 8 | 9 | class Orbit extends EventEmitter { 10 | constructor() { 11 | super(); 12 | } 13 | 14 | // Proxy control events 15 | // 16 | // There's currently no distinction between pan, orbit and zoom events 17 | _initEvents() { 18 | this._controls.addEventListener('start', (event) => { 19 | this._world.emit('controlsMoveStart', event.target.target); 20 | }); 21 | 22 | this._controls.addEventListener('change', (event) => { 23 | this._world.emit('controlsMove', event.target.target); 24 | }); 25 | 26 | this._controls.addEventListener('end', (event) => { 27 | this._world.emit('controlsMoveEnd', event.target.target); 28 | }); 29 | } 30 | 31 | // Moving the camera along the [x,y,z] axis based on a target position 32 | panTo(point, animate) {} 33 | panBy(pointDelta, animate) {} 34 | 35 | // Zooming the camera in and out 36 | zoomTo(metres, animate) {} 37 | zoomBy(metresDelta, animate) {} 38 | 39 | // Force camera to look at something other than the target 40 | lookAt(point, animate) {} 41 | 42 | // Make camera look at the target 43 | lookAtTarget() {} 44 | 45 | // Tilt (up and down) 46 | tiltTo(angle, animate) {} 47 | tiltBy(angleDelta, animate) {} 48 | 49 | // Rotate (left and right) 50 | rotateTo(angle, animate) {} 51 | rotateBy(angleDelta, animate) {} 52 | 53 | // Fly to the given point, animating pan and tilt/rotation to final position 54 | // with nice zoom out and in 55 | // 56 | // TODO: Calling flyTo a second time before the previous animation has 57 | // completed should immediately start the new animation from wherever the 58 | // previous one has got to 59 | // 60 | // TODO: Long-distance pans should prevent the quadtree grid from trying to 61 | // update by not firing the control update events every frame until the 62 | // pan velocity calms down a bit 63 | // 64 | // TODO: Long-distance plans should zoom out further 65 | flyToPoint(point, duration, zoom) { 66 | // Animation time in seconds 67 | var animationTime = duration || 2; 68 | 69 | this._flyTarget = new THREE.Vector3(point.x, 0, point.y); 70 | 71 | // Calculate delta from current position to fly target 72 | var diff = new THREE.Vector3().subVectors(this._controls.target, this._flyTarget); 73 | 74 | this._flyTween = new TweenLite( 75 | { 76 | x: 0, 77 | z: 0, 78 | // zoom: 0, 79 | prev: { 80 | x: 0, 81 | z: 0 82 | } 83 | }, 84 | animationTime, 85 | { 86 | x: diff.x, 87 | z: diff.z, 88 | // zoom: 1, 89 | onUpdate: function(tween) { 90 | var controls = this._controls; 91 | 92 | // Work out difference since last frame 93 | var deltaX = tween.target.x - tween.target.prev.x; 94 | var deltaZ = tween.target.z - tween.target.prev.z; 95 | 96 | // Move some fraction toward the target point 97 | controls.panLeft(deltaX, controls.object.matrix); 98 | controls.panUp(deltaZ, controls.object.matrix); 99 | 100 | tween.target.prev.x = tween.target.x; 101 | tween.target.prev.z = tween.target.z; 102 | 103 | // console.log(Math.sin((tween.target.zoom - 0.5) * Math.PI)); 104 | 105 | // TODO: Get zoom to dolly in and out on pan 106 | // controls.object.zoom -= Math.sin((tween.target.zoom - 0.5) * Math.PI); 107 | // controls.object.updateProjectionMatrix(); 108 | }, 109 | onComplete: function(tween) { 110 | // console.log(`Arrived at flyTarget`); 111 | this._flyTarget = null; 112 | }, 113 | onUpdateParams: ['{self}'], 114 | onCompleteParams: ['{self}'], 115 | callbackScope: this, 116 | ease: Power1.easeInOut 117 | } 118 | ); 119 | 120 | if (!zoom) { 121 | return; 122 | } 123 | 124 | var zoomTime = animationTime / 2; 125 | 126 | this._zoomTweenIn = new TweenLite( 127 | { 128 | zoom: 0 129 | }, 130 | zoomTime, 131 | { 132 | zoom: 1, 133 | onUpdate: function(tween) { 134 | var controls = this._controls; 135 | controls.dollyIn(1 - 0.01 * tween.target.zoom); 136 | }, 137 | onComplete: function(tween) {}, 138 | onUpdateParams: ['{self}'], 139 | onCompleteParams: ['{self}'], 140 | callbackScope: this, 141 | ease: Power1.easeInOut 142 | } 143 | ); 144 | 145 | this._zoomTweenOut = new TweenLite( 146 | { 147 | zoom: 0 148 | }, 149 | zoomTime, 150 | { 151 | zoom: 1, 152 | delay: zoomTime, 153 | onUpdate: function(tween) { 154 | var controls = this._controls; 155 | controls.dollyOut(0.99 + 0.01 * tween.target.zoom); 156 | }, 157 | onComplete: function(tween) {}, 158 | onUpdateParams: ['{self}'], 159 | onCompleteParams: ['{self}'], 160 | callbackScope: this, 161 | ease: Power1.easeInOut 162 | } 163 | ); 164 | } 165 | 166 | flyToLatLon(latlon, duration, noZoom) { 167 | var point = this._world.latLonToPoint(latlon); 168 | this.flyToPoint(point, duration, noZoom); 169 | } 170 | 171 | // TODO: Make this animate over a user-defined period of time 172 | // 173 | // Perhaps use TweenMax for now and implement as a more lightweight solution 174 | // later on once it all works 175 | // _animateFlyTo(delta) { 176 | // var controls = this._controls; 177 | // 178 | // // this._controls.panLeft(50, controls._controls.object.matrix); 179 | // // this._controls.panUp(50, controls._controls.object.matrix); 180 | // // this._controls.dollyIn(this._controls.getZoomScale()); 181 | // // this._controls.dollyOut(this._controls.getZoomScale()); 182 | // 183 | // // Calculate delta from current position to fly target 184 | // var diff = new THREE.Vector3().subVectors(this._controls.target, this._flyTarget); 185 | // 186 | // // 1000 units per second 187 | // var speed = 1000 * (delta / 1000); 188 | // 189 | // // Remove fly target after arrival and snap to target 190 | // if (diff.length() < 0.01) { 191 | // console.log(`Arrived at flyTarget`); 192 | // this._flyTarget = null; 193 | // speed = 1; 194 | // } 195 | // 196 | // // Move some fraction toward the target point 197 | // controls.panLeft(diff.x * speed, controls.object.matrix); 198 | // controls.panUp(diff.z * speed, controls.object.matrix); 199 | // } 200 | 201 | // Proxy to OrbitControls.update() 202 | update(delta) { 203 | this._controls.update(delta); 204 | } 205 | 206 | // Add controls to world instance and store world reference 207 | addTo(world) { 208 | world.addControls(this); 209 | return this; 210 | } 211 | 212 | // Internal method called by World.addControls to actually add the controls 213 | _addToWorld(world) { 214 | this._world = world; 215 | 216 | // TODO: Override panLeft and panUp methods to prevent panning on Y axis 217 | // See: http://stackoverflow.com/a/26188674/997339 218 | this._controls = new OrbitControls(world._engine._camera, world._container); 219 | 220 | // Disable keys for now as no events are fired for them anyway 221 | this._controls.keys = false; 222 | 223 | // 89 degrees 224 | this._controls.maxPolarAngle = 1.5533; 225 | 226 | // this._controls.enableDamping = true; 227 | // this._controls.dampingFactor = 0.25; 228 | 229 | this._initEvents(); 230 | 231 | this.emit('added'); 232 | } 233 | 234 | // Destroys the controls and removes them from memory 235 | destroy() { 236 | // TODO: Remove event listeners 237 | 238 | this._controls.dispose(); 239 | 240 | this._world = null; 241 | this._controls = null; 242 | } 243 | } 244 | 245 | export default Orbit; 246 | 247 | var noNew = function() { 248 | return new Orbit(); 249 | }; 250 | 251 | // Initialise without requiring new keyword 252 | export {noNew as orbit}; 253 | -------------------------------------------------------------------------------- /test/unit/Geo.js: -------------------------------------------------------------------------------- 1 | import extend from 'lodash.assign'; 2 | import Geo from '../../src/geo/Geo'; 3 | import {latLon as LatLon} from '../../src/geo/LatLon'; 4 | import {point as Point} from '../../src/geo/Point'; 5 | 6 | describe('Geo', () => { 7 | describe('#latLonToPoint', () => { 8 | it('projects the center', () => { 9 | var point = Geo.latLonToPoint(LatLon(0, 0)); 10 | 11 | expect(point.x).to.be.closeTo(0, 0.01); 12 | expect(point.y).to.be.closeTo(0, 0.01); 13 | }); 14 | 15 | it('projects the North-West corner', () => { 16 | var bounds = Geo.bounds; 17 | var point = Geo.latLonToPoint(LatLon(85.0511287798, -180)); 18 | 19 | expect(point.x).to.be.closeTo(bounds[0][0], 0.01); 20 | expect(point.y).to.be.closeTo(bounds[0][1], 0.01); 21 | }); 22 | 23 | it('projects the South-East corner', () => { 24 | var bounds = Geo.bounds; 25 | var point = Geo.latLonToPoint(LatLon(-85.0511287798, 180)); 26 | 27 | expect(point.x).to.be.closeTo(bounds[1][0], 0.01); 28 | expect(point.x).to.be.closeTo(bounds[1][1], 0.01); 29 | }); 30 | }); 31 | 32 | describe('#pointToLatLon', () => { 33 | it('unprojects the center', () => { 34 | var latlon = Geo.pointToLatLon(Point(0, 0)); 35 | 36 | expect(latlon.lat).to.be.closeTo(0, 0.01); 37 | expect(latlon.lon).to.be.closeTo(0, 0.01); 38 | }); 39 | 40 | it('unprojects the North-West corner', () => { 41 | var bounds = Geo.bounds; 42 | var latlon = Geo.pointToLatLon(Point(bounds[0][0], bounds[0][1])); 43 | 44 | expect(latlon.lat).to.be.closeTo(85.0511287798, 0.01); 45 | expect(latlon.lon).to.be.closeTo(-180, 0.01); 46 | }); 47 | 48 | it('unprojects the South-East corner', () => { 49 | var bounds = Geo.bounds; 50 | var latlon = Geo.pointToLatLon(Point(bounds[1][0], bounds[1][1])); 51 | 52 | expect(latlon.lat).to.be.closeTo(-85.0511287798, 0.01); 53 | expect(latlon.lon).to.be.closeTo(180, 0.01); 54 | }); 55 | }); 56 | 57 | describe('#project', () => { 58 | it('projects the center', () => { 59 | var point = Geo.project(LatLon(0, 0)); 60 | 61 | expect(point.x).to.be.closeTo(0, 0.01); 62 | expect(point.y).to.be.closeTo(0, 0.01); 63 | }); 64 | 65 | it('projects the North-West corner', () => { 66 | var point = Geo.project(LatLon(85.0511287798, -180)); 67 | 68 | expect(point.x).to.be.closeTo(-20037508.34279, 0.01); 69 | expect(point.y).to.be.closeTo(20037508.34278, 0.01); 70 | }); 71 | 72 | it('projects the South-East corner', () => { 73 | var point = Geo.project(LatLon(-85.0511287798, 180)); 74 | 75 | expect(point.x).to.be.closeTo(20037508.34278, 0.01); 76 | expect(point.y).to.be.closeTo(-20037508.34278, 0.01); 77 | }); 78 | 79 | it('caps the maximum latitude', () => { 80 | var point = Geo.project(LatLon(-90, 180)); 81 | 82 | expect(point.x).to.be.closeTo(20037508.34278, 0.01); 83 | expect(point.y).to.be.closeTo(-20037508.34278, 0.01); 84 | }); 85 | }); 86 | 87 | describe('#unproject', () => { 88 | it('unprojects the center', () => { 89 | var latlon = Geo.unproject(Point(0, 0)); 90 | 91 | expect(latlon.lat).to.be.closeTo(0, 0.01); 92 | expect(latlon.lon).to.be.closeTo(0, 0.01); 93 | }); 94 | 95 | it('unprojects the North-West corner', () => { 96 | var latlon = Geo.unproject(Point(-20037508.34278, 20037508.34278)); 97 | 98 | expect(latlon.lat).to.be.closeTo(85.0511287798, 0.01); 99 | expect(latlon.lon).to.be.closeTo(-180, 0.01); 100 | }); 101 | 102 | it('unprojects the South-East corner', () => { 103 | var latlon = Geo.unproject(Point(20037508.34278, -20037508.34278)); 104 | 105 | expect(latlon.lat).to.be.closeTo(-85.0511287798, 0.01); 106 | expect(latlon.lon).to.be.closeTo(180, 0.01); 107 | }); 108 | }); 109 | 110 | describe('#scale', () => { 111 | it('defaults to 1', () => { 112 | var scale = Geo.scale(); 113 | expect(scale).to.equal(1); 114 | }); 115 | 116 | it('uses the zoom level if provided', () => { 117 | var scale = Geo.scale(1); 118 | expect(scale).to.equal(512); 119 | }); 120 | }); 121 | 122 | describe('#zoom', () => { 123 | it('returns zoom level for given scale', () => { 124 | var scale = 512; 125 | var zoom = Geo.zoom(scale); 126 | 127 | expect(zoom).to.equal(1); 128 | }); 129 | }); 130 | 131 | // describe('#wrapLatLon', () => { 132 | // it('wraps longitude between -180 and 180 by default', () => { 133 | // expect(Geo.wrapLatLon(LatLon(0, 190)).lon).to.equal(-170); 134 | // expect(Geo.wrapLatLon(LatLon(0, 360)).lon).to.equal(0); 135 | // 136 | // expect(Geo.wrapLatLon(LatLon(0, -190)).lon).to.equal(170); 137 | // expect(Geo.wrapLatLon(LatLon(0, -360)).lon).to.equal(0); 138 | // 139 | // expect(Geo.wrapLatLon(LatLon(0, 0)).lon).to.equal(0); 140 | // expect(Geo.wrapLatLon(LatLon(0, 180)).lon).to.equal(180); 141 | // }); 142 | // 143 | // it('keeps altitude value', () => { 144 | // expect(Geo.wrapLatLon(LatLon(0, 190, 100)).lon).to.equal(-170); 145 | // expect(Geo.wrapLatLon(LatLon(0, 190, 100)).alt).to.equal(100); 146 | // }); 147 | // }); 148 | 149 | describe('#distance', () => { 150 | it('returns correct distance using cosine law approximation', () => { 151 | expect(Geo.distance(LatLon(0, 0), LatLon(0.001, 0))).to.be.closeTo(111.31949492321543, 0.1); 152 | }); 153 | 154 | it('returns correct distance using Haversine', () => { 155 | expect(Geo.distance(LatLon(0, 0), LatLon(0.001, 0), true)).to.be.closeTo(111.3194907932736, 0.1); 156 | }); 157 | }); 158 | 159 | describe('#pointScale', () => { 160 | var pointScale; 161 | 162 | it('returns approximate point scale factor', () => { 163 | pointScale = Geo.pointScale(LatLon(0, 0)); 164 | 165 | expect(pointScale[0]).to.be.closeTo(1, 0.1); 166 | expect(pointScale[1]).to.be.closeTo(1, 0.1); 167 | 168 | pointScale = Geo.pointScale(LatLon(60, 0)); 169 | 170 | expect(pointScale[0]).to.be.closeTo(1.9999999999999996, 0.1); 171 | expect(pointScale[1]).to.be.closeTo(1.9999999999999996, 0.1); 172 | }); 173 | 174 | it('returns accurate point scale factor', () => { 175 | pointScale = Geo.pointScale(LatLon(0, 0), true); 176 | 177 | expect(pointScale[0]).to.be.closeTo(1, 0.1); 178 | expect(pointScale[1]).to.be.closeTo(1.0067394967683778, 0.1); 179 | 180 | pointScale = Geo.pointScale(LatLon(60, 0), true); 181 | 182 | expect(pointScale[0]).to.be.closeTo(1.994972897047054, 0.1); 183 | expect(pointScale[1]).to.be.closeTo(1.9983341753952164, 0.1); 184 | }); 185 | }); 186 | 187 | describe('#metresToProjected', () => { 188 | var pointScale; 189 | 190 | it('returns correct projected units', () => { 191 | pointScale = Geo.pointScale(LatLon(0, 0)); 192 | expect(Geo.metresToProjected(1, pointScale)).to.be.closeTo(1, 0.1); 193 | 194 | pointScale = Geo.pointScale(LatLon(60, 0)); 195 | expect(Geo.metresToProjected(1, pointScale)).to.be.closeTo(1.9999999999999996, 0.1); 196 | }); 197 | }); 198 | 199 | describe('#projectedToMetres', () => { 200 | var pointScale; 201 | 202 | it('returns correct real metres', () => { 203 | pointScale = Geo.pointScale(LatLon(0, 0)); 204 | expect(Geo.projectedToMetres(1, pointScale)).to.be.closeTo(1, 0.1); 205 | 206 | pointScale = Geo.pointScale(LatLon(60, 0)); 207 | expect(Geo.projectedToMetres(1.9999999999999996, pointScale)).to.be.closeTo(1, 0.1); 208 | }); 209 | }); 210 | 211 | // These two are combined as it is hard to write an invidual test that can 212 | // be specified without knowing or redifining Geo.scaleFactor 213 | describe('#metresToWorld & #worldToMetres', () => { 214 | var pointScale; 215 | var worldUnits; 216 | var metres; 217 | 218 | it('returns correct world units', () => { 219 | pointScale = Geo.pointScale(LatLon(0, 0)); 220 | worldUnits = Geo.metresToWorld(1, pointScale); 221 | metres = Geo.worldToMetres(worldUnits, pointScale); 222 | 223 | expect(metres).to.be.closeTo(1, 0.1); 224 | 225 | pointScale = Geo.pointScale(LatLon(60, 0)); 226 | worldUnits = Geo.metresToWorld(1, pointScale); 227 | metres = Geo.worldToMetres(worldUnits, pointScale); 228 | 229 | expect(metres).to.be.closeTo(1, 0.1); 230 | }); 231 | }); 232 | }); 233 | -------------------------------------------------------------------------------- /src/util/GeoJSON.js: -------------------------------------------------------------------------------- 1 | /* 2 | * GeoJSON helpers for handling data and generating objects 3 | */ 4 | 5 | import THREE from 'three'; 6 | import * as topojson from 'topojson'; 7 | import geojsonMerge from 'geojson-merge'; 8 | import earcut from 'earcut'; 9 | import extrudePolygon from './extrudePolygon'; 10 | 11 | // TODO: Make it so height can be per-coordinate / point but connected together 12 | // as a linestring (eg. GPS points with an elevation at each point) 13 | // 14 | // This isn't really valid GeoJSON so perhaps something best left to an external 15 | // component for now, until a better approach can be considered 16 | // 17 | // See: http://lists.geojson.org/pipermail/geojson-geojson.org/2009-June/000489.html 18 | 19 | // Light and dark colours used for poor-mans AO gradient on object sides 20 | var light = new THREE.Color(0xffffff); 21 | var shadow = new THREE.Color(0x666666); 22 | 23 | var GeoJSON = (function() { 24 | var defaultStyle = { 25 | color: '#ffffff', 26 | transparent: false, 27 | opacity: 1, 28 | blending: THREE.NormalBlending, 29 | height: 0, 30 | lineOpacity: 1, 31 | lineTransparent: false, 32 | lineColor: '#ffffff', 33 | lineWidth: 1, 34 | lineBlending: THREE.NormalBlending 35 | }; 36 | 37 | // Attempts to merge together multiple GeoJSON Features or FeatureCollections 38 | // into a single FeatureCollection 39 | var collectFeatures = function(data, _topojson) { 40 | var collections = []; 41 | 42 | if (_topojson) { 43 | // TODO: Allow TopoJSON objects to be overridden as an option 44 | 45 | // If not overridden, merge all features from all objects 46 | for (var tk in data.objects) { 47 | collections.push(topojson.feature(data, data.objects[tk])); 48 | } 49 | 50 | return geojsonMerge(collections); 51 | } else { 52 | // If root doesn't have a type then let's see if there are features in the 53 | // next step down 54 | if (!data.type) { 55 | // TODO: Allow GeoJSON objects to be overridden as an option 56 | 57 | // If not overridden, merge all features from all objects 58 | for (var gk in data) { 59 | if (!data[gk].type) { 60 | continue; 61 | } 62 | 63 | collections.push(data[gk]); 64 | } 65 | 66 | return geojsonMerge(collections); 67 | } else if (Array.isArray(data)) { 68 | return geojsonMerge(data); 69 | } else { 70 | return data; 71 | } 72 | } 73 | }; 74 | 75 | // TODO: This is only used by GeoJSONTile so either roll it into that or 76 | // update GeoJSONTile to use the new GeoJSONLayer or geometry layers 77 | var lineStringAttributes = function(coordinates, colour, height) { 78 | var _coords = []; 79 | var _colours = []; 80 | 81 | var nextCoord; 82 | 83 | // Connect coordinate with the next to make a pair 84 | // 85 | // LineSegments requires pairs of vertices so repeat the last point if 86 | // there's an odd number of vertices 87 | coordinates.forEach((coordinate, index) => { 88 | _colours.push([colour.r, colour.g, colour.b]); 89 | _coords.push([coordinate[0], height, coordinate[1]]); 90 | 91 | nextCoord = (coordinates[index + 1]) ? coordinates[index + 1] : coordinate; 92 | 93 | _colours.push([colour.r, colour.g, colour.b]); 94 | _coords.push([nextCoord[0], height, nextCoord[1]]); 95 | }); 96 | 97 | return { 98 | vertices: _coords, 99 | colours: _colours 100 | }; 101 | }; 102 | 103 | // TODO: This is only used by GeoJSONTile so either roll it into that or 104 | // update GeoJSONTile to use the new GeoJSONLayer or geometry layers 105 | var multiLineStringAttributes = function(coordinates, colour, height) { 106 | var _coords = []; 107 | var _colours = []; 108 | 109 | var result; 110 | coordinates.forEach(coordinate => { 111 | result = lineStringAttributes(coordinate, colour, height); 112 | 113 | result.vertices.forEach(coord => { 114 | _coords.push(coord); 115 | }); 116 | 117 | result.colours.forEach(colour => { 118 | _colours.push(colour); 119 | }); 120 | }); 121 | 122 | return { 123 | vertices: _coords, 124 | colours: _colours 125 | }; 126 | }; 127 | 128 | // TODO: This is only used by GeoJSONTile so either roll it into that or 129 | // update GeoJSONTile to use the new GeoJSONLayer or geometry layers 130 | var polygonAttributes = function(coordinates, colour, height) { 131 | var earcutData = _toEarcut(coordinates); 132 | 133 | var faces = _triangulate(earcutData.vertices, earcutData.holes, earcutData.dimensions); 134 | 135 | var groupedVertices = []; 136 | for (i = 0, il = earcutData.vertices.length; i < il; i += earcutData.dimensions) { 137 | groupedVertices.push(earcutData.vertices.slice(i, i + earcutData.dimensions)); 138 | } 139 | 140 | var extruded = extrudePolygon(groupedVertices, faces, { 141 | bottom: 0, 142 | top: height 143 | }); 144 | 145 | var topColor = colour.clone().multiply(light); 146 | var bottomColor = colour.clone().multiply(shadow); 147 | 148 | var _vertices = extruded.positions; 149 | var _faces = []; 150 | var _colours = []; 151 | 152 | var _colour; 153 | extruded.top.forEach((face, fi) => { 154 | _colour = []; 155 | 156 | _colour.push([colour.r, colour.g, colour.b]); 157 | _colour.push([colour.r, colour.g, colour.b]); 158 | _colour.push([colour.r, colour.g, colour.b]); 159 | 160 | _faces.push(face); 161 | _colours.push(_colour); 162 | }); 163 | 164 | var allFlat = true; 165 | 166 | if (extruded.sides) { 167 | if (allFlat) { 168 | allFlat = false; 169 | } 170 | 171 | // Set up colours for every vertex with poor-mans AO on the sides 172 | extruded.sides.forEach((face, fi) => { 173 | _colour = []; 174 | 175 | // First face is always bottom-bottom-top 176 | if (fi % 2 === 0) { 177 | _colour.push([bottomColor.r, bottomColor.g, bottomColor.b]); 178 | _colour.push([bottomColor.r, bottomColor.g, bottomColor.b]); 179 | _colour.push([topColor.r, topColor.g, topColor.b]); 180 | // Reverse winding for the second face 181 | // top-top-bottom 182 | } else { 183 | _colour.push([topColor.r, topColor.g, topColor.b]); 184 | _colour.push([topColor.r, topColor.g, topColor.b]); 185 | _colour.push([bottomColor.r, bottomColor.g, bottomColor.b]); 186 | } 187 | 188 | _faces.push(face); 189 | _colours.push(_colour); 190 | }); 191 | } 192 | 193 | // Skip bottom as there's no point rendering it 194 | // allFaces.push(extruded.faces); 195 | 196 | return { 197 | vertices: _vertices, 198 | faces: _faces, 199 | colours: _colours, 200 | flat: allFlat 201 | }; 202 | }; 203 | 204 | // TODO: This is only used by GeoJSONTile so either roll it into that or 205 | // update GeoJSONTile to use the new GeoJSONLayer or geometry layers 206 | var _toEarcut = function(data) { 207 | var dim = data[0][0].length; 208 | var result = {vertices: [], holes: [], dimensions: dim}; 209 | var holeIndex = 0; 210 | 211 | for (var i = 0; i < data.length; i++) { 212 | for (var j = 0; j < data[i].length; j++) { 213 | for (var d = 0; d < dim; d++) { 214 | result.vertices.push(data[i][j][d]); 215 | } 216 | } 217 | if (i > 0) { 218 | holeIndex += data[i - 1].length; 219 | result.holes.push(holeIndex); 220 | } 221 | } 222 | 223 | return result; 224 | }; 225 | 226 | // TODO: This is only used by GeoJSONTile so either roll it into that or 227 | // update GeoJSONTile to use the new GeoJSONLayer or geometry layers 228 | var _triangulate = function(contour, holes, dim) { 229 | // console.time('earcut'); 230 | 231 | var faces = earcut(contour, holes, dim); 232 | var result = []; 233 | 234 | for (i = 0, il = faces.length; i < il; i += 3) { 235 | result.push(faces.slice(i, i + 3)); 236 | } 237 | 238 | // console.timeEnd('earcut'); 239 | 240 | return result; 241 | }; 242 | 243 | return { 244 | defaultStyle: defaultStyle, 245 | collectFeatures: collectFeatures, 246 | lineStringAttributes: lineStringAttributes, 247 | multiLineStringAttributes: multiLineStringAttributes, 248 | polygonAttributes: polygonAttributes 249 | }; 250 | })(); 251 | 252 | export default GeoJSON; 253 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import loadPlugins from 'gulp-load-plugins'; 3 | import del from 'del'; 4 | import glob from 'glob'; 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import webpackStream from 'webpack-stream'; 8 | import source from 'vinyl-source-stream'; 9 | import { Instrumenter } from 'isparta'; 10 | 11 | import manifest from './package.json'; 12 | 13 | // TODO: Re-implement build process to utilise proper caching 14 | // TODO: Consider bundling three.js within the final build, or at least having 15 | // a different build step for an all-in-one file 16 | 17 | // Load all of our Gulp plugins 18 | const $ = loadPlugins(); 19 | 20 | // Gather the library data from `package.json` 21 | const config = manifest.babelBoilerplateOptions; 22 | const mainFile = manifest.main; 23 | const destinationFolder = path.dirname(mainFile); 24 | const exportFileName = path.basename(mainFile, path.extname(mainFile)); 25 | 26 | // Remove a directory 27 | function _clean(dir, done) { 28 | del([dir], done); 29 | } 30 | 31 | function cleanDist(done) { 32 | _clean(destinationFolder, done); 33 | } 34 | 35 | function cleanTmp(done) { 36 | _clean('tmp', done); 37 | } 38 | 39 | function onError() { 40 | $.util.beep(); 41 | } 42 | 43 | // Lint a set of files 44 | function lint(files) { 45 | return gulp.src(files) 46 | .pipe($.plumber()) 47 | .pipe($.eslint()) 48 | .pipe($.eslint.format()) 49 | .pipe($.eslint.failOnError()) 50 | .pipe($.jscs()) 51 | .pipe($.jscs.reporter('fail')) 52 | .on('error', onError); 53 | } 54 | 55 | function lintSrc() { 56 | return lint('src/**/*.js'); 57 | } 58 | 59 | function lintTest() { 60 | return lint('test/**/*.js'); 61 | } 62 | 63 | function lintGulpfile() { 64 | return lint('gulpfile.babel.js'); 65 | } 66 | 67 | function build() { 68 | return gulp.src(path.join('src', config.entryFileName + '.js')) 69 | .pipe($.plumber()) 70 | .pipe(webpackStream({ 71 | output: { 72 | filename: exportFileName + '.js', 73 | libraryTarget: 'umd', 74 | library: config.mainVarName 75 | }, 76 | externals: { 77 | // Proxy the global THREE variable to require('three') 78 | 'three': 'THREE', 79 | // Proxy the global THREE variable to require('TweenLite') 80 | 'TweenLite': 'TweenLite', 81 | // Proxy the global THREE variable to require('TweenMax') 82 | 'TweenMax': 'TweenMax', 83 | // Proxy the global THREE variable to require('TimelineLite') 84 | 'TimelineLite': 'TimelineLite', 85 | // Proxy the global THREE variable to require('TimelineMax') 86 | 'TimelineMax': 'TimelineMax', 87 | // Proxy the global proj4 variable to require('proj4') 88 | 'proj4': 'proj4' 89 | }, 90 | module: { 91 | loaders: [ 92 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } 93 | ] 94 | }, 95 | devtool: 'source-map' 96 | })) 97 | .pipe(gulp.dest(destinationFolder)) 98 | .pipe($.filter(['*', '!**/*.js.map'])) 99 | .pipe($.rename(exportFileName + '.min.js')) 100 | .pipe($.sourcemaps.init({ loadMaps: true })) 101 | 102 | // Don't mangle class names so we can use them in the console 103 | // jscs:disable 104 | // .pipe($.uglify({ mangle: { keep_fnames: true }})) 105 | // jscs:enable 106 | 107 | // Using the mangle option above breaks the sourcemap for some reason 108 | .pipe($.uglify()) 109 | 110 | .pipe($.sourcemaps.write('./')) 111 | .pipe(gulp.dest(destinationFolder)) 112 | .pipe($.livereload()); 113 | } 114 | 115 | function moveCSS() { 116 | return gulp.src(path.join('src', config.entryFileName + '.css')) 117 | .pipe(gulp.dest(destinationFolder)); 118 | } 119 | 120 | function _mocha() { 121 | return gulp.src(['test/setup/node.js', 'test/unit/**/*.js'], {read: false}) 122 | .pipe($.mocha({ 123 | reporter: 'dot', 124 | globals: config.mochaGlobals, 125 | ignoreLeaks: false 126 | })); 127 | } 128 | 129 | function _registerBabel() { 130 | require('babel-core/register'); 131 | } 132 | 133 | function test() { 134 | _registerBabel(); 135 | return _mocha(); 136 | } 137 | 138 | function coverage(done) { 139 | _registerBabel(); 140 | gulp.src(['src/**/*.js']) 141 | .pipe($.istanbul({ instrumenter: Instrumenter })) 142 | .pipe($.istanbul.hookRequire()) 143 | .on('finish', () => { 144 | return test() 145 | .pipe($.istanbul.writeReports()) 146 | .on('end', done); 147 | }); 148 | } 149 | 150 | const watchFiles = ['src/**/*', 'test/**/*', 'package.json', '**/.eslintrc', '.jscsrc']; 151 | 152 | // Run the headless unit tests as you make changes. 153 | function watch() { 154 | $.livereload.listen(); 155 | gulp.watch(watchFiles, ['build']); 156 | // gulp.watch(watchFiles, ['test']); 157 | } 158 | 159 | function testBrowser() { 160 | // Our testing bundle is made up of our unit tests, which 161 | // should individually load up pieces of our application. 162 | // We also include the browser setup file. 163 | const testFiles = glob.sync('./test/unit/**/*.js'); 164 | const allFiles = ['./test/setup/browser.js'].concat(testFiles); 165 | 166 | // Lets us differentiate between the first build and subsequent builds 167 | var firstBuild = true; 168 | 169 | // This empty stream might seem like a hack, but we need to specify all of our files through 170 | // the `entry` option of webpack. Otherwise, it ignores whatever file(s) are placed in here. 171 | return gulp.src('') 172 | .pipe($.plumber()) 173 | .pipe(webpackStream({ 174 | watch: true, 175 | entry: allFiles, 176 | output: { 177 | filename: '__spec-build.js' 178 | }, 179 | externals: { 180 | // Proxy the global THREE variable to require('three') 181 | 'three': 'THREE', 182 | // Proxy the global THREE variable to require('TweenLite') 183 | 'TweenLite': 'TweenLite', 184 | // Proxy the global THREE variable to require('TweenMax') 185 | 'TweenMax': 'TweenMax', 186 | // Proxy the global THREE variable to require('TimelineLite') 187 | 'TimelineLite': 'TimelineLite', 188 | // Proxy the global THREE variable to require('TimelineMax') 189 | 'TimelineMax': 'TimelineMax', 190 | // Proxy the global proj4 variable to require('proj4') 191 | 'proj4': 'proj4' 192 | }, 193 | module: { 194 | loaders: [ 195 | // This is what allows us to author in future JavaScript 196 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }, 197 | // This allows the test setup scripts to load `package.json` 198 | { test: /\.json$/, exclude: /node_modules/, loader: 'json-loader' } 199 | ] 200 | }, 201 | plugins: [ 202 | // By default, webpack does `n=>n` compilation with entry files. This concatenates 203 | // them into a single chunk. 204 | new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }) 205 | ], 206 | devtool: 'inline-source-map' 207 | }, null, function() { 208 | if (firstBuild) { 209 | $.livereload.listen({port: 35729, host: 'localhost', start: true}); 210 | var watcher = gulp.watch(watchFiles, ['lint']); 211 | } else { 212 | $.livereload.reload('./tmp/__spec-build.js'); 213 | } 214 | firstBuild = false; 215 | })) 216 | .pipe(gulp.dest('./tmp')); 217 | } 218 | 219 | // Remove the built files 220 | gulp.task('clean', cleanDist); 221 | 222 | // Remove our temporary files 223 | gulp.task('clean-tmp', cleanTmp); 224 | 225 | // Lint our source code 226 | gulp.task('lint-src', lintSrc); 227 | 228 | // Lint our test code 229 | gulp.task('lint-test', lintTest); 230 | 231 | // Lint this file 232 | gulp.task('lint-gulpfile', lintGulpfile); 233 | 234 | // Lint everything 235 | gulp.task('lint', ['lint-src', 'lint-test', 'lint-gulpfile']); 236 | 237 | // Move CSS 238 | gulp.task('move-css', ['clean'], moveCSS); 239 | 240 | // Build two versions of the library 241 | gulp.task('build', ['lint', 'move-css'], build); 242 | 243 | // Lint and run our tests 244 | gulp.task('test', ['lint'], test); 245 | 246 | // Set up coverage and run tests 247 | gulp.task('coverage', ['lint'], coverage); 248 | 249 | // Set up a livereload environment for our spec runner `test/runner.html` 250 | gulp.task('test-browser', ['lint', 'clean-tmp'], testBrowser); 251 | 252 | // Run the headless unit tests as you make changes. 253 | gulp.task('watch', watch); 254 | 255 | // An alias of test 256 | gulp.task('default', ['test']); 257 | -------------------------------------------------------------------------------- /src/layer/environment/Sky.js: -------------------------------------------------------------------------------- 1 | // jscs:disable 2 | /*eslint eqeqeq:0*/ 3 | 4 | /** 5 | * @author zz85 / https://github.com/zz85 6 | * 7 | * Based on 'A Practical Analytic Model for Daylight' 8 | * aka The Preetham Model, the de facto standard analytic skydome model 9 | * http://www.cs.utah.edu/~shirley/papers/sunsky/sunsky.pdf 10 | * 11 | * First implemented by Simon Wallner 12 | * http://www.simonwallner.at/projects/atmospheric-scattering 13 | * 14 | * Improved by Martin Upitis 15 | * http://blenderartists.org/forum/showthread.php?245954-preethams-sky-impementation-HDR 16 | * 17 | * Three.js integration by zz85 http://twitter.com/blurspline 18 | */ 19 | 20 | import THREE from 'three'; 21 | 22 | THREE.ShaderLib[ 'sky' ] = { 23 | 24 | uniforms: { 25 | 26 | luminance: { type: 'f', value: 1 }, 27 | turbidity: { type: 'f', value: 2 }, 28 | reileigh: { type: 'f', value: 1 }, 29 | mieCoefficient: { type: 'f', value: 0.005 }, 30 | mieDirectionalG: { type: 'f', value: 0.8 }, 31 | sunPosition: { type: 'v3', value: new THREE.Vector3() } 32 | 33 | }, 34 | 35 | vertexShader: [ 36 | 37 | 'varying vec3 vWorldPosition;', 38 | 39 | 'void main() {', 40 | 41 | 'vec4 worldPosition = modelMatrix * vec4( position, 1.0 );', 42 | 'vWorldPosition = worldPosition.xyz;', 43 | 44 | 'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', 45 | 46 | '}', 47 | 48 | ].join( '\n' ), 49 | 50 | fragmentShader: [ 51 | 52 | 'uniform sampler2D skySampler;', 53 | 'uniform vec3 sunPosition;', 54 | 'varying vec3 vWorldPosition;', 55 | 56 | 'vec3 cameraPos = vec3(0., 0., 0.);', 57 | '// uniform sampler2D sDiffuse;', 58 | '// const float turbidity = 10.0; //', 59 | '// const float reileigh = 2.; //', 60 | '// const float luminance = 1.0; //', 61 | '// const float mieCoefficient = 0.005;', 62 | '// const float mieDirectionalG = 0.8;', 63 | 64 | 'uniform float luminance;', 65 | 'uniform float turbidity;', 66 | 'uniform float reileigh;', 67 | 'uniform float mieCoefficient;', 68 | 'uniform float mieDirectionalG;', 69 | 70 | '// constants for atmospheric scattering', 71 | 'const float e = 2.71828182845904523536028747135266249775724709369995957;', 72 | 'const float pi = 3.141592653589793238462643383279502884197169;', 73 | 74 | 'const float n = 1.0003; // refractive index of air', 75 | 'const float N = 2.545E25; // number of molecules per unit volume for air at', 76 | '// 288.15K and 1013mb (sea level -45 celsius)', 77 | 'const float pn = 0.035; // depolatization factor for standard air', 78 | 79 | '// wavelength of used primaries, according to preetham', 80 | 'const vec3 lambda = vec3(680E-9, 550E-9, 450E-9);', 81 | 82 | '// mie stuff', 83 | '// K coefficient for the primaries', 84 | 'const vec3 K = vec3(0.686, 0.678, 0.666);', 85 | 'const float v = 4.0;', 86 | 87 | '// optical length at zenith for molecules', 88 | 'const float rayleighZenithLength = 8.4E3;', 89 | 'const float mieZenithLength = 1.25E3;', 90 | 'const vec3 up = vec3(0.0, 1.0, 0.0);', 91 | 92 | 'const float EE = 1000.0;', 93 | 'const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;', 94 | '// 66 arc seconds -> degrees, and the cosine of that', 95 | 96 | '// earth shadow hack', 97 | 'const float cutoffAngle = pi/1.95;', 98 | 'const float steepness = 1.5;', 99 | 100 | 101 | 'vec3 totalRayleigh(vec3 lambda)', 102 | '{', 103 | 'return (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn));', 104 | '}', 105 | 106 | // see http://blenderartists.org/forum/showthread.php?321110-Shaders-and-Skybox-madness 107 | '// A simplied version of the total Reayleigh scattering to works on browsers that use ANGLE', 108 | 'vec3 simplifiedRayleigh()', 109 | '{', 110 | 'return 0.0005 / vec3(94, 40, 18);', 111 | // return 0.00054532832366 / (3.0 * 2.545E25 * pow(vec3(680E-9, 550E-9, 450E-9), vec3(4.0)) * 6.245); 112 | '}', 113 | 114 | 'float rayleighPhase(float cosTheta)', 115 | '{ ', 116 | 'return (3.0 / (16.0*pi)) * (1.0 + pow(cosTheta, 2.0));', 117 | '// return (1.0 / (3.0*pi)) * (1.0 + pow(cosTheta, 2.0));', 118 | '// return (3.0 / 4.0) * (1.0 + pow(cosTheta, 2.0));', 119 | '}', 120 | 121 | 'vec3 totalMie(vec3 lambda, vec3 K, float T)', 122 | '{', 123 | 'float c = (0.2 * T ) * 10E-18;', 124 | 'return 0.434 * c * pi * pow((2.0 * pi) / lambda, vec3(v - 2.0)) * K;', 125 | '}', 126 | 127 | 'float hgPhase(float cosTheta, float g)', 128 | '{', 129 | 'return (1.0 / (4.0*pi)) * ((1.0 - pow(g, 2.0)) / pow(1.0 - 2.0*g*cosTheta + pow(g, 2.0), 1.5));', 130 | '}', 131 | 132 | 'float sunIntensity(float zenithAngleCos)', 133 | '{', 134 | 'return EE * max(0.0, 1.0 - exp(-((cutoffAngle - acos(zenithAngleCos))/steepness)));', 135 | '}', 136 | 137 | '// float logLuminance(vec3 c)', 138 | '// {', 139 | '// return log(c.r * 0.2126 + c.g * 0.7152 + c.b * 0.0722);', 140 | '// }', 141 | 142 | '// Filmic ToneMapping http://filmicgames.com/archives/75', 143 | 'float A = 0.15;', 144 | 'float B = 0.50;', 145 | 'float C = 0.10;', 146 | 'float D = 0.20;', 147 | 'float E = 0.02;', 148 | 'float F = 0.30;', 149 | 'float W = 1000.0;', 150 | 151 | 'vec3 Uncharted2Tonemap(vec3 x)', 152 | '{', 153 | 'return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;', 154 | '}', 155 | 156 | 157 | 'void main() ', 158 | '{', 159 | 'float sunfade = 1.0-clamp(1.0-exp((sunPosition.y/450000.0)),0.0,1.0);', 160 | 161 | '// luminance = 1.0 ;// vWorldPosition.y / 450000. + 0.5; //sunPosition.y / 450000. * 1. + 0.5;', 162 | 163 | '// gl_FragColor = vec4(sunfade, sunfade, sunfade, 1.0);', 164 | 165 | 'float reileighCoefficient = reileigh - (1.0* (1.0-sunfade));', 166 | 167 | 'vec3 sunDirection = normalize(sunPosition);', 168 | 169 | 'float sunE = sunIntensity(dot(sunDirection, up));', 170 | 171 | '// extinction (absorbtion + out scattering) ', 172 | '// rayleigh coefficients', 173 | 174 | // 'vec3 betaR = totalRayleigh(lambda) * reileighCoefficient;', 175 | 'vec3 betaR = simplifiedRayleigh() * reileighCoefficient;', 176 | 177 | '// mie coefficients', 178 | 'vec3 betaM = totalMie(lambda, K, turbidity) * mieCoefficient;', 179 | 180 | '// optical length', 181 | '// cutoff angle at 90 to avoid singularity in next formula.', 182 | 'float zenithAngle = acos(max(0.0, dot(up, normalize(vWorldPosition - cameraPos))));', 183 | 'float sR = rayleighZenithLength / (cos(zenithAngle) + 0.15 * pow(93.885 - ((zenithAngle * 180.0) / pi), -1.253));', 184 | 'float sM = mieZenithLength / (cos(zenithAngle) + 0.15 * pow(93.885 - ((zenithAngle * 180.0) / pi), -1.253));', 185 | 186 | 187 | 188 | '// combined extinction factor ', 189 | 'vec3 Fex = exp(-(betaR * sR + betaM * sM));', 190 | 191 | '// in scattering', 192 | 'float cosTheta = dot(normalize(vWorldPosition - cameraPos), sunDirection);', 193 | 194 | 'float rPhase = rayleighPhase(cosTheta*0.5+0.5);', 195 | 'vec3 betaRTheta = betaR * rPhase;', 196 | 197 | 'float mPhase = hgPhase(cosTheta, mieDirectionalG);', 198 | 'vec3 betaMTheta = betaM * mPhase;', 199 | 200 | 201 | 'vec3 Lin = pow(sunE * ((betaRTheta + betaMTheta) / (betaR + betaM)) * (1.0 - Fex),vec3(1.5));', 202 | 'Lin *= mix(vec3(1.0),pow(sunE * ((betaRTheta + betaMTheta) / (betaR + betaM)) * Fex,vec3(1.0/2.0)),clamp(pow(1.0-dot(up, sunDirection),5.0),0.0,1.0));', 203 | 204 | '//nightsky', 205 | 'vec3 direction = normalize(vWorldPosition - cameraPos);', 206 | 'float theta = acos(direction.y); // elevation --> y-axis, [-pi/2, pi/2]', 207 | 'float phi = atan(direction.z, direction.x); // azimuth --> x-axis [-pi/2, pi/2]', 208 | 'vec2 uv = vec2(phi, theta) / vec2(2.0*pi, pi) + vec2(0.5, 0.0);', 209 | '// vec3 L0 = texture2D(skySampler, uv).rgb+0.1 * Fex;', 210 | 'vec3 L0 = vec3(0.1) * Fex;', 211 | 212 | '// composition + solar disc', 213 | '//if (cosTheta > sunAngularDiameterCos)', 214 | 'float sundisk = smoothstep(sunAngularDiameterCos,sunAngularDiameterCos+0.00002,cosTheta);', 215 | '// if (normalize(vWorldPosition - cameraPos).y>0.0)', 216 | 'L0 += (sunE * 19000.0 * Fex)*sundisk;', 217 | 218 | 219 | 'vec3 whiteScale = 1.0/Uncharted2Tonemap(vec3(W));', 220 | 221 | 'vec3 texColor = (Lin+L0); ', 222 | 'texColor *= 0.04 ;', 223 | 'texColor += vec3(0.0,0.001,0.0025)*0.3;', 224 | 225 | 'float g_fMaxLuminance = 1.0;', 226 | 'float fLumScaled = 0.1 / luminance; ', 227 | 'float fLumCompressed = (fLumScaled * (1.0 + (fLumScaled / (g_fMaxLuminance * g_fMaxLuminance)))) / (1.0 + fLumScaled); ', 228 | 229 | 'float ExposureBias = fLumCompressed;', 230 | 231 | 'vec3 curr = Uncharted2Tonemap((log2(2.0/pow(luminance,4.0)))*texColor);', 232 | 'vec3 color = curr*whiteScale;', 233 | 234 | 'vec3 retColor = pow(color,vec3(1.0/(1.2+(1.2*sunfade))));', 235 | 236 | 237 | 'gl_FragColor.rgb = retColor;', 238 | 239 | 'gl_FragColor.a = 1.0;', 240 | '}', 241 | 242 | ].join( '\n' ) 243 | 244 | }; 245 | 246 | var Sky = function () { 247 | 248 | var skyShader = THREE.ShaderLib[ 'sky' ]; 249 | var skyUniforms = THREE.UniformsUtils.clone( skyShader.uniforms ); 250 | 251 | var skyMat = new THREE.ShaderMaterial( { 252 | fragmentShader: skyShader.fragmentShader, 253 | vertexShader: skyShader.vertexShader, 254 | uniforms: skyUniforms, 255 | side: THREE.BackSide 256 | } ); 257 | 258 | var skyGeo = new THREE.SphereBufferGeometry( 450000, 32, 15 ); 259 | var skyMesh = new THREE.Mesh( skyGeo, skyMat ); 260 | 261 | 262 | // Expose variables 263 | this.mesh = skyMesh; 264 | this.uniforms = skyUniforms; 265 | 266 | }; 267 | 268 | export default Sky; 269 | -------------------------------------------------------------------------------- /src/World.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import extend from 'lodash.assign'; 3 | import Geo from './geo/Geo'; 4 | import {point as Point} from './geo/Point'; 5 | import {latLon as LatLon} from './geo/LatLon'; 6 | import Engine from './engine/Engine'; 7 | import EnvironmentLayer from './layer/environment/EnvironmentLayer'; 8 | 9 | // TODO: Make sure nothing is left behind in the heap after calling destroy() 10 | 11 | // Pretty much any event someone using ViziCities would need will be emitted or 12 | // proxied by World (eg. render events, etc) 13 | 14 | class World extends EventEmitter { 15 | constructor(domId, options) { 16 | super(); 17 | 18 | var defaults = { 19 | skybox: false, 20 | postProcessing: false 21 | }; 22 | 23 | this.options = extend({}, defaults, options); 24 | 25 | this._layers = []; 26 | this._controls = []; 27 | 28 | this._initContainer(domId); 29 | this._initAttribution(); 30 | this._initEngine(); 31 | this._initEnvironment(); 32 | this._initEvents(); 33 | 34 | this._pause = false; 35 | 36 | // Kick off the update and render loop 37 | this._update(); 38 | } 39 | 40 | _initContainer(domId) { 41 | this._container = document.getElementById(domId); 42 | } 43 | 44 | _initAttribution() { 45 | var message = 'Powered by ViziCities'; 46 | 47 | var element = document.createElement('div'); 48 | element.classList.add('vizicities-attribution'); 49 | 50 | element.innerHTML = message; 51 | 52 | this._container.appendChild(element); 53 | } 54 | 55 | _initEngine() { 56 | this._engine = new Engine(this._container, this); 57 | 58 | // Engine events 59 | // 60 | // Consider proxying these through events on World for public access 61 | // this._engine.on('preRender', () => {}); 62 | // this._engine.on('postRender', () => {}); 63 | } 64 | 65 | _initEnvironment() { 66 | // Not sure if I want to keep this as a private API 67 | // 68 | // Makes sense to allow others to customise their environment so perhaps 69 | // add some method of disable / overriding the environment settings 70 | this._environment = new EnvironmentLayer({ 71 | skybox: this.options.skybox 72 | }).addTo(this); 73 | } 74 | 75 | _initEvents() { 76 | this.on('controlsMoveEnd', this._onControlsMoveEnd); 77 | } 78 | 79 | _onControlsMoveEnd(point) { 80 | var _point = Point(point.x, point.z); 81 | this._resetView(this.pointToLatLon(_point), _point); 82 | } 83 | 84 | // Reset world view 85 | _resetView(latlon, point) { 86 | this.emit('preResetView'); 87 | 88 | this._moveStart(); 89 | this._move(latlon, point); 90 | this._moveEnd(); 91 | 92 | this.emit('postResetView'); 93 | } 94 | 95 | _moveStart() { 96 | this.emit('moveStart'); 97 | } 98 | 99 | _move(latlon, point) { 100 | this._lastPosition = latlon; 101 | this.emit('move', latlon, point); 102 | } 103 | _moveEnd() { 104 | this.emit('moveEnd'); 105 | } 106 | 107 | _update() { 108 | if (this._pause) { 109 | return; 110 | } 111 | 112 | var delta = this._engine.clock.getDelta(); 113 | 114 | // Once _update is called it will run forever, for now 115 | window.requestAnimationFrame(this._update.bind(this)); 116 | 117 | // Update controls 118 | this._controls.forEach(controls => { 119 | controls.update(delta); 120 | }); 121 | 122 | this.emit('preUpdate', delta); 123 | this._engine.update(delta); 124 | this.emit('postUpdate', delta); 125 | } 126 | 127 | // Set world view 128 | setView(latlon) { 129 | // Store initial geographic coordinate for the [0,0,0] world position 130 | // 131 | // The origin point doesn't move in three.js / 3D space so only set it once 132 | // here instead of every time _resetView is called 133 | // 134 | // If it was updated every time then coorindates would shift over time and 135 | // would be out of place / context with previously-placed points (0,0 would 136 | // refer to a different point each time) 137 | this._originLatlon = latlon; 138 | this._originPoint = this.project(latlon); 139 | 140 | this._resetView(latlon); 141 | return this; 142 | } 143 | 144 | // Return world geographic position 145 | getPosition() { 146 | return this._lastPosition; 147 | } 148 | 149 | // Transform geographic coordinate to world point 150 | // 151 | // This doesn't take into account the origin offset 152 | // 153 | // For example, this takes a geographic coordinate and returns a point 154 | // relative to the origin point of the projection (not the world) 155 | project(latlon) { 156 | return Geo.latLonToPoint(LatLon(latlon)); 157 | } 158 | 159 | // Transform world point to geographic coordinate 160 | // 161 | // This doesn't take into account the origin offset 162 | // 163 | // For example, this takes a point relative to the origin point of the 164 | // projection (not the world) and returns a geographic coordinate 165 | unproject(point) { 166 | return Geo.pointToLatLon(Point(point)); 167 | } 168 | 169 | // Takes into account the origin offset 170 | // 171 | // For example, this takes a geographic coordinate and returns a point 172 | // relative to the three.js / 3D origin (0,0) 173 | latLonToPoint(latlon) { 174 | var projectedPoint = this.project(LatLon(latlon)); 175 | return projectedPoint._subtract(this._originPoint); 176 | } 177 | 178 | // Takes into account the origin offset 179 | // 180 | // For example, this takes a point relative to the three.js / 3D origin (0,0) 181 | // and returns the exact geographic coordinate at that point 182 | pointToLatLon(point) { 183 | var projectedPoint = Point(point).add(this._originPoint); 184 | return this.unproject(projectedPoint); 185 | } 186 | 187 | // Return pointscale for a given geographic coordinate 188 | pointScale(latlon, accurate) { 189 | return Geo.pointScale(latlon, accurate); 190 | } 191 | 192 | // Convert from real meters to world units 193 | // 194 | // TODO: Would be nice not to have to pass in a pointscale here 195 | metresToWorld(metres, pointScale, zoom) { 196 | return Geo.metresToWorld(metres, pointScale, zoom); 197 | } 198 | 199 | // Convert from real meters to world units 200 | // 201 | // TODO: Would be nice not to have to pass in a pointscale here 202 | worldToMetres(worldUnits, pointScale, zoom) { 203 | return Geo.worldToMetres(worldUnits, pointScale, zoom); 204 | } 205 | 206 | // Unsure if it's a good idea to expose this here for components like 207 | // GridLayer to use (eg. to keep track of a frustum) 208 | getCamera() { 209 | return this._engine._camera; 210 | } 211 | 212 | addLayer(layer) { 213 | layer._addToWorld(this); 214 | 215 | this._layers.push(layer); 216 | 217 | if (layer.isOutput() && layer.isOutputToScene()) { 218 | // Could move this into Layer but it'll do here for now 219 | this._engine._scene.add(layer._object3D); 220 | this._engine._domScene3D.add(layer._domObject3D); 221 | this._engine._domScene2D.add(layer._domObject2D); 222 | } 223 | 224 | this.emit('layerAdded', layer); 225 | return this; 226 | } 227 | 228 | // Remove layer from world and scene but don't destroy it entirely 229 | removeLayer(layer) { 230 | var layerIndex = this._layers.indexOf(layer); 231 | 232 | if (layerIndex > -1) { 233 | // Remove from this._layers 234 | this._layers.splice(layerIndex, 1); 235 | }; 236 | 237 | if (layer.isOutput() && layer.isOutputToScene()) { 238 | this._engine._scene.remove(layer._object3D); 239 | this._engine._domScene3D.remove(layer._domObject3D); 240 | this._engine._domScene2D.remove(layer._domObject2D); 241 | } 242 | 243 | this.emit('layerRemoved'); 244 | return this; 245 | } 246 | 247 | addControls(controls) { 248 | controls._addToWorld(this); 249 | 250 | this._controls.push(controls); 251 | 252 | this.emit('controlsAdded', controls); 253 | return this; 254 | } 255 | 256 | // Remove controls from world but don't destroy them entirely 257 | removeControls(controls) { 258 | var controlsIndex = this._controls.indexOf(controlsIndex); 259 | 260 | if (controlsIndex > -1) { 261 | this._controls.splice(controlsIndex, 1); 262 | }; 263 | 264 | this.emit('controlsRemoved', controls); 265 | return this; 266 | } 267 | 268 | stop() { 269 | this._pause = true; 270 | } 271 | 272 | start() { 273 | this._pause = false; 274 | this._update(); 275 | } 276 | 277 | // Destroys the world(!) and removes it from the scene and memory 278 | // 279 | // TODO: World out why so much three.js stuff is left in the heap after this 280 | destroy() { 281 | this.stop(); 282 | 283 | // Remove listeners 284 | this.off('controlsMoveEnd', this._onControlsMoveEnd); 285 | 286 | var i; 287 | 288 | // Remove all controls 289 | var controls; 290 | for (i = this._controls.length - 1; i >= 0; i--) { 291 | controls = this._controls[0]; 292 | this.removeControls(controls); 293 | controls.destroy(); 294 | }; 295 | 296 | // Remove all layers 297 | var layer; 298 | for (i = this._layers.length - 1; i >= 0; i--) { 299 | layer = this._layers[0]; 300 | this.removeLayer(layer); 301 | layer.destroy(); 302 | }; 303 | 304 | // Environment layer is removed with the other layers 305 | this._environment = null; 306 | 307 | this._engine.destroy(); 308 | this._engine = null; 309 | 310 | // Clean the container / remove the canvas 311 | while (this._container.firstChild) { 312 | this._container.removeChild(this._container.firstChild); 313 | } 314 | 315 | this._container = null; 316 | } 317 | } 318 | 319 | export default World; 320 | 321 | var noNew = function(domId, options) { 322 | return new World(domId, options); 323 | }; 324 | 325 | // Initialise without requiring new keyword 326 | export {noNew as world}; 327 | -------------------------------------------------------------------------------- /src/layer/tile/GeoJSONTile.js: -------------------------------------------------------------------------------- 1 | import Tile from './Tile'; 2 | import {geoJSONLayer as GeoJSONLayer} from '../GeoJSONLayer'; 3 | import BoxHelper from '../../vendor/BoxHelper'; 4 | import THREE from 'three'; 5 | import reqwest from 'reqwest'; 6 | import {point as Point} from '../../geo/Point'; 7 | import {latLon as LatLon} from '../../geo/LatLon'; 8 | import extend from 'lodash.assign'; 9 | // import Offset from 'polygon-offset'; 10 | import GeoJSON from '../../util/GeoJSON'; 11 | import Buffer from '../../util/Buffer'; 12 | import PickingMaterial from '../../engine/PickingMaterial'; 13 | 14 | // TODO: Map picking IDs to some reference within the tile data / geometry so 15 | // that something useful can be done when an object is picked / clicked on 16 | 17 | // TODO: Make sure nothing is left behind in the heap after calling destroy() 18 | 19 | // TODO: Perform tile request and processing in a Web Worker 20 | // 21 | // Use Operative (https://github.com/padolsey/operative) 22 | // 23 | // Would it make sense to have the worker functionality defined in a static 24 | // method so it only gets initialised once and not on every tile instance? 25 | // 26 | // Otherwise, worker processing logic would have to go in the tile layer so not 27 | // to waste loads of time setting up a brand new worker with three.js for each 28 | // tile every single time. 29 | // 30 | // Unsure of the best way to get three.js and VIZI into the worker 31 | // 32 | // Would need to set up a CRS / projection identical to the world instance 33 | // 34 | // Is it possible to bypass requirements on external script by having multiple 35 | // simple worker methods that each take enough inputs to perform a single task 36 | // without requiring VIZI or three.js? So long as the heaviest logic is done in 37 | // the worker and transferrable objects are used then it should be better than 38 | // nothing. Would probably still need things like earcut... 39 | // 40 | // After all, the three.js logic and object creation will still need to be 41 | // done on the main thread regardless so the worker should try to do as much as 42 | // possible with as few dependencies as possible. 43 | // 44 | // Have a look at how this is done in Tangram before implementing anything as 45 | // the approach there is pretty similar and robust. 46 | 47 | class GeoJSONTile extends Tile { 48 | constructor(quadcode, path, layer, options) { 49 | super(quadcode, path, layer); 50 | 51 | this._defaultStyle = GeoJSON.defaultStyle; 52 | 53 | var defaults = { 54 | output: true, 55 | outputToScene: false, 56 | interactive: false, 57 | topojson: false, 58 | filter: null, 59 | onEachFeature: null, 60 | polygonMaterial: null, 61 | onPolygonMesh: null, 62 | onPolygonBufferAttributes: null, 63 | polylineMaterial: null, 64 | onPolylineMesh: null, 65 | onPolylineBufferAttributes: null, 66 | pointGeometry: null, 67 | pointMaterial: null, 68 | onPointMesh: null, 69 | style: GeoJSON.defaultStyle, 70 | keepFeatures: false 71 | }; 72 | 73 | var _options = extend({}, defaults, options); 74 | 75 | if (typeof options.style === 'function') { 76 | _options.style = options.style; 77 | } else { 78 | _options.style = extend({}, defaults.style, options.style); 79 | } 80 | 81 | this._options = _options; 82 | } 83 | 84 | // Request data for the tile 85 | requestTileAsync() { 86 | // Making this asynchronous really speeds up the LOD framerate 87 | setTimeout(() => { 88 | if (!this._mesh) { 89 | this._mesh = this._createMesh(); 90 | 91 | // this._shadowCanvas = this._createShadowCanvas(); 92 | 93 | this._requestTile(); 94 | } 95 | }, 0); 96 | } 97 | 98 | // TODO: Destroy GeoJSONLayer 99 | destroy() { 100 | // Cancel any pending requests 101 | this._abortRequest(); 102 | 103 | // Clear request reference 104 | this._request = null; 105 | 106 | if (this._geojsonLayer) { 107 | this._geojsonLayer.destroy(); 108 | this._geojsonLayer = null; 109 | } 110 | 111 | this._mesh = null; 112 | 113 | // TODO: Properly dispose of picking mesh 114 | this._pickingMesh = null; 115 | 116 | super.destroy(); 117 | } 118 | 119 | _createMesh() { 120 | // Something went wrong and the tile 121 | // 122 | // Possibly removed by the cache before loaded 123 | if (!this._center) { 124 | return; 125 | } 126 | 127 | var mesh = new THREE.Object3D(); 128 | // mesh.add(this._createDebugMesh()); 129 | 130 | return mesh; 131 | } 132 | 133 | _createDebugMesh() { 134 | var canvas = document.createElement('canvas'); 135 | canvas.width = 256; 136 | canvas.height = 256; 137 | 138 | var context = canvas.getContext('2d'); 139 | context.font = 'Bold 20px Helvetica Neue, Verdana, Arial'; 140 | context.fillStyle = '#ff0000'; 141 | context.fillText(this._quadcode, 20, canvas.width / 2 - 5); 142 | context.fillText(this._tile.toString(), 20, canvas.width / 2 + 25); 143 | 144 | var texture = new THREE.Texture(canvas); 145 | 146 | // Silky smooth images when tilted 147 | texture.magFilter = THREE.LinearFilter; 148 | texture.minFilter = THREE.LinearMipMapLinearFilter; 149 | 150 | // TODO: Set this to renderer.getMaxAnisotropy() / 4 151 | texture.anisotropy = 4; 152 | 153 | texture.needsUpdate = true; 154 | 155 | var material = new THREE.MeshBasicMaterial({ 156 | map: texture, 157 | transparent: true, 158 | depthWrite: false 159 | }); 160 | 161 | var geom = new THREE.PlaneBufferGeometry(this._side, this._side, 1); 162 | var mesh = new THREE.Mesh(geom, material); 163 | 164 | mesh.rotation.x = -90 * Math.PI / 180; 165 | mesh.position.y = 0.1; 166 | 167 | return mesh; 168 | } 169 | 170 | // _createShadowCanvas() { 171 | // var canvas = document.createElement('canvas'); 172 | // 173 | // // Rendered at a low resolution and later scaled up for a low-quality blur 174 | // canvas.width = 512; 175 | // canvas.height = 512; 176 | // 177 | // return canvas; 178 | // } 179 | 180 | // _addShadow(coordinates) { 181 | // var ctx = this._shadowCanvas.getContext('2d'); 182 | // var width = this._shadowCanvas.width; 183 | // var height = this._shadowCanvas.height; 184 | // 185 | // var _coords; 186 | // var _offset; 187 | // var offset = new Offset(); 188 | // 189 | // // Transform coordinates to shadowCanvas space and draw on canvas 190 | // coordinates.forEach((ring, index) => { 191 | // ctx.beginPath(); 192 | // 193 | // _coords = ring.map(coord => { 194 | // var xFrac = (coord[0] - this._boundsWorld[0]) / this._side; 195 | // var yFrac = (coord[1] - this._boundsWorld[3]) / this._side; 196 | // return [xFrac * width, yFrac * height]; 197 | // }); 198 | // 199 | // if (index > 0) { 200 | // _offset = _coords; 201 | // } else { 202 | // _offset = offset.data(_coords).padding(1.3); 203 | // } 204 | // 205 | // // TODO: This is super flaky and crashes the browser if run on anything 206 | // // put the outer ring (potentially due to winding) 207 | // _offset.forEach((coord, index) => { 208 | // // var xFrac = (coord[0] - this._boundsWorld[0]) / this._side; 209 | // // var yFrac = (coord[1] - this._boundsWorld[3]) / this._side; 210 | // 211 | // if (index === 0) { 212 | // ctx.moveTo(coord[0], coord[1]); 213 | // } else { 214 | // ctx.lineTo(coord[0], coord[1]); 215 | // } 216 | // }); 217 | // 218 | // ctx.closePath(); 219 | // }); 220 | // 221 | // ctx.fillStyle = 'rgba(80, 80, 80, 0.7)'; 222 | // ctx.fill(); 223 | // } 224 | 225 | _requestTile() { 226 | var urlParams = { 227 | x: this._tile[0], 228 | y: this._tile[1], 229 | z: this._tile[2] 230 | }; 231 | 232 | var url = this._getTileURL(urlParams); 233 | 234 | this._request = reqwest({ 235 | url: url, 236 | type: 'json', 237 | crossOrigin: true 238 | }).then(res => { 239 | // Clear request reference 240 | this._request = null; 241 | this._processTileData(res); 242 | }).catch(err => { 243 | console.error(err); 244 | 245 | // Clear request reference 246 | this._request = null; 247 | }); 248 | } 249 | 250 | _processTileData(data) { 251 | console.time(this._tile); 252 | 253 | // Using this creates a huge amount of memory due to the quantity of tiles 254 | this._geojsonLayer = GeoJSONLayer(data, this._options).addTo(this._world); 255 | 256 | this._mesh = this._geojsonLayer._object3D; 257 | this._pickingMesh = this._geojsonLayer._pickingMesh; 258 | 259 | // Free the GeoJSON memory as we don't need it 260 | // 261 | // TODO: This should probably be a method within GeoJSONLayer 262 | this._geojsonLayer._geojson = null; 263 | 264 | // TODO: Fix or store shadow canvas stuff and get rid of this code 265 | // Draw footprint on shadow canvas 266 | // 267 | // TODO: Disabled for the time-being until it can be sped up / moved to 268 | // a worker 269 | // this._addShadow(coordinates); 270 | 271 | // Output shadow canvas 272 | 273 | // TODO: Disabled for the time-being until it can be sped up / moved to 274 | // a worker 275 | 276 | // var texture = new THREE.Texture(this._shadowCanvas); 277 | // 278 | // // Silky smooth images when tilted 279 | // texture.magFilter = THREE.LinearFilter; 280 | // texture.minFilter = THREE.LinearMipMapLinearFilter; 281 | // 282 | // // TODO: Set this to renderer.getMaxAnisotropy() / 4 283 | // texture.anisotropy = 4; 284 | // 285 | // texture.needsUpdate = true; 286 | // 287 | // var material; 288 | // if (!this._world._environment._skybox) { 289 | // material = new THREE.MeshBasicMaterial({ 290 | // map: texture, 291 | // transparent: true, 292 | // depthWrite: false 293 | // }); 294 | // } else { 295 | // material = new THREE.MeshStandardMaterial({ 296 | // map: texture, 297 | // transparent: true, 298 | // depthWrite: false 299 | // }); 300 | // material.roughness = 1; 301 | // material.metalness = 0.1; 302 | // material.envMap = this._world._environment._skybox.getRenderTarget(); 303 | // } 304 | // 305 | // var geom = new THREE.PlaneBufferGeometry(this._side, this._side, 1); 306 | // var mesh = new THREE.Mesh(geom, material); 307 | // 308 | // mesh.castShadow = false; 309 | // mesh.receiveShadow = false; 310 | // mesh.renderOrder = 1; 311 | // 312 | // mesh.rotation.x = -90 * Math.PI / 180; 313 | // 314 | // this._mesh.add(mesh); 315 | 316 | this._ready = true; 317 | console.timeEnd(this._tile); 318 | } 319 | 320 | _abortRequest() { 321 | if (!this._request) { 322 | return; 323 | } 324 | 325 | this._request.abort(); 326 | } 327 | } 328 | 329 | export default GeoJSONTile; 330 | 331 | var noNew = function(quadcode, path, layer, options) { 332 | return new GeoJSONTile(quadcode, path, layer, options); 333 | }; 334 | 335 | // Initialise without requiring new keyword 336 | export {noNew as geoJSONTile}; 337 | --------------------------------------------------------------------------------