├── .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 | [](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 |
--------------------------------------------------------------------------------