diff --git a/.eslintrc b/.eslintrc index de7ce0b3bc4..dee956d3063 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,16 +1,17 @@ { "extends": "mourner", "rules": { - "object-curly-spacing": "off", - "key-spacing": "off", "array-bracket-spacing": "off", - "space-before-function-paren": "off", - "quotes": "off", - "no-eq-null": "off", + "block-scoped-var": "error", "consistent-return": "off", "global-require": "off", - "block-scoped-var": "error", - "no-warning-comments": "error" + "key-spacing": "off", + "no-eq-null": "off", + "no-new": "off", + "no-warning-comments": "error", + "object-curly-spacing": "off", + "quotes": "off", + "space-before-function-paren": "off", }, "env": { "es6": true diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..e8940462ce0 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ + + +## Checklist + + - [ ] briefly describe the changes in this PR + - [ ] write tests for all new functionality + - [ ] [document any changes or additions to public APIs](https://github.com/mapbox/mapbox-gl-js/blob/master/docs/README.md) + - [ ] [post scores for all benchmarks on your branch and `master`](https://github.com/mapbox/mapbox-gl-js/blob/master/bench/README.md#running-benchmarks) + - [ ] [do a quick sanity check on the debug page](https://github.com/mapbox/mapbox-gl-js/blob/master/CONTRIBUTING.md#serving-the-debug-page) + - [ ] get a PR review :+1: / :-1: diff --git a/README.md b/README.md index 36f409ed3e0..c7afb37755e 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,25 @@ These projects are written and maintained by the GL JS community. Feel free to o - [Typescript Interface Definition](https://github.com/Smartrak/mapbox-gl-js-typescript) - [Webtoolkit Integration](https://github.com/yvanvds/wtMapbox) +## Using Mapbox GL JS with [CSP](https://developer.mozilla.org/en-US/docs/Web/Security/CSP) + +You may use a Content Security Policy to restrict the resources your page has +access to, as a way of guarding against Cross-Site Scripting and other types of +attacks. If you do, Mapbox GL JS requires the following directives: + +``` +child-src blob: ; +img-src data: blob: ; +script-src 'unsafe-eval' ; +``` + +Requesting styles from Mapbox or other services will require additional +directives. For Mapbox, you can use this `connect-src` setting: + +``` +connect-src https://*.tiles.mapbox.com https://api.mapbox.com +``` + ## Contributing to Mapbox GL JS See [CONTRIBUTING.md](https://github.com/mapbox/mapbox-gl-js/blob/master/CONTRIBUTING.md). diff --git a/bench/.eslintrc b/bench/.eslintrc new file mode 100644 index 00000000000..d34a4506724 --- /dev/null +++ b/bench/.eslintrc @@ -0,0 +1,7 @@ +{ + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + } +} diff --git a/bench/README.md b/bench/README.md index e89e251b1bf..b4224ce9505 100644 --- a/bench/README.md +++ b/bench/README.md @@ -2,7 +2,7 @@ Benchmarks help us catch performance regressions and improve performance. -## Running a Benchmark +## Running Benchmarks Start the benchmark server @@ -10,10 +10,9 @@ Start the benchmark server MAPBOX_ACCESS_TOKEN={YOUR MAPBOX ACCESS TOKEN} npm start ``` -Open a benchmark runner page +To run all benchmarks, open [the benchmark page, `http://localhost:9966/bench`](http://localhost:9966/bench). - - **buffer benchmark** http://localhost:9966/bench/buffer - - **fps benchmark** http://localhost:9966/bench/fps +To run a specific benchmark, append its name to the url, for example [`http://localhost:9966/bench/buffer`](http://localhost:9966/bench/buffer). ## Writing a Benchmark diff --git a/bench/benchmarks/fps.js b/bench/benchmarks/fps.js index aa7a77f6028..2543e082679 100644 --- a/bench/benchmarks/fps.js +++ b/bench/benchmarks/fps.js @@ -31,7 +31,7 @@ module.exports = function(options) { evented.fire('error', { error: err }); } else { evented.fire('end', { - message: formatNumber(fps) + ' frames per second', + message: formatNumber(fps) + ' fps', score: 1 / fps }); } diff --git a/bench/benchmarks/frame_duration.js b/bench/benchmarks/frame_duration.js index d9374342c2c..c3c115ffc10 100644 --- a/bench/benchmarks/frame_duration.js +++ b/bench/benchmarks/frame_duration.js @@ -30,16 +30,14 @@ module.exports = function(options) { measureFrameTime(options, zooms[index], function(err_, result) { results[index] = result; evented.fire('log', { - message: formatNumber(result.sum / result.count * 10) / 10 + ' ms per frame at zoom ' + zooms[index] + '. ' + - formatNumber(result.countAbove16 / result.count * 100) + '% of frames took longer than 16ms.' + message: formatNumber(result.sum / result.count * 10) / 10 + ' ms, ' + + formatNumber(result.countAbove16 / result.count * 100) + '% > 16 ms at zoom ' + zooms[index] }); callback(); }); } function done() { - document.getElementById('map').remove(); - var sum = 0; var count = 0; var countAbove16 = 0; @@ -50,7 +48,7 @@ module.exports = function(options) { countAbove16 += result.countAbove16; } evented.fire('end', { - message: formatNumber(sum / count * 10) / 10 + ' ms per frame. ' + formatNumber(countAbove16 / count * 100) + '% of frames took longer than 16ms.', + message: formatNumber(sum / count * 10) / 10 + ' ms, ' + formatNumber(countAbove16 / count * 100) + '% > 16ms', score: sum / count }); } @@ -96,6 +94,7 @@ function measureFrameTime(options, zoom, callback) { if (frameEnd - start > DURATION_MILLISECONDS) { map.repaint = false; map.remove(); + map.getContainer().remove(); callback(undefined, { sum: sum, count: count, diff --git a/bench/benchmarks/geojson_setdata_large.js b/bench/benchmarks/geojson_setdata_large.js index 0006b23a90b..39a8e3bbfb3 100644 --- a/bench/benchmarks/geojson_setdata_large.js +++ b/bench/benchmarks/geojson_setdata_large.js @@ -27,7 +27,7 @@ module.exports = function(options) { evented.fire('log', {message: 'loading large feature collection'}); setDataPerf(source, 50, featureCollection, function(err, ms) { if (err) return evented.fire('error', {error: err}); - evented.fire('end', {message: 'average load time: ' + formatNumber(ms) + ' ms', score: ms}); + evented.fire('end', {message: formatNumber(ms) + ' ms', score: ms}); }); }); diff --git a/bench/benchmarks/geojson_setdata_small.js b/bench/benchmarks/geojson_setdata_small.js index 38906586a03..ee5e09eeea4 100644 --- a/bench/benchmarks/geojson_setdata_small.js +++ b/bench/benchmarks/geojson_setdata_small.js @@ -37,7 +37,7 @@ module.exports = function(options) { evented.fire('log', {message: 'loading small feature collection'}); setDataPerf(source, 50, featureCollection, function(err, ms) { if (err) return evented.fire('error', {error: err}); - evented.fire('end', {message: 'average load time: ' + formatNumber(ms) + ' ms', score: ms}); + evented.fire('end', {message: formatNumber(ms) + ' ms', score: ms}); }); }); diff --git a/bench/benchmarks/query_box.js b/bench/benchmarks/query_box.js index 9379f50a617..e2bab94a96d 100644 --- a/bench/benchmarks/query_box.js +++ b/bench/benchmarks/query_box.js @@ -28,7 +28,7 @@ module.exports = function(options) { center: [-77.032194, 38.912753], style: 'mapbox://styles/mapbox/streets-v9' }); - document.getElementById('map').style.display = 'none'; + map.getContainer().style.display = 'none'; map.on('load', function() { @@ -36,7 +36,7 @@ module.exports = function(options) { var zoomCount = 0; asyncSeries(numSamples, function(n, callback) { var start = performance.now(); - map.queryRenderedFeatures(); + map.queryRenderedFeatures({}); var duration = performance.now() - start; sum += duration; count++; @@ -45,7 +45,7 @@ module.exports = function(options) { callback(); }, function() { evented.fire('log', { - message: 'zoom ' + zoomLevel + ' average: ' + (zoomSum / zoomCount).toFixed(2) + ' ms' + message: (zoomSum / zoomCount).toFixed(2) + ' ms at zoom ' + zoomLevel }); callback(); }); @@ -80,4 +80,3 @@ function asyncSeries(times, work, callback) { callback(); } } - diff --git a/bench/benchmarks/query_point.js b/bench/benchmarks/query_point.js index 49962b633bc..1b3ac282483 100644 --- a/bench/benchmarks/query_point.js +++ b/bench/benchmarks/query_point.js @@ -37,7 +37,7 @@ module.exports = function(options) { center: [-77.032194, 38.912753], style: 'mapbox://styles/mapbox/streets-v9' }); - document.getElementById('map').style.display = 'none'; + map.getContainer().style.display = 'none'; map.on('load', function() { @@ -46,7 +46,7 @@ module.exports = function(options) { asyncSeries(queryPoints.length, function(n, callback) { var queryPoint = queryPoints[queryPoints.length - n]; var start = performance.now(); - map.queryRenderedFeatures(queryPoint); + map.queryRenderedFeatures(queryPoint, {}); var duration = performance.now() - start; sum += duration; count++; @@ -55,7 +55,7 @@ module.exports = function(options) { callback(); }, function() { evented.fire('log', { - message: 'zoom ' + zoomLevel + ' average: ' + (zoomSum / zoomCount).toFixed(2) + ' ms' + message: (zoomSum / zoomCount).toFixed(2) + ' ms at zoom ' + zoomLevel }); callback(); }); @@ -90,4 +90,3 @@ function asyncSeries(times, work, callback) { callback(); } } - diff --git a/bench/index.html b/bench/index.html index 9b346bd4175..fc698ac3d92 100644 --- a/bench/index.html +++ b/bench/index.html @@ -8,34 +8,10 @@ - - - +
-
-
diff --git a/bench/index.js b/bench/index.js index 538df4a5a71..b6d11c43111 100644 --- a/bench/index.js +++ b/bench/index.js @@ -1,67 +1,225 @@ 'use strict'; +/*eslint no-unused-vars: ["error", { "varsIgnorePattern": "BenchmarksView|clipboard" }]*/ -var mapboxgl = require('../js/mapbox-gl'); +var React = require('react'); +var ReactDOM = require('react-dom'); var util = require('../js/util/util'); - -try { - main(); -} catch (err) { - log('red', err.toString()); - throw err; +var async = require('async'); +var mapboxgl = require('../js/mapbox-gl'); +var Clipboard = require('clipboard'); +var URL = require('url'); + +var BenchmarksView = React.createClass({ + + render: function() { + return
+ {this.renderBenchmarkSummaries()} + {this.renderBenchmarkDetails()} +
; + }, + + renderBenchmarkSummaries: function() { + return
+

Benchmarks

+
+ {Object.keys(this.state.results).map(this.renderBenchmarkSummary)} +
+ + Copy Results + +
; + }, + + renderBenchmarkSummary: function(name) { + var results = this.state.results[name]; + var that = this; + + return
+ {name}: {results.message || '...'} +
; + }, + + renderBenchmarkTextSummaries: function() { + var output = '# Benchmarks\n\n'; + for (var name in this.state.results) { + var result = this.state.results[name]; + output += '**' + name + ':** ' + (result.message || '...') + '\n'; + } + return output; + }, + + renderBenchmarkDetails: function() { + return
+ {Object.keys(this.state.results).map(this.renderBenchmarkDetail)} +
; + }, + + renderBenchmarkDetail: function(name) { + var results = this.state.results[name]; + return ( +
+ +

"{name}" Benchmark

+ {results.logs.map(function(log, index) { + return
{log.message}
; + })} +
+ ); + }, + + scrollToBenchmark: function(name) { + var duration = 300; + var startTime = (new Date()).getTime(); + var startYOffset = window.pageYOffset; + + requestAnimationFrame(function frame() { + var endYOffset = document.getElementById(name).offsetTop; + var time = (new Date()).getTime(); + var yOffset = Math.min((time - startTime) / duration, 1) * (endYOffset - startYOffset) + startYOffset; + window.scrollTo(0, yOffset); + if (time < startTime + duration) requestAnimationFrame(frame); + }); + }, + + getInitialState: function() { + return { + state: 'waiting', + runningBenchmark: Object.keys(this.props.benchmarks)[0], + results: util.mapObject(this.props.benchmarks, function(_, name) { + return { + name: name, + state: 'waiting', + logs: [] + }; + }) + }; + }, + + componentDidMount: function() { + var that = this; + setTimeout(function() { + async.eachSeries( + Object.keys(that.props.benchmarks), + that.runBenchmark, + function() { + that.setState({state: 'ended'}); + that.scrollToBenchmark(Object.keys(that.props.benchmarks)[0]); + } + ); + }, 500); + }, + + runBenchmark: function(name, outerCallback) { + var that = this; + var results = this.state.results[name]; + var maps = []; + + results.state = 'running'; + this.scrollToBenchmark(name); + this.setState({runningBenchmark: name}); + log('dark', 'starting'); + + this.props.benchmarks[name]({ + accessToken: getAccessToken(), + createMap: createMap + + }).on('log', function(event) { + log(event.color, event.message); + + }).on('end', function(event) { + results.message = event.message; + results.state = 'ended'; + log('green', event.message); + callback(); + + }).on('error', function(event) { + results.state = 'errored'; + log('red', event.error); + callback(); + }); + + function log(color, message) { + results.logs.push({ + color: color || 'blue', + message: message + }); + that.forceUpdate(); + } + + function callback() { + for (var i = 0; i < maps.length; i++) { + maps[i].remove(); + maps[i].getContainer().remove(); + } + setTimeout(function() { + that.setState({runningBenchmark: null}); + outerCallback(); + }, 500); + } + + function createMap(options) { + options = util.extend({width: 512, height: 512}, options); + + var element = document.createElement('div'); + element.style.width = options.width + 'px'; + element.style.height = options.height + 'px'; + element.style.margin = '0 auto'; + document.body.appendChild(element); + + var map = new mapboxgl.Map(util.extend({ + container: element, + style: 'mapbox://styles/mapbox/streets-v9', + interactive: false + }, options)); + maps.push(map); + + return map; + } + } +}); + +var benchmarks = { + buffer: require('./benchmarks/buffer'), + fps: require('./benchmarks/fps'), + 'frame-duration': require('./benchmarks/frame_duration'), + 'query-point': require('./benchmarks/query_point'), + 'query-box': require('./benchmarks/query_box'), + 'geojson-setdata-small': require('./benchmarks/geojson_setdata_small'), + 'geojson-setdata-large': require('./benchmarks/geojson_setdata_large') +}; + +var filteredBenchmarks = {}; +var benchmarkName = URL.parse(window.location.toString()).pathname.split('/')[2]; +if (!benchmarkName) { + filteredBenchmarks = benchmarks; +} else { + filteredBenchmarks[benchmarkName] = benchmarks[benchmarkName]; } -function main() { - log('dark', 'please keep this window in the foreground and close the debugger'); - - var benchmarks = { - buffer: require('./benchmarks/buffer'), - fps: require('./benchmarks/fps'), - 'frame-duration': require('./benchmarks/frame_duration'), - 'query-point': require('./benchmarks/query_point'), - 'query-box': require('./benchmarks/query_box'), - 'geojson-setdata-small': require('./benchmarks/geojson_setdata_small'), - 'geojson-setdata-large': require('./benchmarks/geojson_setdata_large') - }; - - var benchmarksDiv = document.getElementById('benchmarks'); - - Object.keys(benchmarks).forEach(function(id) { - benchmarksDiv.innerHTML += '' + id + ''; - }); - - var pathnameArray = location.pathname.split('/'); - var benchmarkName = pathnameArray[pathnameArray.length - 1] || pathnameArray[pathnameArray.length - 2]; - var createBenchmark = benchmarks[benchmarkName]; - if (!createBenchmark) throw new Error('unknown benchmark "' + benchmarkName + '"'); - - var benchmark = createBenchmark({ - accessToken: getAccessToken(), - createMap: createMap - }); - - benchmark.on('log', function(event) { - log(event.color || 'blue', event.message); - scrollToBottom(); - }); - - benchmark.on('end', function(event) { - log('green', '' + event.message + ''); - scrollToBottom(); - }); - - benchmark.on('error', function(event) { - log('red', event.error); - scrollToBottom(); - }); -} +ReactDOM.render(, document.getElementById('benchmarks')); -function scrollToBottom() { - window.scrollTo(0, document.body.scrollHeight); -} +var clipboard = new Clipboard('.clipboard'); -function log(color, message) { - document.getElementById('logs').innerHTML += '

' + message + '

'; -} +mapboxgl.accessToken = getAccessToken(); function getAccessToken() { var accessToken = ( @@ -76,23 +234,6 @@ function getAccessToken() { function getURLParameter(name) { var regexp = new RegExp('[?&]' + name + '=([^&#]*)', 'i'); - var output = regexp.exec(window.location.href); - return output && output[1]; -} - -function createMap(options) { - var mapElement = document.getElementById('map'); - - options = util.extend({width: 512, height: 512}, options); - - mapElement.style.display = 'block'; - mapElement.style.width = options.width + 'px'; - mapElement.style.height = options.height + 'px'; - - mapboxgl.accessToken = getAccessToken(); - return new mapboxgl.Map(util.extend({ - container: 'map', - style: 'mapbox://styles/mapbox/streets-v9', - interactive: false - }, options)); + var results = regexp.exec(window.location.href); + return results && results[1]; } diff --git a/ci.sh b/ci.sh index 6da584037c4..51601868a2a 100755 --- a/ci.sh +++ b/ci.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash -set -e +source ./nvm/nvm.sh +nvm use ${NODE_VERSION} + +set -eu set -o pipefail # add npm packages to $PATH @@ -22,8 +25,10 @@ tap --reporter dot --coverage --no-coverage-report test/js/*/*.js test/build/web # allow writing core files for render tests ulimit -c unlimited -S echo 'ulimit -c: '`ulimit -c` -echo '/proc/sys/kernel/core_pattern: '`cat /proc/sys/kernel/core_pattern` -sysctl kernel.core_pattern +if [[ $(uname -s) == 'Linux' ]]; then + echo '/proc/sys/kernel/core_pattern: '`cat /proc/sys/kernel/core_pattern` + sysctl kernel.core_pattern +fi # run render tests istanbul cover --dir .nyc_output --include-pid --report none --print none test/render.test.js && @@ -34,10 +39,12 @@ EXIT_CODE=$? if [[ ${EXIT_CODE} != 0 ]]; then echo "The program crashed with exit code ${EXIT_CODE}. We're now trying to output the core dump." fi -for DUMP in $(find ./ -maxdepth 1 -name 'core*' -print); do - gdb `which node` ${DUMP} -ex "thread apply all bt" -ex "set pagination 0" -batch - rm -rf ${DUMP} -done +if [[ $(uname -s) == 'Linux' ]]; then + for DUMP in $(find ./ -maxdepth 1 -name 'core*' -print); do + gdb `which node` ${DUMP} -ex "thread apply all bt" -ex "set pagination 0" -batch + rm -rf ${DUMP} + done +fi # send coverage report to coveralls nyc report --reporter=lcov diff --git a/circle.yml b/circle.yml index 25928305389..484a898a92f 100644 --- a/circle.yml +++ b/circle.yml @@ -1,9 +1,28 @@ machine: - node: - version: '4' + xcode: + version: 7.3 + environment: + XCODE_SCHEME: "no" + XCODE_WORKSPACE: "no" + NODE_VERSION: 4 + dependencies: + cache_directories: + - './nvm' override: - - if [ "$(git symbolic-ref --short -q HEAD)" == "master" ]; then npm update; else npm install; fi; + # disable spotlight to ensure we waste no CPU on needless file indexing + - if [[ $(uname -s) == 'Darwin' ]]; then sudo mdutil -i off /; fi; + - | + if [[ ! -d ./nvm ]]; then + git clone --depth 1 https://github.com/creationix/nvm.git ./nvm + fi + - source ./nvm/nvm.sh && nvm install ${NODE_VERSION} + - | + if [ "$(git symbolic-ref --short -q HEAD)" == "master" ]; then + source ./nvm/nvm.sh && nvm use ${NODE_VERSION} && npm update + else + source ./nvm/nvm.sh && nvm use ${NODE_VERSION} && npm install + fi test: override: - ./ci.sh diff --git a/debug/chinese.html b/debug/chinese.html new file mode 100644 index 00000000000..4da63e62185 --- /dev/null +++ b/debug/chinese.html @@ -0,0 +1,67 @@ + + + + Mapbox GL JS debug page + + + + + + + + +
+ + + + + + + + diff --git a/dist/mapbox-gl.css b/dist/mapbox-gl.css index 45fd3eae429..0f6bf2e6528 100644 --- a/dist/mapbox-gl.css +++ b/dist/mapbox-gl.css @@ -80,7 +80,7 @@ } .mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate { padding: 5px; - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath%20style%3D%27fill%3A%23333333%3B%27%20d%3D%27M13%2C7%20L10.5%2C11.75%20L10.25%2C10%20z%20M13.888%2C6.112%20C13.615%2C5.84%2013.382%2C6.076%2012.5%2C6.5%20C10.14%2C7.634%206%2C10%206%2C10%20L9.5%2C10.5%20L10%2C14%20C10%2C14%2012.366%2C9.86%2013.5%2C7.5%20C13.924%2C6.617%2014.16%2C6.385%2013.888%2C6.112%27%2F%3E%3C%2Fsvg%3E"); + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCIgdmVyc2lvbj0iMS4xIj48cGF0aCBkPSJNMTAgNEM5IDQgOSA1IDkgNUw5IDUuMUE1IDUgMCAwIDAgNS4xIDlMNSA5QzUgOSA0IDkgNCAxMCA0IDExIDUgMTEgNSAxMUw1LjEgMTFBNSA1IDAgMCAwIDkgMTQuOUw5IDE1QzkgMTUgOSAxNiAxMCAxNiAxMSAxNiAxMSAxNSAxMSAxNUwxMSAxNC45QTUgNSAwIDAgMCAxNC45IDExTDE1IDExQzE1IDExIDE2IDExIDE2IDEwIDE2IDkgMTUgOSAxNSA5TDE0LjkgOUE1IDUgMCAwIDAgMTEgNS4xTDExIDVDMTEgNSAxMSA0IDEwIDR6TTEwIDYuNUEzLjUgMy41IDAgMCAxIDEzLjUgMTAgMy41IDMuNSAwIDAgMSAxMCAxMy41IDMuNSAzLjUgMCAwIDEgNi41IDEwIDMuNSAzLjUgMCAwIDEgMTAgNi41ek0xMCA4LjNBMS44IDEuOCAwIDAgMCA4LjMgMTAgMS44IDEuOCAwIDAgMCAxMCAxMS44IDEuOCAxLjggMCAwIDAgMTEuOCAxMCAxLjggMS44IDAgMCAwIDEwIDguM3oiIGZpbGw9IiMzMzMiLz48L3N2Zz4="); } .mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > div.arrow { diff --git a/dist/svg/mapboxgl-ctrl-geolocate.svg b/dist/svg/mapboxgl-ctrl-geolocate.svg new file mode 100644 index 00000000000..efb9a3f79e4 --- /dev/null +++ b/dist/svg/mapboxgl-ctrl-geolocate.svg @@ -0,0 +1 @@ + diff --git a/js/data/bucket/symbol_bucket.js b/js/data/bucket/symbol_bucket.js index b4438db3fab..12404232dcf 100644 --- a/js/data/bucket/symbol_bucket.js +++ b/js/data/bucket/symbol_bucket.js @@ -60,12 +60,12 @@ var layoutVertexArrayType = new Bucket.VertexArrayType([{ components: 2, type: 'Int16' }, { - name: 'a_data1', - components: 4, - type: 'Uint8' -}, { - name: 'a_data2', + name: 'a_texture_pos', components: 2, + type: 'Uint16' +}, { + name: 'a_data', + components: 4, type: 'Uint8' }]); @@ -73,20 +73,23 @@ var elementArrayType = new Bucket.ElementArrayType(); function addVertex(array, x, y, ox, oy, tx, ty, minzoom, maxzoom, labelminzoom, labelangle) { return array.emplaceBack( - // pos + // a_pos x, y, - // offset - Math.round(ox * 64), // use 1/64 pixels for placement + + // a_offset + Math.round(ox * 64), Math.round(oy * 64), - // data1 - tx / 4, // tex - ty / 4, // tex + + // a_texture_pos + tx / 4, // x coordinate of symbol on glyph atlas texture + ty / 4, // y coordinate of symbol on glyph atlas texture + + // a_data (labelminzoom || 0) * 10, // labelminzoom - labelangle, // labelangle - // data2 - (minzoom || 0) * 10, // minzoom - Math.min(maxzoom || 25, 25) * 10); // minzoom + labelangle, // labelangle + (minzoom || 0) * 10, // minzoom + Math.min(maxzoom || 25, 25) * 10); // maxzoom } SymbolBucket.prototype.addCollisionBoxVertex = function(layoutVertexArray, point, extrude, maxZoom, placementZoom) { diff --git a/js/render/draw_fill.js b/js/render/draw_fill.js index f28ac0ad6a4..ac83327296b 100644 --- a/js/render/draw_fill.js +++ b/js/render/draw_fill.js @@ -8,13 +8,20 @@ function draw(painter, source, layer, coords) { var gl = painter.gl; gl.enable(gl.STENCIL_TEST); - var color = layer.paint['fill-color']; - var image = layer.paint['fill-pattern']; - var opacity = layer.paint['fill-opacity']; - var isOutlineColorDefined = layer.getPaintProperty('fill-outline-color'); + var isOpaque; + if (layer.paint['fill-pattern']) { + isOpaque = false; + } else { + isOpaque = ( + layer.isPaintValueFeatureConstant('fill-color') && + layer.isPaintValueFeatureConstant('fill-opacity') && + layer.paint['fill-color'][3] === 1 && + layer.paint['fill-opacity'] === 1 + ); + } // Draw fill - if (image ? !painter.isOpaquePass : painter.isOpaquePass === (color[3] === 1 && opacity === 1)) { + if (painter.isOpaquePass === isOpaque) { // Once we switch to earcut drawing we can pull most of the WebGL setup // outside of this coords loop. painter.setDepthSublayer(1); @@ -27,6 +34,7 @@ function draw(painter, source, layer, coords) { painter.lineWidth(2); painter.depthMask(false); + var isOutlineColorDefined = layer.getPaintProperty('fill-outline-color'); if (isOutlineColorDefined || !layer.paint['fill-pattern']) { if (isOutlineColorDefined) { // If we defined a different color for the fill outline, we are diff --git a/js/source/source_cache.js b/js/source/source_cache.js index c72a10dd4ce..941ccd64182 100644 --- a/js/source/source_cache.js +++ b/js/source/source_cache.js @@ -35,6 +35,7 @@ function SourceCache(id, options, dispatcher) { this.roundZoom = source.roundZoom; this.reparseOverscaled = source.reparseOverscaled; this.isTileClipped = source.isTileClipped; + this.attribution = source.attribution; this.vectorLayerIds = source.vectorLayerIds; diff --git a/js/source/vector_tile_source.js b/js/source/vector_tile_source.js index 05bd1206f5e..7f39f4ed9fd 100644 --- a/js/source/vector_tile_source.js +++ b/js/source/vector_tile_source.js @@ -59,8 +59,13 @@ VectorTileSource.prototype = util.inherit(Evented, { }; if (tile.workerID) { - params.rawTileData = tile.rawTileData; - this.dispatcher.send('reload tile', params, done.bind(this), tile.workerID); + if (tile.state === 'loading') { + // schedule tile reloading after it has been loaded + tile.reloadCallback = callback; + } else { + params.rawTileData = tile.rawTileData; + this.dispatcher.send('reload tile', params, done.bind(this), tile.workerID); + } } else { tile.workerID = this.dispatcher.send('load tile', params, done.bind(this)); } @@ -81,6 +86,11 @@ VectorTileSource.prototype = util.inherit(Evented, { } callback(null); + + if (tile.reloadCallback) { + this.loadTile(tile, tile.reloadCallback); + tile.reloadCallback = null; + } } }, diff --git a/js/style/style_declaration.js b/js/style/style_declaration.js index 8ef97d330af..fee9e97333a 100644 --- a/js/style/style_declaration.js +++ b/js/style/style_declaration.js @@ -13,7 +13,7 @@ function StyleDeclaration(reference, value) { // immutable representation of value. used for comparison this.json = JSON.stringify(this.value); - var parsedValue = reference.type === 'color' ? parseColor(this.value) : value; + var parsedValue = reference.type === 'color' && this.value ? parseColor(this.value) : value; this.calculate = MapboxGLFunction[reference.function || 'piecewise-constant'](parsedValue); this.isFeatureConstant = this.calculate.isFeatureConstant; this.isZoomConstant = this.calculate.isZoomConstant; diff --git a/js/symbol/glyph_atlas.js b/js/symbol/glyph_atlas.js index 1177420f989..75275eb9b85 100644 --- a/js/symbol/glyph_atlas.js +++ b/js/symbol/glyph_atlas.js @@ -3,6 +3,11 @@ var ShelfPack = require('shelf-pack'); var util = require('../util/util'); +var SIZE_GROWTH_RATE = 4; +var DEFAULT_SIZE = 128; +// must be "DEFAULT_SIZE * SIZE_GROWTH_RATE ^ n" for some integer n +var MAX_SIZE = 2048; + module.exports = GlyphAtlas; @@ -10,8 +15,8 @@ function GlyphAtlas(width, height) { this.width = width; this.height = height; - this.sprite = new ShelfPack(width, height); - this.data = new Uint8Array(width * height); + this.sprite = new ShelfPack(this.width, this.height); + this.data = new Uint8Array(this.width * this.height); this.tilebins = {}; } @@ -46,7 +51,7 @@ GlyphAtlas.prototype.addGlyph = function(glyph, uid, buffer) { // Increase to next number divisible by 4, but at least 1. // This is so we can scale down the texture coordinates and pack them - // into 2 bytes rather than 4 bytes. + // into fewer bytes. packWidth += (4 - packWidth % 4); packHeight += (4 - packHeight % 4); @@ -91,12 +96,10 @@ GlyphAtlas.prototype.removeTileGlyphs = function(uid) { GlyphAtlas.prototype.resize = function() { - var origw = this.width, - origh = this.height; + var prevWidth = this.width; + var prevHeight = this.height; - // For now, don't grow the atlas beyond 1024x1024 because of how - // texture coords pack into unsigned byte in symbol bucket. - if (origw > 512 || origh > 512) return; + if (prevWidth >= MAX_SIZE || prevHeight >= MAX_SIZE) return; if (this.texture) { if (this.gl) { @@ -105,15 +108,14 @@ GlyphAtlas.prototype.resize = function() { this.texture = null; } - this.width *= 2; - this.height *= 2; + this.width *= SIZE_GROWTH_RATE; + this.height *= SIZE_GROWTH_RATE; this.sprite.resize(this.width, this.height); - var buf = new ArrayBuffer(this.width * this.height), - src, dst; - for (var i = 0; i < origh; i++) { - src = new Uint8Array(this.data.buffer, origh * i, origw); - dst = new Uint8Array(buf, origh * i * 2, origw); + var buf = new ArrayBuffer(this.width * this.height); + for (var i = 0; i < prevHeight; i++) { + var src = new Uint8Array(this.data.buffer, prevHeight * i, prevWidth); + var dst = new Uint8Array(buf, prevHeight * i * SIZE_GROWTH_RATE, prevWidth); dst.set(src); } this.data = new Uint8Array(buf); diff --git a/js/symbol/glyph_source.js b/js/symbol/glyph_source.js index ca7617d5e89..d31692074ce 100644 --- a/js/symbol/glyph_source.js +++ b/js/symbol/glyph_source.js @@ -30,7 +30,7 @@ GlyphSource.prototype.getSimpleGlyphs = function(fontstack, glyphIDs, uid, callb this.stacks[fontstack] = {}; } if (this.atlases[fontstack] === undefined) { - this.atlases[fontstack] = new GlyphAtlas(128, 128); + this.atlases[fontstack] = new GlyphAtlas(); } var glyphs = {}; diff --git a/js/ui/camera.js b/js/ui/camera.js index 36ee7fe5af2..d9a02de6530 100644 --- a/js/ui/camera.js +++ b/js/ui/camera.js @@ -481,14 +481,16 @@ util.extend(Camera.prototype, /** @lends Map.prototype */{ }, _easeToEnd: function(eventData) { - if (this.zooming) { + var wasZooming = this.zooming; + this.zooming = false; + this.rotating = false; + this.pitching = false; + + if (wasZooming) { this.fire('zoomend', eventData); } this.fire('moveend', eventData); - this.zooming = false; - this.rotating = false; - this.pitching = false; }, /** @@ -688,11 +690,12 @@ util.extend(Camera.prototype, /** @lends Map.prototype */{ this.fire('pitch', eventData); } }, function() { - this.fire('zoomend', eventData); - this.fire('moveend', eventData); this.zooming = false; this.rotating = false; this.pitching = false; + + this.fire('zoomend', eventData); + this.fire('moveend', eventData); }, options); return this; diff --git a/js/ui/map.js b/js/ui/map.js index ec93aa44206..e58955027a4 100755 --- a/js/ui/map.js +++ b/js/ui/map.js @@ -1035,7 +1035,7 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ this._styleDirty = true; } - if (this._sourcesDirty || this._repaint || !this.animationLoop.stopped()) { + if (this._sourcesDirty || this._repaint || this._styleDirty) { this._rerender(); } @@ -1058,6 +1058,8 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ if (typeof window !== 'undefined') { window.removeEventListener('resize', this._onWindowResize, false); } + var extension = this.painter.gl.getExtension('WEBGL_lose_context'); + if (extension) extension.loseContext(); removeNode(this._canvasContainer); removeNode(this._controlContainer); this._container.classList.remove('mapboxgl-map'); diff --git a/js/ui/popup.js b/js/ui/popup.js index 08564ea9f1b..591250ced05 100644 --- a/js/ui/popup.js +++ b/js/ui/popup.js @@ -80,6 +80,17 @@ Popup.prototype = util.inherit(Evented, /** @lends Popup.prototype */{ delete this._map; } + /** + * Fired when the popup is closed manually or programatically. + * + * @event close + * @memberof Popup + * @instance + * @type {Object} + * @property {Popup} popup object that was closed + */ + this.fire('close'); + return this; }, diff --git a/js/util/browser/dispatcher.js b/js/util/browser/dispatcher.js deleted file mode 100644 index 605d5b3bbdb..00000000000 --- a/js/util/browser/dispatcher.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -var util = require('../util'); -var Actor = require('../actor'); -var WebWorkify = require('webworkify'); - -module.exports = Dispatcher; - -/** - * Responsible for sending messages from a {@link Source} to an associated - * {@link WorkerSource}. - * - * @interface Dispatcher - * @private - */ -function Dispatcher(length, parent) { - this.actors = []; - this.currentActor = 0; - for (var i = 0; i < length; i++) { - var worker = new WebWorkify(require('../../source/worker')); - var actor = new Actor(worker, parent); - actor.name = "Worker " + i; - this.actors.push(actor); - } -} - -Dispatcher.prototype = { - /** - * Broadcast a message to all Workers. - * @method - * @name broadcast - * @param {string} type - * @param {object} data - * @param {Function} callback - * @memberof Dispatcher - * @instance - */ - broadcast: function(type, data, cb) { - cb = cb || function () {}; - util.asyncAll(this.actors, function (actor, done) { - actor.send(type, data, done); - }, cb); - }, - - /** - * Send a message to a Worker. - * @method - * @name send - * @param {string} type - * @param {object} data - * @param {Function} callback - * @param {number|undefined} [targetID] The ID of the Worker to which to send this message. Omit to allow the dispatcher to choose. - * @returns {number} The ID of the worker to which the message was sent. - * @memberof Dispatcher - * @instance - */ - send: function(type, data, callback, targetID, buffers) { - if (typeof targetID !== 'number' || isNaN(targetID)) { - // Use round robin to send requests to web workers. - targetID = this.currentActor = (this.currentActor + 1) % this.actors.length; - } - - this.actors[targetID].send(type, data, callback, buffers); - return targetID; - }, - - remove: function() { - for (var i = 0; i < this.actors.length; i++) { - this.actors[i].target.terminate(); - } - this.actors = []; - } -}; diff --git a/js/util/browser/web_worker.js b/js/util/browser/web_worker.js new file mode 100644 index 00000000000..7717220b6cf --- /dev/null +++ b/js/util/browser/web_worker.js @@ -0,0 +1,6 @@ +'use strict'; +var WebWorkify = require('webworkify'); + +module.exports = function () { + return new WebWorkify(require('../../source/worker')); +}; diff --git a/js/util/dispatcher.js b/js/util/dispatcher.js index 3406eaef8f8..7ca9bd7c367 100644 --- a/js/util/dispatcher.js +++ b/js/util/dispatcher.js @@ -1,57 +1,73 @@ 'use strict'; -var Worker = require('../source/worker'); -var Actor = require('../util/actor'); +var util = require('./util'); +var Actor = require('./actor'); +var WebWorker = require('./web_worker'); module.exports = Dispatcher; -function MessageBus(addListeners, postListeners) { - return { - addEventListener: function(event, callback) { - if (event === 'message') { - addListeners.push(callback); - } - }, - postMessage: function(data) { - setImmediate(function() { - for (var i = 0; i < postListeners.length; i++) { - postListeners[i]({data: data, target: this.target}); - } - }.bind(this)); - } - }; -} - +/** + * Responsible for sending messages from a {@link Source} to an associated + * {@link WorkerSource}. + * + * @interface Dispatcher + * @private + */ function Dispatcher(length, parent) { - - this.actors = new Array(length); - - var parentListeners = [], - workerListeners = [], - parentBus = new MessageBus(workerListeners, parentListeners), - workerBus = new MessageBus(parentListeners, workerListeners); - - parentBus.target = workerBus; - workerBus.target = parentBus; - // workerBus substitutes the WebWorker global `self`, and Worker uses - // self.importScripts for the 'load worker source' target. - workerBus.importScripts = function () {}; - - this.worker = new Worker(workerBus); - this.actor = new Actor(parentBus, parent); - - this.remove = function() { - parentListeners.splice(0, parentListeners.length); - workerListeners.splice(0, workerListeners.length); - }; + this.actors = []; + this.currentActor = 0; + for (var i = 0; i < length; i++) { + var worker = new WebWorker(); + var actor = new Actor(worker, parent); + actor.name = "Worker " + i; + this.actors.push(actor); + } } Dispatcher.prototype = { - broadcast: function(type, data, callback) { - this.actor.send(type, data, callback); + /** + * Broadcast a message to all Workers. + * @method + * @name broadcast + * @param {string} type + * @param {object} data + * @param {Function} callback + * @memberof Dispatcher + * @instance + */ + broadcast: function(type, data, cb) { + cb = cb || function () {}; + util.asyncAll(this.actors, function (actor, done) { + actor.send(type, data, done); + }, cb); }, + /** + * Send a message to a Worker. + * @method + * @name send + * @param {string} type + * @param {object} data + * @param {Function} callback + * @param {number|undefined} [targetID] The ID of the Worker to which to send this message. Omit to allow the dispatcher to choose. + * @returns {number} The ID of the worker to which the message was sent. + * @memberof Dispatcher + * @instance + */ send: function(type, data, callback, targetID, buffers) { - this.actor.send(type, data, callback, buffers); + if (typeof targetID !== 'number' || isNaN(targetID)) { + // Use round robin to send requests to web workers. + targetID = this.currentActor = (this.currentActor + 1) % this.actors.length; + } + + this.actors[targetID].send(type, data, callback, buffers); + return targetID; + }, + + remove: function() { + for (var i = 0; i < this.actors.length; i++) { + this.actors[i].target.terminate(); + } + this.actors = []; } }; diff --git a/js/util/vectortile_to_geojson.js b/js/util/vectortile_to_geojson.js index aa3faa475ca..6d415119e36 100644 --- a/js/util/vectortile_to_geojson.js +++ b/js/util/vectortile_to_geojson.js @@ -10,8 +10,8 @@ function Feature(vectorTileFeature, z, x, y) { this.properties = vectorTileFeature.properties; - if (vectorTileFeature._id) { - this.id = vectorTileFeature._id; + if (vectorTileFeature.id != null) { + this.id = vectorTileFeature.id; } } diff --git a/js/util/web_worker.js b/js/util/web_worker.js new file mode 100644 index 00000000000..df323369e31 --- /dev/null +++ b/js/util/web_worker.js @@ -0,0 +1,42 @@ +'use strict'; + +var Worker = require('../source/worker'); + +module.exports = function () { + var parentListeners = [], + workerListeners = [], + parentBus = new MessageBus(workerListeners, parentListeners), + workerBus = new MessageBus(parentListeners, workerListeners); + + parentBus.target = workerBus; + workerBus.target = parentBus; + // workerBus substitutes the WebWorker global `self`, and Worker uses + // self.importScripts for the 'load worker source' target. + workerBus.importScripts = function () {}; + + new Worker(workerBus); + + return parentBus; +}; + +function MessageBus(addListeners, postListeners) { + return { + addEventListener: function(event, callback) { + if (event === 'message') { + addListeners.push(callback); + } + }, + postMessage: function(data) { + setImmediate(function() { + for (var i = 0; i < postListeners.length; i++) { + postListeners[i]({data: data, target: this.target}); + } + }.bind(this)); + }, + terminate: function() { + addListeners.splice(0, addListeners.length); + postListeners.splice(0, postListeners.length); + } + }; +} + diff --git a/package.json b/package.json index ae27299ea9f..4e586ce83c7 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,15 @@ "dependencies": { "csscolorparser": "^1.0.2", "earcut": "^2.0.3", - "feature-filter": "^2.1.0", + "feature-filter": "^2.2.0", "geojson-rewind": "^0.1.0", - "geojson-vt": "^2.3.0", + "geojson-vt": "^2.4.0", "gl-matrix": "^2.3.1", "grid-index": "^1.0.0", "mapbox-gl-function": "^1.2.1", - "mapbox-gl-supported": "^1.2.0", - "mapbox-gl-shaders": "mapbox/mapbox-gl-shaders#4d1f89514bf03536c8e682439df165c33a37122a", + "mapbox-gl-shaders": "mapbox/mapbox-gl-shaders#de2ab007455aa2587c552694c68583f94c9f2747", "mapbox-gl-style-spec": "mapbox/mapbox-gl-style-spec#83b1a3e5837d785af582efd5ed1a212f2df6a4ae", + "mapbox-gl-supported": "^1.2.0", "pbf": "^1.3.2", "pngjs": "^2.2.0", "point-geometry": "^0.0.0", @@ -33,15 +33,19 @@ "supercluster": "^2.0.1", "unassertify": "^2.0.0", "unitbezier": "^0.0.0", - "vector-tile": "1.2.1", + "vector-tile": "^1.3.0", "vt-pbf": "^2.0.2", "webworkify": "^1.3.0", "whoots-js": "^2.0.0" }, "devDependencies": { + "async": "^2.0.1", + "babel-preset-react": "^6.11.1", + "babelify": "^7.3.0", "benchmark": "~2.1.0", "browserify": "^13.0.0", "browserify-middleware": "^7.0.0", + "clipboard": "^1.5.12", "concat-stream": "1.5.1", "coveralls": "^2.11.8", "doctrine": "^1.2.1", @@ -58,11 +62,13 @@ "istanbul": "^0.4.2", "json-loader": "^0.5.4", "lodash": "^4.13.1", - "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#df624912cd514b89dc4405b233fc25db85bbb8b7", + "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#304729f1e66645e0d77e8cbd5d7093a496ee033e", "memory-fs": "^0.3.0", "minifyify": "^7.0.1", "nyc": "6.4.0", "proxyquire": "^1.7.9", + "react": "^15.3.0", + "react-dom": "^15.3.0", "remark": "4.2.2", "remark-html": "3.0.0", "sinon": "^1.15.4", @@ -87,7 +93,7 @@ "./js/util/browser.js": "./js/util/browser/browser.js", "./js/util/canvas.js": "./js/util/browser/canvas.js", "./js/util/dom.js": "./js/util/browser/dom.js", - "./js/util/dispatcher.js": "./js/util/browser/dispatcher.js" + "./js/util/web_worker.js": "./js/util/browser/web_worker.js" }, "scripts": { "build-dev": "browserify js/mapbox-gl.js --debug --ignore-transform unassertify --standalone mapboxgl > dist/mapbox-gl-dev.js && tap --no-coverage test/build/dev.test.js", diff --git a/server.js b/server.js index acd8eaef7ea..a023f439072 100644 --- a/server.js +++ b/server.js @@ -23,26 +23,23 @@ app.get('/access-token.js', browserify('./debug/access-token.js', { })); app.get('/bench/index.js', browserify('./bench/index.js', { - transform: ['unassertify', 'envify'], + transform: [['babelify', {presets: ['react']}], 'unassertify', 'envify'], debug: true, minify: true, cache: 'dynamic', precompile: true })); -app.get('/bench/:name', function(req, res) { +app.get('/bench', function(req, res) { res.sendFile(path.join(__dirname, 'bench', 'index.html')); }); -app.get('/debug', function(req, res) { - res.redirect('/'); +app.get('/bench/:name', function(req, res) { + res.sendFile(path.join(__dirname, 'bench', 'index.html')); }); app.use(express.static(path.join(__dirname, 'debug'))); app.use('/dist', express.static(path.join(__dirname, 'dist'))); - -// serve files in mapbox-gl-test-suite, so that debug files can use its -// data/image/video/etc. assets app.use('/mapbox-gl-test-suite', express.static(path.join(__dirname, 'node_modules/mapbox-gl-test-suite'))); downloadBenchData(function() { diff --git a/test/js/data/symbol_bucket.test.js b/test/js/data/symbol_bucket.test.js index b96bad2ca1e..a7390bfbacb 100644 --- a/test/js/data/symbol_bucket.test.js +++ b/test/js/data/symbol_bucket.test.js @@ -25,7 +25,7 @@ test('SymbolBucket', function(t) { var symbolQuadsArray = new SymbolQuadsArray(); var symbolInstancesArray = new SymbolInstancesArray(); var collision = new Collision(0, 0, collisionBoxArray); - var atlas = new GlyphAtlas(128, 128); + var atlas = new GlyphAtlas(); for (var id in glyphs) { glyphs[id].bitmap = true; glyphs[id].rect = atlas.addGlyph(glyphs[id], 1, 3); diff --git a/test/js/source/source_cache.test.js b/test/js/source/source_cache.test.js index 7e3d1c3b880..467beded1dd 100644 --- a/test/js/source/source_cache.test.js +++ b/test/js/source/source_cache.test.js @@ -49,6 +49,16 @@ function createSourceCache(options, used) { return sc; } +test('SourceCache#attribution is set', function(t) { + var sourceCache = createSourceCache({ + attribution: 'Mapbox Heavy Industries' + }); + sourceCache.on('load', function() { + t.equal(sourceCache.attribution, 'Mapbox Heavy Industries'); + t.end(); + }); +}); + test('SourceCache#addTile', function(t) { t.test('loads tile when uncached', function(t) { var coord = new TileCoord(0, 0, 0); diff --git a/test/js/source/vector_tile_source.test.js b/test/js/source/vector_tile_source.test.js index f48364154aa..a56fb469da6 100644 --- a/test/js/source/vector_tile_source.test.js +++ b/test/js/source/vector_tile_source.test.js @@ -112,6 +112,35 @@ test('VectorTileSource', function(t) { testScheme('xyz', 'http://example.com/10/5/5.png'); testScheme('tms', 'http://example.com/10/5/1018.png'); + t.test('reloads a loading tile properly', function (t) { + var source = createSource({ + tiles: ["http://example.com/{z}/{x}/{y}.png"] + }); + var events = []; + source.dispatcher.send = function(type, params, cb) { + events.push(type); + setTimeout(cb, 0); + return 1; + }; + + source.on('load', function () { + var tile = { + coord: new TileCoord(10, 5, 5, 0), + state: 'loading', + loadVectorData: function () { + this.state = 'loaded'; + events.push('tile loaded'); + } + }; + source.loadTile(tile, function () {}); + t.equal(tile.state, 'loading'); + source.loadTile(tile, function () { + t.same(events, ['load tile', 'tile loaded', 'reload tile', 'tile loaded']); + t.end(); + }); + }); + }); + t.test('after', function(t) { server.close(t.end); }); diff --git a/test/js/style/style_layer.test.js b/test/js/style/style_layer.test.js index d22bd8ca593..a54e02860f9 100644 --- a/test/js/style/style_layer.test.js +++ b/test/js/style/style_layer.test.js @@ -266,6 +266,26 @@ test('StyleLayer#setPaintProperty', function(t) { }); }); + t.test('can unset fill-outline-color #2886', function (t) { + var layer = StyleLayer.create({ + id: 'building', + type: 'fill', + source: 'streets', + paint: { + 'fill-color': '#00f' + } + }); + + layer.setPaintProperty('fill-outline-color', '#f00'); + layer.updatePaintTransitions([], {transition: false}, null, createAnimationLoop()); + t.deepEqual(layer.getPaintValue('fill-outline-color'), [1, 0, 0, 1]); + layer.setPaintProperty('fill-outline-color', undefined); + layer.updatePaintTransitions([], {transition: false}, null, createAnimationLoop()); + t.deepEqual(layer.getPaintValue('fill-outline-color'), [0, 0, 1, 1]); + + t.end(); + }); + t.end(); }); diff --git a/test/js/ui/camera.test.js b/test/js/ui/camera.test.js index 1afcd733a5d..3aa868f30df 100644 --- a/test/js/ui/camera.test.js +++ b/test/js/ui/camera.test.js @@ -627,7 +627,7 @@ test('camera', function(t) { var movestarted, moved, rotated, pitched, zoomstarted, zoomed, eventData = { data: 'ok' }; - t.plan(9); + t.plan(12); camera .on('movestart', function(d) { movestarted = d.data; }) @@ -635,6 +635,10 @@ test('camera', function(t) { .on('rotate', function(d) { rotated = d.data; }) .on('pitch', function(d) { pitched = d.data; }) .on('moveend', function(d) { + t.notOk(camera.zooming); + t.notOk(camera.panning); + t.notOk(camera.rotating); + t.equal(movestarted, 'ok'); t.equal(moved, 'ok'); t.equal(zoomed, 'ok'); @@ -799,6 +803,10 @@ test('camera', function(t) { .on('rotate', function(d) { rotated = d.data; }) .on('pitch', function(d) { pitched = d.data; }) .on('moveend', function(d) { + t.notOk(this.zooming); + t.notOk(this.panning); + t.notOk(this.rotating); + t.equal(movestarted, 'ok'); t.equal(moved, 'ok'); t.equal(zoomed, 'ok'); diff --git a/test/js/ui/map.test.js b/test/js/ui/map.test.js index 3318d0db3df..5a743bf44f1 100755 --- a/test/js/ui/map.test.js +++ b/test/js/ui/map.test.js @@ -13,23 +13,23 @@ var fixed = require('../../testutil/fixed'); var fixedNum = fixed.Num; var fixedLngLat = fixed.LngLat; -test('Map', function(t) { - function createMap(options) { - return new Map(extend({ - container: { - offsetWidth: 200, - offsetHeight: 200, - classList: { - add: function() {}, - remove: function() {} - } - }, - interactive: false, - attributionControl: false, - trackResize: true - }, options)); - } +function createMap(options) { + return new Map(extend({ + container: { + offsetWidth: 200, + offsetHeight: 200, + classList: { + add: function() {}, + remove: function() {} + } + }, + interactive: false, + attributionControl: false, + trackResize: true + }, options)); +} +test('Map', function(t) { t.test('constructor', function(t) { var map = createMap({interactive: true}); t.ok(map.getContainer()); @@ -950,6 +950,39 @@ test('Map', function(t) { t.end(); }); + test('#removeLayer restores Map#loaded() to true', function (t) { + var style = createStyle(); + style.sources.mapbox = { + type: 'vector', + minzoom: 1, + maxzoom: 10, + tiles: ['http://example.com/{z}/{x}/{y}.png'] + }; + style.layers.push({ + id: 'layerId', + type: 'circle', + source: 'mapbox', + 'source-layer': 'sourceLayer' + }); + + var map = createMap({ style: style }); + map.on('render', function () { + if (map.animationLoop.stopped()) { + map.removeLayer('layerId'); + map.off('render'); + map.on('render', check); + } + }); + + function check () { + if (map.loaded()) { + map.remove(); + t.end(); + } + } + }); + + t.end(); });