├── .editorconfig ├── .eslintrc.cjs ├── .gitignore ├── .jshintrc ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── babel.config.js ├── build ├── bundle.mjs └── intro.js ├── circle.yml ├── demos ├── app │ ├── gui.js │ ├── key.js │ ├── rStats.js │ └── url.js ├── css │ └── main.css ├── data │ └── school-districts-polygon.geojson ├── fonts │ └── montserrat.woff ├── images │ ├── LitSphere_test_02.jpg │ ├── matball01.jpg │ ├── pois.png │ ├── sunset.jpg │ └── wheel.png ├── lib │ ├── FileSaver.js │ ├── dat.gui.min.js │ ├── keymaster.js │ ├── rStats.css │ ├── rStats.extras.js │ └── rStats.js ├── main.js ├── scene.yaml └── styles │ ├── crosshatch.zip │ ├── dots.yaml │ ├── halftone.yaml │ ├── popup.yaml │ ├── rainbow.yaml │ ├── water.yaml │ └── wood.yaml ├── dist ├── tangram.debug.js ├── tangram.debug.js.map ├── tangram.debug.mjs ├── tangram.debug.mjs.map ├── tangram.min.js └── tangram.min.mjs ├── index.html ├── karma.conf.js ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── builders │ ├── common.js │ ├── points.js │ ├── polygons.js │ ├── polylines.js │ └── wireframe.js ├── gl │ ├── constants.js │ ├── context.js │ ├── extensions.js │ ├── glsl.js │ ├── render_state.js │ ├── shader_program.js │ ├── texture.js │ ├── vao.js │ ├── vbo_mesh.js │ ├── vertex_data.js │ ├── vertex_elements.js │ └── vertex_layout.js ├── index.js ├── labels │ ├── collision.js │ ├── collision_grid.js │ ├── intersect.js │ ├── label.js │ ├── label_line.js │ ├── label_point.js │ ├── main_pass.js │ ├── point_anchor.js │ ├── point_placement.js │ └── repeat_group.js ├── leaflet_layer.js ├── lights │ ├── ambient_light.glsl │ ├── directional_light.glsl │ ├── light.js │ ├── material.glsl │ ├── material.js │ ├── point_light.glsl │ └── spot_light.glsl ├── scene │ ├── camera.js │ ├── globals.js │ ├── scene.js │ ├── scene_bundle.js │ ├── scene_debug.js │ ├── scene_loader.js │ ├── scene_worker.js │ └── view.js ├── selection │ ├── selection.js │ ├── selection_fragment.glsl │ ├── selection_globals.glsl │ └── selection_vertex.glsl ├── sources │ ├── data_source.js │ ├── geojson.js │ ├── mvt.js │ ├── raster.js │ ├── sources.js │ └── topojson.js ├── styles │ ├── filter.js │ ├── layer.js │ ├── lines │ │ ├── dasharray.js │ │ └── lines.js │ ├── points │ │ ├── points.js │ │ ├── points_fragment.glsl │ │ └── points_vertex.glsl │ ├── polygons │ │ ├── polygons.js │ │ ├── polygons_fragment.glsl │ │ └── polygons_vertex.glsl │ ├── raster │ │ ├── raster.js │ │ └── raster_globals.glsl │ ├── style.js │ ├── style_globals.glsl │ ├── style_manager.js │ ├── style_parser.js │ └── text │ │ ├── font_manager.js │ │ ├── text.js │ │ ├── text_canvas.js │ │ ├── text_labels.js │ │ ├── text_segments.js │ │ ├── text_settings.js │ │ └── text_wrap.js ├── tile │ ├── tile.js │ ├── tile_id.js │ ├── tile_manager.js │ └── tile_pyramid.js └── utils │ ├── debounce.js │ ├── debug_settings.js │ ├── errors.js │ ├── functions.js │ ├── geo.js │ ├── gl-matrix.js │ ├── hash.js │ ├── log.js │ ├── media_capture.js │ ├── merge.js │ ├── obb.js │ ├── props.js │ ├── slice.js │ ├── subscribe.js │ ├── task.js │ ├── thread.js │ ├── urls.js │ ├── utils.js │ ├── vector.js │ ├── version.js │ └── worker_broker.js └── test ├── data_source_spec.js ├── fixtures ├── sample-json-response.json ├── sample-layers.json ├── sample-scene.json ├── sample-scene.yaml ├── sample-tile.json ├── sample-topojson-response.json └── simple-polygon.json ├── geo_spec.js ├── helpers.js ├── layer_spec.js ├── leaflet_layer_spec.js ├── merge_spec.js ├── obb_spec.js ├── rollup.config.worker.js ├── scene_spec.js ├── style_spec.js ├── subscribe_spec.js ├── tile_manager_spec.js ├── tile_pyramid.js ├── tile_spec.js ├── vertex_data_spec.js └── vertex_layout_spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [{Makefile,makefile}] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "worker": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": 2020, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": ["error", 4 ], 14 | "linebreak-style": ["error", "unix"], 15 | "quotes": ["error", "single"], 16 | "semi": ["error", "always"] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gitattributes 2 | 3 | # jshint config 4 | .jshintrc 5 | 6 | # build & test artifacts 7 | node_modules 8 | build 9 | 10 | # OS misc 11 | .DS_Store 12 | 13 | # 14 | *.sublime-project 15 | *.sublime-workspace 16 | 17 | # storing some screenshots here 18 | screenshots 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "node": true, 4 | "indent": 4, 5 | "browser": true, 6 | "freeze": true, 7 | "boss": true, 8 | "curly": true, 9 | "devel": true, 10 | "esnext": true, 11 | "eqnull": true, 12 | "eqeqeq": true, 13 | "expr": true, 14 | "notypeof": true, 15 | "noarg": true, 16 | "funcscope": true, 17 | "globalstrict": false, 18 | "loopfunc": true, 19 | "noempty": true, 20 | "nonstandard": true, 21 | "onecase": true, 22 | "sub": true, 23 | "regexdash": true, 24 | "trailing": true, 25 | "undef": true, 26 | "unused": "vars", 27 | "globals": { 28 | "L": true, 29 | 30 | "describe": true, 31 | "it": true, 32 | "sinon": true, 33 | "_": true, 34 | "before": true, 35 | "beforeEach": true, 36 | "afterEach": true, 37 | "WebGLRenderingContext": true, 38 | "makeScene": true 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Tangram 2 | 3 | The easiest way to help out is to submit bug reports and feature requests on our [issues](http://github.com/tangrams/tangram/issues) page. 4 | 5 | When submitting a bug report, please list: 6 | 7 | - your specific browser and operating system versions 8 | - steps required to recreate the issue 9 | - what happened 10 | - what you expected to happen 11 | 12 | Thanks, and happy mapping! 13 | 14 | ## Quickstart 15 | 16 | To get Tangram up and running locally: 17 | 18 | 1. Clone or download this repository: 19 | - clone in a terminal window with `git clone https://github.com/tangrams/tangram.git` 20 | - or download a zip directly: https://github.com/tangrams/tangram/archive/master.zip 21 | - or use [Bower](http://bower.io/): `bower install tangram` 22 | 2. Start a webserver in the repository's directory: 23 | - in a terminal window, enter: `python -m SimpleHTTPServer 8000` 24 | - if that doesn't work, try: `python -m http.server` 25 | 3. View the map at http://localhost:8000 (or whatever port you started the server on) 26 | 27 | ### Building 28 | 29 | If you'd like to contribute to the project or just make changes to the source code for fun, you'll need to install the development requirements and build the library: 30 | 31 | ```shell 32 | npm install 33 | npm run build 34 | ``` 35 | 36 | The library will be minified in `dist/`, and `index.html` provides an example for rendering from different sources and simple Leaflet integration. 37 | 38 | ### Incremental Building and Live Reloading 39 | 40 | For more rapid development of Tangram we provide a watcher with incremental building and live reloading, simply run 41 | 42 | ```shell 43 | npm start 44 | ``` 45 | 46 | and point your browser to http://localhost:8000 47 | 48 | Any changes you make to the the source files (including shader code) will rebuild and reload on save. 49 | 50 | ### Testing 51 | 52 | Tests are included to ensure that the code functions as expected. To run all of the tests: 53 | 54 | ```shell 55 | npm test 56 | ``` 57 | 58 | Every time this runs, a new browser instance is created. If you wish to have a single browser instance and run the test suite against that instance do the following, 59 | 60 | ```shell 61 | npm run karma-start 62 | ``` 63 | 64 | And then run the tests with, 65 | 66 | ```shell 67 | npm run karma-run 68 | ``` 69 | 70 | ### Lint 71 | We're using jshint to maintain code quality. 72 | 73 | ```shell 74 | npm run lint 75 | ``` 76 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **TANGRAM VERSION:** 2 | 3 | List the version of Tangram you're using here. 4 | 5 | _If you're not using the latest version, please try that first before filing an issue!_ 6 | 7 | - _Latest version:_ https://mapzen.com/tangram/tangram.min.js 8 | - _Notes for latest version:_ https://github.com/tangrams/tangram/releases/latest 9 | 10 | **ENVIRONMENT:** 11 | 12 | List the OS and browser version combinations which exhibit the behavior here. 13 | 14 | **TO REPRODUCE THE ISSUE, FOLLOW THESE STEPS:** 15 | 16 | - List the steps you used 17 | - To produce the result here 18 | - Don't leave any out 19 | 20 | **RESULT:** 21 | 22 | Describe the result here. 23 | 24 | **EXPECTED RESULT:** 25 | 26 | Describe what you expected to see here, plus any other relevant notes. 27 | Thanks for contributing! 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2016 Brett Camper and Mapzen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | export default function(context) { 2 | const ESM = (process.env.ESM !== 'false'); // default to ESM on 3 | const cache = context.cache(() => ESM); 4 | const targets = ESM ? 5 | { esmodules: true } : 6 | { browsers: ['ie >= 11'] }; 7 | 8 | return { 9 | presets: [ 10 | [ 11 | '@babel/preset-env', { 12 | targets, 13 | bugfixes: true, 14 | // debug: true, // uncomment for debugging build issues 15 | exclude: [ 16 | // we don't want these because we're using fast-async instead 17 | 'transform-async-to-generator', 18 | 'transform-regenerator', 19 | 'proposal-async-generator-functions' 20 | ] 21 | } 22 | ], 23 | ], 24 | plugins: [ 25 | '@babel/plugin-transform-runtime', 26 | ESM ? false : ['module:fast-async', { 27 | spec: true // spec setting sticks to pure promises 28 | },] 29 | ].filter(x => x) 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /build/bundle.mjs: -------------------------------------------------------------------------------- 1 | // Loads each of the chunks produced by the first Rollup pass. 2 | // The custom AMD define() in intro.js will combined the shared 3 | // and worker chunks into a worker bundle that can be instantiated 4 | // via blob URL. 5 | 6 | import './shared'; // shared code between main and worker threads 7 | import './scene_worker'; // worker code, gets turned into a blob URL used to instantiate workers 8 | import './index'; // main thread code, gets exported as main library below 9 | 10 | // This allows the rollup ESM build to work within a 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import globals from 'rollup-plugin-node-globals'; 5 | import builtins from 'rollup-plugin-node-builtins'; 6 | import json from '@rollup/plugin-json'; 7 | import { importAsString } from 'rollup-plugin-string-import'; 8 | 9 | export default { 10 | 11 | config.set({ 12 | basePath: '', 13 | frameworks: ['mocha', 'sinon'], 14 | files: [ 15 | 'https://unpkg.com/leaflet@1.3.4/dist/leaflet.js', // TODO: update leaflet version 16 | { 17 | pattern : 'test/fixtures/*', 18 | watched : false, 19 | included : false, 20 | served : true 21 | }, 22 | { 23 | pattern: 'build/worker.test.js', 24 | watched : false, 25 | included: false, 26 | served: true 27 | }, 28 | { 29 | pattern: 'test/**/*.js' 30 | } 31 | ], 32 | 33 | exclude: ['test/rollup.config.worker.js'], // skip rollup config for building worker 34 | preprocessors: { 35 | 'test/**/*.js' : ['rollup'] 36 | }, 37 | 38 | rollupPreprocessor: { 39 | output: { 40 | format: 'umd', 41 | sourcemap: 'inline', 42 | }, 43 | treeshake: false, // treeshaking can remove test code we need! 44 | plugins: [ 45 | resolve({ 46 | browser: true, 47 | preferBuiltins: false 48 | }), 49 | commonjs(), 50 | 51 | json({ 52 | exclude: ['node_modules/**', 'src/**'] // import JSON files 53 | }), 54 | importAsString({ 55 | include: ['**/*.glsl'] // inline shader files 56 | }), 57 | 58 | babel({ 59 | exclude: ['node_modules/**', '*.json'], 60 | babelHelpers: "runtime" 61 | }), 62 | 63 | // These are needed for jszip node-environment compatibility, 64 | // previously provided by browserify 65 | globals(), 66 | builtins() 67 | ] 68 | }, 69 | 70 | plugins: [ 71 | 'karma-rollup-preprocessor', 72 | 'karma-mocha', 73 | 'karma-sinon', 74 | 'karma-chrome-launcher', 75 | 'karma-mocha-reporter' 76 | ], 77 | reporters: ['mocha'], 78 | 79 | port: 9876, 80 | colors: true, 81 | 82 | logLevel: config.LOG_INFO, 83 | autoWatch: false, 84 | browsers: ['Chrome'], 85 | 86 | singleRun: false 87 | 88 | }); 89 | 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tangram", 3 | "version": "0.22.0", 4 | "description": "WebGL Maps for Vector Tiles", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/tangrams/tangram.git" 8 | }, 9 | "main": "dist/tangram.min.mjs", 10 | "type": "module", 11 | "homepage": "https://github.com/tangrams/tangram", 12 | "keywords": [ 13 | "maps", 14 | "graphics", 15 | "rendering", 16 | "visualization", 17 | "WebGL", 18 | "OpenStreetMap" 19 | ], 20 | "config": { 21 | "output": "", 22 | "output_map": "" 23 | }, 24 | "engines": { 25 | "npm": ">=2.0.0" 26 | }, 27 | "scripts": { 28 | "start": "npm run watch", 29 | "build": "npm run build:nomodule && npm run build:nomodule:minify && npm run build:module && npm run build:module:minify && npm run build:size", 30 | "build:module": "ESM=true ./node_modules/.bin/rollup -c", 31 | "build:nomodule": "ESM=false ./node_modules/.bin/rollup -c", 32 | "build:module:minify": "ESM=true MINIFY=true ./node_modules/.bin/rollup -c", 33 | "build:nomodule:minify": "ESM=false MINIFY=true ./node_modules/.bin/rollup -c", 34 | "build:size": "gzip dist/tangram.min.mjs -c | wc -c | awk '{kb=$1/1024; print kb}' OFMT='Module:\t\t%.0fk (min/gzip)' && gzip dist/tangram.min.js -c | wc -c | awk '{kb=$1/1024; print kb}' OFMT='No module:\t%.0fk (min/gzip)'", 35 | "watch": "ESM=\"${ESM:=true}\" SERVE=true ./node_modules/.bin/rollup -cw", 36 | "lint": "./node_modules/.bin/eslint src/", 37 | "test": "npm run lint && npm run test-run", 38 | "test-run": "npm run test-build-worker && ESM=false ./node_modules/karma/bin/karma start karma.conf.js --browsers Chrome --single-run", 39 | "test-build-worker": "ESM=false rollup -c test/rollup.config.worker.js", 40 | "karma-start": "./node_modules/karma/bin/karma start karma.conf.js --browsers Chrome --no-watch", 41 | "karma-run": "./node_modules/karma/bin/karma run karma.conf.js --browsers Chrome" 42 | }, 43 | "files": [ 44 | "src/*", 45 | "dist/tangram.debug.mjs", 46 | "dist/tangram.debug.mjs.map", 47 | "dist/tangram.min.mjs", 48 | "dist/tangram.debug.js", 49 | "dist/tangram.debug.js.map", 50 | "dist/tangram.min.js" 51 | ], 52 | "author": { 53 | "name": "Tangram contributors" 54 | }, 55 | "contributors": [ 56 | { 57 | "name": "Brett Camper" 58 | }, 59 | { 60 | "name": "Peter Richardson" 61 | }, 62 | { 63 | "name": "Patricio Gonzalez Vivo" 64 | }, 65 | { 66 | "name": "Karim Naaji" 67 | }, 68 | { 69 | "name": "Ivan Willig" 70 | }, 71 | { 72 | "name": "Lou Huang" 73 | }, 74 | { 75 | "name": "David Valdman" 76 | }, 77 | { 78 | "name": "Nick Doiron" 79 | }, 80 | { 81 | "name": "Francisco López" 82 | }, 83 | { 84 | "name": "David Manzanares" 85 | } 86 | ], 87 | "license": "MIT", 88 | "dependencies": { 89 | "@mapbox/vector-tile": "1.3.0", 90 | "core-js": "^3.39.0", 91 | "csscolorparser": "1.0.3", 92 | "earcut": "2.2.2", 93 | "fontfaceobserver": "2.0.7", 94 | "geojson-vt": "3.2.1", 95 | "gl-mat3": "1.0.0", 96 | "gl-mat4": "1.1.4", 97 | "gl-shader-errors": "1.0.3", 98 | "js-yaml": "tangrams/js-yaml#read-only", 99 | "jszip": "^3.10.1", 100 | "pbf": "3.1.0", 101 | "quickselect": "3.0.0", 102 | "topojson-client": "tangrams/topojson-client#read-only" 103 | }, 104 | "devDependencies": { 105 | "@babel/core": "^7.26.0", 106 | "@babel/plugin-transform-runtime": "^7.25.9", 107 | "@babel/preset-env": "^7.26.0", 108 | "@rollup/plugin-babel": "^6.0.4", 109 | "@rollup/plugin-commonjs": "^28.0.1", 110 | "@rollup/plugin-json": "^6.1.0", 111 | "@rollup/plugin-node-resolve": "^15.3.0", 112 | "@rollup/plugin-replace": "^6.0.1", 113 | "@rollup/plugin-terser": "^0.4.4", 114 | "@rollup/pluginutils": "^5.1.3", 115 | "chai": "^5.1.2", 116 | "eslint": "7.8.1", 117 | "fast-async": "6.3.8", 118 | "karma": "^6.4.4", 119 | "karma-chrome-launcher": "^3.2.0", 120 | "karma-mocha": "^2.0.1", 121 | "karma-mocha-reporter": "^2.2.5", 122 | "karma-rollup-preprocessor": "^7.0.8", 123 | "karma-sinon": "^1.0.5", 124 | "mocha": "^11.0.1", 125 | "rollup": "4.20.0", 126 | "rollup-plugin-livereload": "^2.0.5", 127 | "rollup-plugin-node-builtins": "^2.1.2", 128 | "rollup-plugin-node-globals": "^1.4.0", 129 | "rollup-plugin-serve": "^1.1.1", 130 | "rollup-plugin-sourcemaps2": "^0.4.2", 131 | "rollup-plugin-string-import": "^1.2.5", 132 | "sinon": "^19.0.2" 133 | }, 134 | "optionalDependencies": { 135 | "@rollup/rollup-linux-x64-gnu": "4.6.1" 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { execSync } from 'child_process'; 3 | 4 | import babel from '@rollup/plugin-babel'; 5 | import resolve from '@rollup/plugin-node-resolve'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | import globals from 'rollup-plugin-node-globals'; 8 | import builtins from 'rollup-plugin-node-builtins'; 9 | import replace from '@rollup/plugin-replace'; 10 | import sourcemaps from 'rollup-plugin-sourcemaps2'; 11 | import terser from '@rollup/plugin-terser'; 12 | import json from '@rollup/plugin-json'; 13 | import { importAsString } from 'rollup-plugin-string-import'; 14 | 15 | import serve from 'rollup-plugin-serve'; 16 | import livereload from 'rollup-plugin-livereload'; 17 | 18 | const ESM = (process.env.ESM !== 'false'); // default to ESM on 19 | const MINIFY = (process.env.MINIFY === 'true'); 20 | const SERVE = (process.env.SERVE === 'true'); 21 | 22 | const outputFile = `dist/tangram.${MINIFY ? 'min' : 'debug'}.${ESM ? 'm' : ''}js`; 23 | 24 | // Use two pass code splitting and re-bundling technique, for another example see: 25 | // https://github.com/mapbox/mapbox-gl-js/blob/master/rollup.config.js 26 | 27 | const config = [{ 28 | input: ['src/index.js', 'src/scene/scene_worker.js'], 29 | output: { 30 | dir: 'build', 31 | format: 'amd', 32 | sourcemap: 'inline', 33 | indent: false, 34 | chunkFileNames: 'shared.js', 35 | }, 36 | plugins: [ 37 | resolve({ 38 | browser: true, 39 | preferBuiltins: false, 40 | }), 41 | commonjs({ 42 | // Avoids Webpack minification errors 43 | ignoreGlobal: true, 44 | }), 45 | json(), // load JSON files 46 | importAsString({ 47 | include: ['**/*.glsl'] // inline shader files 48 | }), 49 | 50 | babel({ 51 | exclude: 'node_modules/**', 52 | babelHelpers: "runtime" 53 | }), 54 | 55 | // These are needed for jszip node-environment compatibility, 56 | // previously provided by browserify 57 | globals(), 58 | builtins(), 59 | 60 | MINIFY ? terser() : false 61 | ] 62 | }, { 63 | // Second pass: combine the chunks from the first pass into a single bundle 64 | input: 'build/bundle.mjs', 65 | output: { 66 | name: 'Tangram', 67 | file: outputFile, 68 | format: ESM ? 'esm' : 'umd', 69 | sourcemap: MINIFY ? false : true, 70 | indent: false, 71 | // intro: fs.readFileSync(require.resolve('./build/intro.js'), 'utf8') 72 | intro: ` 73 | // define() gets called for each chunk generated by the first Rollup pass. 74 | // The order the chunks are called in is controlled by the imports in bundle.js: 75 | // 76 | // shared.js: shared dependencies between main and worker threads 77 | // scene_worker.js: worker thread code 78 | // index.js: main thread code 79 | 80 | // Once all chunks have been provided, the worker thread code is assembled, 81 | // incorporating the shared chunk code, then turned into a blob URL which 82 | // can be used to instantiate the worker. 83 | 84 | var shared, worker, Tangram = {}; 85 | 86 | function define(_, chunk) { 87 | if (!shared) { 88 | shared = chunk; 89 | } else if (!worker) { 90 | worker = chunk; 91 | } else { 92 | var worker_bundle = 'var shared_chunk = {}; (' + shared + ')(shared_chunk); (' + worker + ')(shared_chunk);' 93 | var shared_chunk = {}; 94 | shared(shared_chunk); 95 | Tangram = chunk(shared_chunk); 96 | Tangram.workerURL = window.URL.createObjectURL(new Blob([worker_bundle], { type: 'text/javascript' })); 97 | } 98 | } 99 | ` 100 | }, 101 | treeshake: false, 102 | plugins: [ 103 | replace({ 104 | preventAssignment: true, 105 | values: { 106 | _ESM: ESM, 107 | _SHA: '\'' + String(execSync('git rev-parse HEAD')).trim(1) + '\'' 108 | } 109 | }), 110 | sourcemaps(), // use source maps produced in the first pass 111 | 112 | // optionally start server and watch for rebuild 113 | SERVE ? serve({ 114 | port: 8000, 115 | contentBase: '', 116 | headers: { 117 | 'Access-Control-Allow-Origin': '*' 118 | } 119 | }): false, 120 | SERVE ? livereload({ 121 | watch: 'dist' 122 | }) : false 123 | ], 124 | }]; 125 | 126 | export default config 127 | -------------------------------------------------------------------------------- /src/builders/common.js: -------------------------------------------------------------------------------- 1 | // Geometry building functions 2 | import Geo from '../utils/geo'; 3 | 4 | export const tile_bounds = [ 5 | { x: 0, y: 0}, 6 | { x: Geo.tile_scale, y: -Geo.tile_scale } // TODO: correct for flipped y-axis? 7 | ]; 8 | 9 | export const default_uvs = [0, 0, 1, 1]; 10 | 11 | // Tests if a line segment (from point A to B) is outside the tile bounds 12 | // (within a certain tolerance to account for geometry nearly on tile edges) 13 | export function outsideTile (_a, _b, tolerance) { 14 | let tile_min = tile_bounds[0]; 15 | let tile_max = tile_bounds[1]; 16 | 17 | // TODO: fix flipped Y coords here, confusing with 'max' reference 18 | if ((_a[0] <= tile_min.x + tolerance && _b[0] <= tile_min.x + tolerance) || 19 | (_a[0] >= tile_max.x - tolerance && _b[0] >= tile_max.x - tolerance) || 20 | (_a[1] >= tile_min.y - tolerance && _b[1] >= tile_min.y - tolerance) || 21 | (_a[1] <= tile_max.y + tolerance && _b[1] <= tile_max.y + tolerance)) { 22 | return true; 23 | } 24 | 25 | return false; 26 | } 27 | 28 | export function isCoordOutsideTile (coord, tolerance) { 29 | tolerance = tolerance || 0; 30 | let tile_min = tile_bounds[0]; 31 | let tile_max = tile_bounds[1]; 32 | 33 | return coord[0] <= tile_min.x + tolerance || 34 | coord[0] >= tile_max.x - tolerance || 35 | coord[1] >= tile_min.y - tolerance || 36 | coord[1] <= tile_max.y + tolerance; 37 | } 38 | -------------------------------------------------------------------------------- /src/builders/points.js: -------------------------------------------------------------------------------- 1 | // Point builders 2 | import { default_uvs } from './common'; 3 | 4 | // Scaling values to encode fractional values with fixed-point integer attributes 5 | const pre_angles_normalize = 128 / Math.PI; 6 | const angles_normalize = 16384 / Math.PI; 7 | const offsets_normalize = 64; 8 | const texcoord_normalize = 65535; 9 | const size_normalize = 128; // width/height are 8.8 fixed-point, but are halved (so multiply by 128 instead of 256) 10 | 11 | // These index values map a 4-element vertex position counter from this pattern (used for size and UVs): 12 | // [min_x, min_y, max_x, max_y] 13 | // to this pattern: 14 | // [min_x, min_y], 15 | // [max_x, min_y], 16 | // [max_x, max_y], 17 | // [min_x, max_y] 18 | const ix = [0, 2, 2, 0]; 19 | const iy = [1, 1, 3, 3]; 20 | const shape = new Array(4); // single, reusable allocation 21 | 22 | // Build a billboard sprite quad centered on a point. Sprites are intended to be drawn in screenspace, and have 23 | // properties for width, height, angle, and texture UVs. Curved label segment sprites have additional properties 24 | // for interpolating their position and angle across zooms. 25 | export function buildQuadForPoint ( 26 | point, 27 | vertex_data, 28 | vertex_template, 29 | vindex, 30 | size, 31 | offset, 32 | offsets, 33 | pre_angles, 34 | angle, 35 | angles, 36 | texcoords, 37 | curve) { 38 | 39 | // Half-sized point dimensions in fixed point 40 | const w2 = size[0] * size_normalize; 41 | const h2 = size[1] * size_normalize; 42 | shape[0] = -w2; 43 | shape[1] = -h2; 44 | shape[2] = w2; 45 | shape[3] = h2; 46 | 47 | const uvs = texcoords || default_uvs; 48 | 49 | const vertex_elements = vertex_data.vertex_elements; 50 | let element_offset = vertex_data.vertex_count; 51 | 52 | for (let p=0; p < 4; p++) { 53 | vertex_template[vindex.a_position + 0] = point[0]; 54 | vertex_template[vindex.a_position + 1] = point[1]; 55 | 56 | vertex_template[vindex.a_shape + 0] = shape[ix[p]]; 57 | vertex_template[vindex.a_shape + 1] = shape[iy[p]]; 58 | vertex_template[vindex.a_shape + 2] = angle; 59 | 60 | vertex_template[vindex.a_offset + 0] = offset[0]; 61 | vertex_template[vindex.a_offset + 1] = offset[1]; 62 | 63 | // Add texcoords 64 | if (vindex.a_texcoord) { 65 | vertex_template[vindex.a_texcoord + 0] = uvs[ix[p]] * texcoord_normalize; 66 | vertex_template[vindex.a_texcoord + 1] = uvs[iy[p]] * texcoord_normalize; 67 | } 68 | 69 | // Add curved label segment props 70 | if (curve) { 71 | // 1 byte (signed) range: [-127, 128] 72 | // actual range: [-2pi, 2pi] 73 | // total: multiply by 128 / (2 PI) 74 | vertex_template[vindex.a_pre_angles + 0] = pre_angles_normalize * pre_angles[0]; 75 | vertex_template[vindex.a_pre_angles + 1] = pre_angles_normalize * pre_angles[1]; 76 | vertex_template[vindex.a_pre_angles + 2] = pre_angles_normalize * pre_angles[2]; 77 | vertex_template[vindex.a_pre_angles + 3] = pre_angles_normalize * pre_angles[3]; 78 | 79 | // 2 byte (signed) of resolution [-32767, 32768] 80 | // actual range: [-2pi, 2pi] 81 | // total: multiply by 32768 / (2 PI) = 16384 / PI 82 | vertex_template[vindex.a_angles + 0] = angles_normalize * angles[0]; 83 | vertex_template[vindex.a_angles + 1] = angles_normalize * angles[1]; 84 | vertex_template[vindex.a_angles + 2] = angles_normalize * angles[2]; 85 | vertex_template[vindex.a_angles + 3] = angles_normalize * angles[3]; 86 | 87 | // offset range can be [0, 65535] 88 | // actual range: [0, 1024] 89 | vertex_template[vindex.a_offsets + 0] = offsets_normalize * offsets[0]; 90 | vertex_template[vindex.a_offsets + 1] = offsets_normalize * offsets[1]; 91 | vertex_template[vindex.a_offsets + 2] = offsets_normalize * offsets[2]; 92 | vertex_template[vindex.a_offsets + 3] = offsets_normalize * offsets[3]; 93 | } 94 | 95 | vertex_data.addVertex(vertex_template); 96 | } 97 | 98 | vertex_elements.push(element_offset + 0); 99 | vertex_elements.push(element_offset + 1); 100 | vertex_elements.push(element_offset + 2); 101 | vertex_elements.push(element_offset + 2); 102 | vertex_elements.push(element_offset + 3); 103 | vertex_elements.push(element_offset + 0); 104 | 105 | return 2; // geom count is always two triangles, for one quad 106 | } 107 | -------------------------------------------------------------------------------- /src/builders/wireframe.js: -------------------------------------------------------------------------------- 1 | // Rearranges element array for triangles into a new element array that draws a wireframe 2 | // Used for debugging 3 | export default function makeWireframeForTriangleElementData (element_data) { 4 | const wireframe_data = new Uint16Array(element_data.length * 2); 5 | 6 | // Draw triangles as lines: 7 | // Make a copy of element_data, and for every group of three vertices, duplicate 8 | // each vertex according to the following pattern: 9 | // [1, 2, 3] => [1, 2, 2, 3, 3, 1] 10 | // This takes three vertices which would have been interpreted as a triangle, 11 | // and converts them into three 2-vertex line segments. 12 | for (let i = 0; i < element_data.length; i += 3) { 13 | wireframe_data.set( 14 | [ 15 | element_data[i], 16 | element_data[i+1], 17 | element_data[i+1], 18 | element_data[i+2], 19 | element_data[i+2], 20 | element_data[i] 21 | ], 22 | i * 2 23 | ); 24 | } 25 | return wireframe_data; 26 | } 27 | -------------------------------------------------------------------------------- /src/gl/constants.js: -------------------------------------------------------------------------------- 1 | // WebGL constants - need to import these separately to make them available in the web worker 2 | 3 | var gl; 4 | export default gl = {}; 5 | 6 | /* DataType */ 7 | gl.BYTE = 0x1400; 8 | gl.UNSIGNED_BYTE = 0x1401; 9 | gl.SHORT = 0x1402; 10 | gl.UNSIGNED_SHORT = 0x1403; 11 | gl.INT = 0x1404; 12 | gl.UNSIGNED_INT = 0x1405; 13 | gl.FLOAT = 0x1406; 14 | -------------------------------------------------------------------------------- /src/gl/context.js: -------------------------------------------------------------------------------- 1 | // WebGL context wrapper 2 | 3 | var Context; 4 | export default Context = {}; 5 | 6 | let context_id = 0; 7 | 8 | // Setup a WebGL context 9 | // If no canvas element is provided, one is created and added to the document body 10 | Context.getContext = function getContext (canvas, options) 11 | { 12 | var fullscreen = false; 13 | if (canvas == null) { 14 | canvas = document.createElement('canvas'); 15 | canvas.style.position = 'absolute'; 16 | canvas.style.top = 0; 17 | canvas.style.left = 0; 18 | canvas.style.zIndex = -1; 19 | document.body.appendChild(canvas); 20 | fullscreen = true; 21 | } 22 | 23 | // powerPreference context option spec requires listeners for context loss/restore, 24 | // though it's not clear these are required in practice. 25 | // https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.2.1 26 | canvas.addEventListener('webglcontextlost', () => {}); 27 | canvas.addEventListener('webglcontextrestored', () => {}); 28 | 29 | var gl = canvas.getContext('webgl', options) || canvas.getContext('experimental-webgl', options); 30 | if (!gl) { 31 | throw new Error('Couldn\'t create WebGL context.'); 32 | } 33 | gl._tangram_id = context_id++; 34 | 35 | if (!fullscreen) { 36 | Context.resize(gl, parseFloat(canvas.style.width), parseFloat(canvas.style.height), options.device_pixel_ratio); 37 | } 38 | else { 39 | Context.resize(gl, window.innerWidth, window.innerHeight, options.device_pixel_ratio); 40 | window.addEventListener('resize', function () { 41 | Context.resize(gl, window.innerWidth, window.innerHeight, options.device_pixel_ratio); 42 | }); 43 | } 44 | 45 | return gl; 46 | }; 47 | 48 | Context.resize = function (gl, width, height, device_pixel_ratio) 49 | { 50 | device_pixel_ratio = device_pixel_ratio || window.devicePixelRatio || 1; 51 | gl.canvas.style.width = width + 'px'; 52 | gl.canvas.style.height = height + 'px'; 53 | gl.canvas.width = Math.round(width * device_pixel_ratio); 54 | gl.canvas.height = Math.round(height * device_pixel_ratio); 55 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 56 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 57 | }; 58 | -------------------------------------------------------------------------------- /src/gl/extensions.js: -------------------------------------------------------------------------------- 1 | // WebGL extension wrapper 2 | // Stores extensions by name and GL context 3 | 4 | // list of extension arrays, for each entry, 1st element GL context, 2nd map of extensions by name 5 | let extensions = []; 6 | 7 | export default function getExtension (gl, name) { 8 | let exts = extensions.filter(e => e[0] === gl)[0]; 9 | exts = exts && exts[1]; 10 | 11 | if (!exts) { 12 | extensions.push([gl, {}]); 13 | exts = extensions[extensions.length-1][1]; 14 | } 15 | 16 | if (!exts[name]) { 17 | exts[name] = gl.getExtension(name); 18 | } 19 | return exts[name]; 20 | } 21 | -------------------------------------------------------------------------------- /src/gl/render_state.js: -------------------------------------------------------------------------------- 1 | 2 | export class RenderState { 3 | constructor (value, setup) { 4 | setup(value); 5 | this.value = value; 6 | this.setup = setup; 7 | } 8 | 9 | set (value) { 10 | // if the states are different, call the GL context for a state change 11 | if (JSON.stringify(this.value) !== JSON.stringify(value)) { 12 | this.setup(value); 13 | this.value = value; 14 | } 15 | } 16 | } 17 | 18 | export default class RenderStateManager { 19 | 20 | constructor (gl) { 21 | this.defaults = {}; 22 | 23 | // Culling 24 | this.defaults.culling = true; 25 | this.defaults.culling_face = gl.BACK; 26 | 27 | // Blending 28 | this.defaults.blending = false; 29 | this.defaults.blending_src = gl.ONE_MINUS_SRC_ALPHA; 30 | this.defaults.blending_dst = gl.ONE_MINUS_SRC_ALPHA; 31 | this.defaults.blending_src_alpha = gl.ONE; 32 | this.defaults.blending_dst_alpha = gl.ONE_MINUS_SRC_ALPHA; 33 | 34 | // Depth test/write 35 | this.defaults.depth_write = true; 36 | this.defaults.depth_test = true; 37 | gl.depthFunc(gl.LESS); // depth function only needs to be set once 38 | 39 | // Culling 40 | this.culling = new RenderState( 41 | { cull: this.defaults.culling, face: this.defaults.culling_face }, 42 | (value) => { 43 | if (value.cull) { 44 | gl.enable(gl.CULL_FACE); 45 | gl.cullFace(value.face); 46 | } else { 47 | gl.disable(gl.CULL_FACE); 48 | } 49 | } 50 | ); 51 | 52 | // Blending mode 53 | this.blending = new RenderState({ 54 | blend: this.defaults.blending, 55 | src: this.defaults.blending_src, 56 | dst: this.defaults.blending_dst, 57 | src_alpha: this.defaults.blending_src_alpha, 58 | dst_alpha: this.defaults.blending_dst_alpha 59 | }, 60 | (value) => { 61 | if (value.blend) { 62 | gl.enable(gl.BLEND); 63 | 64 | if (value.src_alpha && value.dst_alpha) { 65 | gl.blendFuncSeparate(value.src, value.dst, value.src_alpha, value.dst_alpha); 66 | } 67 | else { 68 | gl.blendFunc(value.src, value.dst); 69 | } 70 | } else { 71 | gl.disable(gl.BLEND); 72 | } 73 | } 74 | ); 75 | 76 | // Depth write 77 | this.depth_write = new RenderState( 78 | { depth_write: this.defaults.depth_write }, 79 | (value) => { 80 | gl.depthMask(value.depth_write); 81 | } 82 | ); 83 | 84 | // Depth test 85 | this.depth_test = new RenderState( 86 | { depth_test: this.defaults.depth_test }, 87 | (value) => { 88 | if (value.depth_test) { 89 | gl.enable(gl.DEPTH_TEST); 90 | } else { 91 | gl.disable(gl.DEPTH_TEST); 92 | } 93 | } 94 | ); 95 | 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/gl/vao.js: -------------------------------------------------------------------------------- 1 | // Creates a Vertex Array Object if the extension is available, or falls back on standard attribute calls 2 | 3 | import getExtension from './extensions'; 4 | import log from '../utils/log'; 5 | 6 | export default { 7 | 8 | disabled: false, // set to true to disable VAOs even if extension is available 9 | bound_vao: [], // currently bound VAO, by GL context 10 | 11 | init (gl) { 12 | let ext; 13 | if (this.disabled !== true) { 14 | ext = getExtension(gl, 'OES_vertex_array_object'); 15 | } 16 | 17 | if (ext != null) { 18 | log('info', 'Vertex Array Object extension available'); 19 | } 20 | else if (this.disabled !== true) { 21 | log('warn', 'Vertex Array Object extension NOT available'); 22 | } 23 | else { 24 | log('warn', 'Vertex Array Object extension force disabled'); 25 | } 26 | }, 27 | 28 | getExtension(gl, ext_name) { 29 | if (this.disabled !== true) { 30 | return getExtension(gl, ext_name); 31 | } 32 | }, 33 | 34 | create (gl, setup, teardown) { 35 | let vao = {}; 36 | vao.setup = setup; 37 | vao.teardown = teardown; 38 | 39 | let ext = this.getExtension(gl, 'OES_vertex_array_object'); 40 | if (ext != null) { 41 | vao._vao = ext.createVertexArrayOES(); 42 | ext.bindVertexArrayOES(vao._vao); 43 | } 44 | 45 | vao.setup(); 46 | 47 | return vao; 48 | }, 49 | 50 | getCurrentBinding (gl) { 51 | let bound = this.bound_vao.filter(e => e[0] === gl)[0]; 52 | return bound && bound[1]; 53 | }, 54 | 55 | setCurrentBinding (gl, vao) { 56 | let bound_vao = this.bound_vao; 57 | let binding = bound_vao.filter(e => e[0] === gl)[0]; 58 | if (binding == null) { 59 | bound_vao.push([gl, vao]); 60 | } 61 | else { 62 | binding[1] = vao; 63 | } 64 | }, 65 | 66 | bind (gl, vao) { 67 | let ext = this.getExtension(gl, 'OES_vertex_array_object'); 68 | if (vao != null) { 69 | if (ext != null && vao._vao != null) { 70 | ext.bindVertexArrayOES(vao._vao); 71 | this.setCurrentBinding(gl, vao); 72 | } 73 | else { 74 | vao.setup(); 75 | } 76 | } 77 | else { 78 | let bound_vao = this.getCurrentBinding(gl); 79 | if (ext != null) { 80 | ext.bindVertexArrayOES(null); 81 | } 82 | else if (bound_vao != null && typeof bound_vao.teardown === 'function') { 83 | bound_vao.teardown(); 84 | } 85 | this.setCurrentBinding(gl, null); 86 | } 87 | }, 88 | 89 | destroy (gl, vao) { 90 | let ext = this.getExtension(gl, 'OES_vertex_array_object'); 91 | if (ext != null && vao != null && vao._vao != null) { 92 | ext.deleteVertexArrayOES(vao._vao); 93 | vao._vao = null; 94 | } 95 | // destroy is a no-op if VAO extension isn't available 96 | } 97 | 98 | }; 99 | -------------------------------------------------------------------------------- /src/gl/vbo_mesh.js: -------------------------------------------------------------------------------- 1 | // Manage rendering for primitives 2 | import ShaderProgram from './shader_program'; 3 | import VertexArrayObject from './vao'; 4 | import Texture from './texture'; 5 | 6 | // A single mesh/VBO, described by a vertex layout, that can be drawn with one or more programs 7 | export default class VBOMesh { 8 | 9 | constructor(gl, vertex_data, element_data, vertex_layout, options) { 10 | options = options || {}; 11 | 12 | this.gl = gl; 13 | this.vertex_data = vertex_data; // typed array 14 | this.element_data = element_data; // typed array 15 | this.vertex_layout = vertex_layout; 16 | this.vertex_buffer = this.gl.createBuffer(); 17 | this.buffer_size = this.vertex_data.byteLength; 18 | this.draw_mode = options.draw_mode || this.gl.TRIANGLES; 19 | this.data_usage = options.data_usage || this.gl.STATIC_DRAW; 20 | this.vertices_per_geometry = 3; // TODO: support lines, strip, fan, etc. 21 | this.uniforms = options.uniforms; 22 | this.textures = options.textures; // any textures owned by this mesh 23 | this.retain = options.retain || false; // whether to retain mesh data in CPU after uploading to GPU 24 | this.created_at = +new Date(); 25 | this.fade_in_time = options.fade_in_time || 0; // optional time to fade in mesh 26 | 27 | this.vertex_count = this.vertex_data.byteLength / this.vertex_layout.stride; 28 | this.element_count = 0; 29 | this.vaos = {}; // map of VertexArrayObjects, keyed by program 30 | 31 | this.toggle_element_array = false; 32 | if (this.element_data) { 33 | this.toggle_element_array = true; 34 | this.element_count = this.element_data.length; 35 | this.geometry_count = this.element_count / this.vertices_per_geometry; 36 | this.element_type = (this.element_data.constructor === Uint16Array) ? this.gl.UNSIGNED_SHORT: this.gl.UNSIGNED_INT; 37 | this.element_buffer = this.gl.createBuffer(); 38 | this.buffer_size += this.element_data.byteLength; 39 | this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.element_buffer); 40 | this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, this.element_data, this.data_usage); 41 | } 42 | else { 43 | this.geometry_count = this.vertex_count / this.vertices_per_geometry; 44 | } 45 | 46 | this.upload(); 47 | 48 | if (!this.retain) { 49 | delete this.vertex_data; 50 | delete this.element_data; 51 | } 52 | this.valid = true; 53 | } 54 | 55 | // Render, by default with currently bound program, or otherwise with optionally provided one 56 | // Returns true if mesh requests a render on next frame (e.g. for fade animations) 57 | render(options = {}) { 58 | if (!this.valid) { 59 | return false; 60 | } 61 | 62 | var program = options.program || ShaderProgram.current; 63 | program.use(); 64 | 65 | if (this.uniforms) { 66 | program.saveUniforms(this.uniforms); 67 | program.setUniforms(this.uniforms, false); // don't reset texture unit 68 | } 69 | 70 | let visible_time = (+new Date() - this.created_at) / 1000; 71 | program.uniform('1f', 'u_visible_time', visible_time); 72 | 73 | this.bind(program); 74 | 75 | if (this.toggle_element_array){ 76 | this.gl.drawElements(this.draw_mode, this.element_count, this.element_type, 0); 77 | } 78 | else { 79 | this.gl.drawArrays(this.draw_mode, 0, this.vertex_count); 80 | } 81 | 82 | VertexArrayObject.bind(this.gl, null); 83 | 84 | if (this.uniforms) { 85 | program.restoreUniforms(this.uniforms); 86 | } 87 | 88 | // Request next render if mesh is fading in 89 | return (visible_time < this.fade_in_time); 90 | } 91 | 92 | // Bind buffers and vertex attributes to prepare for rendering 93 | bind(program) { 94 | // Bind VAO for this progam, or create one 95 | let vao = this.vaos[program.id]; 96 | if (vao) { 97 | VertexArrayObject.bind(this.gl, vao); 98 | } 99 | else { 100 | this.vaos[program.id] = VertexArrayObject.create(this.gl, () => { 101 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertex_buffer); 102 | if (this.toggle_element_array) { 103 | this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.element_buffer); 104 | } 105 | this.vertex_layout.enableDynamicAttributes(this.gl, program); 106 | }); 107 | } 108 | 109 | this.vertex_layout.enableStaticAttributes(this.gl, program); 110 | } 111 | 112 | // Upload buffer data to GPU 113 | upload() { 114 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertex_buffer); 115 | this.gl.bufferData(this.gl.ARRAY_BUFFER, this.vertex_data, this.data_usage); 116 | } 117 | 118 | destroy() { 119 | if (!this.valid) { 120 | return false; 121 | } 122 | this.valid = false; 123 | 124 | for (let v in this.vaos) { 125 | VertexArrayObject.destroy(this.gl, this.vaos[v]); 126 | } 127 | 128 | this.gl.deleteBuffer(this.vertex_buffer); 129 | this.vertex_buffer = null; 130 | 131 | if (this.element_buffer) { 132 | this.gl.deleteBuffer(this.element_buffer); 133 | this.element_buffer = null; 134 | } 135 | 136 | delete this.vertex_data; 137 | delete this.element_data; 138 | 139 | if (this.textures) { 140 | this.textures.forEach(t => Texture.release(t)); 141 | } 142 | 143 | return true; 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/gl/vertex_data.js: -------------------------------------------------------------------------------- 1 | import gl from './constants'; // web workers don't have access to GL context, so import all GL constants 2 | import log from '../utils/log'; 3 | import VertexElements from './vertex_elements'; 4 | 5 | // Maps GL types to JS array types 6 | let array_types = { 7 | [gl.FLOAT]: Float32Array, 8 | [gl.BYTE]: Int8Array, 9 | [gl.UNSIGNED_BYTE]: Uint8Array, 10 | [gl.INT]: Int32Array, 11 | [gl.UNSIGNED_INT]: Uint32Array, 12 | [gl.SHORT]: Int16Array, 13 | [gl.UNSIGNED_SHORT]: Uint16Array 14 | }; 15 | 16 | // An intermediary object that holds vertex data in typed arrays, according to a given vertex layout 17 | // Used to construct a mesh/VBO for rendering 18 | export default class VertexData { 19 | 20 | constructor (vertex_layout, { prealloc = 500 } = {}) { 21 | this.vertex_layout = vertex_layout; 22 | this.vertex_elements = new VertexElements(); 23 | this.stride = this.vertex_layout.stride; 24 | 25 | if (VertexData.array_pool.length > 0) { 26 | this.vertex_buffer = VertexData.array_pool.pop(); 27 | this.byte_length = this.vertex_buffer.byteLength; 28 | this.size = Math.floor(this.byte_length / this.stride); 29 | log('trace', `VertexData: reused buffer of bytes ${this.byte_length}, ${this.size} vertices`); 30 | } 31 | else { 32 | this.size = prealloc; // # of vertices to allocate 33 | this.byte_length = this.stride * this.size; 34 | this.vertex_buffer = new Uint8Array(this.byte_length); 35 | } 36 | this.offset = 0; // byte offset into currently allocated buffer 37 | 38 | this.vertex_count = 0; 39 | this.realloc_count = 0; 40 | this.setBufferViews(); 41 | this.setAddVertexFunction(); 42 | } 43 | 44 | // (Re-)allocate typed views into the main buffer - only create the types we need for this layout 45 | setBufferViews () { 46 | this.views = {}; 47 | this.views[gl.UNSIGNED_BYTE] = this.vertex_buffer; 48 | this.vertex_layout.dynamic_attribs.forEach(attrib => { 49 | // Need view for this type? 50 | if (this.views[attrib.type] == null) { 51 | var array_type = array_types[attrib.type]; 52 | this.views[attrib.type] = new array_type(this.vertex_buffer.buffer); 53 | } 54 | }); 55 | } 56 | 57 | // Check allocated buffer size, expand/realloc buffer if needed 58 | checkBufferSize () { 59 | if ((this.offset + this.stride) > this.byte_length) { 60 | this.size = Math.floor(this.size * 1.5); 61 | this.size -= this.size % 4; 62 | this.byte_length = this.stride * this.size; 63 | var new_view = new Uint8Array(this.byte_length); 64 | new_view.set(this.vertex_buffer); // copy existing data to new buffer 65 | VertexData.array_pool.push(this.vertex_buffer); // save previous buffer for use by next tile 66 | this.vertex_buffer = new_view; 67 | this.setBufferViews(); 68 | this.realloc_count++; 69 | // log('info', `VertexData: expanded vertex block to ${this.size} vertices`); 70 | } 71 | } 72 | 73 | // Initialize the add vertex function (lazily compiled by vertex layout) 74 | setAddVertexFunction () { 75 | this.vertexLayoutAddVertex = this.vertex_layout.getAddVertexFunction(); 76 | } 77 | 78 | // Add a vertex, copied from a plain JS array of elements matching the order of the vertex layout 79 | addVertex (vertex) { 80 | this.checkBufferSize(); 81 | this.vertexLayoutAddVertex(vertex, this.views, this.offset); 82 | this.offset += this.stride; 83 | this.vertex_count++; 84 | } 85 | 86 | // Finalize vertex buffer for use in constructing a mesh 87 | end () { 88 | // Clip the buffer to size used for this VBO 89 | this.vertex_buffer = this.vertex_buffer.subarray(0, this.offset); 90 | this.element_buffer = this.vertex_elements.end(); 91 | 92 | log('trace', `VertexData: ${this.size} vertices total, realloc count ${this.realloc_count}`); 93 | 94 | return this; 95 | } 96 | 97 | } 98 | 99 | VertexData.array_pool = []; // pool of currently available (previously used) buffers (uint8) 100 | -------------------------------------------------------------------------------- /src/gl/vertex_elements.js: -------------------------------------------------------------------------------- 1 | let MAX_VALUE = Math.pow(2, 16) - 1; 2 | let has_element_index_uint = false; 3 | 4 | export default class VertexElements { 5 | constructor () { 6 | this.array = []; 7 | this.has_overflown = false; 8 | } 9 | push (value) { 10 | // If values have overflown and no Uint32 option is available, do not push values 11 | if (this.has_overflown && !has_element_index_uint) { 12 | return; 13 | } 14 | 15 | // Trigger overflow if value is greater than Uint16 max 16 | if (value > MAX_VALUE) { 17 | this.has_overflown = true; 18 | if (!has_element_index_uint) { 19 | return; 20 | } 21 | } 22 | 23 | this.array.push(value); 24 | } 25 | end () { 26 | if (this.array.length){ 27 | let buffer = createBuffer(this.array, this.has_overflown); 28 | this.array = []; 29 | this.has_overflown = false; 30 | return buffer; 31 | } 32 | else { 33 | return false; 34 | } 35 | } 36 | } 37 | 38 | VertexElements.setElementIndexUint = function(flag) { 39 | has_element_index_uint = flag; 40 | }; 41 | 42 | function createBuffer(array, overflown) { 43 | var typedArray = (overflown && has_element_index_uint) ? Uint32Array : Uint16Array; 44 | return new typedArray(array); 45 | } 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /*jshint worker: true*/ 2 | 3 | // The leaflet layer plugin is currently the primary public API 4 | import {leafletLayer} from './leaflet_layer'; 5 | import Scene from './scene/scene'; 6 | 7 | // Additional modules are exposed for debugging 8 | import version from './utils/version'; 9 | import log from './utils/log'; 10 | import Utils from './utils/utils'; 11 | import Geo from './utils/geo'; 12 | import Vector from './utils/vector'; 13 | import DataSource from './sources/data_source'; 14 | import GLSL from './gl/glsl'; 15 | import ShaderProgram from './gl/shader_program'; 16 | import VertexData from './gl/vertex_data'; 17 | import Texture from './gl/texture'; 18 | import Material from './lights/material'; 19 | import Light from './lights/light'; 20 | import WorkerBroker from './utils/worker_broker'; 21 | import Task from './utils/task'; 22 | import {StyleManager} from './styles/style_manager'; 23 | import StyleParser from './styles/style_parser'; 24 | import {TileID} from './tile/tile_id'; 25 | import Collision from './labels/collision'; 26 | import FeatureSelection from './selection/selection'; 27 | import TextCanvas from './styles/text/text_canvas'; 28 | import debugSettings from './utils/debug_settings'; 29 | 30 | import yaml from 'js-yaml'; 31 | 32 | // Make some modules accessible for debugging 33 | const debug = { 34 | log, 35 | yaml, 36 | Utils, 37 | Geo, 38 | Vector, 39 | DataSource, 40 | GLSL, 41 | ShaderProgram, 42 | VertexData, 43 | Texture, 44 | Material, 45 | Light, 46 | Scene, 47 | WorkerBroker, 48 | Task, 49 | StyleManager, 50 | StyleParser, 51 | TileID, 52 | Collision, 53 | FeatureSelection, 54 | TextCanvas, 55 | debugSettings 56 | }; 57 | 58 | export default { 59 | leafletLayer, 60 | debug, 61 | version 62 | }; 63 | -------------------------------------------------------------------------------- /src/labels/collision_grid.js: -------------------------------------------------------------------------------- 1 | export default class CollisionGrid { 2 | constructor (anchor, span) { 3 | this.anchor = anchor; 4 | this.span = span; 5 | this.cells = {}; 6 | } 7 | 8 | addLabel (label) { 9 | if (label.aabb) { 10 | this.addLabelBboxes(label, label.aabb); 11 | } 12 | 13 | if (label.aabbs) { 14 | label.aabbs.forEach(aabb => this.addLabelBboxes(label, aabb)); 15 | } 16 | } 17 | 18 | addLabelBboxes (label, aabb) { 19 | // min/max cells that the label falls into 20 | // keep grid coordinates at zero or above so any labels that go "below" the anchor are in the lowest grid cell 21 | const cell_bounds = [ 22 | Math.max(Math.floor((aabb[0] - this.anchor.x) / this.span), 0), 23 | Math.max(Math.floor(-(aabb[1] - this.anchor.y) / this.span), 0), 24 | Math.max(Math.floor((aabb[2] - this.anchor.x) / this.span), 0), 25 | Math.max(Math.floor(-(aabb[3] - this.anchor.y) / this.span), 0) 26 | ]; 27 | 28 | label.cells = []; // label knows which cells it falls in 29 | 30 | // initialize each grid cell as necessary, and add to label's list of cells 31 | for (let gy = cell_bounds[1]; gy <= cell_bounds[3]; gy++) { 32 | this.cells[gy] = this.cells[gy] || {}; 33 | for (let gx = cell_bounds[0]; gx <= cell_bounds[2]; gx++) { 34 | this.cells[gy][gx] = this.cells[gy][gx] || { aabb: [], obb: [] }; 35 | label.cells.push(this.cells[gy][gx]); 36 | } 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/labels/intersect.js: -------------------------------------------------------------------------------- 1 | 2 | // Do AABB `a` and `b` intersect? 3 | export function boxIntersectsBox (a, b) { 4 | if (a[2] < b[0] || // a is left of b 5 | a[0] > b[2] || // a is right of b 6 | a[3] < b[1] || // a is above b 7 | a[1] > b[3]) { // a is below b 8 | return false; 9 | } 10 | return true; // boxes overlap 11 | } 12 | 13 | // Does AABB `a` intersect any of the AABBs in array `boxes`? 14 | // Invokes `callback` with index of intersecting box 15 | // Stops intersecting if `callback` returns non-null value (continues otherwise) 16 | export function boxIntersectsList (a, boxes, callback) { 17 | for (let i=0; i < boxes.length; i++) { 18 | if (boxIntersectsBox(a, boxes[i])) { 19 | if (callback(i) != null) { 20 | break; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/labels/label_point.js: -------------------------------------------------------------------------------- 1 | import Label from './label'; 2 | import PointAnchor from './point_anchor'; 3 | import OBB from '../utils/obb'; 4 | import StyleParser from '../styles/style_parser'; 5 | 6 | export default class LabelPoint extends Label { 7 | 8 | constructor (position, size, layout, angle = 0) { 9 | super(size, layout); 10 | this.type = 'point'; 11 | this.position = [position[0], position[1]]; 12 | this.angle = angle; 13 | this.parent = this.layout.parent; 14 | this.update(); 15 | 16 | this.start_anchor_index = 1; 17 | this.degenerate = !this.size[0] && !this.size[1] && !this.layout.buffer[0] && !this.layout.buffer[1]; 18 | this.throw_away = false; 19 | } 20 | 21 | update() { 22 | super.update(); 23 | this.computeOffset(); 24 | this.updateBBoxes(); 25 | } 26 | 27 | computeOffset () { 28 | this.offset = [this.layout.offset[0], this.layout.offset[1]]; 29 | 30 | // Additional anchor/offset for point: 31 | if (this.parent) { 32 | let parent = this.parent; 33 | // point's own anchor, text anchor applied to point, additional point offset 34 | this.offset = PointAnchor.computeOffset(this.offset, parent.size, parent.anchor, PointAnchor.zero_buffer); 35 | this.offset = PointAnchor.computeOffset(this.offset, parent.size, this.anchor, PointAnchor.zero_buffer); 36 | if (parent.offset !== StyleParser.zeroPair) { // point has an offset 37 | if (this.offset === StyleParser.zeroPair) { // no text offset, use point's 38 | this.offset = parent.offset; 39 | } 40 | else { // text has offset, add point's 41 | this.offset[0] += parent.offset[0]; 42 | this.offset[1] += parent.offset[1]; 43 | } 44 | } 45 | } 46 | 47 | this.offset = PointAnchor.computeOffset(this.offset, this.size, this.anchor); 48 | } 49 | 50 | updateBBoxes () { 51 | let width = (this.size[0] + this.layout.buffer[0] * 2) * this.unit_scale * Label.epsilon; 52 | let height = (this.size[1] + this.layout.buffer[1] * 2) * this.unit_scale * Label.epsilon; 53 | 54 | // fudge width value as text may overflow bounding box if it has italic, bold, etc style 55 | if (this.layout.italic) { 56 | width += 5 * this.unit_scale; 57 | } 58 | 59 | // make bounding boxes 60 | this.obb = new OBB( 61 | this.position[0] + (this.offset[0] * this.unit_scale), 62 | this.position[1] - (this.offset[1] * this.unit_scale), 63 | -this.angle, // angle is negative because tile system y axis is pointing down 64 | width, 65 | height 66 | ); 67 | this.aabb = this.obb.getExtent(); 68 | 69 | if (this.inTileBounds) { 70 | this.breach = !this.inTileBounds(); 71 | } 72 | 73 | if (this.mayRepeatAcrossTiles) { 74 | this.may_repeat_across_tiles = this.mayRepeatAcrossTiles(); 75 | } 76 | } 77 | 78 | discard (bboxes, exclude = null) { 79 | if (this.degenerate) { 80 | return false; 81 | } 82 | 83 | if (super.discard(bboxes, exclude)) { 84 | // If more than one anchor specified, try them in order 85 | if (Array.isArray(this.layout.anchor)) { 86 | // Start on second anchor (first anchor was set on creation) 87 | for (let i=this.start_anchor_index; i < this.layout.anchor.length; i++) { 88 | this.anchor = this.layout.anchor[i]; 89 | this.update(); 90 | 91 | if (!super.discard(bboxes, exclude)) { 92 | return false; 93 | } 94 | } 95 | } 96 | return true; 97 | } 98 | return false; 99 | } 100 | 101 | } 102 | 103 | // Placement strategies 104 | LabelPoint.PLACEMENT = { 105 | VERTEX: 0, // place labels at endpoints of line segments 106 | MIDPOINT: 1, // place labels at midpoints of line segments 107 | SPACED: 2, // place labels equally spaced along line 108 | CENTROID: 3 // place labels at center of polygons 109 | }; 110 | -------------------------------------------------------------------------------- /src/labels/point_anchor.js: -------------------------------------------------------------------------------- 1 | // Sets of values to match for directional and corner anchors 2 | const lefts = ['left', 'top-left', 'bottom-left']; 3 | const rights = ['right', 'top-right', 'bottom-right']; 4 | const tops = ['top', 'top-left', 'top-right']; 5 | const bottoms = ['bottom', 'bottom-left', 'bottom-right']; 6 | 7 | const PointAnchor = { 8 | 9 | computeOffset (offset, size, anchor, buffer = null) { 10 | if (!anchor || anchor === 'center') { 11 | return offset; 12 | } 13 | 14 | let offset2 = [offset[0], offset[1]]; 15 | buffer = buffer || this.default_buffer; 16 | 17 | // An optional left/right offset 18 | if (this.isLeftAnchor(anchor)) { 19 | offset2[0] -= size[0] / 2; 20 | if (anchor === 'left') { 21 | offset2[0] -= buffer[0]; 22 | } 23 | } 24 | else if (this.isRightAnchor(anchor)) { 25 | offset2[0] += size[0] / 2; 26 | if (anchor === 'right') { 27 | offset2[0] += buffer[1]; 28 | } 29 | } 30 | 31 | // An optional top/bottom offset 32 | if (this.isTopAnchor(anchor)) { 33 | offset2[1] -= size[1] / 2; 34 | if (anchor === 'top') { 35 | offset2[1] -= buffer[2]; 36 | } 37 | } 38 | else if (this.isBottomAnchor(anchor)) { 39 | offset2[1] += size[1] / 2; 40 | if (anchor === 'bottom') { 41 | offset2[1] += buffer[3]; 42 | } 43 | } 44 | 45 | return offset2; 46 | }, 47 | 48 | alignForAnchor (anchor) { 49 | if (anchor && anchor !== 'center') { 50 | if (this.isLeftAnchor(anchor)) { 51 | return 'right'; 52 | } 53 | else if (this.isRightAnchor(anchor)) { 54 | return 'left'; 55 | } 56 | } 57 | return 'center'; 58 | }, 59 | 60 | isLeftAnchor (anchor) { 61 | return (lefts.indexOf(anchor) > -1); 62 | }, 63 | 64 | isRightAnchor (anchor) { 65 | return (rights.indexOf(anchor) > -1); 66 | }, 67 | 68 | isTopAnchor (anchor) { 69 | return (tops.indexOf(anchor) > -1); 70 | }, 71 | 72 | isBottomAnchor (anchor) { 73 | return (bottoms.indexOf(anchor) > -1); 74 | }, 75 | 76 | // Buffers: [left, right, top, bottom] 77 | default_buffer: [2.5, 2.5, 1.5, 0.75], 78 | zero_buffer: [0, 0, 0, 0] 79 | 80 | }; 81 | 82 | export default PointAnchor; 83 | -------------------------------------------------------------------------------- /src/labels/point_placement.js: -------------------------------------------------------------------------------- 1 | // Logic for placing point labels along a line geometry 2 | 3 | import LabelPoint from './label_point'; 4 | import {isCoordOutsideTile} from '../builders/common'; 5 | 6 | const PLACEMENT = LabelPoint.PLACEMENT; 7 | const default_spacing = 80; // spacing of points along line in pixels 8 | 9 | export default function placePointsOnLine (line, size, layout) { 10 | const labels = []; 11 | const strategy = layout.placement; 12 | const min_length = Math.max(size[0], size[1]) * layout.placement_min_length_ratio * layout.units_per_pixel; 13 | 14 | if (strategy === PLACEMENT.SPACED) { 15 | let result = getPositionsAndAngles(line, min_length, layout); 16 | // false will be returned if line have no length 17 | if (!result) { 18 | return []; 19 | } 20 | 21 | let positions = result.positions; 22 | let angles = result.angles; 23 | for (let i = 0; i < positions.length; i++) { 24 | let position = positions[i]; 25 | let angle = angles[i]; 26 | if (layout.tile_edges === true || !isCoordOutsideTile(position)) { 27 | labels.push(new LabelPoint(position, size, layout, angle)); 28 | } 29 | } 30 | } 31 | else if (strategy === PLACEMENT.VERTEX) { 32 | let p, q; 33 | for (let i = 0; i < line.length - 1; i++) { 34 | p = line[i]; 35 | q = line[i + 1]; 36 | if (layout.tile_edges === true || !isCoordOutsideTile(p)) { 37 | const angle = getAngle(p, q, layout.angle); 38 | labels.push(new LabelPoint(p, size, layout, angle)); 39 | } 40 | } 41 | 42 | // add last endpoint 43 | const angle = getAngle(p, q, layout.angle); 44 | labels.push(new LabelPoint(q, size, layout, angle)); 45 | } 46 | else if (strategy === PLACEMENT.MIDPOINT) { 47 | for (let i = 0; i < line.length - 1; i++) { 48 | let p = line[i]; 49 | let q = line[i + 1]; 50 | let position = [ 51 | 0.5 * (p[0] + q[0]), 52 | 0.5 * (p[1] + q[1]) 53 | ]; 54 | if (layout.tile_edges === true || !isCoordOutsideTile(position)) { 55 | if (!min_length || norm(p, q) > min_length) { 56 | const angle = getAngle(p, q, layout.angle); 57 | labels.push(new LabelPoint(position, size, layout, angle)); 58 | } 59 | } 60 | } 61 | } 62 | return labels; 63 | } 64 | 65 | function getPositionsAndAngles(line, min_length, layout) { 66 | let upp = layout.units_per_pixel; 67 | let spacing = (layout.placement_spacing || default_spacing) * upp; 68 | 69 | let length = getLineLength(line); 70 | if (length <= min_length) { 71 | return false; 72 | } 73 | 74 | let num_labels = Math.max(Math.floor(length / spacing), 1); 75 | let remainder = length - (num_labels - 1) * spacing; 76 | let positions = []; 77 | let angles = []; 78 | 79 | let distance = 0.5 * remainder; 80 | for (let i = 0; i < num_labels; i++) { 81 | let {position, angle} = interpolateLine(line, distance, min_length, layout); 82 | if (position != null && angle != null) { 83 | positions.push(position); 84 | angles.push(angle); 85 | } 86 | distance += spacing; 87 | } 88 | 89 | return {positions, angles}; 90 | } 91 | 92 | function getAngle(p, q, angle = 0) { 93 | return (angle === 'auto') ? Math.atan2(q[0] - p[0], q[1] - p[1]) : angle; 94 | } 95 | 96 | function getLineLength(line) { 97 | let distance = 0; 98 | for (let i = 0; i < line.length - 1; i++) { 99 | distance += norm(line[i], line[i+1]); 100 | } 101 | return distance; 102 | } 103 | 104 | function norm(p, q) { 105 | return Math.sqrt(Math.pow(p[0] - q[0], 2) + Math.pow(p[1] - q[1], 2)); 106 | } 107 | 108 | // TODO: can be optimized. 109 | // you don't have to start from the first index every time for placement 110 | function interpolateLine(line, distance, min_length, layout) { 111 | let sum = 0; 112 | let position, angle; 113 | for (let i = 0; i < line.length-1; i++) { 114 | let p = line[i]; 115 | let q = line[i+1]; 116 | 117 | const length = norm(p, q); 118 | if (length <= min_length) { 119 | continue; 120 | } 121 | 122 | sum += length; 123 | 124 | if (sum > distance) { 125 | position = interpolateSegment(p, q, sum - distance); 126 | angle = getAngle(p, q, layout.angle); 127 | break; 128 | } 129 | } 130 | return {position, angle}; 131 | } 132 | 133 | function interpolateSegment(p, q, distance) { 134 | let length = norm(p, q); 135 | let ratio = distance / length; 136 | return [ 137 | ratio * p[0] + (1 - ratio) * q[0], 138 | ratio * p[1] + (1 - ratio) * q[1] 139 | ]; 140 | } 141 | -------------------------------------------------------------------------------- /src/labels/repeat_group.js: -------------------------------------------------------------------------------- 1 | export default class RepeatGroup { 2 | 3 | constructor (key, repeat_dist) { 4 | this.key = key; 5 | this.repeat_dist = repeat_dist; 6 | this.repeat_dist_sq = this.repeat_dist * this.repeat_dist; 7 | this.positions = []; 8 | } 9 | 10 | // Check an object to see if it's a repeat in this group 11 | check (obj) { 12 | // Check distance from new object to objects already in group 13 | let p1 = obj.position; 14 | for (let i=0; i < this.positions.length; i++) { 15 | let p2 = this.positions[i]; 16 | let dx = p1[0] - p2[0]; 17 | let dy = p1[1] - p2[1]; 18 | let dist_sq = dx * dx + dy * dy; 19 | 20 | // Found an existing object within allowed distance 21 | if (dist_sq < this.repeat_dist_sq) { 22 | return true; 23 | } 24 | } 25 | } 26 | 27 | // Add object to this group 28 | add (obj) { 29 | // only store object's position, to save space / prevent unnecessary references 30 | if (obj && obj.position) { 31 | this.positions.push(obj.position); 32 | } 33 | } 34 | 35 | // Static methods are used to manage repeat groups, within and across tiles 36 | 37 | // Reset all groups for this tile 38 | static clear (tile) { 39 | this.groups[tile] = {}; 40 | } 41 | 42 | // Check an object to see if it's a repeat within its designated group 43 | static check (obj, layout, tile) { 44 | if (layout.repeat_distance && layout.repeat_group && this.groups[tile][layout.repeat_group]) { 45 | return this.groups[tile][layout.repeat_group].check(obj); 46 | } 47 | } 48 | 49 | // Add an object to its designated group 50 | static add (obj, layout, tile) { 51 | if (layout.repeat_distance && layout.repeat_group) { 52 | if (this.groups[tile][layout.repeat_group] == null) { 53 | this.groups[tile][layout.repeat_group] = new RepeatGroup(layout.repeat_group, layout.repeat_distance * layout.repeat_scale); 54 | } 55 | this.groups[tile][layout.repeat_group].add(obj); 56 | } 57 | } 58 | 59 | } 60 | 61 | // Current set of repeat groups, grouped and keyed by tile 62 | RepeatGroup.groups = {}; 63 | -------------------------------------------------------------------------------- /src/lights/ambient_light.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Expected globals: 4 | light_accumulator_* 5 | 6 | */ 7 | 8 | struct AmbientLight { 9 | vec3 ambient; 10 | }; 11 | 12 | void calculateLight(in AmbientLight _light, in vec3 _eyeToPoint, in vec3 _normal) { 13 | light_accumulator_ambient.rgb += _light.ambient; 14 | } 15 | -------------------------------------------------------------------------------- /src/lights/directional_light.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Expected globals: 4 | material 5 | light_accumulator_* 6 | 7 | */ 8 | 9 | struct DirectionalLight { 10 | vec3 ambient; 11 | vec3 diffuse; 12 | vec3 specular; 13 | vec3 direction; 14 | }; 15 | 16 | void calculateLight(in DirectionalLight _light, in vec3 _eyeToPoint, in vec3 _normal) { 17 | 18 | light_accumulator_ambient.rgb += _light.ambient; 19 | 20 | float nDotVP = clamp(dot(_normal, -_light.direction), 0.0, 1.0); 21 | 22 | #ifdef TANGRAM_MATERIAL_DIFFUSE 23 | light_accumulator_diffuse.rgb += _light.diffuse * nDotVP; 24 | #endif 25 | 26 | #ifdef TANGRAM_MATERIAL_SPECULAR 27 | float pf = 0.0; 28 | if (nDotVP > 0.0) { 29 | vec3 reflectVector = reflect(_light.direction, _normal); 30 | float eyeDotR = max(dot(normalize(_eyeToPoint), reflectVector), 0.0); 31 | pf = pow(eyeDotR, material.shininess); 32 | } 33 | light_accumulator_specular.rgb += _light.specular * pf; 34 | #endif 35 | } 36 | -------------------------------------------------------------------------------- /src/lights/material.js: -------------------------------------------------------------------------------- 1 | import GLSL from '../gl/glsl'; 2 | import StyleParser from '../styles/style_parser'; 3 | 4 | import material_source from './material.glsl'; 5 | 6 | const material_props = ['emission', 'ambient', 'diffuse', 'specular']; 7 | 8 | export default class Material { 9 | constructor (config) { 10 | 11 | config = config || {}; 12 | 13 | // These properties all have the same defaults, so they can be set in bulk 14 | material_props.forEach(prop => { 15 | const value = config[prop]; 16 | if (value != null) { 17 | if (value.texture) { 18 | this[prop] = { 19 | texture: value.texture, 20 | mapping: value.mapping || 'spheremap', 21 | scale: GLSL.expandVec3(value.scale != null ? value.scale : 1), 22 | amount: GLSL.expandVec4(value.amount != null ? value.amount : 1) 23 | }; 24 | } 25 | else if (typeof value === 'number' || Array.isArray(value)) { 26 | this[prop] = { amount: GLSL.expandVec4(value) }; 27 | } 28 | else if (typeof value === 'string') { 29 | this[prop] = { amount: StyleParser.parseColor(value) }; 30 | } 31 | else { 32 | this[prop] = value; 33 | } 34 | } 35 | }); 36 | 37 | // Extra specular props 38 | if (this.specular) { 39 | this.specular.shininess = config.shininess ? parseFloat(config.shininess) : 0.2; 40 | } 41 | 42 | // Normal mapping 43 | if (config.normal != null) { 44 | this.normal = { 45 | texture: config.normal.texture, 46 | mapping: config.normal.mapping || 'triplanar', 47 | scale: GLSL.expandVec3(config.normal.scale != null ? config.normal.scale : 1), 48 | amount: config.normal.amount != null ? config.normal.amount : 1 49 | }; 50 | } 51 | } 52 | 53 | // Determine if a material config block has sufficient properties to create a material 54 | static isValid (config) { 55 | if (config == null) { 56 | return false; 57 | } 58 | 59 | if (config.emission == null && 60 | config.ambient == null && 61 | config.diffuse == null && 62 | config.specular == null) { 63 | return false; 64 | } 65 | 66 | return true; 67 | } 68 | 69 | inject (style) { 70 | // For each property, sets defines to configure texture mapping, with a pattern like: 71 | // TANGRAM_MATERIAL_DIFFUSE, TANGRAM_MATERIAL_DIFFUSE_TEXTURE, TANGRAM_MATERIAL_DIFFUSE_TEXTURE_SPHEREMAP 72 | // Also sets flags to keep track of each unique mapping type being used, e.g.: 73 | // TANGRAM_MATERIAL_TEXTURE_SPHEREMAP 74 | // Enables texture coordinates if needed and not already on 75 | material_props.forEach(prop => { 76 | let def = `TANGRAM_MATERIAL_${prop.toUpperCase()}`; 77 | let texdef = def + '_TEXTURE'; 78 | style.defines[def] = (this[prop] != null); 79 | if (this[prop] && this[prop].texture) { 80 | style.defines[texdef] = true; 81 | style.defines[texdef + '_' + this[prop].mapping.toUpperCase()] = true; 82 | style.defines[`TANGRAM_MATERIAL_TEXTURE_${this[prop].mapping.toUpperCase()}`] = true; 83 | style.texcoords = style.texcoords || (this[prop].mapping === 'uv'); 84 | } 85 | }); 86 | 87 | // Normal mapping 88 | // As anove, sets flags to keep track of each unique mapping type being used, e.g.: 89 | // TANGRAM_MATERIAL_TEXTURE_SPHEREMAP 90 | if (this.normal && this.normal.texture) { 91 | style.defines['TANGRAM_MATERIAL_NORMAL_TEXTURE'] = true; 92 | style.defines['TANGRAM_MATERIAL_NORMAL_TEXTURE_' + this.normal.mapping.toUpperCase()] = true; 93 | style.defines[`TANGRAM_MATERIAL_TEXTURE_${this.normal.mapping.toUpperCase()}`] = true; 94 | style.texcoords = style.texcoords || (this.normal.mapping === 'uv'); 95 | } 96 | 97 | style.replaceShaderBlock(Material.block, material_source, 'Material'); 98 | style.addShaderBlock('setup', '\nmaterial = u_material;\n', 'Material'); 99 | } 100 | 101 | setupProgram (_program) { 102 | // For each property, sets uniforms in the pattern: 103 | // u_material.diffuse, u_material.diffuseScale u_material_diffuse_texture 104 | material_props.forEach(prop => { 105 | if (this[prop]) { 106 | if (this[prop].texture) { 107 | _program.setTextureUniform(`u_material_${prop}_texture`, this[prop].texture); 108 | _program.uniform('3fv', `u_material.${prop}Scale`, this[prop].scale); 109 | _program.uniform('4fv', `u_material.${prop}`, this[prop].amount); 110 | } else if (this[prop].amount) { 111 | _program.uniform('4fv', `u_material.${prop}`, this[prop].amount); 112 | } 113 | } 114 | }); 115 | 116 | // Extra specular props 117 | if (this.specular) { 118 | _program.uniform('1f', 'u_material.shininess', this.specular.shininess); 119 | } 120 | 121 | // Normal mapping 122 | if (this.normal && this.normal.texture) { 123 | _program.setTextureUniform('u_material_normal_texture', this.normal.texture); 124 | _program.uniform('3fv', 'u_material.normalScale', this.normal.scale); 125 | _program.uniform('1f', 'u_material.normalAmount', this.normal.amount); 126 | } 127 | } 128 | } 129 | 130 | Material.block = 'material'; 131 | -------------------------------------------------------------------------------- /src/lights/point_light.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Expected globals: 4 | material 5 | light_accumulator_* 6 | 7 | */ 8 | 9 | struct PointLight { 10 | vec3 ambient; 11 | vec3 diffuse; 12 | vec3 specular; 13 | vec4 position; 14 | 15 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_EXPONENT 16 | float attenuationExponent; 17 | #endif 18 | 19 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_INNER_RADIUS 20 | float innerRadius; 21 | #endif 22 | 23 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_OUTER_RADIUS 24 | float outerRadius; 25 | #endif 26 | }; 27 | 28 | void calculateLight(in PointLight _light, in vec3 _eyeToPoint, in vec3 _normal) { 29 | 30 | float dist = length(_light.position.xyz - _eyeToPoint); 31 | 32 | // Compute vector from surface to light position 33 | vec3 VP = (_light.position.xyz - _eyeToPoint) / dist; 34 | 35 | // Normalize the vector from surface to light position 36 | float nDotVP = clamp(dot(VP, _normal), 0.0, 1.0); 37 | 38 | // Attenuation defaults 39 | float attenuation = 1.0; 40 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_EXPONENT 41 | float Rin = 1.0; 42 | float e = _light.attenuationExponent; 43 | 44 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_INNER_RADIUS 45 | Rin = _light.innerRadius; 46 | #endif 47 | 48 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_OUTER_RADIUS 49 | float Rdiff = _light.outerRadius-Rin; 50 | float d = clamp(max(0.0,dist-Rin)/Rdiff, 0.0, 1.0); 51 | attenuation = 1.0-(pow(d,e)); 52 | #else 53 | // If no outer is provide behaves like: 54 | // https://imdoingitwrong.wordpress.com/2011/01/31/light-attenuation/ 55 | float d = max(0.0,dist-Rin)/Rin+1.0; 56 | attenuation = clamp(1.0/(pow(d,e)), 0.0, 1.0); 57 | #endif 58 | #else 59 | float Rin = 0.0; 60 | 61 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_INNER_RADIUS 62 | Rin = _light.innerRadius; 63 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_OUTER_RADIUS 64 | float Rdiff = _light.outerRadius-Rin; 65 | float d = clamp(max(0.0,dist-Rin)/Rdiff, 0.0, 1.0); 66 | attenuation = 1.0-d*d; 67 | #else 68 | // If no outer is provide behaves like: 69 | // https://imdoingitwrong.wordpress.com/2011/01/31/light-attenuation/ 70 | float d = max(0.0,dist-Rin)/Rin+1.0; 71 | attenuation = clamp(1.0/d, 0.0, 1.0); 72 | #endif 73 | #else 74 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_OUTER_RADIUS 75 | float d = clamp(dist/_light.outerRadius, 0.0, 1.0); 76 | attenuation = 1.0-d*d; 77 | #else 78 | attenuation = 1.0; 79 | #endif 80 | #endif 81 | #endif 82 | 83 | // Computer accumulators 84 | light_accumulator_ambient.rgb += _light.ambient * attenuation; 85 | 86 | #ifdef TANGRAM_MATERIAL_DIFFUSE 87 | light_accumulator_diffuse.rgb += _light.diffuse * nDotVP * attenuation; 88 | #endif 89 | 90 | #ifdef TANGRAM_MATERIAL_SPECULAR 91 | float pf = 0.0; // power factor for shiny speculars 92 | if (nDotVP > 0.0) { 93 | vec3 reflectVector = reflect(-VP, _normal); 94 | float eyeDotR = max(0.0, dot(-normalize(_eyeToPoint), reflectVector)); 95 | pf = pow(eyeDotR, material.shininess); 96 | } 97 | 98 | light_accumulator_specular.rgb += _light.specular * pf * attenuation; 99 | #endif 100 | } 101 | -------------------------------------------------------------------------------- /src/lights/spot_light.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Expected globals: 4 | material 5 | light_accumulator_* 6 | 7 | */ 8 | 9 | struct SpotLight { 10 | vec3 ambient; 11 | vec3 diffuse; 12 | vec3 specular; 13 | vec4 position; 14 | 15 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_EXPONENT 16 | float attenuationExponent; 17 | #endif 18 | 19 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_INNER_RADIUS 20 | float innerRadius; 21 | #endif 22 | 23 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_OUTER_RADIUS 24 | float outerRadius; 25 | #endif 26 | 27 | vec3 direction; 28 | float spotCosCutoff; 29 | float spotExponent; 30 | }; 31 | 32 | void calculateLight(in SpotLight _light, in vec3 _eyeToPoint, in vec3 _normal) { 33 | 34 | float dist = length(_light.position.xyz - _eyeToPoint); 35 | 36 | // Compute vector from surface to light position 37 | vec3 VP = (_light.position.xyz - _eyeToPoint) / dist; 38 | 39 | // normal . light direction 40 | float nDotVP = clamp(dot(_normal, VP), 0.0, 1.0); 41 | 42 | // Attenuation defaults 43 | float attenuation = 1.0; 44 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_EXPONENT 45 | float Rin = 1.0; 46 | float e = _light.attenuationExponent; 47 | 48 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_INNER_RADIUS 49 | Rin = _light.innerRadius; 50 | #endif 51 | 52 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_OUTER_RADIUS 53 | float Rdiff = _light.outerRadius-Rin; 54 | float d = clamp(max(0.0,dist-Rin)/Rdiff, 0.0, 1.0); 55 | attenuation = 1.0-(pow(d,e)); 56 | #else 57 | // If no outer is provide behaves like: 58 | // https://imdoingitwrong.wordpress.com/2011/01/31/light-attenuation/ 59 | float d = max(0.0,dist-Rin)/Rin+1.0; 60 | attenuation = clamp(1.0/(pow(d,e)), 0.0, 1.0); 61 | #endif 62 | #else 63 | float Rin = 0.0; 64 | 65 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_INNER_RADIUS 66 | Rin = _light.innerRadius; 67 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_OUTER_RADIUS 68 | float Rdiff = _light.outerRadius-Rin; 69 | float d = clamp(max(0.0,dist-Rin)/Rdiff, 0.0, 1.0); 70 | attenuation = 1.0-d*d; 71 | #else 72 | // If no outer is provide behaves like: 73 | // https://imdoingitwrong.wordpress.com/2011/01/31/light-attenuation/ 74 | float d = max(0.0,dist-Rin)/Rin+1.0; 75 | attenuation = clamp(1.0/d, 0.0, 1.0); 76 | #endif 77 | #else 78 | #ifdef TANGRAM_POINTLIGHT_ATTENUATION_OUTER_RADIUS 79 | float d = clamp(dist/_light.outerRadius, 0.0, 1.0); 80 | attenuation = 1.0-d*d; 81 | #else 82 | attenuation = 1.0; 83 | #endif 84 | #endif 85 | #endif 86 | 87 | // spotlight attenuation factor 88 | float spotAttenuation = 0.0; 89 | 90 | // See if point on surface is inside cone of illumination 91 | float spotDot = clamp(dot(-VP, _light.direction), 0.0, 1.0); 92 | 93 | if (spotDot >= _light.spotCosCutoff) { 94 | spotAttenuation = pow(spotDot, _light.spotExponent); 95 | } 96 | 97 | light_accumulator_ambient.rgb += _light.ambient * attenuation * spotAttenuation; 98 | 99 | #ifdef TANGRAM_MATERIAL_DIFFUSE 100 | light_accumulator_diffuse.rgb += _light.diffuse * nDotVP * attenuation * spotAttenuation; 101 | #endif 102 | 103 | #ifdef TANGRAM_MATERIAL_SPECULAR 104 | // Power factor for shiny speculars 105 | float pf = 0.0; 106 | if (nDotVP > 0.0) { 107 | vec3 reflectVector = reflect(-VP, _normal); 108 | float eyeDotR = max(dot(-normalize(_eyeToPoint), reflectVector), 0.0); 109 | pf = pow(eyeDotR, material.shininess); 110 | } 111 | light_accumulator_specular.rgb += _light.specular * pf * attenuation * spotAttenuation; 112 | #endif 113 | } 114 | -------------------------------------------------------------------------------- /src/scene/globals.js: -------------------------------------------------------------------------------- 1 | import log from '../utils/log'; 2 | import { getPropertyPathTarget } from '../utils/props'; 3 | 4 | // prefix used to identify global property references 5 | const GLOBAL_PREFIX = 'global.'; 6 | const GLOBAL_PREFIX_LENGTH = GLOBAL_PREFIX.length; 7 | 8 | // name of 'hidden' (non-enumerable) property used to track global property references on an object 9 | const GLOBAL_REGISTRY = '__global_prop'; 10 | 11 | // Property name references a global property? 12 | export function isGlobalReference (val) { 13 | return val?.slice(0, GLOBAL_PREFIX_LENGTH) === GLOBAL_PREFIX; 14 | } 15 | 16 | // Has object property been substitued with a value from a global reference? 17 | // Property provided as a single-depth string name, or nested path array (`a.b.c` => ['a', 'b', 'c']) 18 | export function isGlobalSubstitution (object, prop_or_path) { 19 | const path = Array.isArray(prop_or_path) ? prop_or_path : [prop_or_path]; 20 | const target = getPropertyPathTarget(object, path); 21 | const prop = path[path.length - 1]; 22 | return target?.[GLOBAL_REGISTRY]?.[prop] !== undefined; 23 | } 24 | 25 | // Flatten nested global properties for simpler string look-ups 26 | export function flattenGlobalProperties (obj, prefix = null, globals = {}) { 27 | prefix = prefix ? (prefix + '.') : GLOBAL_PREFIX; 28 | 29 | for (const p in obj) { 30 | const key = prefix + p; 31 | const val = obj[p]; 32 | globals[key] = val; 33 | 34 | if (typeof val === 'object' && !Array.isArray(val)) { 35 | flattenGlobalProperties(val, key, globals); 36 | } 37 | } 38 | return globals; 39 | } 40 | 41 | // Find and apply new global properties (and re-apply old ones) 42 | export function applyGlobalProperties (globals, obj, target, key) { 43 | let prop; 44 | 45 | // Check for previously applied global substitution 46 | if (target?.[GLOBAL_REGISTRY]?.[key]) { 47 | prop = target[GLOBAL_REGISTRY][key]; 48 | } 49 | // Check string for new global substitution 50 | else if (typeof obj === 'string' && obj.slice(0, GLOBAL_PREFIX_LENGTH) === GLOBAL_PREFIX) { 51 | prop = obj; 52 | } 53 | 54 | // Found global property to substitute 55 | if (prop) { 56 | // Mark property as global substitution 57 | if (target[GLOBAL_REGISTRY] == null) { 58 | Object.defineProperty(target, GLOBAL_REGISTRY, { value: {} }); 59 | } 60 | target[GLOBAL_REGISTRY][key] = prop; 61 | 62 | // Get current global value 63 | let val = globals[prop]; 64 | let stack; 65 | while (typeof val === 'string' && val.slice(0, GLOBAL_PREFIX_LENGTH) === GLOBAL_PREFIX) { 66 | // handle globals that refer to other globals, detecting any cyclical references 67 | stack = stack || [prop]; 68 | if (stack.indexOf(val) > -1) { 69 | log({ level: 'warn', once: true }, 'Global properties: cyclical reference detected', stack); 70 | val = null; 71 | break; 72 | } 73 | stack.push(val); 74 | val = globals[val]; 75 | } 76 | 77 | // Create getter/setter 78 | Object.defineProperty(target, key, { 79 | enumerable: true, 80 | get: function () { 81 | return val; // return substituted value 82 | }, 83 | set: function (v) { 84 | // clear the global substitution and remove the getter/setter 85 | delete target[GLOBAL_REGISTRY][key]; 86 | delete target[key]; 87 | target[key] = v; // save the new value 88 | } 89 | }); 90 | } 91 | // Loop through object keys or array indices 92 | else if (Array.isArray(obj)) { 93 | for (let p = 0; p < obj.length; p++) { 94 | applyGlobalProperties(globals, obj[p], obj, p); 95 | } 96 | } 97 | else if (typeof obj === 'object') { 98 | for (const p in obj) { 99 | applyGlobalProperties(globals, obj[p], obj, p); 100 | } 101 | } 102 | return obj; 103 | } 104 | -------------------------------------------------------------------------------- /src/scene/scene_debug.js: -------------------------------------------------------------------------------- 1 | import log from '../utils/log'; 2 | import WorkerBroker from '../utils/worker_broker'; 3 | import debugSettings from '../utils/debug_settings'; 4 | import { debugSumLayerStats } from '../tile/tile'; 5 | import Texture from '../gl/texture'; 6 | 7 | // Debug config and functions 8 | export default function setupSceneDebug (scene) { 9 | scene.debug = { 10 | // Profile helpers, issues a profile on main thread & all workers 11 | profile(name) { 12 | console.profile(`main thread: ${name}`); // eslint-disable-line no-console 13 | WorkerBroker.postMessage(scene.workers, 'self.profile', name); 14 | }, 15 | 16 | profileEnd(name) { 17 | console.profileEnd(`main thread: ${name}`); // eslint-disable-line no-console 18 | WorkerBroker.postMessage(scene.workers, 'self.profileEnd', name); 19 | }, 20 | 21 | // Rebuild geometry a given # of times and print average, min, max timings 22 | timeRebuild (num = 1, options = {}) { 23 | let times = []; 24 | let cycle = () => { 25 | let start = +new Date(); 26 | scene.rebuild(options).then(() => { 27 | times.push(+new Date() - start); 28 | 29 | if (times.length < num) { 30 | cycle(); 31 | } 32 | else { 33 | let avg = ~~(times.reduce((a, b) => a + b) / times.length); 34 | log('info', `Profiled rebuild ${num} times: ${avg} avg (${Math.min(...times)} min, ${Math.max(...times)} max)`); 35 | } 36 | }); 37 | }; 38 | cycle(); 39 | }, 40 | 41 | // Return geometry counts of visible tiles, grouped by style name 42 | geometryCountByStyle () { 43 | let counts = {}; 44 | scene.tile_manager.getRenderableTiles().forEach(tile => { 45 | for (let style in tile.meshes) { 46 | counts[style] = counts[style] || 0; 47 | tile.meshes[style].forEach(mesh => { 48 | counts[style] += mesh.geometry_count; 49 | }); 50 | } 51 | }); 52 | return counts; 53 | }, 54 | 55 | // Return geometry counts of visible tiles, grouped by base style name 56 | geometryCountByBaseStyle () { 57 | let style_counts = scene.debug.geometryCountByStyle(); 58 | let counts = {}; 59 | for (let style in style_counts) { 60 | let base = scene.styles[style].baseStyle(); 61 | counts[base] = counts[base] || 0; 62 | counts[base] += style_counts[style]; 63 | } 64 | return counts; 65 | }, 66 | 67 | // Return sum of all geometry counts for visible tiles 68 | geometryCountTotal () { 69 | const styles = scene.debug.geometryCountByStyle(); 70 | return Object.keys(styles).reduce((p, c) => styles[c] + p, 0); 71 | }, 72 | 73 | // Return geometry GL buffer sizes for visible tiles, grouped by style name 74 | geometrySizeByStyle () { 75 | let sizes = {}; 76 | scene.tile_manager.getRenderableTiles().forEach(tile => { 77 | for (let style in tile.meshes) { 78 | sizes[style] = sizes[style] || 0; 79 | tile.meshes[style].forEach(mesh => { 80 | sizes[style] += mesh.buffer_size; 81 | }); 82 | } 83 | }); 84 | return sizes; 85 | }, 86 | 87 | // Return geometry GL buffer sizes for visible tiles, grouped by base style name 88 | geometrySizeByBaseStyle () { 89 | let style_sizes = scene.debug.geometrySizeByStyle(); 90 | let sizes = {}; 91 | for (let style in style_sizes) { 92 | let base = scene.styles[style].baseStyle(); 93 | sizes[base] = sizes[base] || 0; 94 | sizes[base] += style_sizes[style]; 95 | } 96 | return sizes; 97 | }, 98 | 99 | // Return sum of all geometry GL buffer sizes for visible tiles 100 | geometrySizeTotal () { 101 | const styles = scene.debug.geometrySizeByStyle(); 102 | return Object.keys(styles).reduce((p, c) => styles[c] + p, 0); 103 | }, 104 | 105 | // Return sum of all texture memory usage 106 | textureSizeTotal() { 107 | return Object.values(Texture.textures).map(t => t.byteSize()).reduce((p, c) => p + c); 108 | }, 109 | 110 | layerStats () { 111 | if (debugSettings.layer_stats) { 112 | return debugSumLayerStats(scene.tile_manager.getRenderableTiles()); 113 | } 114 | else { 115 | log('warn', 'Enable the \'layer_stats\' debug setting to collect layer stats'); 116 | return {}; 117 | } 118 | }, 119 | 120 | renderableTilesCount () { 121 | return scene.tile_manager.getRenderableTiles().length; 122 | } 123 | }; 124 | } 125 | -------------------------------------------------------------------------------- /src/selection/selection_fragment.glsl: -------------------------------------------------------------------------------- 1 | // Fragment shader for feature selection passes 2 | // Renders in silhouette according to selection (picking) color, or black if none defined 3 | 4 | #ifdef TANGRAM_FEATURE_SELECTION 5 | varying vec4 v_selection_color; 6 | #endif 7 | 8 | void main (void) { 9 | #ifdef TANGRAM_FEATURE_SELECTION 10 | gl_FragColor = v_selection_color; 11 | #else 12 | gl_FragColor = vec4(0., 0., 0., 1.); 13 | #endif 14 | } 15 | -------------------------------------------------------------------------------- /src/selection/selection_globals.glsl: -------------------------------------------------------------------------------- 1 | // Vertex attribute + varying for feature selection 2 | #if defined(TANGRAM_FEATURE_SELECTION) && defined(TANGRAM_VERTEX_SHADER) 3 | attribute vec4 a_selection_color; 4 | varying vec4 v_selection_color; 5 | #endif 6 | -------------------------------------------------------------------------------- /src/selection/selection_vertex.glsl: -------------------------------------------------------------------------------- 1 | // Selection pass-specific rendering 2 | #if defined(TANGRAM_FEATURE_SELECTION) && defined(TANGRAM_VERTEX_SHADER) 3 | if (a_selection_color.rgb == vec3(0.)) { 4 | // Discard by forcing invalid triangle if we're in the feature 5 | // selection pass but have no selection info 6 | // TODO: in some cases we may actually want non-selectable features to occlude selectable ones? 7 | gl_Position = vec4(0., 0., 0., 1.); 8 | return; 9 | } 10 | v_selection_color = a_selection_color; 11 | #endif 12 | -------------------------------------------------------------------------------- /src/sources/sources.js: -------------------------------------------------------------------------------- 1 | // add all data source types 2 | import './geojson'; 3 | import './topojson'; 4 | import './mvt'; 5 | import './raster'; 6 | -------------------------------------------------------------------------------- /src/sources/topojson.js: -------------------------------------------------------------------------------- 1 | import DataSource from './data_source'; 2 | import {GeoJSONSource, GeoJSONTileSource} from './geojson'; 3 | 4 | import * as topojson from 'topojson-client'; 5 | 6 | /** 7 | TopoJSON standalone (non-tiled) source 8 | Uses geojson-vt split into tiles client-side 9 | */ 10 | 11 | export class TopoJSONSource extends GeoJSONSource { 12 | 13 | parseSourceData (tile, source, response) { 14 | let data = typeof response === 'string' ? JSON.parse(response) : response; 15 | data = this.toGeoJSON(data); 16 | 17 | let layers = this.getLayers(data); 18 | super.preprocessLayers(layers, tile); 19 | source.layers = layers; 20 | } 21 | 22 | toGeoJSON (data) { 23 | // Single layer 24 | if (data.objects && 25 | Object.keys(data.objects).length === 1) { 26 | let layer = Object.keys(data.objects)[0]; 27 | data = getTopoJSONFeature(data, data.objects[layer]); 28 | } 29 | // Multiple layers 30 | else { 31 | let layers = {}; 32 | for (let key in data.objects) { 33 | layers[key] = getTopoJSONFeature(data, data.objects[key]); 34 | } 35 | data = layers; 36 | } 37 | return data; 38 | } 39 | 40 | } 41 | 42 | function getTopoJSONFeature (topology, object) { 43 | let feature = topojson.feature(topology, object); 44 | 45 | // Convert single feature to a feature collection 46 | if (feature.type === 'Feature') { 47 | feature = { 48 | type: 'FeatureCollection', 49 | features: [feature] 50 | }; 51 | } 52 | return feature; 53 | } 54 | 55 | 56 | /** 57 | TopoJSON vector tiles 58 | @class TopoJSONTileSource 59 | */ 60 | export class TopoJSONTileSource extends GeoJSONTileSource { 61 | 62 | constructor(source, sources) { 63 | super(source, sources); 64 | } 65 | 66 | parseSourceData (tile, source, response) { 67 | let data = typeof response === 'string' ? JSON.parse(response) : response; 68 | data = TopoJSONSource.prototype.toGeoJSON(data); 69 | this.prepareGeoJSON(data, tile, source); 70 | } 71 | 72 | } 73 | 74 | // Check for URL tile pattern, if not found, treat as standalone GeoJSON/TopoJSON object 75 | DataSource.register('TopoJSON', source => { 76 | return TopoJSONTileSource.urlHasTilePattern(source.url) ? TopoJSONTileSource : TopoJSONSource; 77 | }); 78 | -------------------------------------------------------------------------------- /src/styles/lines/dasharray.js: -------------------------------------------------------------------------------- 1 | // Renders an array specifying a line pattern of alternating dashes and spaces, 2 | // similar to an SVG `dasharray` or Canvas setLineDash(), into a byte array of RGBA pixels 3 | // Returns: 4 | // { 5 | // pixel: rendered image in Uint8Array buffer 6 | // length: pixel length of rendered dash pattern (sum of all dashes and spaces) 7 | // } 8 | // 9 | // https://www.w3.org/TR/SVG/painting.html#StrokeDasharrayProperty 10 | // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray 11 | // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash 12 | 13 | const default_dash_color = [255, 255, 255, 255]; 14 | const default_background_color = [0, 0, 0, 0]; 15 | 16 | export default function renderDashArray (pattern, options = {}) { 17 | const dash_pixel = options.dash_color || default_dash_color; 18 | const background_color = options.background_color || default_background_color; 19 | const dashes = pattern; 20 | const scale = options.scale || 1; 21 | 22 | // If pattern is odd, repeat it to make it even (see SVG spec) 23 | if (dashes.length % 2 === 1) { 24 | Array.prototype.push.apply(dashes, dashes); 25 | } 26 | 27 | let dash = true; 28 | let pixels = []; 29 | for (let i=0; i < dashes.length; i++) { 30 | let segment = Math.floor(dashes[i] * scale); 31 | for (let s=0; s < segment; s++) { 32 | Array.prototype.push.apply(pixels, dash ? dash_pixel : background_color); 33 | } 34 | dash = !dash; // alternate between dashes and spaces 35 | } 36 | 37 | pixels.reverse(); // flip Y (GL textures are upside down) 38 | pixels = new Uint8Array(pixels); // convert to typed array 39 | const length = pixels.length / 4; // one RGBA byte sequences to one pixel 40 | 41 | return { pixels, length }; 42 | } 43 | -------------------------------------------------------------------------------- /src/styles/points/points_fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform vec2 u_resolution; 2 | uniform float u_time; 3 | uniform vec3 u_map_position; 4 | uniform vec4 u_tile_origin; 5 | uniform float u_meters_per_pixel; 6 | uniform float u_device_pixel_ratio; 7 | uniform float u_visible_time; 8 | 9 | uniform mat3 u_normalMatrix; 10 | uniform mat3 u_inverseNormalMatrix; 11 | 12 | uniform sampler2D u_texture; 13 | uniform float u_point_type; 14 | uniform bool u_apply_color_blocks; 15 | 16 | varying vec4 v_color; 17 | varying vec2 v_texcoord; 18 | varying vec4 v_world_position; 19 | varying float v_alpha_factor; 20 | 21 | #ifdef TANGRAM_HAS_SHADER_POINTS 22 | varying vec4 v_outline_color; 23 | varying float v_outline_edge; 24 | varying float v_aa_offset; 25 | #endif 26 | 27 | #ifdef TANGRAM_SHOW_HIDDEN_LABELS 28 | varying float v_label_hidden; 29 | #endif 30 | 31 | #define TANGRAM_NORMAL vec3(0., 0., 1.) 32 | 33 | #pragma tangram: attributes 34 | #pragma tangram: camera 35 | #pragma tangram: material 36 | #pragma tangram: lighting 37 | #pragma tangram: raster 38 | #pragma tangram: global 39 | 40 | #ifdef TANGRAM_HAS_SHADER_POINTS 41 | //l is the distance from the center to the fragment, R is the radius of the drawn point 42 | float _tangram_antialias(float l, float R){ 43 | float low = R - v_aa_offset; 44 | float high = R + v_aa_offset; 45 | return 1. - smoothstep(low, high, l); 46 | } 47 | #endif 48 | 49 | void main (void) { 50 | // Initialize globals 51 | #pragma tangram: setup 52 | 53 | vec4 color = v_color; 54 | 55 | #ifdef TANGRAM_HAS_SHADER_POINTS 56 | // Only apply shader blocks to point, not to attached text (N.B.: for compatibility with ES) 57 | if (u_point_type == TANGRAM_POINT_TYPE_TEXTURE) { // sprite texture 58 | color *= texture2D(u_texture, v_texcoord); 59 | } 60 | else if (u_point_type == TANGRAM_POINT_TYPE_LABEL) { // label texture 61 | color = texture2D(u_texture, v_texcoord); 62 | color.rgb /= max(color.a, 0.001); // un-multiply canvas texture 63 | } 64 | else if (u_point_type == TANGRAM_POINT_TYPE_SHADER) { // shader point 65 | // Mask of outermost circle, either outline or point boundary 66 | float _d = length(v_texcoord); // distance to this fragment from the point center 67 | float _outer_alpha = _tangram_antialias(_d, 1.); 68 | float _fill_alpha = _tangram_antialias(_d, 1. - (v_outline_edge * 0.5)) * color.a; 69 | float _stroke_alpha = (_outer_alpha - _tangram_antialias(_d, 1. - v_outline_edge)) * v_outline_color.a; 70 | 71 | // Apply alpha compositing with stroke 'over' fill. 72 | #ifdef TANGRAM_BLEND_ADD 73 | color.a = _stroke_alpha + _fill_alpha; 74 | color.rgb = color.rgb * _fill_alpha + v_outline_color.rgb * _stroke_alpha; 75 | #else // TANGRAM_BLEND_OVERLAY (and fallback for not implemented blending modes) 76 | color.a = _stroke_alpha + _fill_alpha * (1. - _stroke_alpha); 77 | color.rgb = mix(color.rgb * _fill_alpha, v_outline_color.rgb, _stroke_alpha) / max(color.a, 0.001); // avoid divide by zero 78 | #endif 79 | } 80 | #else 81 | // If shader points not supported, assume label texture 82 | color = texture2D(u_texture, v_texcoord); 83 | color.rgb /= max(color.a, 0.001); // un-multiply canvas texture 84 | #endif 85 | 86 | // Shader blocks for color/filter are only applied for sprites, shader points, and standalone text, 87 | // NOT for text attached to a point (N.B.: for compatibility with ES) 88 | if (u_apply_color_blocks) { 89 | #pragma tangram: color 90 | #pragma tangram: filter 91 | } 92 | 93 | color.a *= v_alpha_factor; 94 | 95 | // highlight hidden label in fragment shader for debugging 96 | #ifdef TANGRAM_SHOW_HIDDEN_LABELS 97 | if (v_label_hidden > 0.) { 98 | color.a *= 0.5; 99 | color.rgb = vec3(1., 0., 0.); 100 | } 101 | #endif 102 | 103 | // Use alpha test as a lower-quality substitute 104 | // For opaque and translucent: avoid transparent pixels writing to depth buffer, obscuring geometry underneath 105 | // For multiply: avoid transparent pixels multiplying geometry underneath to zero/full black 106 | #if defined(TANGRAM_BLEND_OPAQUE) || defined(TANGRAM_BLEND_TRANSLUCENT) || defined(TANGRAM_BLEND_MULTIPLY) 107 | if (color.a < TANGRAM_ALPHA_TEST) { 108 | discard; 109 | } 110 | #endif 111 | 112 | // Make points more visible in wireframe debug mode 113 | #ifdef TANGRAM_WIREFRAME 114 | color = vec4(vec3(0.5), 1.); // use gray outline for textured points 115 | #ifdef TANGRAM_HAS_SHADER_POINTS 116 | if (u_point_type == TANGRAM_POINT_TYPE_SHADER) { 117 | color = vec4(v_color.rgb, 1.); // use original vertex color outline for shader points 118 | } 119 | #endif 120 | #endif 121 | 122 | gl_FragColor = color; 123 | } 124 | -------------------------------------------------------------------------------- /src/styles/polygons/polygons_fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform vec2 u_resolution; 2 | uniform float u_time; 3 | uniform vec3 u_map_position; 4 | uniform vec4 u_tile_origin; 5 | uniform float u_meters_per_pixel; 6 | uniform float u_device_pixel_ratio; 7 | 8 | uniform mat3 u_normalMatrix; 9 | uniform mat3 u_inverseNormalMatrix; 10 | 11 | varying vec4 v_position; 12 | varying vec3 v_normal; 13 | varying vec4 v_color; 14 | varying vec4 v_world_position; 15 | 16 | #ifdef TANGRAM_EXTRUDE_LINES 17 | uniform bool u_has_line_texture; 18 | uniform sampler2D u_texture; 19 | uniform float u_texture_ratio; 20 | uniform vec4 u_dash_background_color; 21 | uniform float u_has_dash; 22 | #endif 23 | 24 | #define TANGRAM_NORMAL v_normal 25 | 26 | #if defined(TANGRAM_TEXTURE_COORDS) || defined(TANGRAM_EXTRUDE_LINES) 27 | varying vec2 v_texcoord; 28 | #endif 29 | 30 | #ifdef TANGRAM_MODEL_POSITION_BASE_ZOOM_VARYING 31 | varying vec4 v_modelpos_base_zoom; 32 | #endif 33 | 34 | #if defined(TANGRAM_LIGHTING_VERTEX) 35 | varying vec4 v_lighting; 36 | #endif 37 | 38 | #pragma tangram: attributes 39 | #pragma tangram: camera 40 | #pragma tangram: material 41 | #pragma tangram: lighting 42 | #pragma tangram: raster 43 | #pragma tangram: global 44 | 45 | void main (void) { 46 | // Initialize globals 47 | #pragma tangram: setup 48 | 49 | vec4 color = v_color; 50 | vec3 normal = TANGRAM_NORMAL; 51 | 52 | // Apply raster to vertex color 53 | #ifdef TANGRAM_RASTER_TEXTURE_COLOR 54 | vec4 _raster_color = sampleRaster(0); 55 | 56 | #if defined(TANGRAM_BLEND_OPAQUE) || defined(TANGRAM_BLEND_TRANSLUCENT) || defined(TANGRAM_BLEND_MULTIPLY) 57 | // Raster sources can optionally mask by the alpha channel, which will render with only full or no alpha. 58 | // This is used for handling transparency outside the raster image in some blend modes, 59 | // which either don't support alpha, or would cause transparent pixels to write to the depth buffer, 60 | // obscuring geometry underneath. 61 | #ifdef TANGRAM_HAS_MASKED_RASTERS // skip masking logic if no masked raster sources 62 | #ifndef TANGRAM_ALL_MASKED_RASTERS // skip source check for masking if *all* raster sources are masked 63 | if (u_raster_mask_alpha) { 64 | #else 65 | { 66 | #endif 67 | #if defined(TANGRAM_BLEND_TRANSLUCENT) || defined(TANGRAM_BLEND_MULTIPLY) 68 | if (_raster_color.a < TANGRAM_EPSILON) { 69 | discard; 70 | } 71 | #else // TANGRAM_BLEND_OPAQUE 72 | if (_raster_color.a < 1. - TANGRAM_EPSILON) { 73 | discard; 74 | } 75 | // only allow full alpha in opaque blend mode (avoids artifacts blending w/canvas tile background) 76 | _raster_color.a = 1.; 77 | #endif 78 | } 79 | #endif 80 | #endif 81 | 82 | color *= _raster_color; // multiplied to tint texture color 83 | #endif 84 | 85 | // Apply line texture 86 | #ifdef TANGRAM_EXTRUDE_LINES 87 | { // enclose in scope to avoid leakage of internal variables 88 | if (u_has_line_texture) { 89 | vec2 _line_st = vec2(v_texcoord.x, fract(v_texcoord.y / u_texture_ratio)); 90 | vec4 _line_color = texture2D(u_texture, _line_st); 91 | 92 | // If the line has a dash pattern, the line texture indicates if the current fragment should be 93 | // the dash foreground or background color. If the line doesn't have a dash pattern, 94 | // the line texture color is used directly (but also tinted by the vertex color). 95 | color = mix( 96 | color * _line_color, // no dash: tint the line texture with the vertex color 97 | mix(u_dash_background_color, color, _line_color.a), // choose dash foreground or background color 98 | u_has_dash // 0 if no dash, 1 if has dash 99 | ); 100 | 101 | // Use alpha discard test as a lower-quality substitute for blending 102 | #if defined(TANGRAM_BLEND_OPAQUE) 103 | if (color.a < TANGRAM_ALPHA_TEST) { 104 | discard; 105 | } 106 | #endif 107 | } 108 | } 109 | #endif 110 | 111 | // First, get normal from raster tile (if applicable) 112 | #ifdef TANGRAM_RASTER_TEXTURE_NORMAL 113 | normal = normalize(sampleRaster(0).rgb * 2. - 1.); 114 | #endif 115 | 116 | // Second, alter normal with normal map texture (if applicable) 117 | #if defined(TANGRAM_LIGHTING_FRAGMENT) && defined(TANGRAM_MATERIAL_NORMAL_TEXTURE) 118 | calculateNormal(normal); 119 | #endif 120 | 121 | // Normal modification applied here for fragment lighting or no lighting, 122 | // and in vertex shader for vertex lighting 123 | #if !defined(TANGRAM_LIGHTING_VERTEX) 124 | #pragma tangram: normal 125 | #endif 126 | 127 | // Color modification before lighting is applied 128 | #pragma tangram: color 129 | 130 | #if defined(TANGRAM_LIGHTING_FRAGMENT) 131 | // Calculate per-fragment lighting 132 | color = calculateLighting(v_position.xyz - u_eye, normal, color); 133 | #elif defined(TANGRAM_LIGHTING_VERTEX) 134 | // Apply lighting intensity interpolated from vertex shader 135 | color *= v_lighting; 136 | #endif 137 | 138 | // Post-processing effects (modify color after lighting) 139 | #pragma tangram: filter 140 | 141 | gl_FragColor = color; 142 | } 143 | -------------------------------------------------------------------------------- /src/styles/raster/raster.js: -------------------------------------------------------------------------------- 1 | // Raster tile rendering style 2 | 3 | import StyleParser from '../style_parser'; 4 | import {Polygons} from '../polygons/polygons'; 5 | 6 | export let RasterStyle = Object.create(Polygons); 7 | 8 | Object.assign(RasterStyle, { 9 | name: 'raster', 10 | super: Polygons, 11 | built_in: true, 12 | 13 | init() { 14 | // Required for raster tiles 15 | this.raster = this.raster || 'color'; 16 | 17 | this.super.init.apply(this, arguments); 18 | 19 | this.selection = false; // raster styles can't support feature selection 20 | }, 21 | 22 | _preprocess (draw) { 23 | // Raster tiles default to white vertex color, as this color will tint the underlying texture 24 | draw.color = draw.color || StyleParser.defaults.color; 25 | return this.super._preprocess.apply(this, arguments); 26 | } 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /src/styles/raster/raster_globals.glsl: -------------------------------------------------------------------------------- 1 | // Uniforms defining raster textures and macros for accessing them 2 | 3 | #ifdef TANGRAM_FRAGMENT_SHADER 4 | uniform sampler2D u_rasters[TANGRAM_NUM_RASTER_SOURCES]; // raster tile texture samplers 5 | uniform vec2 u_raster_sizes[TANGRAM_NUM_RASTER_SOURCES]; // raster tile texture sizes (width/height in pixels) 6 | uniform vec3 u_raster_offsets[TANGRAM_NUM_RASTER_SOURCES]; // raster tile texture UV starting offset for tile 7 | 8 | // Raster sources can optionally mask by the alpha channel (render with only full or no alpha, based on a threshold), 9 | // which is used for handling transparency outside the raster image when rendering with opaque blending 10 | #if defined(TANGRAM_HAS_MASKED_RASTERS) && !defined(TANGRAM_ALL_MASKED_RASTERS) // only add uniform if we need it 11 | uniform bool u_raster_mask_alpha; 12 | #endif 13 | 14 | // Note: the raster accessors below are #defines rather than functions to 15 | // avoid issues with constant integer expressions for array indices 16 | 17 | // Adjusts UVs in model space to account for raster tile texture overzooming 18 | // (applies scale and offset adjustments) 19 | #define adjustRasterUV(raster_index, uv) \ 20 | ((uv) * u_raster_offsets[raster_index].z + u_raster_offsets[raster_index].xy) 21 | 22 | // Returns the UVs of the current model position for a raster sampler 23 | #define currentRasterUV(raster_index) \ 24 | (adjustRasterUV(raster_index, v_modelpos_base_zoom.xy)) 25 | 26 | // Returns pixel location in raster tile texture at current model position 27 | #define currentRasterPixel(raster_index) \ 28 | (currentRasterUV(raster_index) * rasterPixelSize(raster_index)) 29 | 30 | // Samples a raster tile texture for the current model position 31 | #define sampleRaster(raster_index) \ 32 | (texture2D(u_rasters[raster_index], currentRasterUV(raster_index))) 33 | 34 | // Samples a raster tile texture for a given pixel 35 | #define sampleRasterAtPixel(raster_index, pixel) \ 36 | (texture2D(u_rasters[raster_index], (pixel) / rasterPixelSize(raster_index))) 37 | 38 | // Returns size of raster sampler in pixels 39 | #define rasterPixelSize(raster_index) \ 40 | (u_raster_sizes[raster_index]) 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /src/styles/style_globals.glsl: -------------------------------------------------------------------------------- 1 | #ifdef TANGRAM_VERTEX_SHADER 2 | 3 | // Apply layer ordering to avoid z-fighting 4 | void applyLayerOrder (float layer, inout vec4 position) { 5 | position.z -= layer * TANGRAM_LAYER_DELTA * position.w; 6 | } 7 | 8 | // Vertex position in model space: [0, 1] range over the local tile 9 | // Note positions can be outside that range due to unclipped geometry, geometry higher than a unit cube, etc. 10 | vec4 modelPosition() { 11 | return 12 | vec4( 13 | a_position.xyz / TANGRAM_TILE_SCALE // scale coords to ~0-1 range 14 | * exp2(u_tile_origin.z - u_tile_origin.w), // adjust for tile overzooming 15 | 1.) 16 | + vec4(0., 1., 0., 0.); 17 | // NB: additional offset to account for unusual Tangram JS y coords, 18 | // should be refactored to remove 19 | } 20 | 21 | // Position in model space as above, but according to tile coordinate (as opposed to style) zoom 22 | // e.g. unadjusted for tile overzooming 23 | vec4 modelPositionBaseZoom() { 24 | return 25 | vec4( 26 | a_position.xyz / TANGRAM_TILE_SCALE, // scale coords to ~0-1 range 27 | 1.) 28 | + vec4(0., 1., 0., 0.); // see note on offset above 29 | } 30 | 31 | #endif 32 | 33 | // Vertex position in world coordinates, useful for 3d procedural textures, etc. 34 | vec4 worldPosition() { 35 | return v_world_position; 36 | } 37 | 38 | // Optionally wrap world coordinates (allows more precision at higher zooms) 39 | // e.g. at wrap 1000, the world space will wrap every 1000 meters 40 | #ifdef TANGRAM_VERTEX_SHADER 41 | 42 | vec4 wrapWorldPosition(vec4 world_position) { 43 | #if defined(TANGRAM_WORLD_POSITION_WRAP) 44 | vec2 anchor = u_tile_origin.xy - mod(u_tile_origin.xy, TANGRAM_WORLD_POSITION_WRAP); 45 | world_position.xy -= anchor; 46 | #endif 47 | return world_position; 48 | } 49 | 50 | #endif 51 | 52 | // Normal in world space 53 | #if defined(TANGRAM_VERTEX_SHADER) 54 | 55 | vec3 worldNormal() { 56 | return TANGRAM_NORMAL; 57 | } 58 | 59 | #elif defined(TANGRAM_FRAGMENT_SHADER) 60 | 61 | vec3 worldNormal() { 62 | return u_inverseNormalMatrix * TANGRAM_NORMAL; 63 | } 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /src/styles/text/text_wrap.js: -------------------------------------------------------------------------------- 1 | // Word wrapping 2 | 3 | import { isTextRTL, isTextNeutral, RTL_MARKER } from './text_segments'; 4 | 5 | // Private class to arrange text labels into multiple lines based on 6 | // "text wrap" and "max line" values 7 | export default class MultiLine { 8 | constructor (context, max_lines = Infinity, text_wrap = Infinity) { 9 | this.width = 0; 10 | this.height = 0; 11 | this.lines = []; 12 | 13 | this.max_lines = max_lines; 14 | this.text_wrap = text_wrap; 15 | this.context = context; 16 | } 17 | 18 | createLine (line_height){ 19 | if (this.lines.length < this.max_lines){ 20 | return new Line(line_height, this.text_wrap); 21 | } 22 | else { 23 | return false; 24 | } 25 | } 26 | 27 | push (line){ 28 | if (this.lines.length < this.max_lines){ 29 | // measure line width 30 | let line_width = this.context.measureText(line.text).width; 31 | line.width = line_width; 32 | 33 | if (line_width > this.width){ 34 | this.width = Math.ceil(line_width); 35 | } 36 | 37 | // add to lines and increment height 38 | this.lines.push(line); 39 | this.height += line.height; 40 | return true; 41 | } 42 | else { 43 | this.addEllipsis(); 44 | return false; 45 | } 46 | } 47 | 48 | // pushes to the lines array and returns a new line if possible (false otherwise) 49 | advance (line, line_height) { 50 | let can_push = this.push(line); 51 | if (can_push){ 52 | return this.createLine(line_height); 53 | } 54 | else { 55 | return false; 56 | } 57 | } 58 | 59 | addEllipsis (){ 60 | let last_line = this.lines[this.lines.length - 1]; 61 | let ellipsis_width = Math.ceil(this.context.measureText(MultiLine.ellipsis).width); 62 | 63 | last_line.append(MultiLine.ellipsis); 64 | last_line.width += ellipsis_width; 65 | 66 | if (last_line.width > this.width) { 67 | this.width = last_line.width; 68 | } 69 | } 70 | 71 | finish (line){ 72 | if (line){ 73 | this.push(line); 74 | } 75 | else { 76 | this.addEllipsis(); 77 | } 78 | } 79 | 80 | static parse (str, text_wrap, max_lines, line_height, ctx) { 81 | // Word wrapping 82 | // Line breaks can be caused by: 83 | // - implicit line break when a maximum character threshold is exceeded per line (text_wrap) 84 | // - explicit line break in the label text (\n) 85 | let words; 86 | if (typeof text_wrap === 'number') { 87 | words = str.split(' '); // split words on spaces 88 | } 89 | else { 90 | words = [str]; // no max line word wrapping (but new lines will still be in effect) 91 | } 92 | 93 | let multiline = new MultiLine(ctx, max_lines, text_wrap); 94 | let line = multiline.createLine(line_height); 95 | 96 | // First iterate on space-break groups (will be one if max line length off), then iterate on line-break groups 97 | for (let i = 0; i < words.length; i++) { 98 | let breaks = words[i].split('\n'); // split on line breaks 99 | let new_line = (i === 0) ? true : false; 100 | 101 | for (let n=0; n < breaks.length; n++) { 102 | if (!line){ 103 | break; 104 | } 105 | 106 | let word = breaks[n]; 107 | 108 | // force punctuation (neutral chars) at the end of a RTL line, so they stay attached to original word 109 | if (isTextRTL(word) && isTextNeutral(word[word.length - 1])) { 110 | word += RTL_MARKER; 111 | } 112 | 113 | let spaced_word = (new_line) ? word : ' ' + word; 114 | 115 | // if adding current word would overflow, add a new line instead 116 | // first word (i === 0) always appends 117 | if (text_wrap && i > 0 && line.exceedsTextwrap(spaced_word)) { 118 | line = multiline.advance(line, line_height); 119 | if (!line){ 120 | break; 121 | } 122 | line.append(word); 123 | new_line = true; 124 | } 125 | else { 126 | line.append(spaced_word); 127 | } 128 | 129 | // if line breaks present, add new line (unless on last line) 130 | if (n < breaks.length - 1) { 131 | line = multiline.advance(line, line_height); 132 | new_line = true; 133 | } 134 | } 135 | 136 | if (i === words.length - 1){ 137 | multiline.finish(line); 138 | } 139 | } 140 | return multiline; 141 | } 142 | } 143 | 144 | MultiLine.ellipsis = '...'; 145 | 146 | // A Private class used by MultiLine to contain the logic for a single line 147 | // including character count, width, height and text 148 | class Line { 149 | constructor (height = 0, text_wrap = 0){ 150 | this.chars = 0; 151 | this.text = ''; 152 | 153 | this.height = Math.ceil(height); 154 | this.text_wrap = text_wrap; 155 | } 156 | 157 | append (text){ 158 | this.chars += text.length; 159 | this.text += text; 160 | } 161 | 162 | exceedsTextwrap (text){ 163 | return text.length + this.chars > this.text_wrap; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/tile/tile_id.js: -------------------------------------------------------------------------------- 1 | export const TileID = { 2 | 3 | coord(c) { 4 | return {x: c.x, y: c.y, z: c.z, key: this.coordKey(c)}; 5 | }, 6 | 7 | coordKey({x, y, z}) { 8 | return x + '/' + y + '/' + z; 9 | }, 10 | 11 | key (coords, source, style_z) { 12 | if (coords.y < 0 || coords.y >= (1 << coords.z) || coords.z < 0) { 13 | return; // cull tiles out of range (x will wrap) 14 | } 15 | return [source.name, coords.x, coords.y, coords.z, style_z].join('/'); 16 | }, 17 | 18 | normalizedKey (coords, source, style_z) { 19 | return this.key(this.normalizedCoord(coords, source), source, style_z); 20 | }, 21 | 22 | normalizedCoord (coords, source) { 23 | if (source.zoom_bias) { 24 | coords = this.coordAtZoom(coords, Math.max(coords.z - source.zoom_bias, source.zooms[0])); 25 | } 26 | return this.coordForTileZooms(coords, source.zooms); 27 | }, 28 | 29 | coordAtZoom({x, y, z}, zoom) { 30 | zoom = Math.max(0, zoom); // zoom can't go below zero 31 | if (z !== zoom) { 32 | let zscale = Math.pow(2, z - zoom); 33 | x = Math.floor(x / zscale); 34 | y = Math.floor(y / zscale); 35 | z = zoom; 36 | } 37 | return this.coord({x, y, z}); 38 | }, 39 | 40 | coordForTileZooms({ x, y, z }, zooms) { 41 | const nz = this.findZoomInRange(z, zooms); 42 | if (nz !== z) { 43 | return this.coordAtZoom({ x, y, z }, nz); 44 | } 45 | return this.coord({ x, y, z }); 46 | }, 47 | 48 | findZoomInRange(z, zooms) { 49 | return zooms.filter(s => z >= s).reverse()[0] || zooms[0]; 50 | }, 51 | 52 | isDescendant(parent, descendant) { 53 | if (descendant.z > parent.z) { 54 | let {x, y} = this.coordAtZoom(descendant, parent.z); 55 | return (parent.x === x && parent.y === y); 56 | } 57 | return false; 58 | }, 59 | 60 | // Return identifying info for tile's parent tile 61 | parent ({ coords, source, style_z }) { 62 | if (style_z > 0) { // no more tiles above style zoom 0 63 | style_z--; 64 | const sz = Math.max(style_z - source.zoom_bias, source.zooms[0]); // z can't be lower than tile source 65 | const c = this.coordForTileZooms(this.coordAtZoom(coords, sz), source.zooms); 66 | 67 | if (c.z > style_z) { 68 | return null; 69 | } 70 | 71 | return { 72 | key: this.key(c, source, style_z), 73 | coords: c, 74 | style_z, 75 | source 76 | }; 77 | } 78 | }, 79 | 80 | // Return identifying info for tile's child tiles 81 | children ({ coords, source, style_z }, CACHE = {}) { 82 | style_z++; 83 | const c = this.coordForTileZooms(this.coordAtZoom(coords, style_z - source.zoom_bias), source.zooms); 84 | if (c.z === coords.z) { 85 | // same coord zoom for next level down 86 | return [{ 87 | key: this.key(c, source, style_z), 88 | coords: c, 89 | style_z, 90 | source 91 | }]; 92 | } 93 | else { 94 | // coord zoom advanced down 95 | const key = this.key(c, source, style_z); 96 | CACHE[source.id] = CACHE[source.id] || {}; 97 | if (CACHE[source.id][key] == null) { 98 | const span = Math.pow(2, c.z - coords.z); 99 | const x = coords.x * span; 100 | const y = coords.y * span; 101 | let children = []; 102 | for (let nx = x; nx < x + span; nx++) { 103 | for (let ny = y; ny < y + span; ny++) { 104 | let nc = this.coord({ x: nx, y: ny, z: c.z }); 105 | children.push({ 106 | key: this.key(nc, source, style_z), 107 | coords: nc, 108 | style_z, 109 | source 110 | }); 111 | } 112 | } 113 | CACHE[source.id][key] = children; 114 | } 115 | return CACHE[source.id][key]; 116 | } 117 | } 118 | 119 | }; 120 | -------------------------------------------------------------------------------- /src/tile/tile_pyramid.js: -------------------------------------------------------------------------------- 1 | import {TileID} from './tile_id'; 2 | 3 | export default class TilePyramid { 4 | 5 | constructor() { 6 | this.tiles = {}; 7 | this.max_proxy_descendant_depth = 6; // # of levels to search up/down for proxy tiles 8 | this.max_proxy_ancestor_depth = 7; 9 | this.children_cache = {}; // cache for children of coordinates 10 | } 11 | 12 | addTile(tile) { 13 | // Add target tile 14 | this.tiles[tile.key] = this.tiles[tile.key] || { descendants: 0 }; 15 | this.tiles[tile.key].tile = tile; 16 | 17 | // Add to parents 18 | while (tile.style_z >= 0) { 19 | tile = TileID.parent(tile); 20 | if (!tile) { 21 | return; 22 | } 23 | 24 | if (!this.tiles[tile.key]) { 25 | this.tiles[tile.key] = { descendants: 0 }; 26 | } 27 | this.tiles[tile.key].descendants++; 28 | } 29 | } 30 | 31 | removeTile(tile) { 32 | // Remove target tile 33 | if (this.tiles[tile.key]) { 34 | delete this.tiles[tile.key].tile; 35 | 36 | if (this.tiles[tile.key].descendants === 0) { 37 | delete this.tiles[tile.key]; // remove whole tile in tree 38 | } 39 | } 40 | 41 | // Decrement reference count up the tile pyramid 42 | while (tile.style_z >= 0) { 43 | tile = TileID.parent(tile); 44 | if (!tile) { 45 | return; 46 | } 47 | 48 | if (this.tiles[tile.key] && this.tiles[tile.key].descendants > 0) { 49 | this.tiles[tile.key].descendants--; 50 | if (this.tiles[tile.key].descendants === 0 && !this.tiles[tile.key].tile) { 51 | delete this.tiles[tile.key]; // remove whole tile in tree 52 | } 53 | } 54 | } 55 | } 56 | 57 | // Find the parent tile for a given tile and style zoom level 58 | getAncestor (tile) { 59 | let level = 0; 60 | while (level < this.max_proxy_ancestor_depth) { 61 | tile = TileID.parent(tile); 62 | if (!tile) { 63 | return; 64 | } 65 | 66 | if (this.tiles[tile.key] && 67 | this.tiles[tile.key].tile && 68 | this.tiles[tile.key].tile.loaded) { 69 | return this.tiles[tile.key].tile; 70 | } 71 | 72 | level++; 73 | } 74 | } 75 | 76 | // Find the descendant tiles for a given tile and style zoom level 77 | getDescendants (tile, level = 0) { 78 | let descendants = []; 79 | if (level < this.max_proxy_descendant_depth) { 80 | let tiles = TileID.children(tile, this.children_cache); 81 | if (!tiles) { 82 | return; 83 | } 84 | 85 | tiles.forEach(t => { 86 | if (this.tiles[t.key]) { 87 | if (this.tiles[t.key].tile && 88 | this.tiles[t.key].tile.loaded) { 89 | descendants.push(this.tiles[t.key].tile); 90 | } 91 | else if (this.tiles[t.key].descendants > 0) { // didn't find any children, try next level 92 | descendants.push(...this.getDescendants(t, level + 1)); 93 | } 94 | } 95 | }); 96 | } 97 | 98 | return descendants; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | // Debounce a function 2 | // https://davidwalsh.name/javascript-debounce-function 3 | export default function debounce (func, wait) { 4 | var timeout; 5 | return function() { 6 | var context = this, args = arguments; 7 | var later = function() { 8 | timeout = null; 9 | func.apply(context, args); 10 | }; 11 | clearTimeout(timeout); 12 | timeout = setTimeout(later, wait); 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/debug_settings.js: -------------------------------------------------------------------------------- 1 | let debugSettings; 2 | 3 | export default debugSettings = { 4 | // draws a blue rectangle border around the collision box of a label 5 | draw_label_collision_boxes: false, 6 | 7 | // draws a green rectangle border within the texture box of a label 8 | draw_label_texture_boxes: false, 9 | 10 | // suppreses fade-in of labels 11 | suppress_label_fade_in: false, 12 | 13 | // suppress animaton of label snap to pixel grid 14 | suppress_label_snap_animation: false, 15 | 16 | // show hidden labels for debugging 17 | show_hidden_labels: false, 18 | 19 | // collect feature/geometry stats on styling layers 20 | layer_stats: false, 21 | 22 | // draw scene in wireframe mode 23 | wireframe: false 24 | }; 25 | 26 | export function mergeDebugSettings (settings) { 27 | Object.assign(debugSettings, settings); 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/errors.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export class MethodNotImplemented extends Error { 4 | constructor(methodName) { 5 | super(); 6 | this.name = 'MethodNotImplemented'; 7 | this.message = 'Method ' + methodName + ' must be implemented in subclass'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/functions.js: -------------------------------------------------------------------------------- 1 | import hashString from './hash'; 2 | 3 | // cache of functions, keyed by unique source 4 | const cache = { 5 | functions: {}, 6 | num_functions: 0, 7 | num_cached: 0 8 | }; 9 | 10 | export { cache as functionStringCache }; 11 | 12 | export function clearFunctionStringCache () { 13 | cache.functions = {}; 14 | cache.num_functions = 0; 15 | cache.num_cached = 0; 16 | } 17 | 18 | // Recursively parse an object, compiling string properties that look like functions 19 | export function compileFunctionStrings (obj, wrap) { 20 | // Convert string 21 | if (typeof obj === 'string') { 22 | obj = compileFunctionString(obj, wrap); 23 | } 24 | // Loop through object properties 25 | else if (obj != null && typeof obj === 'object') { 26 | for (let p in obj) { 27 | obj[p] = compileFunctionStrings(obj[p], wrap); 28 | } 29 | } 30 | return obj; 31 | } 32 | 33 | // Compile a string that looks like a function 34 | export function compileFunctionString (val, wrap) { 35 | // Parse function signature and body 36 | let fmatch = 37 | (typeof val === 'string') && 38 | val.match(/^\s*function[^(]*\(([^)]*)\)\s*?\{([\s\S]*)\}$/m); 39 | 40 | if (fmatch && fmatch.length > 2) { 41 | try { 42 | // function body 43 | const body = fmatch[2]; 44 | const source = (typeof wrap === 'function') ? wrap(body) : body; // optionally wrap source 45 | 46 | // compile and cache by unique function source 47 | const key = hashString(source); 48 | if (cache.functions[key] === undefined) { 49 | // function arguments extracted from signature 50 | let args = fmatch[1].length > 0 && fmatch[1].split(',').map(x => x.trim()).filter(x => x); 51 | args = args.length > 0 ? args : ['context']; // default to single 'context' argument 52 | 53 | cache.functions[key] = new Function(args.toString(), source); // jshint ignore:line 54 | cache.functions[key].source = body; // save original, un-wrapped function body source 55 | cache.num_functions++; 56 | } 57 | else { 58 | cache.num_cached++; 59 | } 60 | 61 | return cache.functions[key]; 62 | } 63 | catch (e) { 64 | // fall-back to original value if parsing failed 65 | return val; 66 | } 67 | } 68 | return val; 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/gl-matrix.js: -------------------------------------------------------------------------------- 1 | // Partial import of gl-matrix via modularized stack-gl forks 2 | // https://github.com/toji/gl-matrix 3 | // https://github.com/stackgl 4 | 5 | // vec3 6 | 7 | // Substitute 64-bit version 8 | // We need the extra precision when multiplying matrices w/mercator projected values 9 | const vec3 = { 10 | fromValues (x, y, z) { 11 | var out = new Float64Array(3); 12 | out[0] = x; 13 | out[1] = y; 14 | out[2] = z; 15 | return out; 16 | } 17 | }; 18 | 19 | 20 | // mat3 21 | 22 | import {default as mat3_normalFromMat4} from 'gl-mat3/normal-from-mat4'; 23 | import {default as mat3_invert} from 'gl-mat3/invert'; 24 | 25 | const mat3 = { 26 | normalFromMat4: mat3_normalFromMat4, 27 | invert: mat3_invert 28 | }; 29 | 30 | 31 | // mat4 32 | 33 | import {default as mat4_multiply} from 'gl-mat4/multiply'; 34 | import {default as mat4_translate} from 'gl-mat4/translate'; 35 | import {default as mat4_scale} from 'gl-mat4/scale'; 36 | import {default as mat4_perspective} from 'gl-mat4/perspective'; 37 | import {default as mat4_lookAt} from 'gl-mat4/lookAt'; 38 | import {default as mat4_identity} from 'gl-mat4/identity'; 39 | import {default as mat4_copy} from 'gl-mat4/copy'; 40 | 41 | const mat4 = { 42 | multiply: mat4_multiply, 43 | translate: mat4_translate, 44 | scale: mat4_scale, 45 | perspective: mat4_perspective, 46 | lookAt: mat4_lookAt, 47 | identity: mat4_identity, 48 | copy: mat4_copy 49 | }; 50 | 51 | 52 | export {vec3, mat3, mat4}; 53 | -------------------------------------------------------------------------------- /src/utils/hash.js: -------------------------------------------------------------------------------- 1 | // http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ 2 | export default function hashString (string) { 3 | var hash = 0, i, chr, len; 4 | 5 | if (string.length === 0) { 6 | return hash; 7 | } 8 | 9 | for (i = 0, len = string.length; i < len; i++) { 10 | chr = string.charCodeAt(i); 11 | hash = ((hash << 5) - hash) + chr; 12 | hash |= 0; // Convert to 32bit integer 13 | } 14 | return hash; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/log.js: -------------------------------------------------------------------------------- 1 | import version from './version'; 2 | import Thread from './thread'; 3 | import WorkerBroker from './worker_broker'; 4 | 5 | const LEVELS = { 6 | silent: -1, 7 | error: 0, 8 | warn: 1, 9 | info: 2, 10 | debug: 3, 11 | trace: 4 12 | }; 13 | 14 | const methods = {}; 15 | let logged_once = {}; 16 | 17 | function methodForLevel (level) { 18 | if (Thread.is_main) { 19 | methods[level] = methods[level] || (console[level] ? console[level] : console.log).bind(console); // eslint-disable-line no-console 20 | return methods[level]; 21 | } 22 | } 23 | 24 | // Logs message, proxying any log requests from worker threads back to the main thread. 25 | // Returns (asynchronously, due to proxying) a boolean indicating if the message was logged. 26 | // Option `once: true` can be used to only log each unique log message once (e.g. for warnings 27 | // that would otherwise be repetitive or possibly logged thousands of times, such as per feature). 28 | export default function log (opts, ...msg) { 29 | let level = (typeof opts === 'object') ? opts.level : opts; 30 | if (LEVELS[level] <= LEVELS[log.level]) { 31 | if (Thread.is_worker) { 32 | // Proxy to main thread 33 | return WorkerBroker.postMessage({ method: '_logProxy', stringify: true }, opts, ...msg); 34 | } 35 | else { 36 | // Only log message once? 37 | if (typeof opts === 'object' && opts.once === true) { 38 | if (logged_once[JSON.stringify(msg)]) { 39 | return Promise.resolve(false); 40 | } 41 | logged_once[JSON.stringify(msg)] = true; 42 | } 43 | 44 | // Write to console (on main thread) 45 | let logger = methodForLevel(level); 46 | if (msg.length > 1) { 47 | logger(`Tangram ${version} [${level}]: ${msg[0]}`, ...msg.slice(1)); 48 | } 49 | else { 50 | logger(`Tangram ${version} [${level}]: ${msg[0]}`); 51 | } 52 | } 53 | return Promise.resolve(true); 54 | } 55 | return Promise.resolve(false); 56 | } 57 | 58 | log.level = 'info'; 59 | log.workers = null; 60 | 61 | log.setLevel = function (level) { 62 | log.level = level; 63 | 64 | if (Thread.is_main && Array.isArray(log.workers)) { 65 | WorkerBroker.postMessage(log.workers, '_logSetLevelProxy', level); 66 | } 67 | }; 68 | 69 | if (Thread.is_main) { 70 | log.setWorkers = function (workers) { 71 | log.workers = workers; 72 | }; 73 | 74 | log.reset = function () { 75 | logged_once = {}; 76 | }; 77 | } 78 | 79 | WorkerBroker.addTarget('_logProxy', log); // proxy log messages from worker to main thread 80 | WorkerBroker.addTarget('_logSetLevelProxy', log.setLevel); // proxy log level setting from main to worker thread 81 | -------------------------------------------------------------------------------- /src/utils/merge.js: -------------------------------------------------------------------------------- 1 | // Deep/recursive merge of one or more source objects into a destination object 2 | export default function mergeObjects (dest, ...sources) { 3 | for (let s=0; s < sources.length; s++) { 4 | let source = sources[s]; 5 | if (!source) { 6 | continue; 7 | } 8 | for (let key in source) { 9 | let value = source[key]; 10 | // Recursively merge the source into the destination if it is a a non-null key/value object 11 | // (e.g. don't merge arrays, those are treated as scalar values; null values will overwrite/erase 12 | // the previous destination value) 13 | if (value !== null && typeof value === 'object' && !Array.isArray(value)) { 14 | if (dest[key] !== null && typeof dest[key] === 'object' && !Array.isArray(dest[key])) { 15 | dest[key] = mergeObjects(dest[key], value); 16 | } 17 | else { 18 | dest[key] = mergeObjects({}, value); // destination not an object, overwrite 19 | } 20 | } 21 | // Overwrite the previous destination value if the source property is: a scalar (number/string), 22 | // an array, or a null value 23 | else if (value !== undefined) { 24 | dest[key] = value; 25 | } 26 | // Undefined source properties are ignored 27 | } 28 | 29 | } 30 | return dest; 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/obb.js: -------------------------------------------------------------------------------- 1 | import Vector from './vector'; 2 | 3 | // single-allocation, reusable objects 4 | const ZERO_AXES = [[1, 0], [0, 1]]; 5 | const proj_a = [], proj_b = []; 6 | let d0, d1, d2, d3; 7 | 8 | export default class OBB { 9 | 10 | constructor (x, y, a, w, h) { 11 | this.dimension = [w / 2, h / 2]; // store half-dimension as that's what's needed in calculations below 12 | this.angle = a; 13 | this.centroid = [x, y]; 14 | this.quad = null; 15 | this.axis_0 = null; 16 | this.axis_1 = null; 17 | 18 | this.update(); 19 | } 20 | 21 | toJSON () { 22 | return { 23 | x: this.centroid[0], 24 | y: this.centroid[1], 25 | a: this.angle, 26 | w: this.dimension[0], 27 | h: this.dimension[1] 28 | }; 29 | } 30 | 31 | getExtent () { 32 | // special handling to skip calculations for 0-angle 33 | if (this.angle === 0) { 34 | return [ 35 | this.quad[0], this.quad[1], // lower-left 36 | this.quad[4], this.quad[5] // upper-right 37 | ]; 38 | } 39 | 40 | let aabb = [ 41 | Math.min(this.quad[0], this.quad[2], this.quad[4], this.quad[6]), // min x 42 | Math.min(this.quad[1], this.quad[3], this.quad[5], this.quad[7]), // min y 43 | Math.max(this.quad[0], this.quad[2], this.quad[4], this.quad[6]), // max x 44 | Math.max(this.quad[1], this.quad[3], this.quad[5], this.quad[7]) // max y 45 | ]; 46 | 47 | return aabb; 48 | } 49 | 50 | updateAxes () { 51 | // upper-left to upper-right 52 | this.axis_0 = Vector.normalize([this.quad[4] - this.quad[6], this.quad[5] - this.quad[7]]); 53 | 54 | // lower-right to upper-right 55 | this.axis_1 = Vector.normalize([this.quad[4] - this.quad[2], this.quad[5] - this.quad[3]]); 56 | } 57 | 58 | update () { 59 | const c = this.centroid; 60 | const w2 = this.dimension[0]; 61 | const h2 = this.dimension[1]; 62 | 63 | // special handling to skip calculations for 0-angle 64 | if (this.angle === 0) { 65 | // quad is a flat array storing 4 [x, y] vectors 66 | this.quad = [ 67 | c[0] - w2, c[1] - h2, // lower-left 68 | c[0] + w2, c[1] - h2, // lower-right 69 | c[0] + w2, c[1] + h2, // upper-right 70 | c[0] - w2, c[1] + h2 // upper-left 71 | ]; 72 | 73 | this.axis_0 = ZERO_AXES[0]; 74 | this.axis_1 = ZERO_AXES[1]; 75 | } 76 | // calculate axes and enclosing quad 77 | else { 78 | let x0 = Math.cos(this.angle) * w2; 79 | let x1 = Math.sin(this.angle) * w2; 80 | 81 | let y0 = -Math.sin(this.angle) * h2; 82 | let y1 = Math.cos(this.angle) * h2; 83 | 84 | // quad is a flat array storing 4 [x, y] vectors 85 | this.quad = [ 86 | c[0] - x0 - y0, c[1] - x1 - y1, // lower-left 87 | c[0] + x0 - y0, c[1] + x1 - y1, // lower-right 88 | c[0] + x0 + y0, c[1] + x1 + y1, // upper-right 89 | c[0] - x0 + y0, c[1] - x1 + y1 // upper-left 90 | ]; 91 | 92 | this.updateAxes(); 93 | } 94 | } 95 | 96 | static projectToAxis (obb, axis, proj) { 97 | // for each axis, project obb quad to it and find min and max values 98 | let quad = obb.quad; 99 | d0 = quad[0] * axis[0] + quad[1] * axis[1]; 100 | d1 = quad[2] * axis[0] + quad[3] * axis[1]; 101 | d2 = quad[4] * axis[0] + quad[5] * axis[1]; 102 | d3 = quad[6] * axis[0] + quad[7] * axis[1]; 103 | 104 | proj[0] = Math.min(d0, d1, d2, d3); 105 | proj[1] = Math.max(d0, d1, d2, d3); 106 | return proj; 107 | } 108 | 109 | static axisCollide(obb_a, obb_b, axis_0, axis_1) { 110 | OBB.projectToAxis(obb_a, axis_0, proj_a); 111 | OBB.projectToAxis(obb_b, axis_0, proj_b); 112 | if (proj_b[0] > proj_a[1] || proj_b[1] < proj_a[0]) { 113 | return false; 114 | } 115 | 116 | OBB.projectToAxis(obb_a, axis_1, proj_a); 117 | OBB.projectToAxis(obb_b, axis_1, proj_b); 118 | if (proj_b[0] > proj_a[1] || proj_b[1] < proj_a[0]) { 119 | return false; 120 | } 121 | 122 | return true; 123 | } 124 | 125 | static intersect(obb_a, obb_b) { 126 | return OBB.axisCollide(obb_a, obb_b, obb_a.axis_0, obb_a.axis_1) && 127 | OBB.axisCollide(obb_a, obb_b, obb_b.axis_0, obb_b.axis_1); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/utils/props.js: -------------------------------------------------------------------------------- 1 | // Get a value for a nested property with path provided as an array (`a.b.c` => ['a', 'b', 'c']) 2 | export function getPropertyPath (object, path) { 3 | const prop = path[path.length - 1]; 4 | return getPropertyPathTarget(object, path)?.[prop]; 5 | } 6 | 7 | // Set a value for a nested property with path provided as an array (`a.b.c` => ['a', 'b', 'c']) 8 | export function setPropertyPath (object, path, value) { 9 | const prop = path[path.length - 1]; 10 | const target = getPropertyPathTarget(object, path); 11 | if (target) { 12 | target[prop] = value; 13 | } 14 | } 15 | 16 | // Get the immediate parent object for a property path name provided as an array 17 | // e.g. for a single-depth path, this is just `object`, for path ['a', 'b'], this is `object[a]` 18 | export function getPropertyPathTarget (object, path) { 19 | if (path.length === 0) { 20 | return; 21 | } 22 | 23 | let target = object; 24 | for (let i = 0; i < path.length - 1; i++) { 25 | const prop = path[i]; 26 | target = target[prop]; 27 | if (target == null) { 28 | return; 29 | } 30 | } 31 | return target; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/slice.js: -------------------------------------------------------------------------------- 1 | export default function sliceObject (obj, keys) { 2 | let sliced = {}; 3 | keys.forEach(k => sliced[k] = obj[k]); 4 | return sliced; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/subscribe.js: -------------------------------------------------------------------------------- 1 | import log from './log'; 2 | 3 | export default function subscribeMixin (target) { 4 | 5 | let listeners = []; 6 | 7 | return Object.assign(target, { 8 | 9 | subscribe(listener) { 10 | if (listeners.indexOf(listener) === -1) { 11 | listeners.push(listener); 12 | } 13 | }, 14 | 15 | unsubscribe(listener) { 16 | let index = listeners.indexOf(listener); 17 | if (index > -1) { 18 | listeners.splice(index, 1); 19 | } 20 | }, 21 | 22 | unsubscribeAll() { 23 | listeners = []; 24 | }, 25 | 26 | trigger(event, ...data) { 27 | listeners.forEach(listener => { 28 | if (typeof listener[event] === 'function') { 29 | try { 30 | listener[event](...data); 31 | } 32 | catch(e) { 33 | log('warn', `Caught exception in listener for event '${event}':`, e); 34 | } 35 | } 36 | }); 37 | }, 38 | 39 | hasSubscribersFor(event) { 40 | let has = false; 41 | listeners.forEach(listener => { 42 | if (typeof listener[event] === 'function') { 43 | has = true; 44 | } 45 | }); 46 | return has; 47 | } 48 | 49 | }); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/task.js: -------------------------------------------------------------------------------- 1 | // import log from './log'; 2 | 3 | const Task = { 4 | id: 0, // unique id per task 5 | queue: [], // current queue of outstanding tasks 6 | max_time: 20, // default time in which all tasks should complete per frame 7 | start_time: null, // start time for tasks in current frame 8 | state: {}, // track flags about environment state (ex: whether user is currently moving the view) 9 | 10 | add (task) { 11 | task.id = Task.id++; 12 | task.max_time = task.max_time || Task.max_time; // allow task to run for this much time (tasks have a global collective limit per frame, too) 13 | task.pause_factor = task.pause_factor || 1; // pause tasks by this many frames when they run too long 14 | let promise = new Promise((resolve, reject) => { 15 | task.resolve = resolve; 16 | task.reject = reject; 17 | }); 18 | task.promise = promise; 19 | 20 | task.elapsed = 0; 21 | task.total_elapsed = 0; 22 | task.stats = { calls: 0 }; 23 | this.queue.push(task); 24 | 25 | // Run task immediately if under total frame time 26 | this.start_time = this.start_time || performance.now(); // start frame timer if necessary 27 | this.elapsed = performance.now() - this.start_time; 28 | if (this.elapsed < Task.max_time || task.immediate) { 29 | this.process(task); 30 | } 31 | 32 | return task.promise; 33 | }, 34 | 35 | remove (task) { 36 | let idx = this.queue.indexOf(task); 37 | if (idx > -1) { 38 | this.queue.splice(idx, 1); 39 | } 40 | }, 41 | 42 | process (task) { 43 | // Skip task while user is moving the view, if the task requests it 44 | // (for intensive tasks that lock the UI, like canvas rasterization) 45 | if (this.state.user_moving_view && task.user_moving_view === false) { 46 | // log('debug', `*** SKIPPING task id ${task.id}, ${task.type} while user is moving view`); 47 | return; 48 | } 49 | 50 | // Skip task if it's currently paused 51 | if (task.pause) { 52 | // log('debug', `*** PAUSING task id ${task.id}, ${task.type} (${task.pause})`); 53 | task.pause--; 54 | return true; 55 | } 56 | 57 | task.stats.calls++; 58 | task.start_time = performance.now(); // start task timer 59 | return task.run(task); 60 | }, 61 | 62 | processAll () { 63 | this.start_time = this.start_time || performance.now(); // start frame timer if necessary 64 | for (let i=0; i < this.queue.length; i++) { 65 | // Exceeded either total task time, or total frame time 66 | let task = this.queue[i]; 67 | 68 | if (this.process(task) !== true) { 69 | // If the task didn't complete, pause it for a task-specific number of frames 70 | // (can be disabled by setting pause_factor to 0) 71 | if (!task.pause) { 72 | task.pause = (task.elapsed > task.max_time) ? task.pause_factor : 0; 73 | } 74 | task.total_elapsed += task.elapsed; 75 | } 76 | 77 | // Check total frame time 78 | this.elapsed = performance.now() - this.start_time; 79 | if (this.elapsed >= Task.max_time) { 80 | this.start_time = null; // reset frame timer 81 | break; 82 | } 83 | } 84 | 85 | }, 86 | 87 | finish (task, value) { 88 | task.elapsed = performance.now() - task.start_time; 89 | task.total_elapsed += task.elapsed; 90 | // log('debug', `task type ${task.type}, tile ${task.id}, finish after ${task.stats.calls} calls, ${task.total_elapsed.toFixed(2)} elapsed`); 91 | this.remove(task); 92 | task.resolve(value); 93 | return task.promise; 94 | }, 95 | 96 | cancel (task) { 97 | let val; 98 | 99 | if (task.cancel instanceof Function) { 100 | val = task.cancel(task); // optional cancel function 101 | } 102 | 103 | task.resolve(val); 104 | }, 105 | 106 | shouldContinue (task) { 107 | // Suspend task if it runs over its specific per-frame limit, or the global limit 108 | task.elapsed = performance.now() - task.start_time; 109 | this.elapsed = performance.now() - this.start_time; 110 | return ((task.elapsed < task.max_time) && (this.elapsed < Task.max_time)); 111 | }, 112 | 113 | removeForTile (tile_id) { 114 | for (let idx = this.queue.length-1; idx >= 0; idx--) { 115 | if (this.queue[idx].tile_id === tile_id) { 116 | // log('trace', `Task: remove tasks for tile ${tile_id}`); 117 | this.cancel(this.queue[idx]); 118 | this.queue.splice(idx, 1); 119 | } 120 | } 121 | }, 122 | 123 | setState (state) { 124 | this.state = state; 125 | } 126 | 127 | }; 128 | 129 | export default Task; 130 | -------------------------------------------------------------------------------- /src/utils/thread.js: -------------------------------------------------------------------------------- 1 | /*jshint worker: true*/ 2 | 3 | // Mark thread as main or worker 4 | const Thread = {}; 5 | 6 | try { 7 | if (window instanceof Window && window.document instanceof HTMLDocument) { // jshint ignore:line 8 | Thread.is_worker = false; 9 | Thread.is_main = true; 10 | } 11 | } 12 | catch(e) { 13 | Thread.is_worker = true; 14 | Thread.is_main = false; 15 | 16 | // Patch for 3rd party libs that require these globals to be present. Specifically, FontFaceObserver. 17 | // Brittle solution but allows that library to load on worker threads. 18 | self.window = { document: {} }; 19 | self.document = self.window.document; 20 | } 21 | 22 | export default Thread; 23 | -------------------------------------------------------------------------------- /src/utils/vector.js: -------------------------------------------------------------------------------- 1 | /*** Vector functions - vectors provided as [x, y] or [x, y, z] arrays ***/ 2 | 3 | var Vector; 4 | export default Vector = {}; 5 | 6 | Vector.copy = function (v) { 7 | var V = []; 8 | var lim = v.length; 9 | for (var i = 0; i < lim; i++) { 10 | V[i] = v[i]; 11 | } 12 | return V; 13 | }; 14 | 15 | Vector.neg = function (v) { 16 | var V = []; 17 | var lim = v.length; 18 | for (var i = 0; i < lim; i++) { 19 | V[i] = -v[i]; 20 | } 21 | return V; 22 | }; 23 | 24 | // Addition of two vectors 25 | Vector.add = function (v1, v2) { 26 | var v = []; 27 | var lim = Math.min(v1.length,v2.length); 28 | for (var i = 0; i < lim; i++) { 29 | v[i] = v1[i] + v2[i]; 30 | } 31 | return v; 32 | }; 33 | 34 | // Substraction of two vectors 35 | Vector.sub = function (v1, v2) { 36 | var v = []; 37 | var lim = Math.min(v1.length,v2.length); 38 | 39 | for (var i = 0; i < lim; i++) { 40 | v[i] = v1[i] - v2[i]; 41 | } 42 | return v; 43 | }; 44 | 45 | Vector.signed_area = function (v1, v2, v3) { 46 | return (v2[0]-v1[0])*(v3[1]-v1[1]) - (v3[0]-v1[0])*(v2[1]-v1[1]); 47 | }; 48 | 49 | // Multiplication of two vectors, or a vector and a scalar 50 | Vector.mult = function (v1, v2) { 51 | var v = [], 52 | len = v1.length, 53 | i; 54 | 55 | if (typeof v2 === 'number') { 56 | // Mulitply by scalar 57 | for (i = 0; i < len; i++) { 58 | v[i] = v1[i] * v2; 59 | } 60 | } 61 | else { 62 | // Multiply two vectors 63 | len = Math.min(v1.length,v2.length); 64 | for (i = 0; i < len; i++) { 65 | v[i] = v1[i] * v2[i]; 66 | } 67 | } 68 | return v; 69 | }; 70 | 71 | // Division of two vectors 72 | Vector.div = function (v1, v2) { 73 | var v = [], 74 | i; 75 | if(typeof v2 === 'number'){ 76 | // Divide by scalar 77 | for (i = 0; i < v1.length; i++){ 78 | v[i] = v1[i] / v2; 79 | } 80 | } else { 81 | // Divide to vectors 82 | var len = Math.min(v1.length,v2.length); 83 | for (i = 0; i < len; i++) { 84 | v[i] = v1[i] / v2[i]; 85 | } 86 | } 87 | return v; 88 | }; 89 | 90 | // Get 2D perpendicular 91 | Vector.perp = function (v1, v2) { 92 | return [ 93 | v2[1] - v1[1], 94 | v1[0] - v2[0] 95 | ]; 96 | }; 97 | 98 | // Get 2D vector rotated 99 | Vector.rot = function (v, a) { 100 | var c = Math.cos(a); 101 | var s = Math.sin(a); 102 | return [ 103 | v[0] * c - v[1] * s, 104 | v[0] * s + v[1] * c 105 | ]; 106 | }; 107 | 108 | // Get 2D counter-clockwise angle 109 | // Angles in quadrant I and II are mapped to [0, PI) 110 | // Angles in quadrant III and IV are mapped to [-PI, 0] 111 | Vector.angle = function ([x, y]) { 112 | return Math.atan2(y,x); 113 | }; 114 | 115 | // Get angle between two vectors 116 | Vector.angleBetween = function(A, B){ 117 | var delta = Vector.dot( 118 | Vector.normalize(Vector.copy(A)), 119 | Vector.normalize(Vector.copy(B)) 120 | ); 121 | if (delta > 1) {delta = 1;} // protect against floating point error 122 | return Math.acos(delta); 123 | }; 124 | 125 | // Compare two points 126 | Vector.isEqual = function (v1, v2) { 127 | var len = v1.length; 128 | for (var i = 0; i < len; i++) { 129 | if (v1[i] !== v2[i]){ 130 | return false; 131 | } 132 | } 133 | return true; 134 | }; 135 | 136 | // Vector length squared 137 | Vector.lengthSq = function (v) { 138 | if (v.length === 2) { 139 | return (v[0]*v[0] + v[1]*v[1]); 140 | } 141 | else if (v.length >= 3) { 142 | return (v[0]*v[0] + v[1]*v[1] + v[2]*v[2]); 143 | } 144 | return 0; 145 | }; 146 | 147 | // Vector length 148 | Vector.length = function (v) { 149 | return Math.sqrt(Vector.lengthSq(v)); 150 | }; 151 | 152 | // Normalize a vector *in place* (use Vector.copy() if you need a new vector instance) 153 | Vector.normalize = function (v) { 154 | var d; 155 | if (v.length === 2) { 156 | d = v[0]*v[0] + v[1]*v[1]; 157 | 158 | if (d === 1) { 159 | return v; 160 | } 161 | 162 | d = Math.sqrt(d); 163 | 164 | if (d !== 0) { 165 | v[0] /= d; 166 | v[1] /= d; 167 | } 168 | else { 169 | v[0] = 0, v[1] = 0; 170 | } 171 | } else if (v.length >= 3) { 172 | d = v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; 173 | 174 | if (d === 1) { 175 | return v; 176 | } 177 | 178 | d = Math.sqrt(d); 179 | 180 | if (d !== 0) { 181 | v[0] /= d; 182 | v[1] /= d; 183 | v[2] /= d; 184 | } 185 | else { 186 | v[0] = 0, v[1] = 0, v[2] = 0; 187 | } 188 | } 189 | return v; 190 | }; 191 | 192 | // Cross product of two vectors 193 | Vector.cross = function (v1, v2) { 194 | if (v1.length === 2){ 195 | return v1[0] * v2[1] - v1[1] * v2[0]; 196 | } 197 | else if (v1.length === 3){ 198 | return [ 199 | (v1[1] * v2[2]) - (v1[2] * v2[1]), 200 | (v1[2] * v2[0]) - (v1[0] * v2[2]), 201 | (v1[0] * v2[1]) - (v1[1] * v2[0]) 202 | ]; 203 | } 204 | }; 205 | 206 | // Dot product of two vectors 207 | Vector.dot = function (v1, v2) { 208 | var n = 0; 209 | var lim = Math.min(v1.length, v2.length); 210 | for (var i = 0; i < lim; i++) { 211 | n += v1[i] * v2[i]; 212 | } 213 | return n; 214 | }; 215 | -------------------------------------------------------------------------------- /src/utils/version.js: -------------------------------------------------------------------------------- 1 | import {version} from '../../package.json'; 2 | 3 | export default 'v' + version; 4 | -------------------------------------------------------------------------------- /test/fixtures/sample-layers.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": { 3 | "filter": { 4 | "kind": "highway" 5 | }, 6 | "draw": { 7 | "group": { 8 | "color": [255, 255, 255] 9 | } 10 | }, 11 | 12 | "fill": { 13 | "filter": { 14 | "name": "FDR" 15 | }, 16 | "draw": { 17 | "group": { 18 | "color": [1, 1, 0] 19 | } 20 | }, 21 | "b": { 22 | "draw": { 23 | "group": { 24 | "a": "b", 25 | "color": [3.14, 3.14, 3.14] 26 | } 27 | } 28 | } 29 | }, 30 | 31 | "rule1": { 32 | "filter": { 33 | "a": 1 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/fixtures/sample-scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "sources": { 3 | "osm": { 4 | "max_zoom": 20, 5 | "type": "GeoJSON", 6 | "url": "http://tile.mapzen.com/mapzen/vector/v1/all/{z}/{x}/{y}.json", 7 | "url_params": { 8 | "api_key": "mapzen-T3tPjn7" 9 | } 10 | } 11 | }, 12 | 13 | "layers": { 14 | "earth": { 15 | "data": { 16 | "source": "osm" 17 | }, 18 | "draw": { 19 | "polygons": { 20 | "color": [0.175, 0.175, 0.175], 21 | "order": 0 22 | } 23 | } 24 | }, 25 | "landuse": { 26 | "data": { 27 | "source": "osm" 28 | }, 29 | "draw": { 30 | "polygons": { 31 | "color": [0.5, 0.875, 0.5], 32 | "order": 1 33 | } 34 | } 35 | }, 36 | "water": { 37 | "data": { 38 | "source": "osm" 39 | }, 40 | "draw": { 41 | "polygons": { 42 | "color": [0.5, 0.5, 0.875], 43 | "order": 2 44 | } 45 | } 46 | }, 47 | "roads": { 48 | "data": { 49 | "source": "osm" 50 | }, 51 | "draw": { 52 | "lines": { 53 | "color": [0.4, 0.4, 0.4], 54 | "order": 3 55 | } 56 | } 57 | }, 58 | "buildings": { 59 | "data": { 60 | "source": "osm" 61 | }, 62 | "draw": { 63 | "polygons": { 64 | "style": "rainbow", 65 | "color": [0.6, 0.6, 0.6], 66 | "order": 4 67 | } 68 | } 69 | } 70 | }, 71 | "lights": { 72 | "ambient": { 73 | "type": "ambient", 74 | "ambient": 0.5 75 | } 76 | }, 77 | "styles": { 78 | "rainbow_child": { 79 | "mix": "rainbow", 80 | "animated": true, 81 | "shaders": { 82 | "blocks": { 83 | "color": 84 | "color.rgb = clamp(hsv2rgb(c) * 2., 0., 1.);" 85 | } 86 | } 87 | }, 88 | "rainbow": { 89 | "base": "polygons", 90 | "animated": true, 91 | "shaders": { 92 | "blocks": { 93 | "global": 94 | "vec3 hsv2rgb(vec3 c) { vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); }", 95 | "color": 96 | "vec3 c = vec3(v_world_position.z * .003 + u_time / 10., 1.0, 1.0); color.rgb = hsv2rgb(c);" 97 | } 98 | } 99 | }, 100 | "scale": { 101 | "base": "polygons", 102 | "shaders": { 103 | "uniforms": { 104 | "scale": [1, 2, 3] 105 | }, 106 | "blocks": { 107 | "position": 108 | "position.xyz *= scale;" 109 | } 110 | } 111 | }, 112 | 113 | "ancestor": { 114 | "base": "polygons", 115 | "shaders": { 116 | "blocks": { 117 | "global": "vec3 test () { return vec3(1.); }" 118 | } 119 | } 120 | }, 121 | "parent1": { 122 | "mix": "ancestor" 123 | }, 124 | "parent2": { 125 | "mix": "ancestor" 126 | }, 127 | "descendant": { 128 | "mix": ["parent1", "parent2"] 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/fixtures/sample-scene.yaml: -------------------------------------------------------------------------------- 1 | sources: 2 | osm: 3 | type: GeoJSON 4 | url: https://tile.mapzen.com/mapzen/vector/v1/all/{z}/{x}/{y}.json 5 | url_params: 6 | api_key: mapzen-T3tPjn7 7 | 8 | layers: 9 | earth: 10 | data: { source: osm } 11 | draw: 12 | polygons: 13 | order: 0 14 | color: [0.175, 0.175, 0.175] 15 | 16 | landuse: 17 | data: { source: osm } 18 | draw: 19 | polygons: 20 | order: 1 21 | color: [0.5, 0.875, 0.5] 22 | 23 | 24 | water: 25 | data: { source: osm } 26 | draw: 27 | polygons: 28 | order: 2 29 | color: [0.5, 0.5, 0.875] 30 | 31 | roads: 32 | data: { source: osm } 33 | draw: 34 | lines: 35 | order: 3 36 | color: [0.4, 0.4, 0.4] 37 | 38 | 39 | buildings: 40 | data: { source: osm } 41 | draw: 42 | polygons: 43 | order: 4 44 | style: rainbow 45 | color: [0.6, 0.6, 0.6] 46 | 47 | lights: 48 | ambient: 49 | type: ambient 50 | ambient: 0.5 51 | 52 | styles: 53 | rainbow_child: 54 | mix: rainbow 55 | shaders: 56 | blocks: 57 | color: 58 | color.rgb = clamp(hsv2rgb(c) * 2., 0., 1.); 59 | 60 | rainbow: 61 | base: polygons 62 | animated: true 63 | shaders: 64 | blocks: 65 | global: | 66 | vec3 hsv2rgb(vec3 c) { 67 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 68 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 69 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 70 | } 71 | color: | 72 | vec3 c = vec3(worldPosition().z * .003 + u_time / 10., 1.0, 1.0); 73 | color.rgb = hsv2rgb(c); 74 | 75 | scale: 76 | base: polygons 77 | shaders: 78 | uniforms: 79 | scale: [1, 2, 3] 80 | blocks: 81 | position: 82 | position.xyz *= scale; 83 | -------------------------------------------------------------------------------- /test/fixtures/sample-tile.json: -------------------------------------------------------------------------------- 1 | { 2 | "min": { "x": 1, "y": 1 }, 3 | "debug": {}, 4 | "loading": true, 5 | "coords": { "z": 10 }, 6 | "layers": { 7 | "water": { 8 | "features": [ 9 | { 10 | "geometry": { 11 | "type": "Point", 12 | "coordinates": [10, 10] 13 | }, 14 | "properties": { 15 | "name": "bob" 16 | } 17 | }, 18 | { 19 | "geometry": { 20 | "type": "Point", 21 | "coordinates": [11, 11] 22 | }, 23 | "properties": { 24 | "name": "bob" 25 | } 26 | } 27 | 28 | ] 29 | }, 30 | "land": { 31 | "features": [ 32 | { 33 | "geometry": { 34 | "type": "Point", 35 | "coordinates": [10, 10] 36 | }, 37 | "properties": { 38 | "name": "bob" 39 | } 40 | } 41 | ] 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /test/fixtures/simple-polygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "123", 3 | "geometry": { 4 | "type": "Polygon", 5 | "coordinates": [ 6 | [ 7 | [2,12],[8,8],[8,16],[6.8,12],[6,16],[4,16],[2,12] 8 | ] 9 | ] 10 | }, 11 | "properties": { 12 | "kind": "debug", 13 | "description": "A single-ring test polygon", 14 | "bounds": [2, 8, 8, 16] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/geo_spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | let assert = chai.assert; 3 | 4 | import Geo from '../src/utils/geo'; 5 | import simplePolygon from './fixtures/simple-polygon.json'; 6 | 7 | describe('Geo', () => { 8 | 9 | describe('Geo.findBoundingBox(polygon)', () => { 10 | let bbox; 11 | beforeEach(() => { 12 | bbox = Geo.findBoundingBox(simplePolygon.geometry.coordinates); 13 | }); 14 | 15 | it('calculates the expected bounding box', () => { 16 | assert.deepEqual(bbox, simplePolygon.properties.bounds); 17 | }); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import Scene from '../src/scene/scene'; 2 | 3 | let container = document.createElement('div'); 4 | container.style.width = '250px'; 5 | container.style.height = '250px'; 6 | document.body.appendChild(container); 7 | 8 | // Use test-specific worker build for web workers 9 | window.Tangram = window.Tangram || {}; 10 | window.Tangram.workerURL = 'http://localhost:9876/base/build/worker.test.js'; 11 | 12 | // Helper for loading scene 13 | window.makeScene = function (options) { 14 | options = options || {}; 15 | 16 | options.disableRenderLoop = options.disableRenderLoop || true; 17 | options.container = options.container || container; 18 | options.logLevel = options.logLevel || 'info'; 19 | 20 | return new Scene( 21 | options.config || 'http://localhost:9876/base/test/fixtures/sample-scene.yaml', 22 | options 23 | ); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /test/leaflet_layer_spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import Scene from '../src/scene/scene'; 3 | import {leafletLayer, LeafletLayer} from '../src/leaflet_layer'; 4 | import sampleScene from './fixtures/sample-scene.json'; 5 | let assert = chai.assert; 6 | 7 | let map = window.L.map( 8 | document.createElement('div'), 9 | { maxZoom: 20, inertia: false, keyboard: false} 10 | ); 11 | map.setView([0, 0], 0); // required to put leaflet in a "ready" state, or it will never call the layer's onAdd() method 12 | 13 | let makeOne = () => { 14 | let layer = leafletLayer({ 15 | scene: sampleScene, 16 | disableRenderLoop: true 17 | }); 18 | 19 | return layer; 20 | }; 21 | 22 | describe('Leaflet plugin', () => { 23 | 24 | describe('.constructor()', () => { 25 | let subject; 26 | 27 | beforeEach(() => { 28 | subject = makeOne(); 29 | }); 30 | 31 | afterEach(() => { 32 | subject.scene.destroy(); 33 | }); 34 | 35 | it('returns a new instance', () => { 36 | assert.instanceOf(subject, LeafletLayer); 37 | }); 38 | 39 | it('wires up the scene', () => { 40 | assert.instanceOf(subject.scene, Scene); 41 | }); 42 | }); 43 | 44 | describe('.addTo(map)', () => { 45 | let subject; 46 | 47 | beforeEach(function (done) { 48 | subject = makeOne(); 49 | sinon.spy(subject, 'getContainer'); 50 | sinon.spy(subject.scene, 'load'); 51 | 52 | subject.on('init', () => { 53 | done(); 54 | }); 55 | 56 | subject.addTo(map); 57 | }); 58 | 59 | 60 | afterEach(() => { 61 | subject.remove(); 62 | subject.getContainer.restore(); 63 | }); 64 | 65 | it('calls the layer\'s .getContainer() method', () => { 66 | sinon.assert.called(subject.getContainer); 67 | }); 68 | 69 | it('initializes the scene', () => { 70 | sinon.assert.called(subject.scene.load); 71 | }); 72 | 73 | }); 74 | 75 | describe('.remove()', () => { 76 | let subject, scene; 77 | 78 | beforeEach((done) => { 79 | subject = makeOne(); 80 | scene = subject.scene; 81 | sinon.spy(LeafletLayer.layerBaseClass.prototype, 'onRemove'); 82 | sinon.spy(scene, 'destroy'); 83 | let called = false; 84 | 85 | subject.on('init', () => { 86 | subject.remove(); 87 | }); 88 | subject.on('remove', () => { 89 | if (!called) { 90 | called = true; 91 | done(); 92 | } 93 | }); 94 | 95 | subject.addTo(map); 96 | }); 97 | 98 | afterEach(() => { 99 | LeafletLayer.layerBaseClass.prototype.onRemove.restore(); 100 | }); 101 | 102 | it('calls the .super', () => { 103 | sinon.assert.called(LeafletLayer.layerBaseClass.prototype.onRemove); 104 | }); 105 | 106 | it('destroys the scene', () => { 107 | sinon.assert.called(scene.destroy); 108 | assert.isNull(subject.scene); 109 | }); 110 | }); 111 | 112 | describe.skip('removing and then re-adding to a map', () => { 113 | let subject, scene; 114 | 115 | beforeEach((done) => { 116 | var counter = 0; 117 | subject = makeOne(); 118 | scene = subject.scene; 119 | sinon.spy(subject.scene, 'destroy'); 120 | subject.on('init', () => { 121 | counter += 1; 122 | if (counter === 2) { 123 | done(); 124 | } 125 | }); 126 | subject.addTo(map); 127 | subject.remove(); 128 | subject.addTo(map); 129 | }); 130 | 131 | it('destroys the initial scene', () => { 132 | sinon.assert.called(scene.destroy); 133 | }); 134 | 135 | it('re-initializes a new scene', () => { 136 | assert.isTrue(subject.scene.initialized); 137 | }); 138 | }); 139 | 140 | }); 141 | -------------------------------------------------------------------------------- /test/merge_spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | let assert = chai.assert; 3 | 4 | import mergeObjects from '../src/utils/merge'; 5 | 6 | describe('mergeObjects', () => { 7 | 8 | let dest; 9 | 10 | beforeEach(() => { 11 | dest = { 12 | a: 5, 13 | b: 10, 14 | c: { 15 | x: 1, y: 2, z: 3 16 | }, 17 | d: { 18 | e: { 19 | x: 4, y: 5, z: 6 20 | } 21 | } 22 | }; 23 | }); 24 | 25 | describe('non-null source property', () => { 26 | 27 | let source = { a: 7 }; 28 | 29 | it('overwrites previous destination property', () => { 30 | mergeObjects(dest, source); 31 | assert.equal(dest.a, 7); 32 | }); 33 | 34 | }); 35 | 36 | describe('null source property', () => { 37 | 38 | let source = { a: null }; 39 | 40 | it('overwrites previous destination property', () => { 41 | mergeObjects(dest, source); 42 | assert.isNull(dest.a); 43 | }); 44 | 45 | }); 46 | 47 | describe('undefined source property', () => { 48 | 49 | let source = { a: undefined }; 50 | 51 | it('does NOT overwrite previous destination property', () => { 52 | mergeObjects(dest, source); 53 | assert.equal(dest.a, 5); 54 | }); 55 | 56 | }); 57 | 58 | describe('array source property', () => { 59 | 60 | let source = { b: [1, 2, 3] }; 61 | 62 | it('overwrites previous destination property', () => { 63 | mergeObjects(dest, source); 64 | assert.deepEqual(dest.b, [1, 2, 3]); 65 | }); 66 | 67 | }); 68 | 69 | describe('object source property', () => { 70 | 71 | let source = { 72 | c: { w: 4 } 73 | }; 74 | 75 | it('merge with previous destination property', () => { 76 | mergeObjects(dest, source); 77 | assert.deepEqual(dest.c, { x: 1, y: 2, z: 3, w: 4}); 78 | }); 79 | 80 | }); 81 | 82 | describe('nested source property', () => { 83 | 84 | let source = { 85 | d: { 86 | e: { w: 7 }, // new property second nested level 87 | f: 'x' // new property first nested level 88 | } 89 | }; 90 | 91 | it('deep merges with previous destination property', () => { 92 | mergeObjects(dest, source); 93 | assert.deepEqual(dest.d, { 94 | e: { x: 4, y: 5, z: 6, w: 7 }, 95 | f: 'x' 96 | }); 97 | }); 98 | 99 | }); 100 | 101 | describe('multiple source objects', () => { 102 | 103 | let source1 = { a: 7, b: 3 }; 104 | let source2 = { a: 10 }; 105 | 106 | it('last source takes precedence', () => { 107 | mergeObjects(dest, source1, source2); 108 | assert.equal(dest.a, 10); // from source2 109 | assert.equal(dest.b, 3); // from source1 110 | assert.deepEqual(dest.c, { x: 1, y: 2, z: 3 }); // unmodified 111 | }); 112 | 113 | }); 114 | 115 | }); 116 | -------------------------------------------------------------------------------- /test/obb_spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | let assert = chai.assert; 3 | import OBB from '../src/utils/obb.js'; 4 | 5 | describe('OBB', () => { 6 | 7 | describe('.intersect(obb) (aligned)', () => { 8 | let obb1 = new OBB(1.0, 1.0, 0.0, 2.0, 2.0); 9 | let obb2 = new OBB(2.0, 2.0, 0.0, 1.0, 1.0); 10 | let obb3 = new OBB(2.5, 2.5, 0.0, 0.4, 0.4); 11 | 12 | it('test collision between oriented bounding boxes', () => { 13 | assert.isTrue(OBB.intersect(obb1, obb2)); 14 | assert.isTrue(OBB.intersect(obb3, obb2)); 15 | assert.isFalse(OBB.intersect(obb1, obb3)); 16 | }); 17 | }); 18 | 19 | describe('.intersect(obb) (non-aligned)', () => { 20 | let obb1 = new OBB(1.0, 1.0, Math.PI / 4.0, 1.0, 1.0); 21 | let obb2 = new OBB(0.0, 0.0, 0.0, 1.0, 1.0); 22 | let obb3 = new OBB(0.0, 1.0, Math.PI * 2.0, 1.0, 0.999); 23 | 24 | it('test collision between oriented bounding boxes', () => { 25 | assert.isFalse(OBB.intersect(obb1, obb2)); 26 | assert.isFalse(OBB.intersect(obb2, obb1)); 27 | assert.isFalse(OBB.intersect(obb2, obb3)); 28 | assert.isTrue(OBB.intersect(obb1, obb3)); 29 | }); 30 | }); 31 | 32 | describe('.intersect(obb) (non-aligned with negative positions)', () => { 33 | let obb1 = new OBB(-1.0, -1.0, Math.PI / 4.0, 1.0, 1.0); 34 | let obb2 = new OBB(0.0, 0.0, 0.0, 1.0, 1.0); 35 | let obb3 = new OBB(0.0, -1.0, -Math.PI * 2.0, 1.0, 0.999); 36 | let obb4 = new OBB(1.0, -1.0, Math.PI / 4.0, 1.0, 1.0); 37 | let obb5 = new OBB(1.0, -0.5, Math.PI / 8.0, 1.0, 1.0); 38 | 39 | it('test collision between oriented bounding boxes', () => { 40 | assert.isFalse(OBB.intersect(obb1, obb2)); 41 | assert.isFalse(OBB.intersect(obb2, obb1)); 42 | assert.isFalse(OBB.intersect(obb2, obb3)); 43 | assert.isFalse(OBB.intersect(obb1, obb4)); 44 | assert.isFalse(OBB.intersect(obb2, obb4)); 45 | assert.isTrue(OBB.intersect(obb1, obb3)); 46 | assert.isTrue(OBB.intersect(obb5, obb4)); 47 | }); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/rollup.config.worker.js: -------------------------------------------------------------------------------- 1 | // Create a standalone worker bundle, to allow the Karma suite to load the web worker 2 | // (regular two-pass code-splitting build process is not easily adaptable to Karma) 3 | 4 | import babel from '@rollup/plugin-babel'; 5 | import resolve from '@rollup/plugin-node-resolve'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | import json from '@rollup/plugin-json'; 8 | import { importAsString } from 'rollup-plugin-string-import'; 9 | 10 | const config = { 11 | input: 'src/scene/scene_worker.js', 12 | output: { 13 | file: 'build/worker.test.js', 14 | format: 'umd', 15 | sourcemap: 'inline', 16 | indent: false, 17 | }, 18 | plugins: [ 19 | resolve({ 20 | browser: true, 21 | preferBuiltins: false 22 | }), 23 | commonjs(), 24 | json(), // load JSON files 25 | importAsString({ 26 | include: ['**/*.glsl'] // inline imported JSON and shader files 27 | }), 28 | babel({ 29 | exclude: 'node_modules/**', 30 | babelHelpers: "runtime" 31 | }) 32 | ] 33 | }; 34 | 35 | export default config; 36 | -------------------------------------------------------------------------------- /test/style_spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | let assert = chai.assert; 3 | 4 | import {StyleManager} from '../src/styles/style_manager'; 5 | import {Style} from '../src/styles/style'; 6 | import Context from '../src/gl/context'; 7 | import ShaderProgram from '../src/gl/shader_program'; 8 | import Camera from '../src/scene/camera'; 9 | import Light from '../src/lights/light'; 10 | 11 | import sampleScene from './fixtures/sample-scene.json'; 12 | 13 | var canvas, gl; 14 | 15 | describe('Styles:', () => { 16 | 17 | let style_manager; 18 | 19 | beforeEach(() => { 20 | style_manager = new StyleManager(); 21 | }); 22 | 23 | describe('StyleManager:', () => { 24 | 25 | beforeEach(() => { 26 | // These create global shader blocks required by all rendering styles 27 | Camera.create('default', null, { type: 'flat' }); 28 | Light.inject(); 29 | 30 | canvas = document.createElement('canvas'); 31 | gl = Context.getContext(canvas, { alpha: false }); 32 | 33 | style_manager.init(); 34 | }); 35 | 36 | afterEach(() => { 37 | style_manager.destroy(); 38 | canvas = null; 39 | gl = null; 40 | }); 41 | 42 | it('initializes built-in styles', () => { 43 | assert.equal(style_manager.styles.polygons.constructor, Style.constructor); 44 | assert.equal(style_manager.styles.points.constructor, Style.constructor); 45 | assert.equal(style_manager.styles.text.constructor, Style.constructor); 46 | }); 47 | 48 | it('creates a custom style', () => { 49 | style_manager.create('rainbow', sampleScene.styles.rainbow); 50 | assert.equal(style_manager.styles.rainbow.constructor, Style.constructor); 51 | assert.equal(style_manager.styles.rainbow.base, 'polygons'); 52 | }); 53 | 54 | describe('builds custom styles w/dependencies from stylesheet', () => { 55 | 56 | beforeEach(() => { 57 | ShaderProgram.reset(); 58 | style_manager.build(sampleScene.styles); 59 | style_manager.initStyles(); 60 | }); 61 | 62 | it('compiles parent custom style', () => { 63 | style_manager.styles.rainbow.setGL(gl); 64 | style_manager.styles.rainbow.getProgram(); 65 | assert.equal(style_manager.styles.rainbow.constructor, Style.constructor); 66 | assert.equal(style_manager.styles.rainbow.base, 'polygons'); 67 | assert.ok(style_manager.styles.rainbow.program.compiled); 68 | }); 69 | 70 | it('compiles child style dependent on another custom style', () => { 71 | style_manager.styles.rainbow_child.setGL(gl); 72 | style_manager.styles.rainbow_child.getProgram(); 73 | assert.equal(style_manager.styles.rainbow_child.constructor, Style.constructor); 74 | assert.equal(style_manager.styles.rainbow_child.base, 'polygons'); 75 | assert.ok(style_manager.styles.rainbow_child.program.compiled); 76 | }); 77 | 78 | it('compiles a style with the same style mixed by multiple ancestors', () => { 79 | style_manager.styles.descendant.setGL(gl); 80 | style_manager.styles.descendant.getProgram(); 81 | assert.equal(style_manager.styles.descendant.constructor, Style.constructor); 82 | assert.equal(style_manager.styles.descendant.base, 'polygons'); 83 | assert.ok(style_manager.styles.descendant.program.compiled); 84 | }); 85 | 86 | }); 87 | 88 | }); 89 | 90 | describe('Style:', () => { 91 | 92 | beforeEach(() => { 93 | canvas = document.createElement('canvas'); 94 | gl = Context.getContext(canvas, { alpha: false }); 95 | style_manager.init(); 96 | }); 97 | 98 | afterEach(() => { 99 | style_manager.destroy(); 100 | canvas = null; 101 | gl = null; 102 | }); 103 | 104 | it('compiles a program', () => { 105 | style_manager.styles.polygons.init(); 106 | style_manager.styles.polygons.setGL(gl); 107 | style_manager.styles.polygons.getProgram(); 108 | assert.ok(style_manager.styles.polygons.program.compiled); 109 | }); 110 | 111 | it('injects a dependent uniform in a custom style', () => { 112 | style_manager.create('scale', sampleScene.styles.scale); 113 | style_manager.styles.scale.init(); 114 | style_manager.styles.scale.setGL(gl); 115 | style_manager.styles.scale.getProgram(); 116 | assert.ok(style_manager.styles.scale.program.compiled); 117 | }); 118 | 119 | }); 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /test/subscribe_spec.js: -------------------------------------------------------------------------------- 1 | import subscribeMixin from '../src/utils/subscribe'; 2 | 3 | describe('subscribeMixin', () => { 4 | let subject; 5 | 6 | class A { 7 | constructor() { 8 | subscribeMixin(this); 9 | } 10 | } 11 | 12 | beforeEach(() => { 13 | subject = new A(); 14 | }); 15 | 16 | afterEach(() => { 17 | subject = undefined; 18 | }); 19 | 20 | it('fires all of the events that are subscribed', () => { 21 | let spyA = sinon.spy(), 22 | spyB = sinon.spy(), 23 | spyC = sinon.spy(), 24 | spyD = sinon.spy(); 25 | 26 | subject.subscribe({ test: spyA }); 27 | subject.subscribe({ test: spyB }); 28 | subject.subscribe({ test: spyC }); 29 | subject.subscribe({ test: spyD }); 30 | 31 | sinon.assert.notCalled(spyA); 32 | sinon.assert.notCalled(spyB); 33 | sinon.assert.notCalled(spyC); 34 | sinon.assert.notCalled(spyD); 35 | 36 | subject.trigger('test'); 37 | 38 | sinon.assert.called(spyA); 39 | sinon.assert.called(spyB); 40 | sinon.assert.called(spyC); 41 | sinon.assert.called(spyD); 42 | }); 43 | 44 | it('does not fires events that are unsubscribed', () => { 45 | let spyA = sinon.spy(), 46 | spyB = sinon.spy(), 47 | spyC = sinon.spy(), 48 | spyD = sinon.spy(); 49 | 50 | let subscriberA = { test: spyA }, 51 | subscriberB = { test: spyB }, 52 | subscriberC = { test: spyC }, 53 | subscriberD = { test: spyD }; 54 | 55 | subject.subscribe(subscriberA); 56 | subject.subscribe(subscriberB); 57 | subject.subscribe(subscriberC); 58 | subject.subscribe(subscriberD); 59 | 60 | subject.unsubscribe(subscriberA); 61 | subject.unsubscribe(subscriberB); 62 | subject.unsubscribe(subscriberC); 63 | 64 | subject.trigger('test'); 65 | 66 | sinon.assert.notCalled(spyA); 67 | sinon.assert.notCalled(spyB); 68 | sinon.assert.notCalled(spyC); 69 | sinon.assert.called(spyD); 70 | }); 71 | 72 | it('does not fire any events when they are all unsubscribed', () => { 73 | let spyA = sinon.spy(), 74 | spyB = sinon.spy(), 75 | spyC = sinon.spy(), 76 | spyD = sinon.spy(); 77 | 78 | subject.subscribe({ test: spyA }); 79 | subject.subscribe({ test: spyB }); 80 | subject.subscribe({ test: spyC }); 81 | subject.subscribe({ test: spyD }); 82 | 83 | sinon.assert.notCalled(spyA); 84 | sinon.assert.notCalled(spyB); 85 | sinon.assert.notCalled(spyC); 86 | sinon.assert.notCalled(spyD); 87 | 88 | subject.unsubscribeAll(); 89 | subject.trigger('test'); 90 | 91 | sinon.assert.notCalled(spyA); 92 | sinon.assert.notCalled(spyB); 93 | sinon.assert.notCalled(spyC); 94 | sinon.assert.notCalled(spyD); 95 | }); 96 | 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /test/tile_manager_spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | let assert = chai.assert; 3 | 4 | let nycLatLng = { lng: -73.97229909896852, lat: 40.76456761707639, zoom: 17 }; 5 | let midtownTile = { x: 38603, y: 49255, z: 17 }; 6 | 7 | describe('TileManager', function () { 8 | 9 | let scene, view, tile_manager; 10 | 11 | beforeEach(() => { 12 | scene = makeScene({}); 13 | view = scene.view; 14 | tile_manager = scene.tile_manager; 15 | sinon.stub(view, 'findVisibleTileCoordinates').returns([]); 16 | view.setView(nycLatLng); 17 | }); 18 | 19 | afterEach(() => { 20 | scene = null; 21 | }); 22 | 23 | describe('.queueCoordinate(coords)', () => { 24 | 25 | let coords = midtownTile; 26 | 27 | beforeEach(() => { 28 | sinon.spy(tile_manager, 'queueCoordinate'); 29 | 30 | return scene.load().then(() => { 31 | tile_manager.queueCoordinate(coords); 32 | tile_manager.loadQueuedCoordinates(); 33 | }); 34 | }); 35 | 36 | }); 37 | 38 | describe('.loadCoordinate(coords)', () => { 39 | 40 | let coords = midtownTile; 41 | 42 | beforeEach(() => { 43 | return scene.load(); 44 | }); 45 | 46 | describe('when the tile manager has not loaded the tile', () => { 47 | 48 | let tile, tiles; 49 | 50 | beforeEach(() => { 51 | tile_manager.loadCoordinate(coords); 52 | tiles = tile_manager.tiles; 53 | tile = tiles[Object.keys(tiles)[0]]; 54 | }); 55 | 56 | it('loads and keeps the tile', () => { 57 | tile_manager.loadCoordinate(coords); 58 | assert.isTrue(Object.keys(tiles).length === 1); 59 | assert.isTrue(tile.constructor.name === 'Tile'); 60 | }); 61 | 62 | }); 63 | 64 | describe('when the tile manager already has the tile', () => { 65 | let tile, tiles; 66 | 67 | beforeEach(() => { 68 | tile_manager.loadCoordinate(coords); 69 | 70 | tiles = tile_manager.tiles; 71 | tile = tiles[Object.keys(tiles)[0]]; 72 | 73 | sinon.spy(tile_manager, 'keepTile'); 74 | sinon.spy(tile, 'build'); 75 | 76 | tile_manager.loadCoordinate(coords); 77 | }); 78 | 79 | afterEach(() => { 80 | tile_manager.keepTile.restore(); 81 | tile.build.restore(); 82 | }); 83 | 84 | it('does not build the tile', () => { 85 | assert.isFalse(tile.build.called); 86 | }); 87 | 88 | it('does not keep the tile', () => { 89 | assert.isFalse(tile_manager.keepTile.called); 90 | }); 91 | 92 | }); 93 | 94 | }); 95 | 96 | }); 97 | -------------------------------------------------------------------------------- /test/tile_pyramid.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | let assert = chai.assert; 3 | import TilePyramid from '../src/tile/tile_pyramid'; 4 | import { TileID } from '../src/tile/tile_id'; 5 | 6 | describe('TilePyramid', function() { 7 | 8 | let coords = { x: 38603, y: 49255, z: 17 }; 9 | let source, style_z; 10 | let tile; 11 | let pyramid; 12 | 13 | describe('overzooming', () => { 14 | 15 | beforeEach(() => { 16 | pyramid = new TilePyramid(); 17 | 18 | style_z = coords.z; 19 | source = { 20 | id: 0, 21 | name: 'test', 22 | zooms: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], 23 | zoom_bias: 0 24 | }; 25 | 26 | tile = { 27 | coords: TileID.coord(coords), 28 | style_z, 29 | source, 30 | loaded: true 31 | }; 32 | tile.key = TileID.key(tile.coords, source, style_z); 33 | }); 34 | 35 | it('creates one entry per zoom', () => { 36 | pyramid.addTile(tile); 37 | 38 | assert.equal(Object.keys(pyramid.tiles).length, coords.z + 1); 39 | }); 40 | 41 | it('creates one entry for non-overzoomed tile', () => { 42 | pyramid.addTile(tile); 43 | 44 | assert.isNotNull(pyramid.tiles[tile.key]); 45 | }); 46 | 47 | it('creates entries for overzoomed tiles', () => { 48 | tile = Object.assign({}, tile); 49 | tile.coords = TileID.coordAtZoom(coords, 18); 50 | tile.style_z = 20; 51 | pyramid.addTile(tile); 52 | 53 | assert.isNotNull(pyramid.tiles[TileID.key(tile.coords, source, tile.style_z)]); 54 | }); 55 | 56 | it('removes all entries for single tile', () => { 57 | pyramid.addTile(tile); 58 | pyramid.removeTile(tile); 59 | 60 | assert.equal(Object.keys(pyramid.tiles).length, 0); 61 | }); 62 | 63 | it('gets tile ancestor', () => { 64 | pyramid.addTile(tile); 65 | let ancestor = pyramid.getAncestor(tile); 66 | 67 | assert.isNotNull(ancestor); 68 | }); 69 | 70 | it('gets tile descendant', () => { 71 | pyramid.addTile(tile); 72 | let ancestor = TileID.parent(TileID.parent(tile)); 73 | let descendants = pyramid.getDescendants(ancestor); 74 | 75 | assert.equal(descendants.length, 1); 76 | }); 77 | 78 | }); 79 | 80 | }); 81 | -------------------------------------------------------------------------------- /test/tile_spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | let assert = chai.assert; 3 | import { TileID } from '../src/tile/tile_id'; 4 | 5 | describe('Tile', function() { 6 | 7 | let coords = { x: 38603, y: 49255, z: 17 }; 8 | 9 | describe('overzooming', () => { 10 | 11 | it('does NOT overzoom a coordinate at the max zoom', () => { 12 | let coords2 = TileID.coordForTileZooms(coords, [0, 12, 17]); 13 | 14 | assert.deepEqual(coords2.x, coords.x); 15 | assert.deepEqual(coords2.y, coords.y); 16 | assert.deepEqual(coords2.z, coords.z); 17 | }); 18 | 19 | it('does NOT overzoom a coordinate below the max zoom', () => { 20 | let coords2 = TileID.coordForTileZooms(coords, [0, 12, 16, 17, 18]); 21 | 22 | assert.deepEqual(coords2.x, coords.x); 23 | assert.deepEqual(coords2.y, coords.y); 24 | assert.deepEqual(coords2.z, coords.z); 25 | }); 26 | 27 | it('does overzoom a coordinate above the max zoom', () => { 28 | let unzoomed = { x: Math.floor(coords.x*2), y: Math.floor(coords.y*2), z: coords.z + 1 }; 29 | let overzoomed = { x: Math.floor(coords.x/4), y: Math.floor(coords.y/4), z: coords.z - 2 }; 30 | 31 | let coords2 = TileID.coordForTileZooms(unzoomed, [0, 12, 15]); 32 | 33 | assert.deepEqual(coords2.x, overzoomed.x); 34 | assert.deepEqual(coords2.y, overzoomed.y); 35 | assert.deepEqual(coords2.z, overzoomed.z); 36 | }); 37 | 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /test/vertex_data_spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | let assert = chai.assert; 3 | import VertexLayout from '../src/gl/vertex_layout'; 4 | import VertexData from '../src/gl/vertex_data'; 5 | import gl from '../src/gl/constants'; 6 | 7 | describe('VertexData', () => { 8 | 9 | // Note: a_color is intentionally not a multiple of 4, to test padding 10 | let attribs = [ 11 | { name: 'a_position', size: 3, type: gl.FLOAT, normalized: false }, 12 | { name: 'a_color', size: 3, type: gl.UNSIGNED_BYTE, normalized: true }, // should be padded to 4 bytes 13 | { name: 'a_layer', size: 1, type: gl.FLOAT, normalized: false } 14 | ]; 15 | 16 | describe('.constructor(vertex_layout)', () => { 17 | let subject; 18 | let layout; 19 | 20 | beforeEach(() => { 21 | layout = new VertexLayout(attribs); 22 | subject = new VertexData(layout); 23 | }); 24 | 25 | it('returns a new instance', () => { 26 | assert.instanceOf(subject, VertexData); 27 | }); 28 | it('sets up buffer views', () => { 29 | assert.instanceOf(subject.views[gl.FLOAT], Float32Array); 30 | assert.instanceOf(subject.views[gl.UNSIGNED_BYTE], Uint8Array); 31 | }); 32 | }); 33 | 34 | describe('.addVertex(vertex)', () => { 35 | let subject; 36 | let layout; 37 | let vertex = [ 38 | 25, 50, 100, // position 39 | 255, 0, 0, // color 40 | 2 // layer 41 | ]; 42 | 43 | beforeEach(() => { 44 | layout = new VertexLayout(attribs); 45 | subject = layout.createVertexData(); 46 | subject.addVertex(vertex); 47 | }); 48 | 49 | it('advances the buffer offset', () => { 50 | assert.equal(subject.offset, layout.stride); 51 | }); 52 | it('sets a vertex attribute value in the buffer', () => { 53 | assert.equal(subject.views[gl.FLOAT][0], vertex[0]); 54 | assert.equal(subject.views[gl.FLOAT][1], vertex[1]); 55 | assert.equal(subject.views[gl.FLOAT][2], vertex[2]); 56 | }); 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /test/vertex_layout_spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | let assert = chai.assert; 3 | import VertexLayout from '../src/gl/vertex_layout'; 4 | import VertexData from '../src/gl/vertex_data'; 5 | import gl from '../src/gl/constants'; 6 | 7 | describe('VertexLayout', () => { 8 | 9 | // Note: a_color is intentionally not a multiple of 4, to test padding 10 | let attribs = [ 11 | { name: 'a_position', size: 3, type: gl.FLOAT, normalized: false }, 12 | { name: 'a_color', size: 3, type: gl.UNSIGNED_BYTE, normalized: true }, // should be padded to 4 bytes 13 | { name: 'a_layer', size: 1, type: gl.FLOAT, normalized: false } 14 | ]; 15 | 16 | describe('.constructor(attribs)', () => { 17 | let subject; 18 | beforeEach(() => { 19 | subject = new VertexLayout(attribs); 20 | }); 21 | 22 | it('returns a new instance', () => { 23 | assert.instanceOf(subject, VertexLayout); 24 | }); 25 | it('calculates the right vertex stride', () => { 26 | assert.equal(subject.stride, 20); 27 | }); 28 | }); 29 | 30 | describe('.createVertexData()', () => { 31 | let subject; 32 | let vertex_data; 33 | 34 | beforeEach(() => { 35 | subject = new VertexLayout(attribs); 36 | vertex_data = subject.createVertexData(); 37 | }); 38 | 39 | it('creates a vertex data buffer', () => { 40 | assert.instanceOf(vertex_data, VertexData); 41 | }); 42 | }); 43 | 44 | }); 45 | --------------------------------------------------------------------------------