diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c6f5e60a94..10e79b234d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -109,11 +109,11 @@ See [`bench/README.md`](https://github.com/mapbox/mapbox-gl-js/blob/master/bench * Classes * Template strings * Computed and shorthand object properties -* The following ES6 features are not to be used, in order to maintain support for Node 4.x, IE 11, and older mobile browsers. This may change in the future. * Default parameters * Rest parameters - * Spread (`...`) operator * Destructuring +* The following ES6 features are not to be used, in order to maintain support for IE 11 and older mobile browsers. This may change in the future. + * Spread (`...`) operator (because it requires Object.assign) * Iterators and generators * "Library" features such as `Map`, `Set`, `array.find`, etc. * Modules diff --git a/bench/benchmarks_view.js b/bench/benchmarks_view.js index b8f57b1e0c0..3aaa4c3de0a 100644 --- a/bench/benchmarks_view.js +++ b/bench/benchmarks_view.js @@ -12,58 +12,55 @@ const BenchmarksView = React.createClass({ render: function() { return
- {this.renderSidebarBenchmarks()} - {this.renderBenchmarks()} -
; - }, - - renderSidebarBenchmarks: function() { - return
-

Benchmarks

-
- {Object.keys(this.state.results).map(this.renderSidebarBenchmark)} -
- + Benchmarks + - Copy Results - + Copy Results + + + + + + + {this.versions().map((v) => )} + + + + {Object.keys(this.state.results).map(this.renderBenchmark)} + +
Benchmark{v}
; }, - renderSidebarBenchmark: function(name) { - return
-

{name}

- {Object.keys(this.state.results[name]).map(this.renderSidebarBenchmarkVersion.bind(this, name))} -
; + renderBenchmark: function(name) { + return + {name} + {Object.keys(this.state.results[name]).map(this.renderBenchmarkVersion.bind(this, name))} + ; }, - renderSidebarBenchmarkVersion: function(name, version) { + renderBenchmarkVersion: function(name, version) { const results = this.state.results[name][version]; - const that = this; - - return
- {version}: {results.message || '...'} -
; + {results.logs.map((log, index) => { + return
{log.message}
; + })} + + ); }, - renderTextBenchmarks: function() { + versions: function() { const versions = []; for (const name in this.state.results) { for (const version in this.state.results[name]) { @@ -72,7 +69,11 @@ const BenchmarksView = React.createClass({ } } } + return versions; + }, + renderTextBenchmarks: function() { + const versions = this.versions(); let output = `benchmark | ${versions.join(' | ')}\n---`; for (let i = 0; i < versions.length; i++) { output += ' | ---'; @@ -90,49 +91,6 @@ const BenchmarksView = React.createClass({ return output; }, - renderBenchmarks: function() { - return
- {Object.keys(this.state.results).map(this.renderBenchmark)} -
; - }, - - renderBenchmark: function(name) { - return
- {Object.keys(this.state.results[name]).map(this.renderBenchmarkVersion.bind(this, name))} -
; - }, - - renderBenchmarkVersion: function(name, version) { - const results = this.state.results[name][version]; - return ( -
- -

{name} on {version}

- {results.logs.map((log, index) => { - return
{log.message}
; - })} -
- ); - }, - - scrollToBenchmark: function(name, version) { - const duration = 300; - const startTime = (new Date()).getTime(); - const startYOffset = window.pageYOffset; - - requestAnimationFrame(function frame() { - const endYOffset = document.getElementById(name + version).offsetTop; - const time = (new Date()).getTime(); - const yOffset = Math.min((time - startTime) / duration, 1) * (endYOffset - startYOffset) + startYOffset; - window.scrollTo(0, yOffset); - if (time < startTime + duration) requestAnimationFrame(frame); - }); - }, - getInitialState: function() { const results = {}; @@ -156,7 +114,6 @@ const BenchmarksView = React.createClass({ asyncSeries(Object.keys(that.state.results), (name, callback) => { asyncSeries(Object.keys(that.state.results[name]), (version, callback) => { - that.scrollToBenchmark(name, version); that.runBenchmark(name, version, callback); }, callback); }, (err) => { @@ -181,7 +138,6 @@ const BenchmarksView = React.createClass({ } results.status = 'running'; - this.scrollToBenchmark(name, version); log('dark', 'starting'); setTimeout(() => { @@ -223,6 +179,10 @@ const BenchmarksView = React.createClass({ return reduceStatuses(Object.keys(this.state.results).map(function(name) { return this.getBenchmarkStatus(name); }, this)); + }, + + reload() { + location.reload(); } }); diff --git a/debug/canvas.html b/debug/canvas.html new file mode 100644 index 00000000000..3b2a0087df6 --- /dev/null +++ b/debug/canvas.html @@ -0,0 +1,80 @@ + + + + Mapbox GL JS debug page + + + + + + + + +Canvas not supported + +
+ + + + + + + diff --git a/dist/mapbox-gl.css b/dist/mapbox-gl.css index efca2ec68cb..a0ffa2b937a 100644 --- a/dist/mapbox-gl.css +++ b/dist/mapbox-gl.css @@ -118,7 +118,7 @@ a.mapboxgl-ctrl-logo { display: block; background-repeat: no-repeat; cursor: pointer; - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiAgIHZpZXdCb3g9IjAgMCA4NC40OSAyMSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgODQuNDkgMjE7IiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGUgdHlwZT0idGV4dC9jc3MiPiAgLnN0MHtvcGFjaXR5OjAuOTtmaWxsOiNGRkZGRkY7ZW5hYmxlLWJhY2tncm91bmQ6bmV3ICAgIDt9ICAuc3Qxe29wYWNpdHk6MC4zNTtlbmFibGUtYmFja2dyb3VuZDpuZXcgICAgO308L3N0eWxlPjxnPiAgPHBhdGggY2xhc3M9InN0MCIgZD0iTTgzLjI1LDE0LjI2YzAsMC4xMi0wLjA5LDAuMjEtMC4yMSwwLjIxaC0xLjYxYy0wLjEzLDAtMC4yNC0wLjA2LTAuMy0wLjE3bC0xLjQ0LTIuMzlsLTEuNDQsMi4zOSAgICBjLTAuMDYsMC4xMS0wLjE4LDAuMTctMC4zLDAuMTdoLTEuNjFjLTAuMDQsMC0wLjA4LTAuMDEtMC4xMi0wLjAzYy0wLjA5LTAuMDYtMC4xMy0wLjE5LTAuMDYtMC4yOGwwLDBsMi40My0zLjY4TDc2LjIsNi44NCAgICBjLTAuMDItMC4wMy0wLjAzLTAuMDctMC4wMy0wLjEyYzAtMC4xMiwwLjA5LTAuMjEsMC4yMS0wLjIxaDEuNjFjMC4xMywwLDAuMjQsMC4wNiwwLjMsMC4xN2wxLjQxLDIuMzZsMS40LTIuMzUgICAgYzAuMDYtMC4xMSwwLjE4LTAuMTcsMC4zLTAuMTdIODNjMC4wNCwwLDAuMDgsMC4wMSwwLjEyLDAuMDNjMC4wOSwwLjA2LDAuMTMsMC4xOSwwLjA2LDAuMjhsMCwwbC0yLjM3LDMuNjNsMi40MywzLjY3ICAgIEM4My4yNCwxNC4xOCw4My4yNSwxNC4yMiw4My4yNSwxNC4yNnoiLz4gIDxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik02Ni4yNCw5LjU5Yy0wLjM5LTEuODgtMS45Ni0zLjI4LTMuODQtMy4yOGMtMS4wMywwLTIuMDMsMC40Mi0yLjczLDEuMThWMy41MWMwLTAuMTMtMC4xLTAuMjMtMC4yMy0wLjIzaC0xLjQgICAgYy0wLjEzLDAtMC4yMywwLjExLTAuMjMsMC4yM3YxMC43MmMwLDAuMTMsMC4xLDAuMjMsMC4yMywwLjIzaDEuNGMwLjEzLDAsMC4yMy0wLjExLDAuMjMtMC4yM1YxMy41YzAuNzEsMC43NSwxLjcsMS4xOCwyLjczLDEuMTggICAgYzEuODgsMCwzLjQ1LTEuNDEsMy44NC0zLjI5QzY2LjM3LDEwLjc5LDY2LjM3LDEwLjE4LDY2LjI0LDkuNTlMNjYuMjQsOS41OXogTTYyLjA4LDEzYy0xLjMyLDAtMi4zOS0xLjExLTIuNDEtMi40OHYtMC4wNiAgICBjMC4wMi0xLjM4LDEuMDktMi40OCwyLjQxLTIuNDhzMi40MiwxLjEyLDIuNDIsMi41MVM2My40MSwxMyw2Mi4wOCwxM3oiLz4gIDxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik03MS42Nyw2LjMyYy0xLjk4LTAuMDEtMy43MiwxLjM1LTQuMTYsMy4yOWMtMC4xMywwLjU5LTAuMTMsMS4xOSwwLDEuNzdjMC40NCwxLjk0LDIuMTcsMy4zMiw0LjE3LDMuMyAgICBjMi4zNSwwLDQuMjYtMS44Nyw0LjI2LTQuMTlTNzQuMDQsNi4zMiw3MS42Nyw2LjMyeiBNNzEuNjUsMTMuMDFjLTEuMzMsMC0yLjQyLTEuMTItMi40Mi0yLjUxczEuMDgtMi41MiwyLjQyLTIuNTIgICAgYzEuMzMsMCwyLjQyLDEuMTIsMi40MiwyLjUxUzcyLjk5LDEzLDcxLjY1LDEzLjAxTDcxLjY1LDEzLjAxeiIvPiAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTYyLjA4LDcuOThjLTEuMzIsMC0yLjM5LDEuMTEtMi40MSwyLjQ4djAuMDZDNTkuNjgsMTEuOSw2MC43NSwxMyw2Mi4wOCwxM3MyLjQyLTEuMTIsMi40Mi0yLjUxICAgIFM2My40MSw3Ljk4LDYyLjA4LDcuOTh6IE02Mi4wOCwxMS43NmMtMC42MywwLTEuMTQtMC41Ni0xLjE3LTEuMjV2LTAuMDRjMC4wMS0wLjY5LDAuNTQtMS4yNSwxLjE3LTEuMjUgICAgYzAuNjMsMCwxLjE3LDAuNTcsMS4xNywxLjI3QzYzLjI0LDExLjIsNjIuNzMsMTEuNzYsNjIuMDgsMTEuNzZ6Ii8+ICA8cGF0aCBjbGFzcz0ic3QxIiBkPSJNNzEuNjUsNy45OGMtMS4zMywwLTIuNDIsMS4xMi0yLjQyLDIuNTFTNzAuMzIsMTMsNzEuNjUsMTNzMi40Mi0xLjEyLDIuNDItMi41MVM3Mi45OSw3Ljk4LDcxLjY1LDcuOTh6ICAgICBNNzEuNjUsMTEuNzZjLTAuNjQsMC0xLjE3LTAuNTctMS4xNy0xLjI3YzAtMC43LDAuNTMtMS4yNiwxLjE3LTEuMjZzMS4xNywwLjU3LDEuMTcsMS4yN0M3Mi44MiwxMS4yMSw3Mi4yOSwxMS43Niw3MS42NSwxMS43NnoiICAgIC8+ICA8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNDUuNzQsNi41M2gtMS40Yy0wLjEzLDAtMC4yMywwLjExLTAuMjMsMC4yM3YwLjczYy0wLjcxLTAuNzUtMS43LTEuMTgtMi43My0xLjE4ICAgIGMtMi4xNywwLTMuOTQsMS44Ny0zLjk0LDQuMTlzMS43Nyw0LjE5LDMuOTQsNC4xOWMxLjA0LDAsMi4wMy0wLjQzLDIuNzMtMS4xOXYwLjczYzAsMC4xMywwLjEsMC4yMywwLjIzLDAuMjNoMS40ICAgIGMwLjEzLDAsMC4yMy0wLjExLDAuMjMtMC4yM1Y2Ljc0YzAtMC4xMi0wLjA5LTAuMjItMC4yMi0wLjIyQzQ1Ljc1LDYuNTMsNDUuNzUsNi41Myw0NS43NCw2LjUzeiBNNDQuMTIsMTAuNTMgICAgQzQ0LjExLDExLjksNDMuMDMsMTMsNDEuNzEsMTNzLTIuNDItMS4xMi0yLjQyLTIuNTFzMS4wOC0yLjUyLDIuNC0yLjUyYzEuMzMsMCwyLjM5LDEuMTEsMi40MSwyLjQ4TDQ0LjEyLDEwLjUzeiIvPiAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTQxLjcxLDcuOThjLTEuMzMsMC0yLjQyLDEuMTItMi40MiwyLjUxUzQwLjM3LDEzLDQxLjcxLDEzczIuMzktMS4xMSwyLjQxLTIuNDh2LTAuMDYgICAgQzQ0LjEsOS4wOSw0My4wMyw3Ljk4LDQxLjcxLDcuOTh6IE00MC41NSwxMC40OWMwLTAuNywwLjUyLTEuMjcsMS4xNy0xLjI3YzAuNjQsMCwxLjE0LDAuNTYsMS4xNywxLjI1djAuMDQgICAgYy0wLjAxLDAuNjgtMC41MywxLjI0LTEuMTcsMS4yNEM0MS4wOCwxMS43NSw0MC41NSwxMS4xOSw0MC41NSwxMC40OXoiLz4gIDxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik01Mi40MSw2LjMyYy0xLjAzLDAtMi4wMywwLjQyLTIuNzMsMS4xOFY2Ljc1YzAtMC4xMy0wLjEtMC4yMy0wLjIzLTAuMjNoLTEuNGMtMC4xMywwLTAuMjMsMC4xMS0wLjIzLDAuMjMgICAgdjEwLjcyYzAsMC4xMywwLjEsMC4yMywwLjIzLDAuMjNoMS40YzAuMTMsMCwwLjIzLTAuMSwwLjIzLTAuMjNWMTMuNWMwLjcxLDAuNzUsMS43LDEuMTgsMi43NCwxLjE4YzIuMTcsMCwzLjk0LTEuODcsMy45NC00LjE5ICAgIFM1NC41OCw2LjMyLDUyLjQxLDYuMzJ6IE01Mi4wOCwxMy4wMWMtMS4zMiwwLTIuMzktMS4xMS0yLjQyLTIuNDh2LTAuMDdjMC4wMi0xLjM4LDEuMDktMi40OSwyLjQtMi40OWMxLjMyLDAsMi40MSwxLjEyLDIuNDEsMi41MSAgICBTNTMuNCwxMyw1Mi4wOCwxMy4wMUw1Mi4wOCwxMy4wMXoiLz4gIDxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik01Mi4wOCw3Ljk4Yy0xLjMyLDAtMi4zOSwxLjExLTIuNDIsMi40OHYwLjA2YzAuMDMsMS4zOCwxLjEsMi40OCwyLjQyLDIuNDhzMi40MS0xLjEyLDIuNDEtMi41MSAgICBTNTMuNCw3Ljk4LDUyLjA4LDcuOTh6IE01Mi4wOCwxMS43NmMtMC42MywwLTEuMTQtMC41Ni0xLjE3LTEuMjV2LTAuMDRjMC4wMS0wLjY5LDAuNTQtMS4yNSwxLjE3LTEuMjVjMC42MywwLDEuMTcsMC41OCwxLjE3LDEuMjcgICAgUzUyLjcyLDExLjc2LDUyLjA4LDExLjc2eiIvPiAgPHBhdGggY2xhc3M9InN0MCIgZD0iTTM2LjA4LDE0LjI0YzAsMC4xMy0wLjEsMC4yMy0wLjIzLDAuMjNoLTEuNDFjLTAuMTMsMC0wLjIzLTAuMTEtMC4yMy0wLjIzVjkuNjhjMC0wLjk4LTAuNzQtMS43MS0xLjYyLTEuNzEgICAgYy0wLjgsMC0xLjQ2LDAuNy0xLjU5LDEuNjJsMC4wMSw0LjY2YzAsMC4xMy0wLjExLDAuMjMtMC4yMywwLjIzaC0xLjQxYy0wLjEzLDAtMC4yMy0wLjExLTAuMjMtMC4yM1Y5LjY4ICAgIGMwLTAuOTgtMC43NC0xLjcxLTEuNjItMS43MWMtMC44NSwwLTEuNTQsMC43OS0xLjYsMS44djQuNDhjMCwwLjEzLTAuMSwwLjIzLTAuMjMsMC4yM2gtMS40Yy0wLjEzLDAtMC4yMy0wLjExLTAuMjMtMC4yM1Y2Ljc0ICAgIGMwLjAxLTAuMTMsMC4xLTAuMjIsMC4yMy0wLjIyaDEuNGMwLjEzLDAsMC4yMiwwLjExLDAuMjMsMC4yMlY3LjRjMC41LTAuNjgsMS4zLTEuMDksMi4xNi0xLjFoMC4wM2MxLjA5LDAsMi4wOSwwLjYsMi42LDEuNTUgICAgYzAuNDUtMC45NSwxLjQtMS41NSwyLjQ0LTEuNTZjMS42MiwwLDIuOTMsMS4yNSwyLjksMi43OEwzNi4wOCwxNC4yNHoiLz4gIDxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik04NC4zNCwxMy41OWwtMC4wNy0wLjEzbC0xLjk2LTIuOTlsMS45NC0yLjk1YzAuNDQtMC42NywwLjI2LTEuNTYtMC40MS0yLjAyYy0wLjAyLDAtMC4wMywwLTAuMDQtMC4wMSAgICBjLTAuMjMtMC4xNS0wLjUtMC4yMi0wLjc4LTAuMjJoLTEuNjFjLTAuNTYsMC0xLjA4LDAuMjktMS4zNywwLjc4TDc5LjcyLDYuNmwtMC4zNC0wLjU2Qzc5LjA5LDUuNTYsNzguNTcsNS4yNyw3OCw1LjI3aC0xLjYgICAgYy0wLjYsMC0xLjEzLDAuMzctMS4zNSwwLjkyYy0yLjE5LTEuNjYtNS4yOC0xLjQ3LTcuMjYsMC40NWMtMC4zNSwwLjM0LTAuNjUsMC43Mi0wLjg5LDEuMTRjLTAuOS0xLjYyLTIuNTgtMi43Mi00LjUtMi43MiAgICBjLTAuNSwwLTEuMDEsMC4wNy0xLjQ4LDAuMjNWMy41MWMwLTAuODItMC42Ni0xLjQ4LTEuNDctMS40OGgtMS40Yy0wLjgxLDAtMS40NywwLjY2LTEuNDcsMS40N3YzLjc1ICAgIGMtMC45NS0xLjM2LTIuNS0yLjE4LTQuMTctMi4xOWMtMC43NCwwLTEuNDYsMC4xNi0yLjEyLDAuNDdjLTAuMjQtMC4xNy0wLjU0LTAuMjYtMC44NC0wLjI2aC0xLjRjLTAuNDUsMC0wLjg3LDAuMjEtMS4xNSwwLjU2ICAgIGMtMC4wMi0wLjAzLTAuMDQtMC4wNS0wLjA3LTAuMDhjLTAuMjgtMC4zLTAuNjgtMC40Ny0xLjA5LTAuNDdoLTEuMzljLTAuMywwLTAuNiwwLjA5LTAuODQsMC4yNmMtMC42Ny0wLjMtMS4zOS0wLjQ2LTIuMTItMC40NiAgICBjLTEuODMsMC0zLjQzLDEtNC4zNywyLjVjLTAuMi0wLjQ2LTAuNDgtMC44OS0wLjgzLTEuMjVjLTAuOC0wLjgxLTEuODktMS4yNS0zLjAyLTEuMjVoLTAuMDFjLTAuODksMC4wMS0xLjc1LDAuMzMtMi40NiwwLjg4ICAgIGMtMC43NC0wLjU3LTEuNjQtMC44OC0yLjU3LTAuODhIMjguMWMtMC4yOSwwLTAuNTgsMC4wMy0wLjg2LDAuMTFjLTAuMjgsMC4wNi0wLjU2LDAuMTYtMC44MiwwLjI4Yy0wLjIxLTAuMTItMC40NS0wLjE4LTAuNy0wLjE4ICAgIGgtMS40Yy0wLjgyLDAtMS40NywwLjY2LTEuNDcsMS40N3Y3LjVjMCwwLjgyLDAuNjYsMS40NywxLjQ3LDEuNDdoMS40YzAuODIsMCwxLjQ4LTAuNjYsMS40OC0xLjQ4bDAsMFY5Ljc5ICAgIGMwLjAzLTAuMzYsMC4yMy0wLjU5LDAuMzYtMC41OWMwLjE4LDAsMC4zOCwwLjE4LDAuMzgsMC40N3Y0LjU3YzAsMC44MiwwLjY2LDEuNDcsMS40NywxLjQ3aDEuNDFjMC44MiwwLDEuNDctMC42NiwxLjQ3LTEuNDcgICAgbC0wLjAxLTQuNTdjMC4wNi0wLjMyLDAuMjUtMC40NywwLjM1LTAuNDdjMC4xOCwwLDAuMzgsMC4xOCwwLjM4LDAuNDd2NC41N2MwLDAuODIsMC42NiwxLjQ3LDEuNDcsMS40N2gxLjQxICAgIGMwLjgyLDAsMS40Ny0wLjY2LDEuNDctMS40N3YtMC4zOGMwLjk2LDEuMjksMi40NiwyLjA2LDQuMDYsMi4wNmMwLjc0LDAsMS40Ni0wLjE2LDIuMTItMC40N2MwLjI0LDAuMTcsMC41NCwwLjI2LDAuODQsMC4yNmgxLjM5ICAgIGMwLjMsMCwwLjYtMC4wOSwwLjg0LTAuMjZ2Mi4wMWMwLDAuODIsMC42NiwxLjQ3LDEuNDcsMS40N2gxLjRjMC44MiwwLDEuNDctMC42NiwxLjQ3LTEuNDd2LTEuNzdjMC40OCwwLjE1LDAuOTksMC4yMywxLjQ5LDAuMjIgICAgYzEuNywwLDMuMjItMC44Nyw0LjE3LTIuMnYwLjUyYzAsMC44MiwwLjY2LDEuNDcsMS40NywxLjQ3aDEuNGMwLjMsMCwwLjYtMC4wOSwwLjg0LTAuMjZjMC42NiwwLjMxLDEuMzksMC40NywyLjEyLDAuNDcgICAgYzEuOTIsMCwzLjYtMS4xLDQuNDktMi43M2MxLjU0LDIuNjUsNC45NSwzLjUzLDcuNTgsMS45OGMwLjE4LTAuMTEsMC4zNi0wLjIyLDAuNTMtMC4zNmMwLjIyLDAuNTUsMC43NiwwLjkxLDEuMzUsMC45SDc4ICAgIGMwLjU2LDAsMS4wOC0wLjI5LDEuMzctMC43OGwwLjM3LTAuNjFsMC4zNywwLjYxYzAuMjksMC40OCwwLjgxLDAuNzgsMS4zOCwwLjc4aDEuNmMwLjgxLDAsMS40Ni0wLjY2LDEuNDUtMS40NiAgICBDODQuNDksMTQuMDIsODQuNDQsMTMuOCw4NC4zNCwxMy41OUw4NC4zNCwxMy41OXogTTM1Ljg2LDE0LjQ3aC0xLjQxYy0wLjEzLDAtMC4yMy0wLjExLTAuMjMtMC4yM1Y5LjY4ICAgIGMwLTAuOTgtMC43NC0xLjcxLTEuNjItMS43MWMtMC44LDAtMS40NiwwLjctMS41OSwxLjYybDAuMDEsNC42NmMwLDAuMTMtMC4xLDAuMjMtMC4yMywwLjIzaC0xLjQxYy0wLjEzLDAtMC4yMy0wLjExLTAuMjMtMC4yMyAgICBWOS42OGMwLTAuOTgtMC43NC0xLjcxLTEuNjItMS43MWMtMC44NSwwLTEuNTQsMC43OS0xLjYsMS44djQuNDhjMCwwLjEzLTAuMSwwLjIzLTAuMjMsMC4yM2gtMS40Yy0wLjEzLDAtMC4yMy0wLjExLTAuMjMtMC4yMyAgICBWNi43NGMwLjAxLTAuMTMsMC4xMS0wLjIyLDAuMjMtMC4yMmgxLjRjMC4xMywwLDAuMjIsMC4xMSwwLjIzLDAuMjJWNy40YzAuNS0wLjY4LDEuMy0xLjA5LDIuMTYtMS4xaDAuMDMgICAgYzEuMDksMCwyLjA5LDAuNiwyLjYsMS41NWMwLjQ1LTAuOTUsMS40LTEuNTUsMi40NC0xLjU2YzEuNjIsMCwyLjkzLDEuMjUsMi45LDIuNzhsMC4wMSw1LjE2QzM2LjA5LDE0LjM2LDM1Ljk4LDE0LjQ2LDM1Ljg2LDE0LjQ3ICAgIEwzNS44NiwxNC40N3ogTTQ1Ljk3LDE0LjI0YzAsMC4xMy0wLjEsMC4yMy0wLjIzLDAuMjNoLTEuNGMtMC4xMywwLTAuMjMtMC4xMS0wLjIzLTAuMjNWMTMuNWMtMC43LDAuNzYtMS42OSwxLjE4LTIuNzIsMS4xOCAgICBjLTIuMTcsMC0zLjk0LTEuODctMy45NC00LjE5czEuNzctNC4xOSwzLjk0LTQuMTljMS4wMywwLDIuMDIsMC40MywyLjczLDEuMThWNi43NGMwLTAuMTMsMC4xLTAuMjMsMC4yMy0wLjIzaDEuNCAgICBjMC4xMi0wLjAxLDAuMjIsMC4wOCwwLjIzLDAuMjFjMCwwLjAxLDAsMC4wMSwwLDAuMDJ2Ny41MWgtMC4wMVYxNC4yNHogTTUyLjQxLDE0LjY3Yy0xLjAzLDAtMi4wMi0wLjQzLTIuNzMtMS4xOHYzLjk3ICAgIGMwLDAuMTMtMC4xLDAuMjMtMC4yMywwLjIzaC0xLjRjLTAuMTMsMC0wLjIzLTAuMS0wLjIzLTAuMjNWNi43NWMwLTAuMTMsMC4xLTAuMjIsMC4yMy0wLjIyaDEuNGMwLjEzLDAsMC4yMywwLjExLDAuMjMsMC4yM3YwLjczICAgIGMwLjcxLTAuNzYsMS43LTEuMTgsMi43My0xLjE4YzIuMTcsMCwzLjk0LDEuODYsMy45NCw0LjE4UzU0LjU4LDE0LjY3LDUyLjQxLDE0LjY3eiBNNjYuMjQsMTEuMzljLTAuMzksMS44Ny0xLjk2LDMuMjktMy44NCwzLjI5ICAgIGMtMS4wMywwLTIuMDItMC40My0yLjczLTEuMTh2MC43M2MwLDAuMTMtMC4xLDAuMjMtMC4yMywwLjIzaC0xLjRjLTAuMTMsMC0wLjIzLTAuMTEtMC4yMy0wLjIzVjMuNTFjMC0wLjEzLDAuMS0wLjIzLDAuMjMtMC4yMyAgICBoMS40YzAuMTMsMCwwLjIzLDAuMTEsMC4yMywwLjIzdjMuOTdjMC43MS0wLjc1LDEuNy0xLjE4LDIuNzMtMS4xN2MxLjg4LDAsMy40NSwxLjQsMy44NCwzLjI4QzY2LjM3LDEwLjE5LDY2LjM3LDEwLjgsNjYuMjQsMTEuMzkgICAgTDY2LjI0LDExLjM5TDY2LjI0LDExLjM5eiBNNzEuNjcsMTQuNjhjLTIsMC4wMS0zLjczLTEuMzUtNC4xNy0zLjNjLTAuMTMtMC41OS0wLjEzLTEuMTksMC0xLjc3YzAuNDQtMS45NCwyLjE3LTMuMzEsNC4xNy0zLjMgICAgYzIuMzYsMCw0LjI2LDEuODcsNC4yNiw0LjE5Uzc0LjAzLDE0LjY4LDcxLjY3LDE0LjY4TDcxLjY3LDE0LjY4eiBNODMuMDQsMTQuNDdoLTEuNjFjLTAuMTMsMC0wLjI0LTAuMDYtMC4zLTAuMTdsLTEuNDQtMi4zOSAgICBsLTEuNDQsMi4zOWMtMC4wNiwwLjExLTAuMTgsMC4xNy0wLjMsMC4xN2gtMS42MWMtMC4wNCwwLTAuMDgtMC4wMS0wLjEyLTAuMDNjLTAuMDktMC4wNi0wLjEzLTAuMTktMC4wNi0wLjI4bDAsMGwyLjQzLTMuNjggICAgTDc2LjIsNi44NGMtMC4wMi0wLjAzLTAuMDMtMC4wNy0wLjAzLTAuMTJjMC0wLjEyLDAuMDktMC4yMSwwLjIxLTAuMjFoMS42MWMwLjEzLDAsMC4yNCwwLjA2LDAuMywwLjE3bDEuNDEsMi4zNmwxLjQxLTIuMzYgICAgYzAuMDYtMC4xMSwwLjE4LTAuMTcsMC4zLTAuMTdoMS42MWMwLjA0LDAsMC4wOCwwLjAxLDAuMTIsMC4wM2MwLjA5LDAuMDYsMC4xMywwLjE5LDAuMDYsMC4yOGwwLDBsLTIuMzgsMy42NGwyLjQzLDMuNjcgICAgYzAuMDIsMC4wMywwLjAzLDAuMDcsMC4wMywwLjEyQzgzLjI1LDE0LjM4LDgzLjE2LDE0LjQ3LDgzLjA0LDE0LjQ3TDgzLjA0LDE0LjQ3TDgzLjA0LDE0LjQ3eiIvPiAgPHBhdGggY2xhc3M9InN0MCIgZD0iTTEwLjUsMS4yNGMtNS4xMSwwLTkuMjUsNC4xNS05LjI1LDkuMjVzNC4xNSw5LjI1LDkuMjUsOS4yNXM5LjI1LTQuMTUsOS4yNS05LjI1ICAgIEMxOS43NSw1LjM4LDE1LjYxLDEuMjQsMTAuNSwxLjI0eiBNMTQuODksMTIuNzdjLTEuOTMsMS45My00Ljc4LDIuMzEtNi43LDIuMzFjLTAuNywwLTEuNDEtMC4wNS0yLjEtMC4xNmMwLDAtMS4wMi01LjY0LDIuMTQtOC44MSAgICBjMC44My0wLjgzLDEuOTUtMS4yOCwzLjEzLTEuMjhjMS4yNywwLDIuNDksMC41MSwzLjM5LDEuNDJDMTYuNTksOC4wOSwxNi42NCwxMSwxNC44OSwxMi43N3oiLz4gIDxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xMC41LTAuMDFDNC43LTAuMDEsMCw0LjcsMCwxMC40OXM0LjcsMTAuNSwxMC41LDEwLjVTMjEsMTYuMjksMjEsMTAuNDlDMjAuOTksNC43LDE2LjMtMC4wMSwxMC41LTAuMDF6ICAgICBNMTAuNSwxOS43NGMtNS4xMSwwLTkuMjUtNC4xNS05LjI1LTkuMjVzNC4xNC05LjI2LDkuMjUtOS4yNnM5LjI1LDQuMTUsOS4yNSw5LjI1QzE5Ljc1LDE1LjYxLDE1LjYxLDE5Ljc0LDEwLjUsMTkuNzR6Ii8+ICA8cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTQuNzQsNi4yNUMxMi45LDQuNDEsOS45OCw0LjM1LDguMjMsNi4xYy0zLjE2LDMuMTctMi4xNCw4LjgxLTIuMTQsOC44MXM1LjY0LDEuMDIsOC44MS0yLjE0ICAgIEMxNi42NCwxMSwxNi41OSw4LjA5LDE0Ljc0LDYuMjV6IE0xMi40NywxMC4zNGwtMC45MSwxLjg3bC0wLjktMS44N0w4LjgsOS40M2wxLjg2LTAuOWwwLjktMS44N2wwLjkxLDEuODdsMS44NiwwLjlMMTIuNDcsMTAuMzR6IiAgICAvPiAgPHBvbHlnb24gY2xhc3M9InN0MCIgcG9pbnRzPSIxNC4zMyw5LjQzIDEyLjQ3LDEwLjM0IDExLjU2LDEyLjIxIDEwLjY2LDEwLjM0IDguOCw5LjQzIDEwLjY2LDguNTMgMTEuNTYsNi42NiAxMi40Nyw4LjUzICAgIi8+PC9nPjwvc3ZnPg==); + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiAgIHZpZXdCb3g9IjAgMCA4NC40OSAyMSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgODQuNDkgMjE7IiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz4gIDxwYXRoIGNsYXNzPSJzdDAiIHN0eWxlPSJvcGFjaXR5OjAuOTsgZmlsbDogI0ZGRkZGRjsgZW5hYmxlLWJhY2tncm91bmQ6IG5ldzsiIGQ9Ik04My4yNSwxNC4yNmMwLDAuMTItMC4wOSwwLjIxLTAuMjEsMC4yMWgtMS42MWMtMC4xMywwLTAuMjQtMC4wNi0wLjMtMC4xN2wtMS40NC0yLjM5bC0xLjQ0LDIuMzkgICAgYy0wLjA2LDAuMTEtMC4xOCwwLjE3LTAuMywwLjE3aC0xLjYxYy0wLjA0LDAtMC4wOC0wLjAxLTAuMTItMC4wM2MtMC4wOS0wLjA2LTAuMTMtMC4xOS0wLjA2LTAuMjhsMCwwbDIuNDMtMy42OEw3Ni4yLDYuODQgICAgYy0wLjAyLTAuMDMtMC4wMy0wLjA3LTAuMDMtMC4xMmMwLTAuMTIsMC4wOS0wLjIxLDAuMjEtMC4yMWgxLjYxYzAuMTMsMCwwLjI0LDAuMDYsMC4zLDAuMTdsMS40MSwyLjM2bDEuNC0yLjM1ICAgIGMwLjA2LTAuMTEsMC4xOC0wLjE3LDAuMy0wLjE3SDgzYzAuMDQsMCwwLjA4LDAuMDEsMC4xMiwwLjAzYzAuMDksMC4wNiwwLjEzLDAuMTksMC4wNiwwLjI4bDAsMGwtMi4zNywzLjYzbDIuNDMsMy42NyAgICBDODMuMjQsMTQuMTgsODMuMjUsMTQuMjIsODMuMjUsMTQuMjZ6Ii8+ICA8cGF0aCBjbGFzcz0ic3QwIiBzdHlsZT0ib3BhY2l0eTowLjk7IGZpbGw6ICNGRkZGRkY7IGVuYWJsZS1iYWNrZ3JvdW5kOiBuZXc7IiBkPSJNNjYuMjQsOS41OWMtMC4zOS0xLjg4LTEuOTYtMy4yOC0zLjg0LTMuMjhjLTEuMDMsMC0yLjAzLDAuNDItMi43MywxLjE4VjMuNTFjMC0wLjEzLTAuMS0wLjIzLTAuMjMtMC4yM2gtMS40ICAgIGMtMC4xMywwLTAuMjMsMC4xMS0wLjIzLDAuMjN2MTAuNzJjMCwwLjEzLDAuMSwwLjIzLDAuMjMsMC4yM2gxLjRjMC4xMywwLDAuMjMtMC4xMSwwLjIzLTAuMjNWMTMuNWMwLjcxLDAuNzUsMS43LDEuMTgsMi43MywxLjE4ICAgIGMxLjg4LDAsMy40NS0xLjQxLDMuODQtMy4yOUM2Ni4zNywxMC43OSw2Ni4zNywxMC4xOCw2Ni4yNCw5LjU5TDY2LjI0LDkuNTl6IE02Mi4wOCwxM2MtMS4zMiwwLTIuMzktMS4xMS0yLjQxLTIuNDh2LTAuMDYgICAgYzAuMDItMS4zOCwxLjA5LTIuNDgsMi40MS0yLjQ4czIuNDIsMS4xMiwyLjQyLDIuNTFTNjMuNDEsMTMsNjIuMDgsMTN6Ii8+ICA8cGF0aCBjbGFzcz0ic3QwIiBzdHlsZT0ib3BhY2l0eTowLjk7IGZpbGw6ICNGRkZGRkY7IGVuYWJsZS1iYWNrZ3JvdW5kOiBuZXc7IiBkPSJNNzEuNjcsNi4zMmMtMS45OC0wLjAxLTMuNzIsMS4zNS00LjE2LDMuMjljLTAuMTMsMC41OS0wLjEzLDEuMTksMCwxLjc3YzAuNDQsMS45NCwyLjE3LDMuMzIsNC4xNywzLjMgICAgYzIuMzUsMCw0LjI2LTEuODcsNC4yNi00LjE5Uzc0LjA0LDYuMzIsNzEuNjcsNi4zMnogTTcxLjY1LDEzLjAxYy0xLjMzLDAtMi40Mi0xLjEyLTIuNDItMi41MXMxLjA4LTIuNTIsMi40Mi0yLjUyICAgIGMxLjMzLDAsMi40MiwxLjEyLDIuNDIsMi41MVM3Mi45OSwxMyw3MS42NSwxMy4wMUw3MS42NSwxMy4wMXoiLz4gIDxwYXRoIGNsYXNzPSJzdDEiIHN0eWxlPSJvcGFjaXR5OjAuMzU7IGVuYWJsZS1iYWNrZ3JvdW5kOm5ldzsiIGQ9Ik02Mi4wOCw3Ljk4Yy0xLjMyLDAtMi4zOSwxLjExLTIuNDEsMi40OHYwLjA2QzU5LjY4LDExLjksNjAuNzUsMTMsNjIuMDgsMTNzMi40Mi0xLjEyLDIuNDItMi41MSAgICBTNjMuNDEsNy45OCw2Mi4wOCw3Ljk4eiBNNjIuMDgsMTEuNzZjLTAuNjMsMC0xLjE0LTAuNTYtMS4xNy0xLjI1di0wLjA0YzAuMDEtMC42OSwwLjU0LTEuMjUsMS4xNy0xLjI1ICAgIGMwLjYzLDAsMS4xNywwLjU3LDEuMTcsMS4yN0M2My4yNCwxMS4yLDYyLjczLDExLjc2LDYyLjA4LDExLjc2eiIvPiAgPHBhdGggY2xhc3M9InN0MSIgc3R5bGU9Im9wYWNpdHk6MC4zNTsgZW5hYmxlLWJhY2tncm91bmQ6bmV3OyIgZD0iTTcxLjY1LDcuOThjLTEuMzMsMC0yLjQyLDEuMTItMi40MiwyLjUxUzcwLjMyLDEzLDcxLjY1LDEzczIuNDItMS4xMiwyLjQyLTIuNTFTNzIuOTksNy45OCw3MS42NSw3Ljk4eiAgICAgTTcxLjY1LDExLjc2Yy0wLjY0LDAtMS4xNy0wLjU3LTEuMTctMS4yN2MwLTAuNywwLjUzLTEuMjYsMS4xNy0xLjI2czEuMTcsMC41NywxLjE3LDEuMjdDNzIuODIsMTEuMjEsNzIuMjksMTEuNzYsNzEuNjUsMTEuNzZ6IiAgICAvPiAgPHBhdGggY2xhc3M9InN0MCIgc3R5bGU9Im9wYWNpdHk6MC45OyBmaWxsOiAjRkZGRkZGOyBlbmFibGUtYmFja2dyb3VuZDogbmV3OyIgZD0iTTQ1Ljc0LDYuNTNoLTEuNGMtMC4xMywwLTAuMjMsMC4xMS0wLjIzLDAuMjN2MC43M2MtMC43MS0wLjc1LTEuNy0xLjE4LTIuNzMtMS4xOCAgICBjLTIuMTcsMC0zLjk0LDEuODctMy45NCw0LjE5czEuNzcsNC4xOSwzLjk0LDQuMTljMS4wNCwwLDIuMDMtMC40MywyLjczLTEuMTl2MC43M2MwLDAuMTMsMC4xLDAuMjMsMC4yMywwLjIzaDEuNCAgICBjMC4xMywwLDAuMjMtMC4xMSwwLjIzLTAuMjNWNi43NGMwLTAuMTItMC4wOS0wLjIyLTAuMjItMC4yMkM0NS43NSw2LjUzLDQ1Ljc1LDYuNTMsNDUuNzQsNi41M3ogTTQ0LjEyLDEwLjUzICAgIEM0NC4xMSwxMS45LDQzLjAzLDEzLDQxLjcxLDEzcy0yLjQyLTEuMTItMi40Mi0yLjUxczEuMDgtMi41MiwyLjQtMi41MmMxLjMzLDAsMi4zOSwxLjExLDIuNDEsMi40OEw0NC4xMiwxMC41M3oiLz4gIDxwYXRoIGNsYXNzPSJzdDEiIHN0eWxlPSJvcGFjaXR5OjAuMzU7IGVuYWJsZS1iYWNrZ3JvdW5kOm5ldzsiIGQ9Ik00MS43MSw3Ljk4Yy0xLjMzLDAtMi40MiwxLjEyLTIuNDIsMi41MVM0MC4zNywxMyw0MS43MSwxM3MyLjM5LTEuMTEsMi40MS0yLjQ4di0wLjA2ICAgIEM0NC4xLDkuMDksNDMuMDMsNy45OCw0MS43MSw3Ljk4eiBNNDAuNTUsMTAuNDljMC0wLjcsMC41Mi0xLjI3LDEuMTctMS4yN2MwLjY0LDAsMS4xNCwwLjU2LDEuMTcsMS4yNXYwLjA0ICAgIGMtMC4wMSwwLjY4LTAuNTMsMS4yNC0xLjE3LDEuMjRDNDEuMDgsMTEuNzUsNDAuNTUsMTEuMTksNDAuNTUsMTAuNDl6Ii8+ICA8cGF0aCBjbGFzcz0ic3QwIiBzdHlsZT0ib3BhY2l0eTowLjk7IGZpbGw6ICNGRkZGRkY7IGVuYWJsZS1iYWNrZ3JvdW5kOiBuZXc7IiBkPSJNNTIuNDEsNi4zMmMtMS4wMywwLTIuMDMsMC40Mi0yLjczLDEuMThWNi43NWMwLTAuMTMtMC4xLTAuMjMtMC4yMy0wLjIzaC0xLjRjLTAuMTMsMC0wLjIzLDAuMTEtMC4yMywwLjIzICAgIHYxMC43MmMwLDAuMTMsMC4xLDAuMjMsMC4yMywwLjIzaDEuNGMwLjEzLDAsMC4yMy0wLjEsMC4yMy0wLjIzVjEzLjVjMC43MSwwLjc1LDEuNywxLjE4LDIuNzQsMS4xOGMyLjE3LDAsMy45NC0xLjg3LDMuOTQtNC4xOSAgICBTNTQuNTgsNi4zMiw1Mi40MSw2LjMyeiBNNTIuMDgsMTMuMDFjLTEuMzIsMC0yLjM5LTEuMTEtMi40Mi0yLjQ4di0wLjA3YzAuMDItMS4zOCwxLjA5LTIuNDksMi40LTIuNDljMS4zMiwwLDIuNDEsMS4xMiwyLjQxLDIuNTEgICAgUzUzLjQsMTMsNTIuMDgsMTMuMDFMNTIuMDgsMTMuMDF6Ii8+ICA8cGF0aCBjbGFzcz0ic3QxIiBzdHlsZT0ib3BhY2l0eTowLjM1OyBlbmFibGUtYmFja2dyb3VuZDpuZXc7IiBkPSJNNTIuMDgsNy45OGMtMS4zMiwwLTIuMzksMS4xMS0yLjQyLDIuNDh2MC4wNmMwLjAzLDEuMzgsMS4xLDIuNDgsMi40MiwyLjQ4czIuNDEtMS4xMiwyLjQxLTIuNTEgICAgUzUzLjQsNy45OCw1Mi4wOCw3Ljk4eiBNNTIuMDgsMTEuNzZjLTAuNjMsMC0xLjE0LTAuNTYtMS4xNy0xLjI1di0wLjA0YzAuMDEtMC42OSwwLjU0LTEuMjUsMS4xNy0xLjI1YzAuNjMsMCwxLjE3LDAuNTgsMS4xNywxLjI3ICAgIFM1Mi43MiwxMS43Niw1Mi4wOCwxMS43NnoiLz4gIDxwYXRoIGNsYXNzPSJzdDAiIHN0eWxlPSJvcGFjaXR5OjAuOTsgZmlsbDogI0ZGRkZGRjsgZW5hYmxlLWJhY2tncm91bmQ6IG5ldzsiIGQ9Ik0zNi4wOCwxNC4yNGMwLDAuMTMtMC4xLDAuMjMtMC4yMywwLjIzaC0xLjQxYy0wLjEzLDAtMC4yMy0wLjExLTAuMjMtMC4yM1Y5LjY4YzAtMC45OC0wLjc0LTEuNzEtMS42Mi0xLjcxICAgIGMtMC44LDAtMS40NiwwLjctMS41OSwxLjYybDAuMDEsNC42NmMwLDAuMTMtMC4xMSwwLjIzLTAuMjMsMC4yM2gtMS40MWMtMC4xMywwLTAuMjMtMC4xMS0wLjIzLTAuMjNWOS42OCAgICBjMC0wLjk4LTAuNzQtMS43MS0xLjYyLTEuNzFjLTAuODUsMC0xLjU0LDAuNzktMS42LDEuOHY0LjQ4YzAsMC4xMy0wLjEsMC4yMy0wLjIzLDAuMjNoLTEuNGMtMC4xMywwLTAuMjMtMC4xMS0wLjIzLTAuMjNWNi43NCAgICBjMC4wMS0wLjEzLDAuMS0wLjIyLDAuMjMtMC4yMmgxLjRjMC4xMywwLDAuMjIsMC4xMSwwLjIzLDAuMjJWNy40YzAuNS0wLjY4LDEuMy0xLjA5LDIuMTYtMS4xaDAuMDNjMS4wOSwwLDIuMDksMC42LDIuNiwxLjU1ICAgIGMwLjQ1LTAuOTUsMS40LTEuNTUsMi40NC0xLjU2YzEuNjIsMCwyLjkzLDEuMjUsMi45LDIuNzhMMzYuMDgsMTQuMjR6Ii8+ICA8cGF0aCBjbGFzcz0ic3QxIiBzdHlsZT0ib3BhY2l0eTowLjM1OyBlbmFibGUtYmFja2dyb3VuZDpuZXc7IiBkPSJNODQuMzQsMTMuNTlsLTAuMDctMC4xM2wtMS45Ni0yLjk5bDEuOTQtMi45NWMwLjQ0LTAuNjcsMC4yNi0xLjU2LTAuNDEtMi4wMmMtMC4wMiwwLTAuMDMsMC0wLjA0LTAuMDEgICAgYy0wLjIzLTAuMTUtMC41LTAuMjItMC43OC0wLjIyaC0xLjYxYy0wLjU2LDAtMS4wOCwwLjI5LTEuMzcsMC43OEw3OS43Miw2LjZsLTAuMzQtMC41NkM3OS4wOSw1LjU2LDc4LjU3LDUuMjcsNzgsNS4yN2gtMS42ICAgIGMtMC42LDAtMS4xMywwLjM3LTEuMzUsMC45MmMtMi4xOS0xLjY2LTUuMjgtMS40Ny03LjI2LDAuNDVjLTAuMzUsMC4zNC0wLjY1LDAuNzItMC44OSwxLjE0Yy0wLjktMS42Mi0yLjU4LTIuNzItNC41LTIuNzIgICAgYy0wLjUsMC0xLjAxLDAuMDctMS40OCwwLjIzVjMuNTFjMC0wLjgyLTAuNjYtMS40OC0xLjQ3LTEuNDhoLTEuNGMtMC44MSwwLTEuNDcsMC42Ni0xLjQ3LDEuNDd2My43NSAgICBjLTAuOTUtMS4zNi0yLjUtMi4xOC00LjE3LTIuMTljLTAuNzQsMC0xLjQ2LDAuMTYtMi4xMiwwLjQ3Yy0wLjI0LTAuMTctMC41NC0wLjI2LTAuODQtMC4yNmgtMS40Yy0wLjQ1LDAtMC44NywwLjIxLTEuMTUsMC41NiAgICBjLTAuMDItMC4wMy0wLjA0LTAuMDUtMC4wNy0wLjA4Yy0wLjI4LTAuMy0wLjY4LTAuNDctMS4wOS0wLjQ3aC0xLjM5Yy0wLjMsMC0wLjYsMC4wOS0wLjg0LDAuMjZjLTAuNjctMC4zLTEuMzktMC40Ni0yLjEyLTAuNDYgICAgYy0xLjgzLDAtMy40MywxLTQuMzcsMi41Yy0wLjItMC40Ni0wLjQ4LTAuODktMC44My0xLjI1Yy0wLjgtMC44MS0xLjg5LTEuMjUtMy4wMi0xLjI1aC0wLjAxYy0wLjg5LDAuMDEtMS43NSwwLjMzLTIuNDYsMC44OCAgICBjLTAuNzQtMC41Ny0xLjY0LTAuODgtMi41Ny0wLjg4SDI4LjFjLTAuMjksMC0wLjU4LDAuMDMtMC44NiwwLjExYy0wLjI4LDAuMDYtMC41NiwwLjE2LTAuODIsMC4yOGMtMC4yMS0wLjEyLTAuNDUtMC4xOC0wLjctMC4xOCAgICBoLTEuNGMtMC44MiwwLTEuNDcsMC42Ni0xLjQ3LDEuNDd2Ny41YzAsMC44MiwwLjY2LDEuNDcsMS40NywxLjQ3aDEuNGMwLjgyLDAsMS40OC0wLjY2LDEuNDgtMS40OGwwLDBWOS43OSAgICBjMC4wMy0wLjM2LDAuMjMtMC41OSwwLjM2LTAuNTljMC4xOCwwLDAuMzgsMC4xOCwwLjM4LDAuNDd2NC41N2MwLDAuODIsMC42NiwxLjQ3LDEuNDcsMS40N2gxLjQxYzAuODIsMCwxLjQ3LTAuNjYsMS40Ny0xLjQ3ICAgIGwtMC4wMS00LjU3YzAuMDYtMC4zMiwwLjI1LTAuNDcsMC4zNS0wLjQ3YzAuMTgsMCwwLjM4LDAuMTgsMC4zOCwwLjQ3djQuNTdjMCwwLjgyLDAuNjYsMS40NywxLjQ3LDEuNDdoMS40MSAgICBjMC44MiwwLDEuNDctMC42NiwxLjQ3LTEuNDd2LTAuMzhjMC45NiwxLjI5LDIuNDYsMi4wNiw0LjA2LDIuMDZjMC43NCwwLDEuNDYtMC4xNiwyLjEyLTAuNDdjMC4yNCwwLjE3LDAuNTQsMC4yNiwwLjg0LDAuMjZoMS4zOSAgICBjMC4zLDAsMC42LTAuMDksMC44NC0wLjI2djIuMDFjMCwwLjgyLDAuNjYsMS40NywxLjQ3LDEuNDdoMS40YzAuODIsMCwxLjQ3LTAuNjYsMS40Ny0xLjQ3di0xLjc3YzAuNDgsMC4xNSwwLjk5LDAuMjMsMS40OSwwLjIyICAgIGMxLjcsMCwzLjIyLTAuODcsNC4xNy0yLjJ2MC41MmMwLDAuODIsMC42NiwxLjQ3LDEuNDcsMS40N2gxLjRjMC4zLDAsMC42LTAuMDksMC44NC0wLjI2YzAuNjYsMC4zMSwxLjM5LDAuNDcsMi4xMiwwLjQ3ICAgIGMxLjkyLDAsMy42LTEuMSw0LjQ5LTIuNzNjMS41NCwyLjY1LDQuOTUsMy41Myw3LjU4LDEuOThjMC4xOC0wLjExLDAuMzYtMC4yMiwwLjUzLTAuMzZjMC4yMiwwLjU1LDAuNzYsMC45MSwxLjM1LDAuOUg3OCAgICBjMC41NiwwLDEuMDgtMC4yOSwxLjM3LTAuNzhsMC4zNy0wLjYxbDAuMzcsMC42MWMwLjI5LDAuNDgsMC44MSwwLjc4LDEuMzgsMC43OGgxLjZjMC44MSwwLDEuNDYtMC42NiwxLjQ1LTEuNDYgICAgQzg0LjQ5LDE0LjAyLDg0LjQ0LDEzLjgsODQuMzQsMTMuNTlMODQuMzQsMTMuNTl6IE0zNS44NiwxNC40N2gtMS40MWMtMC4xMywwLTAuMjMtMC4xMS0wLjIzLTAuMjNWOS42OCAgICBjMC0wLjk4LTAuNzQtMS43MS0xLjYyLTEuNzFjLTAuOCwwLTEuNDYsMC43LTEuNTksMS42MmwwLjAxLDQuNjZjMCwwLjEzLTAuMSwwLjIzLTAuMjMsMC4yM2gtMS40MWMtMC4xMywwLTAuMjMtMC4xMS0wLjIzLTAuMjMgICAgVjkuNjhjMC0wLjk4LTAuNzQtMS43MS0xLjYyLTEuNzFjLTAuODUsMC0xLjU0LDAuNzktMS42LDEuOHY0LjQ4YzAsMC4xMy0wLjEsMC4yMy0wLjIzLDAuMjNoLTEuNGMtMC4xMywwLTAuMjMtMC4xMS0wLjIzLTAuMjMgICAgVjYuNzRjMC4wMS0wLjEzLDAuMTEtMC4yMiwwLjIzLTAuMjJoMS40YzAuMTMsMCwwLjIyLDAuMTEsMC4yMywwLjIyVjcuNGMwLjUtMC42OCwxLjMtMS4wOSwyLjE2LTEuMWgwLjAzICAgIGMxLjA5LDAsMi4wOSwwLjYsMi42LDEuNTVjMC40NS0wLjk1LDEuNC0xLjU1LDIuNDQtMS41NmMxLjYyLDAsMi45MywxLjI1LDIuOSwyLjc4bDAuMDEsNS4xNkMzNi4wOSwxNC4zNiwzNS45OCwxNC40NiwzNS44NiwxNC40NyAgICBMMzUuODYsMTQuNDd6IE00NS45NywxNC4yNGMwLDAuMTMtMC4xLDAuMjMtMC4yMywwLjIzaC0xLjRjLTAuMTMsMC0wLjIzLTAuMTEtMC4yMy0wLjIzVjEzLjVjLTAuNywwLjc2LTEuNjksMS4xOC0yLjcyLDEuMTggICAgYy0yLjE3LDAtMy45NC0xLjg3LTMuOTQtNC4xOXMxLjc3LTQuMTksMy45NC00LjE5YzEuMDMsMCwyLjAyLDAuNDMsMi43MywxLjE4VjYuNzRjMC0wLjEzLDAuMS0wLjIzLDAuMjMtMC4yM2gxLjQgICAgYzAuMTItMC4wMSwwLjIyLDAuMDgsMC4yMywwLjIxYzAsMC4wMSwwLDAuMDEsMCwwLjAydjcuNTFoLTAuMDFWMTQuMjR6IE01Mi40MSwxNC42N2MtMS4wMywwLTIuMDItMC40My0yLjczLTEuMTh2My45NyAgICBjMCwwLjEzLTAuMSwwLjIzLTAuMjMsMC4yM2gtMS40Yy0wLjEzLDAtMC4yMy0wLjEtMC4yMy0wLjIzVjYuNzVjMC0wLjEzLDAuMS0wLjIyLDAuMjMtMC4yMmgxLjRjMC4xMywwLDAuMjMsMC4xMSwwLjIzLDAuMjN2MC43MyAgICBjMC43MS0wLjc2LDEuNy0xLjE4LDIuNzMtMS4xOGMyLjE3LDAsMy45NCwxLjg2LDMuOTQsNC4xOFM1NC41OCwxNC42Nyw1Mi40MSwxNC42N3ogTTY2LjI0LDExLjM5Yy0wLjM5LDEuODctMS45NiwzLjI5LTMuODQsMy4yOSAgICBjLTEuMDMsMC0yLjAyLTAuNDMtMi43My0xLjE4djAuNzNjMCwwLjEzLTAuMSwwLjIzLTAuMjMsMC4yM2gtMS40Yy0wLjEzLDAtMC4yMy0wLjExLTAuMjMtMC4yM1YzLjUxYzAtMC4xMywwLjEtMC4yMywwLjIzLTAuMjMgICAgaDEuNGMwLjEzLDAsMC4yMywwLjExLDAuMjMsMC4yM3YzLjk3YzAuNzEtMC43NSwxLjctMS4xOCwyLjczLTEuMTdjMS44OCwwLDMuNDUsMS40LDMuODQsMy4yOEM2Ni4zNywxMC4xOSw2Ni4zNywxMC44LDY2LjI0LDExLjM5ICAgIEw2Ni4yNCwxMS4zOUw2Ni4yNCwxMS4zOXogTTcxLjY3LDE0LjY4Yy0yLDAuMDEtMy43My0xLjM1LTQuMTctMy4zYy0wLjEzLTAuNTktMC4xMy0xLjE5LDAtMS43N2MwLjQ0LTEuOTQsMi4xNy0zLjMxLDQuMTctMy4zICAgIGMyLjM2LDAsNC4yNiwxLjg3LDQuMjYsNC4xOVM3NC4wMywxNC42OCw3MS42NywxNC42OEw3MS42NywxNC42OHogTTgzLjA0LDE0LjQ3aC0xLjYxYy0wLjEzLDAtMC4yNC0wLjA2LTAuMy0wLjE3bC0xLjQ0LTIuMzkgICAgbC0xLjQ0LDIuMzljLTAuMDYsMC4xMS0wLjE4LDAuMTctMC4zLDAuMTdoLTEuNjFjLTAuMDQsMC0wLjA4LTAuMDEtMC4xMi0wLjAzYy0wLjA5LTAuMDYtMC4xMy0wLjE5LTAuMDYtMC4yOGwwLDBsMi40My0zLjY4ICAgIEw3Ni4yLDYuODRjLTAuMDItMC4wMy0wLjAzLTAuMDctMC4wMy0wLjEyYzAtMC4xMiwwLjA5LTAuMjEsMC4yMS0wLjIxaDEuNjFjMC4xMywwLDAuMjQsMC4wNiwwLjMsMC4xN2wxLjQxLDIuMzZsMS40MS0yLjM2ICAgIGMwLjA2LTAuMTEsMC4xOC0wLjE3LDAuMy0wLjE3aDEuNjFjMC4wNCwwLDAuMDgsMC4wMSwwLjEyLDAuMDNjMC4wOSwwLjA2LDAuMTMsMC4xOSwwLjA2LDAuMjhsMCwwbC0yLjM4LDMuNjRsMi40MywzLjY3ICAgIGMwLjAyLDAuMDMsMC4wMywwLjA3LDAuMDMsMC4xMkM4My4yNSwxNC4zOCw4My4xNiwxNC40Nyw4My4wNCwxNC40N0w4My4wNCwxNC40N0w4My4wNCwxNC40N3oiLz4gIDxwYXRoIGNsYXNzPSJzdDAiIHN0eWxlPSJvcGFjaXR5OjAuOTsgZmlsbDogI0ZGRkZGRjsgZW5hYmxlLWJhY2tncm91bmQ6IG5ldzsiIGQ9Ik0xMC41LDEuMjRjLTUuMTEsMC05LjI1LDQuMTUtOS4yNSw5LjI1czQuMTUsOS4yNSw5LjI1LDkuMjVzOS4yNS00LjE1LDkuMjUtOS4yNSAgICBDMTkuNzUsNS4zOCwxNS42MSwxLjI0LDEwLjUsMS4yNHogTTE0Ljg5LDEyLjc3Yy0xLjkzLDEuOTMtNC43OCwyLjMxLTYuNywyLjMxYy0wLjcsMC0xLjQxLTAuMDUtMi4xLTAuMTZjMCwwLTEuMDItNS42NCwyLjE0LTguODEgICAgYzAuODMtMC44MywxLjk1LTEuMjgsMy4xMy0xLjI4YzEuMjcsMCwyLjQ5LDAuNTEsMy4zOSwxLjQyQzE2LjU5LDguMDksMTYuNjQsMTEsMTQuODksMTIuNzd6Ii8+ICA8cGF0aCBjbGFzcz0ic3QxIiBzdHlsZT0ib3BhY2l0eTowLjM1OyBlbmFibGUtYmFja2dyb3VuZDpuZXc7IiBkPSJNMTAuNS0wLjAxQzQuNy0wLjAxLDAsNC43LDAsMTAuNDlzNC43LDEwLjUsMTAuNSwxMC41UzIxLDE2LjI5LDIxLDEwLjQ5QzIwLjk5LDQuNywxNi4zLTAuMDEsMTAuNS0wLjAxeiAgICAgTTEwLjUsMTkuNzRjLTUuMTEsMC05LjI1LTQuMTUtOS4yNS05LjI1czQuMTQtOS4yNiw5LjI1LTkuMjZzOS4yNSw0LjE1LDkuMjUsOS4yNUMxOS43NSwxNS42MSwxNS42MSwxOS43NCwxMC41LDE5Ljc0eiIvPiAgPHBhdGggY2xhc3M9InN0MSIgc3R5bGU9Im9wYWNpdHk6MC4zNTsgZW5hYmxlLWJhY2tncm91bmQ6bmV3OyIgZD0iTTE0Ljc0LDYuMjVDMTIuOSw0LjQxLDkuOTgsNC4zNSw4LjIzLDYuMWMtMy4xNiwzLjE3LTIuMTQsOC44MS0yLjE0LDguODFzNS42NCwxLjAyLDguODEtMi4xNCAgICBDMTYuNjQsMTEsMTYuNTksOC4wOSwxNC43NCw2LjI1eiBNMTIuNDcsMTAuMzRsLTAuOTEsMS44N2wtMC45LTEuODdMOC44LDkuNDNsMS44Ni0wLjlsMC45LTEuODdsMC45MSwxLjg3bDEuODYsMC45TDEyLjQ3LDEwLjM0eiIgICAgLz4gIDxwb2x5Z29uIGNsYXNzPSJzdDAiIHN0eWxlPSJvcGFjaXR5OjAuOTsgZmlsbDogI0ZGRkZGRjsgZW5hYmxlLWJhY2tncm91bmQ6IG5ldzsiIHBvaW50cz0iMTQuMzMsOS40MyAxMi40NywxMC4zNCAxMS41NiwxMi4yMSAxMC42NiwxMC4zNCA4LjgsOS40MyAxMC42Niw4LjUzIDExLjU2LDYuNjYgMTIuNDcsOC41MyAgICIvPjwvZz48L3N2Zz4=); } .mapboxgl-ctrl.mapboxgl-ctrl-attrib { @@ -161,11 +161,12 @@ a.mapboxgl-ctrl-logo { color: inherit; text-decoration: underline; } -.mapboxgl-ctrl-attrib .mapboxgl-improve-map { +/* stylelint-disable */ +.mapboxgl-ctrl-attrib .mapbox-improve-map { font-weight: bold; margin-left: 2px; } - +/*stylelint-enable*/ .mapboxgl-ctrl-scale { background-color: rgba(255,255,255,0.75); font-size: 10px; @@ -318,8 +319,11 @@ a.mapboxgl-ctrl-logo { border: 2px dotted #202020; opacity: 0.5; } + @media print { - .mapboxgl-improve-map { +/* stylelint-disable */ + .mapbox-improve-map { display:none; } +/* stylelint-enable */ } diff --git a/docs/_posts/examples/3400-01-31-animate-a-line.html b/docs/_posts/examples/3400-01-31-animate-a-line.html new file mode 100644 index 00000000000..0e908dfb4f5 --- /dev/null +++ b/docs/_posts/examples/3400-01-31-animate-a-line.html @@ -0,0 +1,125 @@ +--- +layout: example +category: example +title: Animate a line +description: Animate a line by updating a GeoJSON source on each frame. +tags: +- layers +- sources +--- + +
+ + diff --git a/package.json b/package.json index f5cdc4d0b67..bb8424c562d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "url": "git://github.com/mapbox/mapbox-gl-js.git" }, "engines": { - "node": ">=4.0.0" + "node": ">=6.4.0" }, "dependencies": { "@mapbox/gl-matrix": "^0.0.1", diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index f6ccdf8dd1b..9a15cba5f73 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -32,6 +32,7 @@ const elementArrayType = createElementArrayType(); const layoutAttributes = [ {name: 'a_pos_offset', components: 4, type: 'Int16'}, + {name: 'a_label_pos', components: 2, type: 'Int16'}, {name: 'a_data', components: 4, type: 'Uint16'} ]; @@ -60,15 +61,16 @@ const symbolInterfaces = { }, collisionBox: { // used to render collision boxes for debugging purposes layoutAttributes: [ - {name: 'a_pos', components: 2, type: 'Int16'}, - {name: 'a_extrude', components: 2, type: 'Int16'}, - {name: 'a_data', components: 2, type: 'Uint8'} + {name: 'a_pos', components: 2, type: 'Int16'}, + {name: 'a_anchor_pos', components: 2, type: 'Int16'}, + {name: 'a_extrude', components: 2, type: 'Int16'}, + {name: 'a_data', components: 2, type: 'Uint8'} ], elementArrayType: createElementArrayType(2) } }; -function addVertex(array, x, y, ox, oy, tx, ty, sizeVertex, minzoom, maxzoom, labelminzoom, labelangle) { +function addVertex(array, x, y, ox, oy, labelX, labelY, tx, ty, sizeVertex, minzoom, maxzoom, labelminzoom, labelangle) { array.emplaceBack( // a_pos_offset x, @@ -76,9 +78,13 @@ function addVertex(array, x, y, ox, oy, tx, ty, sizeVertex, minzoom, maxzoom, la Math.round(ox * 64), Math.round(oy * 64), + // a_label_pos + labelX, + labelY, + // a_data - tx / 4, // x coordinate of symbol on glyph atlas texture - ty / 4, // y coordinate of symbol on glyph atlas texture + tx, // x coordinate of symbol on glyph atlas texture + ty, // y coordinate of symbol on glyph atlas texture packUint8ToFloat( (labelminzoom || 0) * 10, // labelminzoom labelangle % 256 // labelangle @@ -95,11 +101,14 @@ function addVertex(array, x, y, ox, oy, tx, ty, sizeVertex, minzoom, maxzoom, la ); } -function addCollisionBoxVertex(layoutVertexArray, point, extrude, maxZoom, placementZoom) { +function addCollisionBoxVertex(layoutVertexArray, point, anchor, extrude, maxZoom, placementZoom) { return layoutVertexArray.emplaceBack( // pos point.x, point.y, + // a_anchor_pos + anchor.x, + anchor.y, // extrude Math.round(extrude.x), Math.round(extrude.y), @@ -360,10 +369,11 @@ class SymbolBucket { if (feature.text) { const allowsVerticalWritingMode = scriptDetection.allowsVerticalWritingMode(feature.text); const textOffset = this.layers[0].getLayoutValue('text-offset', {zoom: this.zoom}, feature.properties).map((t)=> t * oneEm); + const spacingIfAllowed = scriptDetection.allowsLetterSpacing(feature.text) ? spacing : 0; shapedTextOrientations = { - [WritingMode.horizontal]: shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, textOffset, oneEm, WritingMode.horizontal), - [WritingMode.vertical]: allowsVerticalWritingMode && textAlongLine && shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, textOffset, oneEm, WritingMode.vertical) + [WritingMode.horizontal]: shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacingIfAllowed, textOffset, oneEm, WritingMode.horizontal), + [WritingMode.vertical]: allowsVerticalWritingMode && textAlongLine && shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacingIfAllowed, textOffset, oneEm, WritingMode.vertical) }; } else { shapedTextOrientations = {}; @@ -372,16 +382,15 @@ class SymbolBucket { let shapedIcon; if (feature.icon) { const image = icons[feature.icon]; - const iconOffset = this.layers[0].getLayoutValue('icon-offset', {zoom: this.zoom}, feature.properties); - shapedIcon = shapeIcon(image, iconOffset); - if (image) { + shapedIcon = shapeIcon(image, + this.layers[0].getLayoutValue('icon-offset', {zoom: this.zoom}, feature.properties)); if (this.sdfIcons === undefined) { this.sdfIcons = image.sdf; } else if (this.sdfIcons !== image.sdf) { util.warnOnce('Style sheet warning: Cannot mix SDF and non-SDF icons in one buffer'); } - if (image.pixelRatio !== 1) { + if (!image.isNativePixelRatio) { this.iconsNeedLinear = true; } else if (layout['icon-rotate'] !== 0 || !this.layers[0].isLayoutValueFeatureConstant('icon-rotate')) { this.iconsNeedLinear = true; @@ -523,6 +532,10 @@ class SymbolBucket { const layer = this.layers[0]; const layout = layer.layout; + // Symbols that don't show until greater than the CollisionTile's maxScale won't even be added + // to the buffers. Even though pan operations on a tilted map might cause the symbol to be + // displayable, we have to stay conservative here because the CollisionTile didn't consider + // this scale range. const maxScale = collisionTile.maxScale; const textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line'; @@ -608,7 +621,8 @@ class SymbolBucket { textAlongLine, collisionTile.angle, symbolInstance.featureProperties, - symbolInstance.writingModes); + symbolInstance.writingModes, + symbolInstance.anchor); } } @@ -629,7 +643,9 @@ class SymbolBucket { layout['icon-keep-upright'], iconAlongLine, collisionTile.angle, - symbolInstance.featureProperties + symbolInstance.featureProperties, + null, + symbolInstance.anchor ); } } @@ -639,7 +655,7 @@ class SymbolBucket { if (showCollisionBoxes) this.addToDebugBuffers(collisionTile); } - addSymbols(arrays, quads, scale, sizeVertex, keepUpright, alongLine, placementAngle, featureProperties, writingModes) { + addSymbols(arrays, quads, scale, sizeVertex, keepUpright, alongLine, placementAngle, featureProperties, writingModes, labelAnchor) { const elementArray = arrays.elementArray; const layoutVertexArray = arrays.layoutVertexArray; @@ -676,10 +692,10 @@ class SymbolBucket { const segment = arrays.prepareSegment(4); const index = segment.vertexLength; - addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, tl.x, tl.y, tex.x, tex.y, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); - addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, tr.x, tr.y, tex.x + tex.w, tex.y, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); - addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, bl.x, bl.y, tex.x, tex.y + tex.h, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); - addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, br.x, br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); + addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, tl.x, tl.y, labelAnchor.x, labelAnchor.y, tex.x, tex.y, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); + addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, tr.x, tr.y, labelAnchor.x, labelAnchor.y, tex.x + tex.w, tex.y, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); + addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, bl.x, bl.y, labelAnchor.x, labelAnchor.y, tex.x, tex.y + tex.h, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); + addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, br.x, br.y, labelAnchor.x, labelAnchor.y, tex.x + tex.w, tex.y + tex.h, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); elementArray.emplaceBack(index, index + 1, index + 2); elementArray.emplaceBack(index + 1, index + 2, index + 3); @@ -709,7 +725,12 @@ class SymbolBucket { for (let b = feature.boxStartIndex; b < feature.boxEndIndex; b++) { const box = this.collisionBoxArray.get(b); - const anchorPoint = box.anchorPoint; + if (collisionTile.perspectiveRatio === 1 && box.maxScale < 1) { + // These boxes aren't used on unpitched maps + // See CollisionTile#insertCollisionFeature + continue; + } + const boxAnchorPoint = box.anchorPoint; const tl = new Point(box.x1, box.y1 * yStretch)._rotate(angle); const tr = new Point(box.x2, box.y1 * yStretch)._rotate(angle); @@ -722,10 +743,10 @@ class SymbolBucket { const segment = arrays.prepareSegment(4); const index = segment.vertexLength; - addCollisionBoxVertex(layoutVertexArray, anchorPoint, tl, maxZoom, placementZoom); - addCollisionBoxVertex(layoutVertexArray, anchorPoint, tr, maxZoom, placementZoom); - addCollisionBoxVertex(layoutVertexArray, anchorPoint, br, maxZoom, placementZoom); - addCollisionBoxVertex(layoutVertexArray, anchorPoint, bl, maxZoom, placementZoom); + addCollisionBoxVertex(layoutVertexArray, boxAnchorPoint, symbolInstance.anchor, tl, maxZoom, placementZoom); + addCollisionBoxVertex(layoutVertexArray, boxAnchorPoint, symbolInstance.anchor, tr, maxZoom, placementZoom); + addCollisionBoxVertex(layoutVertexArray, boxAnchorPoint, symbolInstance.anchor, br, maxZoom, placementZoom); + addCollisionBoxVertex(layoutVertexArray, boxAnchorPoint, symbolInstance.anchor, bl, maxZoom, placementZoom); elementArray.emplaceBack(index, index + 1); elementArray.emplaceBack(index + 1, index + 2); diff --git a/src/data/feature_index.js b/src/data/feature_index.js index 9a1589e671a..f89ad9b4e22 100644 --- a/src/data/feature_index.js +++ b/src/data/feature_index.js @@ -242,7 +242,7 @@ class FeatureIndex { if (layerResult === undefined) { layerResult = result[layerID] = []; } - layerResult.push(geojsonFeature); + layerResult.push({ featureIndex: index, feature: geojsonFeature }); } } } diff --git a/src/geo/transform.js b/src/geo/transform.js index c51fd979c3b..193224d96fa 100644 --- a/src/geo/transform.js +++ b/src/geo/transform.js @@ -394,6 +394,21 @@ class Transform { return new Float32Array(posMatrix); } + /** + * Calculate the distance from the center of a tile to the camera + * These distances are in view-space dimensions derived from the size of the + * viewport, similar to this.cameraToCenterDistance + * If the tile is dead-center in the viewport, then cameraToTileDistance == cameraToCenterDistance + * + * @param {Tile} tile + */ + cameraToTileDistance(tile: Object) { + const posMatrix = this.calculatePosMatrix(tile.coord, tile.sourceMaxZoom); + const tileCenter = [tile.tileSize / 2, tile.tileSize / 2, 0, 1]; + vec4.transformMat4(tileCenter, tileCenter, posMatrix); + return tileCenter[3]; + } + _constrain() { if (!this.center || !this.width || !this.height || this._constraining) return; diff --git a/src/render/draw_background.js b/src/render/draw_background.js index ad75fbfa241..d90a1a9f39a 100644 --- a/src/render/draw_background.js +++ b/src/render/draw_background.js @@ -21,6 +21,7 @@ function drawBackground(painter, sourceCache, layer) { let program; if (image) { + if (pattern.isPatternMissing(image, painter)) return; program = painter.useProgram('fillPattern', painter.basicFillProgramConfiguration); pattern.prepare(image, painter, program); painter.tileExtentPatternVAO.bind(gl, program, painter.tileExtentBuffer); diff --git a/src/render/draw_collision_debug.js b/src/render/draw_collision_debug.js index e7b1388eadc..6f29e0e060b 100644 --- a/src/render/draw_collision_debug.js +++ b/src/render/draw_collision_debug.js @@ -7,6 +7,10 @@ function drawCollisionDebug(painter, sourceCache, layer, coords) { gl.enable(gl.STENCIL_TEST); const program = painter.useProgram('collisionBox'); + gl.activeTexture(gl.TEXTURE1); + painter.frameHistory.bind(gl); + gl.uniform1i(program.u_fadetexture, 1); + for (let i = 0; i < coords.length; i++) { const coord = coords[i]; const tile = sourceCache.getTile(coord); @@ -22,7 +26,12 @@ function drawCollisionDebug(painter, sourceCache, layer, coords) { painter.lineWidth(1); gl.uniform1f(program.u_scale, Math.pow(2, painter.transform.zoom - tile.coord.z)); gl.uniform1f(program.u_zoom, painter.transform.zoom * 10); - gl.uniform1f(program.u_maxzoom, (tile.coord.z + 1) * 10); + const maxZoom = Math.max(0, Math.min(25, tile.coord.z + Math.log(tile.collisionTile.maxScale) / Math.LN2)); + gl.uniform1f(program.u_maxzoom, maxZoom * 10); + + gl.uniform1f(program.u_collision_y_stretch, tile.collisionTile.yStretch); + gl.uniform1f(program.u_pitch, painter.transform.pitch / 360 * 2 * Math.PI); + gl.uniform1f(program.u_camera_to_center_distance, painter.transform.cameraToCenterDistance); for (const segment of buffers.segments) { segment.vaos[layer.id].bind(gl, program, buffers.layoutVertexBuffer, buffers.elementBuffer, null, segment.vertexOffset); diff --git a/src/render/draw_fill.js b/src/render/draw_fill.js index a08454d12b7..b01b2f2039a 100644 --- a/src/render/draw_fill.js +++ b/src/render/draw_fill.js @@ -42,6 +42,8 @@ function drawFill(painter, sourceCache, layer, coords) { } function drawFillTiles(painter, sourceCache, layer, coords, drawFn) { + if (pattern.isPatternMissing(layer.paint['fill-pattern'], painter)) return; + let firstTile = true; for (const coord of coords) { const tile = sourceCache.getTile(coord); diff --git a/src/render/draw_fill_extrusion.js b/src/render/draw_fill_extrusion.js index a5a4c7c9161..e429d730fc0 100644 --- a/src/render/draw_fill_extrusion.js +++ b/src/render/draw_fill_extrusion.js @@ -151,6 +151,7 @@ function drawExtrusion(painter, source, layer, coord) { programConfiguration.setUniforms(gl, program, layer, {zoom: painter.transform.zoom}); if (image) { + if (pattern.isPatternMissing(image, painter)) return; pattern.prepare(image, painter, program); pattern.setTile(tile, painter, program); gl.uniform1f(program.u_height_factor, -Math.pow(2, coord.z) / tile.tileSize / 8); diff --git a/src/render/draw_line.js b/src/render/draw_line.js index c8599e7c3cc..5274ab04b0a 100644 --- a/src/render/draw_line.js +++ b/src/render/draw_line.js @@ -73,12 +73,13 @@ function drawLineTile(program, painter, tile, buffers, layer, coord, layerData, gl.uniform1f(program.u_sdfgamma, painter.lineAtlas.width / (Math.min(widthA, widthB) * 256 * browser.devicePixelRatio) / 2); } else if (image) { - imagePosA = painter.spriteAtlas.getPosition(image.from, true); - imagePosB = painter.spriteAtlas.getPosition(image.to, true); + imagePosA = painter.spriteAtlas.getPattern(image.from); + imagePosB = painter.spriteAtlas.getPattern(image.to); if (!imagePosA || !imagePosB) return; - gl.uniform2f(program.u_pattern_size_a, imagePosA.size[0] * image.fromScale / tileRatio, imagePosB.size[1]); - gl.uniform2f(program.u_pattern_size_b, imagePosB.size[0] * image.toScale / tileRatio, imagePosB.size[1]); + gl.uniform2f(program.u_pattern_size_a, imagePosA.displaySize[0] * image.fromScale / tileRatio, imagePosB.displaySize[1]); + gl.uniform2f(program.u_pattern_size_b, imagePosB.displaySize[0] * image.toScale / tileRatio, imagePosB.displaySize[1]); + gl.uniform2fv(program.u_texsize, painter.spriteAtlas.getPixelSize()); } gl.uniform2f(program.u_gl_units_to_pixels, 1 / painter.transform.pixelsToGLUnits[0], 1 / painter.transform.pixelsToGLUnits[1]); diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index 0b56185c127..7fd8f95abde 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -2,7 +2,6 @@ const assert = require('assert'); const util = require('../util/util'); -const browser = require('../util/browser'); const drawCollisionDebug = require('./draw_collision_debug'); const pixelsToTileUnits = require('../source/pixels_to_tile_units'); const interpolationFactor = require('../style-spec/function').interpolationFactor; @@ -101,6 +100,8 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate gl.uniformMatrix4fv(program.u_matrix, false, painter.translatePosMatrix(coord.posMatrix, tile, translate, translateAnchor)); + gl.uniform1f(program.u_collision_y_stretch, tile.collisionTile.yStretch); + drawTileSymbols(program, programConfiguration, painter, layer, tile, buffers, isText, isSDF, pitchWithMap); @@ -130,16 +131,16 @@ function setSymbolDrawState(program, painter, layer, tileZoom, isText, isSDF, ro if (!glyphAtlas) return; glyphAtlas.updateTexture(gl); - gl.uniform2f(program.u_texsize, glyphAtlas.width / 4, glyphAtlas.height / 4); + gl.uniform2f(program.u_texsize, glyphAtlas.width, glyphAtlas.height); } else { const mapMoving = painter.options.rotating || painter.options.zooming; const iconSizeScaled = !layer.isLayoutValueFeatureConstant('icon-size') || !layer.isLayoutValueZoomConstant('icon-size') || layer.getLayoutValue('icon-size', { zoom: tr.zoom }) !== 1; - const iconScaled = iconSizeScaled || browser.devicePixelRatio !== painter.spriteAtlas.pixelRatio || iconsNeedLinear; + const iconScaled = iconSizeScaled || iconsNeedLinear; const iconTransformed = pitchWithMap || tr.pitch; painter.spriteAtlas.bind(gl, isSDF || mapMoving || iconScaled || iconTransformed); - gl.uniform2f(program.u_texsize, painter.spriteAtlas.width / 4, painter.spriteAtlas.height / 4); + gl.uniform2fv(program.u_texsize, painter.spriteAtlas.getPixelSize()); } gl.activeTexture(gl.TEXTURE1); @@ -192,6 +193,22 @@ function setSymbolDrawState(program, painter, layer, tileZoom, isText, isSDF, ro } else if (sizeData.isFeatureConstant && sizeData.isZoomConstant) { gl.uniform1f(program.u_size, sizeData.layoutSize); } + gl.uniform1f(program.u_camera_to_center_distance, tr.cameraToCenterDistance); + if (layer.layout['symbol-placement'] === 'line' && + layer.layout['text-rotation-alignment'] === 'map' && + layer.layout['text-pitch-alignment'] === 'viewport' && + layer.layout['text-field']) { + // We hide line labels with viewport alignment as they move into the distance + // because the approximations we use for drawing their glyphs get progressively worse + // The "1.5" here means we start hiding them when the distance from the label + // to the camera is 50% greater than the distance from the center of the map + // to the camera. Depending on viewport properties, you might expect this to filter + // the top third of the screen at pitch 60, and do almost nothing at pitch 45 + gl.uniform1f(program.u_max_camera_distance, 1.5); + } else { + // "10" is effectively infinite at any pitch we support + gl.uniform1f(program.u_max_camera_distance, 10); + } } function drawTileSymbols(program, programConfiguration, painter, layer, tile, buffers, isText, isSDF, pitchWithMap) { diff --git a/src/render/painter.js b/src/render/painter.js index 04c7ade994b..26d12870aaf 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -393,6 +393,14 @@ class Painter { assert(gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS), gl.getShaderInfoLog(vertexShader)); gl.attachShader(program, vertexShader); + + // For the symbol program, manually ensure the attrib bound to position 0 is always used (either a_data or a_pos_offset would work here). + // This is needed to fix https://github.com/mapbox/mapbox-gl-js/issues/4607 — otherwise a_size can be bound first, causing rendering to fail. + // All remaining attribs will be bound dynamically below. + if (name === 'symbolSDF') { + gl.bindAttribLocation(program, 0, 'a_data'); + } + gl.linkProgram(program); assert(gl.getProgramParameter(program, gl.LINK_STATUS), gl.getProgramInfoLog(program)); diff --git a/src/render/pattern.js b/src/render/pattern.js index cfe3539caaf..851d4834415 100644 --- a/src/render/pattern.js +++ b/src/render/pattern.js @@ -1,22 +1,37 @@ 'use strict'; +const assert = require('assert'); + const pixelsToTileUnits = require('../source/pixels_to_tile_units'); +/** + * Checks whether a pattern image is needed, and if it is, whether it is not loaded. + * + * @returns {boolean} true if a needed image is missing and rendering needs to be skipped. + */ +exports.isPatternMissing = function(image, painter) { + if (!image) return false; + const imagePosA = painter.spriteAtlas.getPattern(image.from); + const imagePosB = painter.spriteAtlas.getPattern(image.to); + return !imagePosA || !imagePosB; +}; + exports.prepare = function (image, painter, program) { const gl = painter.gl; - const imagePosA = painter.spriteAtlas.getPosition(image.from, true); - const imagePosB = painter.spriteAtlas.getPosition(image.to, true); - if (!imagePosA || !imagePosB) return; + const imagePosA = painter.spriteAtlas.getPattern(image.from); + const imagePosB = painter.spriteAtlas.getPattern(image.to); + assert(imagePosA && imagePosB); gl.uniform1i(program.u_image, 0); gl.uniform2fv(program.u_pattern_tl_a, imagePosA.tl); gl.uniform2fv(program.u_pattern_br_a, imagePosA.br); gl.uniform2fv(program.u_pattern_tl_b, imagePosB.tl); gl.uniform2fv(program.u_pattern_br_b, imagePosB.br); + gl.uniform2fv(program.u_texsize, painter.spriteAtlas.getPixelSize()); gl.uniform1f(program.u_mix, image.t); - gl.uniform2fv(program.u_pattern_size_a, imagePosA.size); - gl.uniform2fv(program.u_pattern_size_b, imagePosB.size); + gl.uniform2fv(program.u_pattern_size_a, imagePosA.displaySize); + gl.uniform2fv(program.u_pattern_size_b, imagePosB.displaySize); gl.uniform1f(program.u_scale_a, image.fromScale); gl.uniform1f(program.u_scale_b, image.toScale); diff --git a/src/render/shaders.js b/src/render/shaders.js index b4305c853b6..b3e735f608c 100644 --- a/src/render/shaders.js +++ b/src/render/shaders.js @@ -1,76 +1,77 @@ 'use strict'; const fs = require('fs'); -const path = require('path'); // readFileSync calls must be written out long-form for brfs. +/* eslint-disable prefer-template, no-path-concat */ + module.exports = { prelude: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/_prelude.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/_prelude.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/_prelude.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/_prelude.vertex.glsl', 'utf8') }, circle: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/circle.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/circle.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/circle.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/circle.vertex.glsl', 'utf8') }, collisionBox: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/collision_box.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/collision_box.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/collision_box.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/collision_box.vertex.glsl', 'utf8') }, debug: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/debug.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/debug.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/debug.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/debug.vertex.glsl', 'utf8') }, fill: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/fill.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/fill.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/fill.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/fill.vertex.glsl', 'utf8') }, fillOutline: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/fill_outline.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/fill_outline.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/fill_outline.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/fill_outline.vertex.glsl', 'utf8') }, fillOutlinePattern: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/fill_outline_pattern.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/fill_outline_pattern.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/fill_outline_pattern.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/fill_outline_pattern.vertex.glsl', 'utf8') }, fillPattern: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/fill_pattern.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/fill_pattern.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/fill_pattern.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/fill_pattern.vertex.glsl', 'utf8') }, fillExtrusion: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/fill_extrusion.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/fill_extrusion.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/fill_extrusion.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/fill_extrusion.vertex.glsl', 'utf8') }, fillExtrusionPattern: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/fill_extrusion_pattern.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/fill_extrusion_pattern.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/fill_extrusion_pattern.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/fill_extrusion_pattern.vertex.glsl', 'utf8') }, extrusionTexture: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/extrusion_texture.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/extrusion_texture.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/extrusion_texture.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/extrusion_texture.vertex.glsl', 'utf8') }, line: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/line.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/line.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/line.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/line.vertex.glsl', 'utf8') }, linePattern: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/line_pattern.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/line_pattern.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/line_pattern.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/line_pattern.vertex.glsl', 'utf8') }, lineSDF: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/line_sdf.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/line_sdf.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/line_sdf.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/line_sdf.vertex.glsl', 'utf8') }, raster: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/raster.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/raster.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/raster.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/raster.vertex.glsl', 'utf8') }, symbolIcon: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/symbol_icon.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/symbol_icon.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/symbol_icon.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/symbol_icon.vertex.glsl', 'utf8') }, symbolSDF: { - fragmentSource: fs.readFileSync(path.join(__dirname, '../shaders/symbol_sdf.fragment.glsl'), 'utf8'), - vertexSource: fs.readFileSync(path.join(__dirname, '../shaders/symbol_sdf.vertex.glsl'), 'utf8') + fragmentSource: fs.readFileSync(__dirname + '/../shaders/symbol_sdf.fragment.glsl', 'utf8'), + vertexSource: fs.readFileSync(__dirname + '/../shaders/symbol_sdf.vertex.glsl', 'utf8') } }; diff --git a/src/shaders/collision_box.fragment.glsl b/src/shaders/collision_box.fragment.glsl index 030f1219f8e..aa0e5df0d5f 100644 --- a/src/shaders/collision_box.fragment.glsl +++ b/src/shaders/collision_box.fragment.glsl @@ -1,23 +1,37 @@ uniform float u_zoom; +// u_maxzoom is derived from the maximum scale considered by the CollisionTile +// Labels with placement zoom greater than this value will not be placed, +// regardless of perspective effects. uniform float u_maxzoom; +uniform sampler2D u_fadetexture; +// v_max_zoom is a collision-box-specific value that controls when line-following +// collision boxes are used. varying float v_max_zoom; varying float v_placement_zoom; +varying float v_perspective_zoom_adjust; +varying vec2 v_fade_tex; void main() { float alpha = 0.5; + // Green = no collisions, label is showing gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0) * alpha; - if (v_placement_zoom > u_zoom) { + // Red = collision, label hidden + if (texture2D(u_fadetexture, v_fade_tex).a < 1.0) { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0) * alpha; } - if (u_zoom >= v_max_zoom) { + // Faded black = this collision box is not used at this zoom (for curved labels) + if (u_zoom >= v_max_zoom + v_perspective_zoom_adjust) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0) * alpha * 0.25; } + // Faded blue = the placement scale for this label is beyond the CollisionTile + // max scale, so it's impossible for this label to show without collision detection + // being run again (the label's glyphs haven't even been added to the symbol bucket) if (v_placement_zoom >= u_maxzoom) { gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0) * alpha * 0.2; } diff --git a/src/shaders/collision_box.vertex.glsl b/src/shaders/collision_box.vertex.glsl index d141b13873a..3ae55780341 100644 --- a/src/shaders/collision_box.vertex.glsl +++ b/src/shaders/collision_box.vertex.glsl @@ -1,16 +1,32 @@ attribute vec2 a_pos; +attribute vec2 a_anchor_pos; attribute vec2 a_extrude; attribute vec2 a_data; uniform mat4 u_matrix; uniform float u_scale; +uniform float u_pitch; +uniform float u_collision_y_stretch; +uniform float u_camera_to_center_distance; varying float v_max_zoom; varying float v_placement_zoom; +varying float v_perspective_zoom_adjust; +varying vec2 v_fade_tex; void main() { - gl_Position = u_matrix * vec4(a_pos + a_extrude / u_scale, 0.0, 1.0); + vec4 projectedPoint = u_matrix * vec4(a_anchor_pos, 0, 1); + highp float camera_to_anchor_distance = projectedPoint.w; + highp float collision_perspective_ratio = 1.0 + 0.5 * ((camera_to_anchor_distance / u_camera_to_center_distance) - 1.0); + + highp float incidence_stretch = camera_to_anchor_distance / (u_camera_to_center_distance * cos(u_pitch)); + highp float collision_adjustment = max(1.0, incidence_stretch / u_collision_y_stretch); + + gl_Position = u_matrix * vec4(a_pos + a_extrude * collision_perspective_ratio * collision_adjustment / u_scale, 0.0, 1.0); v_max_zoom = a_data.x; v_placement_zoom = a_data.y; + + v_perspective_zoom_adjust = floor(log2(collision_perspective_ratio * collision_adjustment) * 10.0); + v_fade_tex = vec2((v_placement_zoom + v_perspective_zoom_adjust) / 255.0, 0.0); } diff --git a/src/shaders/fill_extrusion_pattern.fragment.glsl b/src/shaders/fill_extrusion_pattern.fragment.glsl index d216f3b8a59..9aa068a6c91 100644 --- a/src/shaders/fill_extrusion_pattern.fragment.glsl +++ b/src/shaders/fill_extrusion_pattern.fragment.glsl @@ -2,6 +2,7 @@ uniform vec2 u_pattern_tl_a; uniform vec2 u_pattern_br_a; uniform vec2 u_pattern_tl_b; uniform vec2 u_pattern_br_b; +uniform vec2 u_texsize; uniform float u_mix; uniform sampler2D u_image; @@ -18,11 +19,11 @@ void main() { #pragma mapbox: initialize lowp float height vec2 imagecoord = mod(v_pos_a, 1.0); - vec2 pos = mix(u_pattern_tl_a, u_pattern_br_a, imagecoord); + vec2 pos = mix(u_pattern_tl_a / u_texsize, u_pattern_br_a / u_texsize, imagecoord); vec4 color1 = texture2D(u_image, pos); vec2 imagecoord_b = mod(v_pos_b, 1.0); - vec2 pos2 = mix(u_pattern_tl_b, u_pattern_br_b, imagecoord_b); + vec2 pos2 = mix(u_pattern_tl_b / u_texsize, u_pattern_br_b / u_texsize, imagecoord_b); vec4 color2 = texture2D(u_image, pos2); vec4 mixedColor = mix(color1, color2, u_mix); diff --git a/src/shaders/fill_outline_pattern.fragment.glsl b/src/shaders/fill_outline_pattern.fragment.glsl index 2c95c4c2e80..8346d5a9ccd 100644 --- a/src/shaders/fill_outline_pattern.fragment.glsl +++ b/src/shaders/fill_outline_pattern.fragment.glsl @@ -2,6 +2,7 @@ uniform vec2 u_pattern_tl_a; uniform vec2 u_pattern_br_a; uniform vec2 u_pattern_tl_b; uniform vec2 u_pattern_br_b; +uniform vec2 u_texsize; uniform float u_mix; uniform sampler2D u_image; @@ -16,11 +17,11 @@ void main() { #pragma mapbox: initialize lowp float opacity vec2 imagecoord = mod(v_pos_a, 1.0); - vec2 pos = mix(u_pattern_tl_a, u_pattern_br_a, imagecoord); + vec2 pos = mix(u_pattern_tl_a / u_texsize, u_pattern_br_a / u_texsize, imagecoord); vec4 color1 = texture2D(u_image, pos); vec2 imagecoord_b = mod(v_pos_b, 1.0); - vec2 pos2 = mix(u_pattern_tl_b, u_pattern_br_b, imagecoord_b); + vec2 pos2 = mix(u_pattern_tl_b / u_texsize, u_pattern_br_b / u_texsize, imagecoord_b); vec4 color2 = texture2D(u_image, pos2); // find distance to outline for alpha interpolation diff --git a/src/shaders/fill_pattern.fragment.glsl b/src/shaders/fill_pattern.fragment.glsl index 18527cb492e..05b6cc729f1 100644 --- a/src/shaders/fill_pattern.fragment.glsl +++ b/src/shaders/fill_pattern.fragment.glsl @@ -2,6 +2,7 @@ uniform vec2 u_pattern_tl_a; uniform vec2 u_pattern_br_a; uniform vec2 u_pattern_tl_b; uniform vec2 u_pattern_br_b; +uniform vec2 u_texsize; uniform float u_mix; uniform sampler2D u_image; @@ -15,11 +16,11 @@ void main() { #pragma mapbox: initialize lowp float opacity vec2 imagecoord = mod(v_pos_a, 1.0); - vec2 pos = mix(u_pattern_tl_a, u_pattern_br_a, imagecoord); + vec2 pos = mix(u_pattern_tl_a / u_texsize, u_pattern_br_a / u_texsize, imagecoord); vec4 color1 = texture2D(u_image, pos); vec2 imagecoord_b = mod(v_pos_b, 1.0); - vec2 pos2 = mix(u_pattern_tl_b, u_pattern_br_b, imagecoord_b); + vec2 pos2 = mix(u_pattern_tl_b / u_texsize, u_pattern_br_b / u_texsize, imagecoord_b); vec4 color2 = texture2D(u_image, pos2); gl_FragColor = mix(color1, color2, u_mix) * opacity; diff --git a/src/shaders/line_pattern.fragment.glsl b/src/shaders/line_pattern.fragment.glsl index 33003a77442..59aa2495799 100644 --- a/src/shaders/line_pattern.fragment.glsl +++ b/src/shaders/line_pattern.fragment.glsl @@ -4,6 +4,7 @@ uniform vec2 u_pattern_tl_a; uniform vec2 u_pattern_br_a; uniform vec2 u_pattern_tl_b; uniform vec2 u_pattern_br_b; +uniform vec2 u_texsize; uniform float u_fade; uniform sampler2D u_image; @@ -33,8 +34,8 @@ void main() { float x_b = mod(v_linesofar / u_pattern_size_b.x, 1.0); float y_a = 0.5 + (v_normal.y * v_width2.s / u_pattern_size_a.y); float y_b = 0.5 + (v_normal.y * v_width2.s / u_pattern_size_b.y); - vec2 pos_a = mix(u_pattern_tl_a, u_pattern_br_a, vec2(x_a, y_a)); - vec2 pos_b = mix(u_pattern_tl_b, u_pattern_br_b, vec2(x_b, y_b)); + vec2 pos_a = mix(u_pattern_tl_a / u_texsize, u_pattern_br_a / u_texsize, vec2(x_a, y_a)); + vec2 pos_b = mix(u_pattern_tl_b / u_texsize, u_pattern_br_b / u_texsize, vec2(x_b, y_b)); vec4 color = mix(texture2D(u_image, pos_a), texture2D(u_image, pos_b), u_fade); diff --git a/src/shaders/symbol_icon.vertex.glsl b/src/shaders/symbol_icon.vertex.glsl index fe2f5f6e20b..2cc973c5613 100644 --- a/src/shaders/symbol_icon.vertex.glsl +++ b/src/shaders/symbol_icon.vertex.glsl @@ -1,14 +1,17 @@ - attribute vec4 a_pos_offset; +attribute vec2 a_label_pos; attribute vec4 a_data; // icon-size data (see symbol_sdf.vertex.glsl for more) attribute vec3 a_size; uniform bool u_is_size_zoom_constant; uniform bool u_is_size_feature_constant; -uniform mediump float u_size_t; // used to interpolate between zoom stops when size is a composite function -uniform mediump float u_size; // used when size is both zoom and feature constant -uniform mediump float u_layout_size; // used when size is feature constant +uniform highp float u_size_t; // used to interpolate between zoom stops when size is a composite function +uniform highp float u_size; // used when size is both zoom and feature constant +uniform highp float u_layout_size; // used when size is feature constant +uniform highp float u_camera_to_center_distance; +uniform highp float u_pitch; +uniform highp float u_collision_y_stretch; #pragma mapbox: define lowp float opacity @@ -16,7 +19,7 @@ uniform mediump float u_layout_size; // used when size is feature constant uniform mat4 u_matrix; uniform bool u_is_text; -uniform mediump float u_zoom; +uniform highp float u_zoom; uniform bool u_rotate_with_map; uniform vec2 u_extrude_scale; @@ -32,11 +35,11 @@ void main() { vec2 a_offset = a_pos_offset.zw; vec2 a_tex = a_data.xy; - mediump vec2 label_data = unpack_float(a_data[2]); - mediump float a_labelminzoom = label_data[0]; - mediump vec2 a_zoom = unpack_float(a_data[3]); - mediump float a_minzoom = a_zoom[0]; - mediump float a_maxzoom = a_zoom[1]; + highp vec2 label_data = unpack_float(a_data[2]); + highp float a_labelminzoom = label_data[0]; + highp vec2 a_zoom = unpack_float(a_data[3]); + highp float a_minzoom = a_zoom[0]; + highp float a_maxzoom = a_zoom[1]; float size; // In order to accommodate placing labels around corners in @@ -47,7 +50,7 @@ void main() { // currently rendered zoom level if text-size is zoom-dependent. // Thus, we compensate for this difference by calculating an adjustment // based on the scale of rendered text size relative to layout text size. - mediump float layoutSize; + highp float layoutSize; if (!u_is_size_zoom_constant && !u_is_size_feature_constant) { size = mix(a_size[0], a_size[1], u_size_t) / 10.0; layoutSize = a_size[2] / 10.0; @@ -64,12 +67,16 @@ void main() { float fontScale = u_is_text ? size / 24.0 : size; - mediump float zoomAdjust = log2(size / layoutSize); - mediump float adjustedZoom = (u_zoom - zoomAdjust) * 10.0; + highp float zoomAdjust = log2(size / layoutSize); + highp float adjustedZoom = (u_zoom - zoomAdjust) * 10.0; // result: z = 0 if a_minzoom <= adjustedZoom < a_maxzoom, and 1 otherwise - mediump float z = 2.0 - step(a_minzoom, adjustedZoom) - (1.0 - step(a_maxzoom, adjustedZoom)); + highp float z = 2.0 - step(a_minzoom, adjustedZoom) - (1.0 - step(a_maxzoom, adjustedZoom)); + + vec4 projectedPoint = u_matrix * vec4(a_label_pos, 0, 1); + highp float camera_to_anchor_distance = projectedPoint.w; + highp float perspective_ratio = 1.0 + 0.5*((camera_to_anchor_distance / u_camera_to_center_distance) - 1.0); - vec2 extrude = fontScale * u_extrude_scale * (a_offset / 64.0); + vec2 extrude = fontScale * u_extrude_scale * perspective_ratio * (a_offset / 64.0); if (u_rotate_with_map) { gl_Position = u_matrix * vec4(a_pos + extrude, 0, 1); gl_Position.z += z * gl_Position.w; @@ -78,5 +85,10 @@ void main() { } v_tex = a_tex / u_texsize; - v_fade_tex = vec2(a_labelminzoom / 255.0, 0.0); + // See comments in symbol_sdf.vertex + highp float incidence_stretch = camera_to_anchor_distance / (u_camera_to_center_distance * cos(u_pitch)); + highp float collision_adjustment = max(1.0, incidence_stretch / u_collision_y_stretch); + + highp float perspective_zoom_adjust = floor(log2(perspective_ratio * collision_adjustment) * 10.0); + v_fade_tex = vec2((a_labelminzoom + perspective_zoom_adjust) / 255.0, 0.0); } diff --git a/src/shaders/symbol_sdf.vertex.glsl b/src/shaders/symbol_sdf.vertex.glsl index 90ed1956582..cb5d44f19af 100644 --- a/src/shaders/symbol_sdf.vertex.glsl +++ b/src/shaders/symbol_sdf.vertex.glsl @@ -1,6 +1,9 @@ const float PI = 3.141592653589793; +// NOTE: the a_data attribute in this shader is manually bound (see https://github.com/mapbox/mapbox-gl-js/issues/4607). +// If removing or renaming a_data, revisit the manual binding in painter.js accordingly. attribute vec4 a_pos_offset; +attribute vec2 a_label_pos; attribute vec4 a_data; // contents of a_size vary based on the type of property value @@ -14,9 +17,9 @@ attribute vec4 a_data; attribute vec3 a_size; uniform bool u_is_size_zoom_constant; uniform bool u_is_size_feature_constant; -uniform mediump float u_size_t; // used to interpolate between zoom stops when size is a composite function -uniform mediump float u_size; // used when size is both zoom and feature constant -uniform mediump float u_layout_size; // used when size is feature constant +uniform highp float u_size_t; // used to interpolate between zoom stops when size is a composite function +uniform highp float u_size; // used when size is both zoom and feature constant +uniform highp float u_layout_size; // used when size is feature constant #pragma mapbox: define highp vec4 fill_color #pragma mapbox: define highp vec4 halo_color @@ -28,12 +31,15 @@ uniform mediump float u_layout_size; // used when size is feature constant uniform mat4 u_matrix; uniform bool u_is_text; -uniform mediump float u_zoom; +uniform highp float u_zoom; uniform bool u_rotate_with_map; uniform bool u_pitch_with_map; -uniform mediump float u_pitch; -uniform mediump float u_bearing; -uniform mediump float u_aspect_ratio; +uniform highp float u_pitch; +uniform highp float u_bearing; +uniform highp float u_aspect_ratio; +uniform highp float u_camera_to_center_distance; +uniform highp float u_max_camera_distance; +uniform highp float u_collision_y_stretch; uniform vec2 u_extrude_scale; uniform vec2 u_texsize; @@ -43,6 +49,18 @@ varying vec2 v_fade_tex; varying float v_gamma_scale; varying float v_size; +// Used below to move the vertex out of the clip space for when the current +// zoom is out of the glyph's zoom range. +highp float clipUnusedGlyphAngles(const highp float render_size, + const highp float layout_size, + const highp float min_zoom, + const highp float max_zoom) { + highp float zoom_adjust = log2(render_size / layout_size); + highp float adjusted_zoom = (u_zoom - zoom_adjust) * 10.0; + // result: 0 if min_zoom <= adjusted_zoom < max_zoom, and 1 otherwise + return 2.0 - step(min_zoom, adjusted_zoom) - (1.0 - step(max_zoom, adjusted_zoom)); +} + void main() { #pragma mapbox: initialize highp vec4 fill_color #pragma mapbox: initialize highp vec4 halo_color @@ -55,13 +73,12 @@ void main() { vec2 a_tex = a_data.xy; - mediump vec2 label_data = unpack_float(a_data[2]); - mediump float a_labelminzoom = label_data[0]; - mediump float a_labelangle = label_data[1]; - - mediump vec2 a_zoom = unpack_float(a_data[3]); - mediump float a_minzoom = a_zoom[0]; - mediump float a_maxzoom = a_zoom[1]; + highp vec2 label_data = unpack_float(a_data[2]); + highp float a_labelminzoom = label_data[0]; + highp float a_lineangle = (label_data[1] / 256.0 * 2.0 * PI); + highp vec2 a_zoom = unpack_float(a_data[3]); + highp float a_minzoom = a_zoom[0]; + highp float a_maxzoom = a_zoom[1]; // In order to accommodate placing labels around corners in // symbol-placement: line, each glyph in a label could have multiple @@ -71,7 +88,7 @@ void main() { // currently rendered zoom level if text-size is zoom-dependent. // Thus, we compensate for this difference by calculating an adjustment // based on the scale of rendered text size relative to layout text size. - mediump float layoutSize; + highp float layoutSize; if (!u_is_size_zoom_constant && !u_is_size_feature_constant) { v_size = mix(a_size[0], a_size[1], u_size_t) / 10.0; layoutSize = a_size[2] / 10.0; @@ -88,57 +105,78 @@ void main() { float fontScale = u_is_text ? v_size / 24.0 : v_size; - mediump float zoomAdjust = log2(v_size / layoutSize); - mediump float adjustedZoom = (u_zoom - zoomAdjust) * 10.0; - // result: z = 0 if a_minzoom <= adjustedZoom < a_maxzoom, and 1 otherwise - // Used below to move the vertex out of the clip space for when the current - // zoom is out of the glyph's zoom range. - mediump float z = 2.0 - step(a_minzoom, adjustedZoom) - (1.0 - step(a_maxzoom, adjustedZoom)); + vec4 projectedPoint = u_matrix * vec4(a_label_pos, 0, 1); + highp float camera_to_anchor_distance = projectedPoint.w; + highp float perspective_ratio = 1.0 + 0.5*((camera_to_anchor_distance / u_camera_to_center_distance) - 1.0); // pitch-alignment: map // rotation-alignment: map | viewport if (u_pitch_with_map) { - lowp float angle = u_rotate_with_map ? (a_labelangle / 256.0 * 2.0 * PI) : u_bearing; - lowp float asin = sin(angle); - lowp float acos = cos(angle); + highp float angle = u_rotate_with_map ? a_lineangle : u_bearing; + highp float asin = sin(angle); + highp float acos = cos(angle); mat2 RotationMatrix = mat2(acos, asin, -1.0 * asin, acos); vec2 offset = RotationMatrix * a_offset; - vec2 extrude = fontScale * u_extrude_scale * (offset / 64.0); + vec2 extrude = fontScale * u_extrude_scale * perspective_ratio * (offset / 64.0); + gl_Position = u_matrix * vec4(a_pos + extrude, 0, 1); - gl_Position.z += z * gl_Position.w; + gl_Position.z += clipUnusedGlyphAngles(v_size*perspective_ratio, layoutSize, a_minzoom, a_maxzoom) * gl_Position.w; // pitch-alignment: viewport // rotation-alignment: map } else if (u_rotate_with_map) { // foreshortening factor to apply on pitched maps // as a label goes from horizontal <=> vertical in angle // it goes from 0% foreshortening to up to around 70% foreshortening - lowp float pitchfactor = 1.0 - cos(u_pitch * sin(u_pitch * 0.75)); - - lowp float lineangle = a_labelangle / 256.0 * 2.0 * PI; + highp float pitchfactor = 1.0 - cos(u_pitch * sin(u_pitch * 0.75)); // use the lineangle to position points a,b along the line // project the points and calculate the label angle in projected space // this calculation allows labels to be rendered unskewed on pitched maps vec4 a = u_matrix * vec4(a_pos, 0, 1); - vec4 b = u_matrix * vec4(a_pos + vec2(cos(lineangle),sin(lineangle)), 0, 1); - lowp float angle = atan((b[1]/b[3] - a[1]/a[3])/u_aspect_ratio, b[0]/b[3] - a[0]/a[3]); - lowp float asin = sin(angle); - lowp float acos = cos(angle); + vec4 b = u_matrix * vec4(a_pos + vec2(cos(a_lineangle), sin(a_lineangle)), 0, 1); + highp float angle = atan((b[1] / b[3] - a[1] / a[3]) / u_aspect_ratio, b[0] / b[3] - a[0] / a[3]); + highp float asin = sin(angle); + highp float acos = cos(angle); mat2 RotationMatrix = mat2(acos, -1.0 * asin, asin, acos); + highp float foreshortening = (1.0 - pitchfactor) + (pitchfactor * cos(angle * 2.0)); + + vec2 offset = RotationMatrix * (vec2(foreshortening, 1.0) * a_offset); + vec2 extrude = fontScale * u_extrude_scale * perspective_ratio * (offset / 64.0); - vec2 offset = RotationMatrix * (vec2((1.0-pitchfactor)+(pitchfactor*cos(angle*2.0)), 1.0) * a_offset); - vec2 extrude = fontScale * u_extrude_scale * (offset / 64.0); gl_Position = u_matrix * vec4(a_pos, 0, 1) + vec4(extrude, 0, 0); - gl_Position.z += z * gl_Position.w; + gl_Position.z += clipUnusedGlyphAngles(v_size * perspective_ratio, layoutSize, a_minzoom, a_maxzoom) * gl_Position.w; // pitch-alignment: viewport // rotation-alignment: viewport } else { - vec2 extrude = fontScale * u_extrude_scale * (a_offset / 64.0); + vec2 extrude = fontScale * u_extrude_scale * perspective_ratio * (a_offset / 64.0); gl_Position = u_matrix * vec4(a_pos, 0, 1) + vec4(extrude, 0, 0); } - v_gamma_scale = gl_Position.w; + gl_Position.z += + step(u_max_camera_distance * u_camera_to_center_distance, camera_to_anchor_distance) * gl_Position.w; + + v_gamma_scale = gl_Position.w / perspective_ratio; v_tex = a_tex / u_texsize; - v_fade_tex = vec2(a_labelminzoom / 255.0, 0.0); + // incidence_stretch is the ratio of how much y space a label takes up on a tile while drawn perpendicular to the viewport vs + // how much space it would take up if it were drawn flat on the tile + // Using law of sines, camera_to_anchor/sin(ground_angle) = camera_to_center/sin(incidence_angle) + // sin(incidence_angle) = 1/incidence_stretch + // Incidence angle 90 -> head on, sin(incidence_angle) = 1, no incidence stretch + // Incidence angle 1 -> very oblique, sin(incidence_angle) =~ 0, lots of incidence stretch + // ground_angle = u_pitch + PI/2 -> sin(ground_angle) = cos(u_pitch) + // This 2D calculation is only exactly correct when gl_Position.x is in the center of the viewport, + // but it's a close enough approximation for our purposes + highp float incidence_stretch = camera_to_anchor_distance / (u_camera_to_center_distance * cos(u_pitch)); + // incidence_stretch only applies to the y-axis, but without re-calculating the collision tile, we can't + // adjust the size of only one axis. So, we do a crude approximation at placement time to get the aspect ratio + // about right, and then do the rest of the adjustment here: there will be some extra padding on the x-axis, + // but hopefully not too much. + // Never make the adjustment less than 1.0: instead of allowing collisions on the x-axis, be conservative on + // the y-axis. + highp float collision_adjustment = max(1.0, incidence_stretch / u_collision_y_stretch); + + // Floor to 1/10th zoom to dodge precision issues that can cause partially hidden labels + highp float perspective_zoom_adjust = floor(log2(perspective_ratio * collision_adjustment) * 10.0); + v_fade_tex = vec2((a_labelminzoom + perspective_zoom_adjust) / 255.0, 0.0); } diff --git a/src/source/canvas_source.js b/src/source/canvas_source.js index 8439b09b657..e797ba3439e 100644 --- a/src/source/canvas_source.js +++ b/src/source/canvas_source.js @@ -102,7 +102,7 @@ class CanvasSource extends ImageSource { } if (this._hasInvalidDimensions()) return; - if (!this.tile) return; // not enough data for current position + if (Object.keys(this.tiles).length === 0) return; // not enough data for current position this._prepareImage(this.map.painter.gl, this.canvas, resize); } diff --git a/src/source/geojson_source.js b/src/source/geojson_source.js index 7517812cd4c..6eaa669e9a8 100644 --- a/src/source/geojson_source.js +++ b/src/source/geojson_source.js @@ -175,6 +175,8 @@ class GeoJSONSource extends Evented { overscaling: overscaling, angle: this.map.transform.angle, pitch: this.map.transform.pitch, + cameraToCenterDistance: this.map.transform.cameraToCenterDistance, + cameraToTileDistance: this.map.transform.cameraToTileDistance(tile), showCollisionBoxes: this.map.showCollisionBoxes }; diff --git a/src/source/image_source.js b/src/source/image_source.js index 6946595d3bc..2de718c8905 100644 --- a/src/source/image_source.js +++ b/src/source/image_source.js @@ -54,10 +54,12 @@ class ImageSource extends Evented { this.minzoom = 0; this.maxzoom = 22; this.tileSize = 512; + this.tiles = {}; this.setEventedParent(eventedParent); this.options = options; + this.textureLoaded = false; } load() { @@ -140,7 +142,7 @@ class ImageSource extends Evented { } _setTile(tile) { - this.tile = tile; + this.tiles[tile.coord.w] = tile; const maxInt16 = 32767; const array = new RasterBoundsArray(); array.emplaceBack(this._tileCoords[0].x, this._tileCoords[0].y, 0, 0); @@ -148,22 +150,22 @@ class ImageSource extends Evented { array.emplaceBack(this._tileCoords[3].x, this._tileCoords[3].y, 0, maxInt16); array.emplaceBack(this._tileCoords[2].x, this._tileCoords[2].y, maxInt16, maxInt16); - this.tile.buckets = {}; + tile.buckets = {}; - this.tile.boundsBuffer = Buffer.fromStructArray(array, Buffer.BufferType.VERTEX); - this.tile.boundsVAO = new VertexArrayObject(); + tile.boundsBuffer = Buffer.fromStructArray(array, Buffer.BufferType.VERTEX); + tile.boundsVAO = new VertexArrayObject(); } prepare() { - if (!this.tile || !this.image) return; + if (Object.keys(this.tiles).length === 0 === 0 || !this.image) return; this._prepareImage(this.map.painter.gl, this.image); } _prepareImage(gl, image, resize) { - if (this.tile.state !== 'loaded') { - this.tile.state = 'loaded'; - this.tile.texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, this.tile.texture); + if (!this.textureLoaded) { + this.textureLoaded = true; + this.texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); @@ -172,9 +174,17 @@ class ImageSource extends Evented { } else if (resize) { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); } else if (image instanceof window.HTMLVideoElement || image instanceof window.ImageData || image instanceof window.HTMLCanvasElement) { - gl.bindTexture(gl.TEXTURE_2D, this.tile.texture); + gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, image); } + + for (const w in this.tiles) { + const tile = this.tiles[w]; + if (tile.state !== 'loaded') { + tile.state = 'loaded'; + tile.texture = this.texture; + } + } } loadTile(tile, callback) { @@ -182,6 +192,8 @@ class ImageSource extends Evented { // covers the image we want to render. If that's the one being // requested, set it up with the image; otherwise, mark the tile as // `errored` to indicate that we have no data for it. + // If the world wraps, we may have multiple "wrapped" copies of the + // single tile. if (this.coord && this.coord.toString() === tile.coord.toString()) { this._setTile(tile); callback(null); diff --git a/src/source/query_features.js b/src/source/query_features.js index 473492f0c2e..9fae1ca590d 100644 --- a/src/source/query_features.js +++ b/src/source/query_features.js @@ -11,13 +11,15 @@ exports.rendered = function(sourceCache, styleLayers, queryGeometry, params, zoo const tileIn = tilesIn[r]; if (!tileIn.tile.featureIndex) continue; - renderedFeatureLayers.push(tileIn.tile.featureIndex.query({ - queryGeometry: tileIn.queryGeometry, - scale: tileIn.scale, - tileSize: tileIn.tile.tileSize, - bearing: bearing, - params: params - }, styleLayers)); + renderedFeatureLayers.push({ + wrappedTileID: tileIn.coord.wrapped().id, + queryResults: tileIn.tile.featureIndex.query({ + queryGeometry: tileIn.queryGeometry, + scale: tileIn.scale, + tileSize: tileIn.tile.tileSize, + bearing: bearing, + params: params + }, styleLayers)}); } return mergeRenderedFeatureLayers(renderedFeatureLayers); }; @@ -49,21 +51,25 @@ function sortTilesIn(a, b) { } function mergeRenderedFeatureLayers(tiles) { - const result = tiles[0] || {}; - for (let i = 1; i < tiles.length; i++) { - const tile = tiles[i]; - for (const layerID in tile) { - const tileFeatures = tile[layerID]; - let resultFeatures = result[layerID]; - if (resultFeatures === undefined) { - resultFeatures = result[layerID] = tileFeatures; - } else { - for (let f = 0; f < tileFeatures.length; f++) { - resultFeatures.push(tileFeatures[f]); + // Merge results from all tiles, but if two tiles share the same + // wrapped ID, don't duplicate features between the two tiles + const result = {}; + const wrappedIDLayerMap = {}; + for (const tile of tiles) { + const queryResults = tile.queryResults; + const wrappedID = tile.wrappedTileID; + const wrappedIDLayers = wrappedIDLayerMap[wrappedID] = wrappedIDLayerMap[wrappedID] || {}; + for (const layerID in queryResults) { + const tileFeatures = queryResults[layerID]; + const wrappedIDFeatures = wrappedIDLayers[layerID] = wrappedIDLayers[layerID] || {}; + const resultFeatures = result[layerID] = result[layerID] || []; + for (const tileFeature of tileFeatures) { + if (!wrappedIDFeatures[tileFeature.featureIndex]) { + wrappedIDFeatures[tileFeature.featureIndex] = true; + resultFeatures.push(tileFeature.feature); } } } } return result; } - diff --git a/src/source/source_cache.js b/src/source/source_cache.js index b09075b659a..4ab8b4745e3 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -213,7 +213,7 @@ class SourceCache extends Evented { * Recursively find children of the given tile (up to maxCoveringZoom) that are already loaded; * adds found tiles to retain object; returns true if any child is found. * - * @param {Coordinate} coord + * @param {TileCoord} coord * @param {number} maxCoveringZoom * @param {boolean} retain * @returns {boolean} whether the operation was complete @@ -256,7 +256,7 @@ class SourceCache extends Evented { * Find a loaded parent of the given tile (up to minCoveringZoom); * adds the found tile to retain object and returns the tile if found * - * @param {Coordinate} coord + * @param {TileCoord} coord * @param {number} minCoveringZoom * @param {boolean} retain * @returns {Tile} tile object @@ -407,39 +407,35 @@ class SourceCache extends Evented { /** * Add a tile, given its coordinate, to the pyramid. - * @param {Coordinate} coord - * @returns {Coordinate} the coordinate. + * @param {TileCoord} tileCoord + * @returns {Tile} the added Tile. * @private */ - addTile(coord) { - let tile = this._tiles[coord.id]; + addTile(tileCoord) { + let tile = this._tiles[tileCoord.id]; if (tile) return tile; - const wrapped = coord.wrapped(); - tile = this._tiles[wrapped.id]; - - if (!tile) { - tile = this._cache.get(wrapped.id); - if (tile) { - tile.redoPlacement(this._source); - if (this._cacheTimers[wrapped.id]) { - clearTimeout(this._cacheTimers[wrapped.id]); - this._cacheTimers[wrapped.id] = undefined; - this._setTileReloadTimer(wrapped.id, tile); - } + tile = this._cache.get(tileCoord.id); + if (tile) { + tile.redoPlacement(this._source); + if (this._cacheTimers[tileCoord.id]) { + clearTimeout(this._cacheTimers[tileCoord.id]); + this._cacheTimers[tileCoord.id] = undefined; + this._setTileReloadTimer(tileCoord.id, tile); } } + const cached = Boolean(tile); if (!cached) { - const zoom = coord.z; + const zoom = tileCoord.z; const overscaling = zoom > this._source.maxzoom ? Math.pow(2, zoom - this._source.maxzoom) : 1; - tile = new Tile(wrapped, this._source.tileSize * overscaling, this._source.maxzoom); - this.loadTile(tile, this._tileLoaded.bind(this, tile, coord.id, tile.state)); + tile = new Tile(tileCoord, this._source.tileSize * overscaling, this._source.maxzoom); + this.loadTile(tile, this._tileLoaded.bind(this, tile, tileCoord.id, tile.state)); } tile.uses++; - this._tiles[coord.id] = tile; + this._tiles[tileCoord.id] = tile; if (!cached) this._source.fire('dataloading', {tile: tile, coord: tile.coord, dataType: 'source'}); return tile; @@ -486,6 +482,8 @@ class SourceCache extends Evented { if (tile.uses > 0) return; + tile.stopPlacementThrottler(); + if (tile.hasData()) { const wrappedId = tile.coord.wrapped().id; this._cache.add(wrappedId, tile); @@ -515,7 +513,7 @@ class SourceCache extends Evented { * @private */ tilesIn(queryGeometry) { - const tileResults = {}; + const tileResults = []; const ids = this.getIds(); let minX = Infinity; @@ -532,6 +530,7 @@ class SourceCache extends Evented { maxY = Math.max(maxY, p.row); } + for (let i = 0; i < ids.length; i++) { const tile = this._tiles[ids[i]]; const coord = TileCoord.fromID(ids[i]); @@ -549,26 +548,16 @@ class SourceCache extends Evented { tileSpaceQueryGeometry.push(coordinateToTilePoint(coord, tile.sourceMaxZoom, queryGeometry[j])); } - let tileResult = tileResults[tile.coord.id]; - if (tileResult === undefined) { - tileResult = tileResults[tile.coord.id] = { - tile: tile, - coord: coord, - queryGeometry: [], - scale: Math.pow(2, this.transform.zoom - tile.coord.z) - }; - } - - // Wrapped tiles share one tileResult object but can have multiple queryGeometry parts - tileResult.queryGeometry.push(tileSpaceQueryGeometry); + tileResults.push({ + tile: tile, + coord: coord, + queryGeometry: [tileSpaceQueryGeometry], + scale: Math.pow(2, this.transform.zoom - tile.coord.z) + }); } } - const results = []; - for (const t in tileResults) { - results.push(tileResults[t]); - } - return results; + return tileResults; } redoPlacement() { @@ -593,7 +582,8 @@ SourceCache.maxUnderzooming = 3; /** * Convert a coordinate to a point in a tile's coordinate space. - * @param {Coordinate} tileCoord + * @param {TileCoord} tileCoord + * @param {number} sourceMaxZoom * @param {Coordinate} coord * @returns {Object} position * @private diff --git a/src/source/tile.js b/src/source/tile.js index 7b13a62aba7..be9110a14ee 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -9,6 +9,7 @@ const GeoJSONFeature = require('../util/vectortile_to_geojson'); const featureFilter = require('../style-spec/feature_filter'); const CollisionTile = require('../symbol/collision_tile'); const CollisionBoxArray = require('../symbol/collision_box'); +const Throttler = require('../util/throttler'); const CLOCK_SKEW_RETRY_TIMEOUT = 30000; @@ -20,7 +21,7 @@ const CLOCK_SKEW_RETRY_TIMEOUT = 30000; */ class Tile { /** - * @param {Coordinate} coord + * @param {TileCoord} coord * @param {number} size */ constructor(coord, size, sourceMaxZoom) { @@ -47,6 +48,8 @@ class Tile { // - `errored`: Tile data was not loaded because of an error. // - `expired`: Tile data was previously loaded, but has expired per its HTTP headers and is in the process of refreshing. this.state = 'loading'; + + this.placementThrottler = new Throttler(300, this._immediateRedoPlacement.bind(this)); } registerFadeDuration(animationLoop, duration) { @@ -144,26 +147,56 @@ class Tile { return; } + const cameraToTileDistance = source.map.transform.cameraToTileDistance(this); + if (this.angle === source.map.transform.angle && + this.pitch === source.map.transform.pitch && + this.cameraToCenterDistance === source.map.transform.cameraToCenterDistance && + this.showCollisionBoxes === source.map.showCollisionBoxes) { + if (this.cameraToTileDistance === cameraToTileDistance) { + return; + } else if (this.pitch < 25) { + // At low pitch tile distance doesn't affect placement very + // much, so we skip the cost of redoPlacement + // However, we might as well store the latest value of + // cameraToTileDistance in case a redoPlacement request + // is already queued. + this.cameraToTileDistance = cameraToTileDistance; + return; + } + } + + this.angle = source.map.transform.angle; + this.pitch = source.map.transform.pitch; + this.cameraToCenterDistance = source.map.transform.cameraToCenterDistance; + this.cameraToTileDistance = cameraToTileDistance; + this.showCollisionBoxes = source.map.showCollisionBoxes; + this.placementSource = source; + this.state = 'reloading'; + this.placementThrottler.invoke(); + } - source.dispatcher.send('redoPlacement', { - type: source.type, + _immediateRedoPlacement() { + this.placementSource.dispatcher.send('redoPlacement', { + type: this.placementSource.type, uid: this.uid, - source: source.id, - angle: source.map.transform.angle, - pitch: source.map.transform.pitch, - showCollisionBoxes: source.map.showCollisionBoxes + source: this.placementSource.id, + angle: this.angle, + pitch: this.pitch, + cameraToCenterDistance: this.cameraToCenterDistance, + cameraToTileDistance: this.cameraToTileDistance, + showCollisionBoxes: this.showCollisionBoxes }, (_, data) => { - this.reloadSymbolData(data, source.map.style); - + this.reloadSymbolData(data, this.placementSource.map.style); + if (this.placementSource.map.showCollisionBoxes) this.placementSource.fire('data', {tile: this, coord: this.coord, dataType: 'source'}); // HACK this is nescessary to fix https://github.com/mapbox/mapbox-gl-js/issues/2986 - if (source.map) source.map.painter.tileExtentVAO.vao = null; + if (this.placementSource.map) this.placementSource.map.painter.tileExtentVAO.vao = null; this.state = 'loaded'; if (this.redoWhenDone) { this.redoWhenDone = false; - this.redoPlacement(source); + this._immediateRedoPlacement(); } }, this.workerID); } @@ -259,6 +292,13 @@ class Tile { } } } + + stopPlacementThrottler() { + this.placementThrottler.stop(); + if (this.state === 'reloading') { + this.state = 'loaded'; + } + } } module.exports = Tile; diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index c554b1ad901..b81e4622dd2 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -84,6 +84,8 @@ class VectorTileSource extends Evented { overscaling: overscaling, angle: this.map.transform.angle, pitch: this.map.transform.pitch, + cameraToCenterDistance: this.map.transform.cameraToCenterDistance, + cameraToTileDistance: this.map.transform.cameraToTileDistance(tile), showCollisionBoxes: this.map.showCollisionBoxes }; diff --git a/src/source/vector_tile_worker_source.js b/src/source/vector_tile_worker_source.js index c0c4ffe30c5..23c3d008b4f 100644 --- a/src/source/vector_tile_worker_source.js +++ b/src/source/vector_tile_worker_source.js @@ -39,6 +39,8 @@ class VectorTileWorkerSource { * @param {number} params.overscaling * @param {number} params.angle * @param {number} params.pitch + * @param {number} params.cameraToCenterDistance + * @param {number} params.cameraToTileDistance * @param {boolean} params.showCollisionBoxes */ loadTile(params, callback) { @@ -184,7 +186,7 @@ class VectorTileWorkerSource { if (loaded && loaded[uid]) { const workerTile = loaded[uid]; - const result = workerTile.redoPlacement(params.angle, params.pitch, params.showCollisionBoxes); + const result = workerTile.redoPlacement(params.angle, params.pitch, params.cameraToCenterDistance, params.cameraToTileDistance, params.showCollisionBoxes); if (result.result) { callback(null, result.result, result.transferables); diff --git a/src/source/video_source.js b/src/source/video_source.js index b1155c727cc..5e4ee61fbf5 100644 --- a/src/source/video_source.js +++ b/src/source/video_source.js @@ -107,7 +107,7 @@ class VideoSource extends ImageSource { // setCoordinates inherited from ImageSource prepare() { - if (!this.tile || this.video.readyState < 2) return; // not enough data for current position + if (Object.keys(this.tiles).length === 0 || this.video.readyState < 2) return; // not enough data for current position this._prepareImage(this.map.painter.gl, this.video); } diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index b91421f9ce8..2a0b662e707 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -17,6 +17,8 @@ class WorkerTile { this.overscaling = params.overscaling; this.angle = params.angle; this.pitch = params.pitch; + this.cameraToCenterDistance = params.cameraToCenterDistance; + this.cameraToTileDistance = params.cameraToTileDistance; this.showCollisionBoxes = params.showCollisionBoxes; } @@ -126,7 +128,7 @@ class WorkerTile { } if (this.symbolBuckets.length === 0) { - return done(new CollisionTile(this.angle, this.pitch, this.collisionBoxArray)); + return done(new CollisionTile(this.angle, this.pitch, this.cameraToCenterDistance, this.cameraToTileDistance, this.collisionBoxArray)); } let deps = 0; @@ -137,7 +139,11 @@ class WorkerTile { if (err) return callback(err); deps++; if (deps === 2) { - const collisionTile = new CollisionTile(this.angle, this.pitch, this.collisionBoxArray); + const collisionTile = new CollisionTile(this.angle, + this.pitch, + this.cameraToCenterDistance, + this.cameraToTileDistance, + this.collisionBoxArray); for (const bucket of this.symbolBuckets) { recalculateLayers(bucket, this.zoom); @@ -169,15 +175,21 @@ class WorkerTile { } } - redoPlacement(angle, pitch, showCollisionBoxes) { + redoPlacement(angle, pitch, cameraToCenterDistance, cameraToTileDistance, showCollisionBoxes) { this.angle = angle; this.pitch = pitch; + this.cameraToCenterDistance = cameraToCenterDistance; + this.cameraToTileDistance = cameraToTileDistance; if (this.status !== 'done') { return {}; } - const collisionTile = new CollisionTile(this.angle, this.pitch, this.collisionBoxArray); + const collisionTile = new CollisionTile(this.angle, + this.pitch, + this.cameraToCenterDistance, + this.cameraToTileDistance, + this.collisionBoxArray); for (const bucket of this.symbolBuckets) { recalculateLayers(bucket, this.zoom); diff --git a/src/style-spec/error/validation_error.js b/src/style-spec/error/validation_error.js index 5a55e34652e..dce87fab40d 100644 --- a/src/style-spec/error/validation_error.js +++ b/src/style-spec/error/validation_error.js @@ -2,11 +2,8 @@ const format = require('util').format; -function ValidationError(key, value /*, message, ...*/) { - this.message = ( - (key ? `${key}: ` : '') + - format.apply(format, Array.prototype.slice.call(arguments, 2)) - ); +function ValidationError(key, value, ...args) { + this.message = (key ? `${key}: ` : '') + format.apply(format, args); if (value !== null && value !== undefined && value.__line__) { this.line = value.__line__; diff --git a/src/style-spec/function/index.js b/src/style-spec/function/index.js index 1a72e9de524..7a22b919cbe 100644 --- a/src/style-spec/function/index.js +++ b/src/style-spec/function/index.js @@ -196,9 +196,9 @@ function evaluateExponentialFunction(parameters, propertySpec, input) { const interp = interpolate[propertySpec.type] || identityFunction; if (typeof outputLower === 'function') { - return function() { - const evaluatedLower = outputLower.apply(undefined, arguments); - const evaluatedUpper = outputUpper.apply(undefined, arguments); + return function(...args) { + const evaluatedLower = outputLower.apply(undefined, args); + const evaluatedUpper = outputUpper.apply(undefined, args); // Special case for fill-outline-color, which has no spec default. if (evaluatedLower === undefined || evaluatedUpper === undefined) { return undefined; diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index 2da93cc484b..74da0b212f0 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -1705,6 +1705,9 @@ } }, "transition": false, + "zoom-function": true, + "property-function": false, + "function": "piecewise-constant", "doc": "Whether extruded geometries are lit relative to the map or viewport.", "example": "map", "sdk-support": { diff --git a/src/style-spec/util/extend.js b/src/style-spec/util/extend.js index a3a746cad40..3e1efd69ccb 100644 --- a/src/style-spec/util/extend.js +++ b/src/style-spec/util/extend.js @@ -1,8 +1,7 @@ 'use strict'; -module.exports = function (output) { - for (let i = 1; i < arguments.length; i++) { - const input = arguments[i]; +module.exports = function (output, ...inputs) { + for (const input of inputs) { for (const k in input) { output[k] = input[k]; } diff --git a/src/symbol/collision_feature.js b/src/symbol/collision_feature.js index b9be1f03718..bbc42d66765 100644 --- a/src/symbol/collision_feature.js +++ b/src/symbol/collision_feature.js @@ -72,34 +72,52 @@ class CollisionFeature { _addLineCollisionBoxes(collisionBoxArray, line, anchor, segment, labelLength, boxSize, featureIndex, sourceLayerIndex, bucketIndex) { const step = boxSize / 2; const nBoxes = Math.floor(labelLength / step); + // We calculate line collision boxes out to 150% of what would normally be our + // max size, to allow collision detection to work on labels that expand as + // they move into the distance + const nPitchPaddingBoxes = Math.floor(nBoxes / 4); // offset the center of the first box by half a box so that the edge of the // box is at the edge of the label. const firstBoxOffset = -boxSize / 2; - const bboxes = this.boxes; - let p = anchor; let index = segment + 1; let anchorDistance = firstBoxOffset; + const labelStartDistance = -labelLength / 2; + const paddingStartDistance = labelStartDistance - labelLength / 8; // move backwards along the line to the first segment the label appears on do { index--; - // there isn't enough room for the label after the beginning of the line - // checkMaxAngle should have already caught this - if (index < 0) return bboxes; - - anchorDistance -= line[index].dist(p); - p = line[index]; - } while (anchorDistance > -labelLength / 2); + if (index < 0) { + if (anchorDistance > labelStartDistance) { + // there isn't enough room for the label after the beginning of the line + // checkMaxAngle should have already caught this + return; + } else { + // The line doesn't extend far enough back for all of our padding, + // but we got far enough to show the label under most conditions. + index = 0; + break; + } + } else { + anchorDistance -= line[index].dist(p); + p = line[index]; + } + } while (anchorDistance > paddingStartDistance); let segmentLength = line[index].dist(line[index + 1]); - for (let i = 0; i < nBoxes; i++) { + for (let i = -nPitchPaddingBoxes; i < nBoxes + nPitchPaddingBoxes; i++) { // the distance the box will be from the anchor - const boxDistanceToAnchor = -labelLength / 2 + i * step; + const boxDistanceToAnchor = labelStartDistance + i * step; + if (boxDistanceToAnchor < anchorDistance) { + // The line doesn't extend far enough back for this box, skip it + // (This could allow for line collisions on distant tiles) + continue; + } // the box is not on the current segment. Move to the next segment. while (anchorDistance + segmentLength < boxDistanceToAnchor) { @@ -107,7 +125,7 @@ class CollisionFeature { index++; // There isn't enough room before the end of the line. - if (index + 1 >= line.length) return bboxes; + if (index + 1 >= line.length) return; segmentLength = line[index].dist(line[index + 1]); } @@ -119,16 +137,35 @@ class CollisionFeature { const p1 = line[index + 1]; const boxAnchorPoint = p1.sub(p0)._unit()._mult(segmentBoxDistance)._add(p0)._round(); + // Distance from label anchor point to inner (towards center) edge of this box + // The tricky thing here is that box positioning doesn't change with scale, + // but box size does change with scale. + // Technically, distanceToInnerEdge should be: + // Math.max(Math.abs(boxDistanceToAnchor - firstBoxOffset) - (step / scale), 0); + // But using that formula would make solving for maxScale more difficult, so we + // approximate with scale=2. + // This makes our calculation spot-on at scale=2, and on the conservative side for + // lower scales const distanceToInnerEdge = Math.max(Math.abs(boxDistanceToAnchor - firstBoxOffset) - step / 2, 0); - const maxScale = labelLength / 2 / distanceToInnerEdge; + let maxScale = labelLength / 2 / distanceToInnerEdge; + + // The box maxScale calculations are designed to be conservative on collisions in the scale range + // [1,2]. At scale=1, each box has 50% overlap, and at scale=2, the boxes are lined up edge + // to edge (beyond scale 2, gaps start to appear, which could potentially allow missed collisions). + // We add "pitch padding" boxes to the left and right to handle effective underzooming + // (scale < 1) when labels are in the distance. The overlap approximation could cause us to use + // these boxes when the scale is greater than 1, but we prevent that because we know + // they're only necessary for scales less than one. + // This preserves the pre-pitch-padding behavior for unpitched maps. + if (i < 0 || i >= nBoxes) { + maxScale = Math.min(maxScale, 0.99); + } collisionBoxArray.emplaceBack(boxAnchorPoint.x, boxAnchorPoint.y, -boxSize / 2, -boxSize / 2, boxSize / 2, boxSize / 2, maxScale, featureIndex, sourceLayerIndex, bucketIndex, 0, 0, 0, 0, 0); } - - return bboxes; } } diff --git a/src/symbol/collision_tile.js b/src/symbol/collision_tile.js index 91ec367668b..705971b6b10 100644 --- a/src/symbol/collision_tile.js +++ b/src/symbol/collision_tile.js @@ -14,12 +14,14 @@ const intersectionTests = require('../util/intersection_tests'); * @private */ class CollisionTile { - constructor(angle, pitch, collisionBoxArray) { + constructor(angle, pitch, cameraToCenterDistance, cameraToTileDistance, collisionBoxArray) { if (typeof angle === 'object') { const serialized = angle; collisionBoxArray = pitch; angle = serialized.angle; pitch = serialized.pitch; + cameraToCenterDistance = serialized.cameraToCenterDistance; + cameraToTileDistance = serialized.cameraToTileDistance; this.grid = new Grid(serialized.grid); this.ignoredGrid = new Grid(serialized.ignoredGrid); } else { @@ -27,11 +29,18 @@ class CollisionTile { this.ignoredGrid = new Grid(EXTENT, 12, 0); } - this.minScale = 0.5; - this.maxScale = 2; + this.perspectiveRatio = 1 + 0.5 * ((cameraToTileDistance / cameraToCenterDistance) - 1); + + // High perspective ratio means we're effectively "underzooming" + // the tile. Adjust the minScale and maxScale range accordingly + // to constrain the number of collision calculations + this.minScale = .5 / this.perspectiveRatio; + this.maxScale = 2 / this.perspectiveRatio; this.angle = angle; this.pitch = pitch; + this.cameraToCenterDistance = cameraToCenterDistance; + this.cameraToTileDistance = cameraToTileDistance; const sin = Math.sin(angle), cos = Math.cos(angle); @@ -39,11 +48,14 @@ class CollisionTile { this.reverseRotationMatrix = [cos, sin, -sin, cos]; // Stretch boxes in y direction to account for the map tilt. - this.yStretch = 1 / Math.cos(pitch / 180 * Math.PI); - // The amount the map is squished depends on the y position. - // Sort of account for this by making all boxes a bit bigger. - this.yStretch = Math.pow(this.yStretch, 1.3); + // We can only approximate here based on the y position of the tile + // The shaders calculate a more accurate "incidence_stretch" + // at render time to calculate an effective scale for collision + // purposes, but we still want to use the yStretch approximation + // here because we can't adjust the aspect ratio of the collision + // boxes at render time. + this.yStretch = Math.max(1, cameraToTileDistance / (cameraToCenterDistance * Math.cos(pitch / 180 * Math.PI))); this.collisionBoxArray = collisionBoxArray; if (collisionBoxArray.length === 0) { @@ -90,6 +102,8 @@ class CollisionTile { return { angle: this.angle, pitch: this.pitch, + cameraToCenterDistance: this.cameraToCenterDistance, + cameraToTileDistance: this.cameraToTileDistance, grid: grid, ignoredGrid: ignoredGrid }; @@ -118,10 +132,18 @@ class CollisionTile { const x = anchorPoint.x; const y = anchorPoint.y; - const x1 = x + box.x1; - const y1 = y + box.y1 * yStretch; - const x2 = x + box.x2; - const y2 = y + box.y2 * yStretch; + // When the 'perspectiveRatio' is high, we're effectively underzooming + // the tile because it's in the distance. + // In order to detect collisions that only happen while underzoomed, + // we have to query a larger portion of the grid. + // This extra work is offset by having a lower 'maxScale' bound + // Note that this adjustment ONLY affects the bounding boxes + // in the grid. It doesn't affect the boxes used for the + // minPlacementScale calculations. + const x1 = x + box.x1 * this.perspectiveRatio; + const y1 = y + box.y1 * yStretch * this.perspectiveRatio; + const x2 = x + box.x2 * this.perspectiveRatio; + const y2 = y + box.y2 * yStretch * this.perspectiveRatio; box.bbox0 = x1; box.bbox1 = y1; @@ -181,7 +203,7 @@ class CollisionTile { const sourceLayerFeatures = {}; const result = []; - if (queryGeometry.length === 0 || (this.grid.length === 0 && this.ignoredGrid.length === 0)) { + if (queryGeometry.length === 0 || (this.grid.keys.length === 0 && this.ignoredGrid.keys.length === 0)) { return result; } @@ -304,14 +326,16 @@ class CollisionTile { * @private */ insertCollisionFeature(collisionFeature, minPlacementScale, ignorePlacement) { - const grid = ignorePlacement ? this.ignoredGrid : this.grid; const collisionBoxArray = this.collisionBoxArray; for (let k = collisionFeature.boxStartIndex; k < collisionFeature.boxEndIndex; k++) { const box = collisionBoxArray.get(k); box.placementScale = minPlacementScale; - if (minPlacementScale < this.maxScale) { + if (minPlacementScale < this.maxScale && + (this.perspectiveRatio === 1 || box.maxScale >= 1)) { + // Boxes with maxScale < 1 are only relevant in pitched maps, + // so filter them out in unpitched maps to keep the grid sparse grid.insert(k, box.bbox0, box.bbox1, box.bbox2, box.bbox3); } } diff --git a/src/symbol/glyph_atlas.js b/src/symbol/glyph_atlas.js index c0dbdffe429..c4beadd1b1a 100644 --- a/src/symbol/glyph_atlas.js +++ b/src/symbol/glyph_atlas.js @@ -79,14 +79,8 @@ class GlyphAtlas { // Add a 1px border around every image. const padding = 1; - let packWidth = bufferedWidth + 2 * padding; - let packHeight = bufferedHeight + 2 * padding; - - // 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 fewer bytes. - packWidth += (4 - packWidth % 4); - packHeight += (4 - packHeight % 4); + const packWidth = bufferedWidth + 2 * padding; + const packHeight = bufferedHeight + 2 * padding; let rect = this.atlas.packOne(packWidth, packHeight); if (!rect) { diff --git a/src/symbol/quads.js b/src/symbol/quads.js index 16ce489db34..62e244bd56c 100644 --- a/src/symbol/quads.js +++ b/src/symbol/quads.js @@ -59,14 +59,18 @@ function SymbolQuad(anchorPoint, tl, tr, bl, br, tex, anchorAngle, glyphAngle, m * @private */ function getIconQuads(anchor, shapedIcon, boxScale, line, layer, alongLine, shapedText, globalProperties, featureProperties) { - const rect = shapedIcon.image.rect; + const image = shapedIcon.image; const layout = layer.layout; + // If you have a 10px icon that isn't perfectly aligned to the pixel grid it will cover 11 actual + // pixels. The quad needs to be padded to account for this, otherwise they'll look slightly clipped + // on one edge in some cases. const border = 1; - const left = shapedIcon.left - border; - const right = left + rect.w / shapedIcon.image.pixelRatio; - const top = shapedIcon.top - border; - const bottom = top + rect.h / shapedIcon.image.pixelRatio; + + const top = shapedIcon.top - border / image.pixelRatio; + const left = shapedIcon.left - border / image.pixelRatio; + const bottom = shapedIcon.bottom + border / image.pixelRatio; + const right = shapedIcon.right + border / image.pixelRatio; let tl, tr, br, bl; // text-fit mode @@ -122,7 +126,15 @@ function getIconQuads(anchor, shapedIcon, boxScale, line, layer, alongLine, shap br = br.matMult(matrix); } - return [new SymbolQuad(new Point(anchor.x, anchor.y), tl, tr, bl, br, shapedIcon.image.rect, 0, 0, minScale, Infinity)]; + // Icon quad is padded, so texture coordinates also need to be padded. + const textureRect = { + x: image.textureRect.x - border, + y: image.textureRect.y - border, + w: image.textureRect.w + border * 2, + h: image.textureRect.h + border * 2 + }; + + return [new SymbolQuad(new Point(anchor.x, anchor.y), tl, tr, bl, br, textureRect, 0, 0, minScale, Infinity)]; } /** @@ -147,6 +159,8 @@ function getGlyphQuads(anchor, shaping, boxScale, line, layer, alongLine, global const positionedGlyphs = shaping.positionedGlyphs; const quads = []; + let labelMinScale = minScale; + for (let k = 0; k < positionedGlyphs.length; k++) { const positionedGlyph = positionedGlyphs[k]; const glyph = positionedGlyph.glyph; @@ -158,12 +172,11 @@ function getGlyphQuads(anchor, shaping, boxScale, line, layer, alongLine, global const centerX = (positionedGlyph.x + glyph.advance / 2) * boxScale; let glyphInstances; - let labelMinScale = minScale; if (alongLine) { glyphInstances = []; - labelMinScale = getLineGlyphs(glyphInstances, anchor, centerX, line, anchor.segment, false); + labelMinScale = Math.max(labelMinScale, getLineGlyphs(glyphInstances, anchor, centerX, line, anchor.segment, false)); if (keepUpright) { - labelMinScale = Math.min(labelMinScale, getLineGlyphs(glyphInstances, anchor, centerX, line, anchor.segment, true)); + labelMinScale = Math.max(labelMinScale, getLineGlyphs(glyphInstances, anchor, centerX, line, anchor.segment, true)); } } else { diff --git a/src/symbol/shaping.js b/src/symbol/shaping.js index e2446b0a0ed..20076ea44bc 100644 --- a/src/symbol/shaping.js +++ b/src/symbol/shaping.js @@ -310,14 +310,12 @@ function align(positionedGlyphs, justify, horizontalAlign, verticalAlign, maxLin } function shapeIcon(image, iconOffset) { - if (!image || !image.rect) return null; - const dx = iconOffset[0]; const dy = iconOffset[1]; - const x1 = dx - image.width / 2; - const x2 = x1 + image.width; - const y1 = dy - image.height / 2; - const y2 = y1 + image.height; + const x1 = dx - image.displaySize[0] / 2; + const x2 = x1 + image.displaySize[0]; + const y1 = dy - image.displaySize[1] / 2; + const y2 = y1 + image.displaySize[1]; return new PositionedIcon(image, y1, y2, x1, x2); } diff --git a/src/symbol/sprite_atlas.js b/src/symbol/sprite_atlas.js index afcd4477b55..5239941eb41 100644 --- a/src/symbol/sprite_atlas.js +++ b/src/symbol/sprite_atlas.js @@ -5,6 +5,37 @@ const browser = require('../util/browser'); const util = require('../util/util'); const window = require('../util/window'); const Evented = require('../util/evented'); +const padding = 1; + +// This wants to be a class, but is sent to workers, so must be a plain JSON blob. +function spriteAtlasElement(image) { + const textureRect = { + x: image.rect.x + padding, + y: image.rect.y + padding, + w: image.rect.w - padding * 2, + h: image.rect.h - padding * 2 + }; + return { + sdf: image.sdf, + pixelRatio: image.pixelRatio, + isNativePixelRatio: image.pixelRatio === browser.devicePixelRatio, + textureRect: textureRect, + + // Redundant calculated members. + tl: [ + textureRect.x, + textureRect.y + ], + br: [ + textureRect.x + textureRect.w, + textureRect.y + textureRect.h + ], + displaySize: [ + textureRect.w / image.pixelRatio, + textureRect.h / image.pixelRatio + ] + }; +} // The SpriteAtlas class is responsible for turning a sprite and assorted // other images added at runtime into a texture that can be consumed by WebGL. @@ -12,32 +43,28 @@ class SpriteAtlas extends Evented { constructor(width, height) { super(); - - this.width = width; - this.height = height; - - this.shelfPack = new ShelfPack(width, height); this.images = {}; this.data = false; this.texture = 0; // WebGL ID this.filter = 0; // WebGL ID - this.pixelRatio = browser.devicePixelRatio > 1 ? 2 : 1; + this.width = width * browser.devicePixelRatio; + this.height = height * browser.devicePixelRatio; + this.shelfPack = new ShelfPack(this.width, this.height); this.dirty = true; } + getPixelSize() { + return [ + this.width, + this.height + ]; + } + allocateImage(pixelWidth, pixelHeight) { - pixelWidth = pixelWidth / this.pixelRatio; - pixelHeight = pixelHeight / this.pixelRatio; - - // 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. - // Pad icons to prevent them from polluting neighbours during linear interpolation - const padding = 2; - const packWidth = pixelWidth + padding + (4 - (pixelWidth + padding) % 4); - const packHeight = pixelHeight + padding + (4 - (pixelHeight + padding) % 4);// + 4; - - const rect = this.shelfPack.packOne(packWidth, packHeight); + const width = pixelWidth + 2 * padding; + const height = pixelHeight + 2 * padding; + + const rect = this.shelfPack.packOne(width, height); if (!rect) { util.warnOnce('SpriteAtlas out of space.'); return null; @@ -76,16 +103,15 @@ class SpriteAtlas extends Evented { return this.fire('error', {error: new Error('There is not enough space to add this image.')}); } - const image = { + this.images[name] = { rect, - width: width / pixelRatio, - height: height / pixelRatio, - sdf: false, - pixelRatio: pixelRatio / this.pixelRatio + width, + height, + pixelRatio, + sdf: false }; - this.images[name] = image; - this.copy(pixels, width, rect, {pixelRatio, x: 0, y: 0, width, height}, false); + this.copy(pixels, width, rect, {x: 0, y: 0, width, height}, false); this.fire('data', {dataType: 'style'}); } @@ -102,9 +128,19 @@ class SpriteAtlas extends Evented { this.fire('data', {dataType: 'style'}); } - getImage(name, wrap) { + // Return metrics for an icon image. + getIcon(name) { + return this._getImage(name, false); + } + + // Return metrics for repeating pattern image. + getPattern(name) { + return this._getImage(name, true); + } + + _getImage(name, wrap) { if (this.images[name]) { - return this.images[name]; + return spriteAtlasElement(this.images[name]); } if (!this.sprite) { @@ -123,10 +159,10 @@ class SpriteAtlas extends Evented { const image = { rect, - width: pos.width / pos.pixelRatio, - height: pos.height / pos.pixelRatio, + width: pos.width, + height: pos.height, sdf: pos.sdf, - pixelRatio: pos.pixelRatio / this.pixelRatio + pixelRatio: pos.pixelRatio }; this.images[name] = image; @@ -134,34 +170,12 @@ class SpriteAtlas extends Evented { const srcImg = new Uint32Array(this.sprite.imgData.buffer); this.copy(srcImg, this.sprite.width, rect, pos, wrap); - return image; - } - - // Return position of a repeating fill pattern. - getPosition(name, repeating) { - const image = this.getImage(name, repeating); - const rect = image && image.rect; - - if (!rect) { - return null; - } - - const width = image.width * image.pixelRatio; - const height = image.height * image.pixelRatio; - const padding = 1; - - return { - size: [image.width, image.height], - tl: [(rect.x + padding) / this.width, (rect.y + padding) / this.height], - br: [(rect.x + padding + width) / this.width, (rect.y + padding + height) / this.height] - }; + return spriteAtlasElement(image); } allocate() { if (!this.data) { - const w = Math.floor(this.width * this.pixelRatio); - const h = Math.floor(this.height * this.pixelRatio); - this.data = new Uint32Array(w * h); + this.data = new Uint32Array(this.width * this.height); for (let i = 0; i < this.data.length; i++) { this.data[i] = 0; } @@ -173,17 +187,15 @@ class SpriteAtlas extends Evented { this.allocate(); const dstImg = this.data; - const padding = 1; - copyBitmap( /* source buffer */ srcImg, /* source stride */ srcImgWidth, /* source x */ srcPos.x, /* source y */ srcPos.y, /* dest buffer */ dstImg, - /* dest stride */ this.width * this.pixelRatio, - /* dest x */ (dstPos.x + padding) * this.pixelRatio, - /* dest y */ (dstPos.y + padding) * this.pixelRatio, + /* dest stride */ this.getPixelSize()[0], + /* dest x */ dstPos.x + padding, + /* dest y */ dstPos.y + padding, /* icon dimension */ srcPos.width, /* icon dimension */ srcPos.height, /* wrap */ wrap @@ -196,18 +208,18 @@ class SpriteAtlas extends Evented { setSprite(sprite) { if (sprite && this.canvas) { - this.canvas.width = this.width * this.pixelRatio; - this.canvas.height = this.height * this.pixelRatio; + this.canvas.width = this.width; + this.canvas.height = this.height; } this.sprite = sprite; } addIcons(icons, callback) { - for (let i = 0; i < icons.length; i++) { - this.getImage(icons[i]); + const result = {}; + for (const icon of icons) { + result[icon] = this.getIcon(icon); } - - callback(null, this.images); + callback(null, result); } bind(gl, linear) { @@ -238,8 +250,8 @@ class SpriteAtlas extends Evented { gl.TEXTURE_2D, // enum target 0, // ind level gl.RGBA, // ind internalformat - this.width * this.pixelRatio, // GLsizei width - this.height * this.pixelRatio, // GLsizei height + this.width, // GLsizei width + this.height, // GLsizei height 0, // ind border gl.RGBA, // enum format gl.UNSIGNED_BYTE, // enum type @@ -251,8 +263,8 @@ class SpriteAtlas extends Evented { 0, // int level 0, // int xoffset 0, // int yoffset - this.width * this.pixelRatio, // long width - this.height * this.pixelRatio, // long height + this.width, // long width + this.height, // long height gl.RGBA, // enum format gl.UNSIGNED_BYTE, // enum type new Uint8Array(this.data.buffer) // Object pixels diff --git a/src/ui/camera.js b/src/ui/camera.js index bbc97ba5caa..3003ea53562 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -689,7 +689,7 @@ class Camera extends Evented { startBearing = this.getBearing(), startPitch = this.getPitch(); - const zoom = 'zoom' in options ? +options.zoom : startZoom; + const zoom = 'zoom' in options ? util.clamp(+options.zoom, tr.minZoom, tr.maxZoom) : startZoom; const bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing; const pitch = 'pitch' in options ? +options.pitch : startPitch; @@ -758,7 +758,7 @@ class Camera extends Evented { S = (r(1) - r0) / rho; // When u₀ = u₁, the optimal path doesn’t require both ascent and descent. - if (Math.abs(u1) < 0.000001) { + if (Math.abs(u1) < 0.000001 || isNaN(S)) { // Perform a more or less instantaneous transition if the path is too short. if (Math.abs(w0 - w1) < 0.000001) return this.easeTo(options, eventData); diff --git a/src/ui/control/attribution_control.js b/src/ui/control/attribution_control.js index f84d1c038ef..0ce99e81884 100644 --- a/src/ui/control/attribution_control.js +++ b/src/ui/control/attribution_control.js @@ -2,6 +2,7 @@ const DOM = require('../../util/dom'); const util = require('../../util/util'); +const config = require('../../util/config'); /** * An `AttributionControl` control presents the map's [attribution information](https://www.mapbox.com/help/attribution/). @@ -66,14 +67,26 @@ class AttributionControl { } _updateEditLink() { - if (!this._editLink) this._editLink = this._container.querySelector('.mapboxgl-improve-map'); + if (!this._editLink) this._editLink = this._container.querySelector('.mapbox-improve-map'); + const params = [ + {key: "owner", value: this.styleOwner}, + {key: "id", value: this.styleId}, + {key: "access_token", value: config.ACCESS_TOKEN} + ]; + if (this._editLink) { - const center = this._map.getCenter(); - this._editLink.href = `https://www.mapbox.com/map-feedback/#/${ - center.lng}/${center.lat}/${Math.round(this._map.getZoom() + 1)}`; + const paramString = params.reduce((acc, next, i) => { + if (next.value !== undefined) { + acc += `${next.key}=${next.value}${i < params.length - 1 ? '&' : ''}`; + } + return acc; + }, `?`); + this._editLink.href = `https://www.mapbox.com/feedback/${paramString}${this._map._hash ? this._map._hash.getHashString(true) : ''}`; + } } + _updateData(e) { if (e && e.sourceDataType === 'metadata') { this._updateAttributions(); @@ -85,6 +98,12 @@ class AttributionControl { if (!this._map.style) return; let attributions = []; + if (this._map.style.stylesheet) { + const stylesheet = this._map.style.stylesheet; + this.styleOwner = stylesheet.owner; + this.styleId = stylesheet.id; + } + const sourceCaches = this._map.style.sourceCaches; for (const id in sourceCaches) { const source = sourceCaches[id].getSource(); diff --git a/src/ui/hash.js b/src/ui/hash.js index 9068c055d7b..82efb9e0503 100644 --- a/src/ui/hash.js +++ b/src/ui/hash.js @@ -42,6 +42,28 @@ class Hash { return this; } + getHashString(mapFeedback) { + const center = this._map.getCenter(), + zoom = Math.round(this._map.getZoom() * 100) / 100, + precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)), + lng = Math.round(center.lng * Math.pow(10, precision)) / Math.pow(10, precision), + lat = Math.round(center.lat * Math.pow(10, precision)) / Math.pow(10, precision), + bearing = this._map.getBearing(), + pitch = this._map.getPitch(); + let hash = ''; + if (mapFeedback) { + // new map feedback site has some constraints that don't allow + // us to use the same hash format as we do for the Map hash option. + hash += `#/${lng}/${lat}/${zoom}`; + } else { + hash += `#${zoom}/${lat}/${lng}`; + } + + if (bearing || pitch) hash += (`/${Math.round(bearing * 10) / 10}`); + if (pitch) hash += (`/${Math.round(pitch)}`); + return hash; + } + _onHashChange() { const loc = window.location.hash.replace('#', '').split('/'); if (loc.length >= 3) { @@ -57,21 +79,10 @@ class Hash { } _updateHash() { - const center = this._map.getCenter(), - zoom = this._map.getZoom(), - bearing = this._map.getBearing(), - pitch = this._map.getPitch(), - precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); - - let hash = `#${Math.round(zoom * 100) / 100 - }/${center.lat.toFixed(precision) - }/${center.lng.toFixed(precision)}`; - - if (bearing || pitch) hash += (`/${Math.round(bearing * 10) / 10}`); - if (pitch) hash += (`/${Math.round(pitch)}`); - + const hash = this.getHashString(); window.history.replaceState('', '', hash); } + } module.exports = Hash; diff --git a/src/ui/map.js b/src/ui/map.js index 315edd95540..8f1e091eb6e 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -848,6 +848,7 @@ class Map extends Camera { this.style._remove(); this.off('rotate', this.style._redoPlacement); this.off('pitch', this.style._redoPlacement); + this.off('move', this.style._redoPlacement); } if (!style) { @@ -863,6 +864,7 @@ class Map extends Camera { this.on('rotate', this.style._redoPlacement); this.on('pitch', this.style._redoPlacement); + this.on('move', this.style._redoPlacement); return this; } @@ -1328,6 +1330,7 @@ class Map extends Camera { event.preventDefault(); if (this._frameId) { browser.cancelFrame(this._frameId); + this._frameId = null; } this.fire('webglcontextlost', {originalEvent: event}); } @@ -1460,6 +1463,7 @@ class Map extends Camera { remove() { if (this._hash) this._hash.remove(); browser.cancelFrame(this._frameId); + this._frameId = null; this.setStyle(null); if (typeof window !== 'undefined') { window.removeEventListener('resize', this._onWindowResize, false); diff --git a/src/util/is_char_in_unicode_block.js b/src/util/is_char_in_unicode_block.js index 5eb83f8174d..1c1c70da29e 100644 --- a/src/util/is_char_in_unicode_block.js +++ b/src/util/is_char_in_unicode_block.js @@ -18,15 +18,15 @@ const unicodeBlockLookup: UnicodeBlockLookup = { // 'Cyrillic': (char) => char >= 0x0400 && char <= 0x04FF, // 'Cyrillic Supplement': (char) => char >= 0x0500 && char <= 0x052F, // 'Armenian': (char) => char >= 0x0530 && char <= 0x058F, - // 'Hebrew': (char) => char >= 0x0590 && char <= 0x05FF, - // 'Arabic': (char) => char >= 0x0600 && char <= 0x06FF, - // 'Syriac': (char) => char >= 0x0700 && char <= 0x074F, - // 'Arabic Supplement': (char) => char >= 0x0750 && char <= 0x077F, + //'Hebrew': (char) => char >= 0x0590 && char <= 0x05FF, + 'Arabic': (char) => char >= 0x0600 && char <= 0x06FF, + //'Syriac': (char) => char >= 0x0700 && char <= 0x074F, + 'Arabic Supplement': (char) => char >= 0x0750 && char <= 0x077F, // 'Thaana': (char) => char >= 0x0780 && char <= 0x07BF, // 'NKo': (char) => char >= 0x07C0 && char <= 0x07FF, // 'Samaritan': (char) => char >= 0x0800 && char <= 0x083F, // 'Mandaic': (char) => char >= 0x0840 && char <= 0x085F, - // 'Arabic Extended-A': (char) => char >= 0x08A0 && char <= 0x08FF, + 'Arabic Extended-A': (char) => char >= 0x08A0 && char <= 0x08FF, // 'Devanagari': (char) => char >= 0x0900 && char <= 0x097F, // 'Bengali': (char) => char >= 0x0980 && char <= 0x09FF, // 'Gurmukhi': (char) => char >= 0x0A00 && char <= 0x0A7F, @@ -159,13 +159,13 @@ const unicodeBlockLookup: UnicodeBlockLookup = { 'Private Use Area': (char) => char >= 0xE000 && char <= 0xF8FF, 'CJK Compatibility Ideographs': (char) => char >= 0xF900 && char <= 0xFAFF, // 'Alphabetic Presentation Forms': (char) => char >= 0xFB00 && char <= 0xFB4F, - // 'Arabic Presentation Forms-A': (char) => char >= 0xFB50 && char <= 0xFDFF, + 'Arabic Presentation Forms-A': (char) => char >= 0xFB50 && char <= 0xFDFF, // 'Variation Selectors': (char) => char >= 0xFE00 && char <= 0xFE0F, 'Vertical Forms': (char) => char >= 0xFE10 && char <= 0xFE1F, // 'Combining Half Marks': (char) => char >= 0xFE20 && char <= 0xFE2F, 'CJK Compatibility Forms': (char) => char >= 0xFE30 && char <= 0xFE4F, 'Small Form Variants': (char) => char >= 0xFE50 && char <= 0xFE6F, - // 'Arabic Presentation Forms-B': (char) => char >= 0xFE70 && char <= 0xFEFF, + 'Arabic Presentation Forms-B': (char) => char >= 0xFE70 && char <= 0xFEFF, 'Halfwidth and Fullwidth Forms': (char) => char >= 0xFF00 && char <= 0xFFEF // 'Specials': (char) => char >= 0xFFF0 && char <= 0xFFFF, // 'Linear B Syllabary': (char) => char >= 0x10000 && char <= 0x1007F, diff --git a/src/util/script_detection.js b/src/util/script_detection.js index ad8fb597cfe..89b9564a804 100644 --- a/src/util/script_detection.js +++ b/src/util/script_detection.js @@ -17,6 +17,23 @@ module.exports.allowsVerticalWritingMode = function(chars) { return false; }; +module.exports.allowsLetterSpacing = function(chars) { + for (const char of chars) { + if (!exports.charAllowsLetterSpacing(char.charCodeAt(0))) return false; + } + return true; +}; + +module.exports.charAllowsLetterSpacing = function(char) { + if (isChar['Arabic'](char)) return false; + if (isChar['Arabic Supplement'](char)) return false; + if (isChar['Arabic Extended-A'](char)) return false; + if (isChar['Arabic Presentation Forms-A'](char)) return false; + if (isChar['Arabic Presentation Forms-B'](char)) return false; + + return true; +}; + module.exports.charAllowsIdeographicBreaking = function(char) { // Return early for characters outside all ideographic ranges. if (char < 0x2E80) return false; diff --git a/src/util/throttler.js b/src/util/throttler.js new file mode 100644 index 00000000000..ab5120adb0f --- /dev/null +++ b/src/util/throttler.js @@ -0,0 +1,55 @@ +'use strict'; + +const browser = require('./browser'); + +/** + * Throttles the provided function to run at most every + * 'frequency' milliseconds + * + * @interface Throttler + * @private + */ +class Throttler { + + constructor(frequency, throttledFunction) { + this.frequency = frequency; + this.throttledFunction = throttledFunction; + this.lastInvocation = 0; + } + + /** + * Request an invocation of the throttled function. + * + * @memberof Throttler + * @instance + */ + invoke() { + if (this.pendingInvocation) { + return; + } + + const timeToNextInvocation = this.lastInvocation === 0 ? + 0 : + (this.lastInvocation + this.frequency) - browser.now(); + + if (timeToNextInvocation <= 0) { + this.lastInvocation = browser.now(); + this.throttledFunction(); + } else { + this.pendingInvocation = setTimeout(() => { + this.pendingInvocation = undefined; + this.lastInvocation = browser.now(); + this.throttledFunction(); + }, timeToNextInvocation); + } + } + + stop() { + if (this.pendingInvocation) { + clearTimeout(this.pendingInvocation); + this.pendingInvocation = undefined; + } + } +} + +module.exports = Throttler; diff --git a/src/util/util.js b/src/util/util.js index 54bf987487c..a52c1361289 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -139,13 +139,11 @@ exports.keysDifference = function (obj: {[key: string]: mixed}, other: {[key: st * source objects. * * @param dest destination object - * @param {...Object} sources sources from which properties are pulled + * @param sources sources from which properties are pulled * @private */ -// eslint-disable-next-line no-unused-vars -exports.extend = function (dest: Object, source0: Object, source1?: Object, source2?: Object): Object { - for (let i = 1; i < arguments.length; i++) { - const src = arguments[i]; +exports.extend = function (dest: Object, ...sources: Array): Object { + for (const src of sources) { for (const k in src) { dest[k] = src[k]; } diff --git a/test/integration/image/rocket.png b/test/integration/image/rocket.png index 803c12830b7..36b68b19655 100644 Binary files a/test/integration/image/rocket.png and b/test/integration/image/rocket.png differ diff --git a/test/integration/query-tests/edge-cases/box-cutting-antimeridian-z0/expected.json b/test/integration/query-tests/edge-cases/box-cutting-antimeridian-z0/expected.json index 912955465e3..fcbe2f6360e 100644 --- a/test/integration/query-tests/edge-cases/box-cutting-antimeridian-z0/expected.json +++ b/test/integration/query-tests/edge-cases/box-cutting-antimeridian-z0/expected.json @@ -1,54 +1,54 @@ [ - { - "properties": { - "id": "B" + { + "properties": { + "id": "B" + }, + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 90, + 0 + ] + } }, - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -270, - 0 - ] - } - }, - { - "properties": { - "id": "C" - }, - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -90, - 0 - ] - } - }, - { - "properties": { - "id": "B" + { + "properties": { + "id": "C" + }, + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 270, + 0 + ] + } }, - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 90, - 0 - ] - } - }, - { - "properties": { - "id": "C" + { + "properties": { + "id": "B" + }, + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -270, + 0 + ] + } }, - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 270, - 0 - ] + { + "properties": { + "id": "C" + }, + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -90, + 0 + ] + } } - } -] \ No newline at end of file +] diff --git a/test/integration/query-tests/results.html.tmpl b/test/integration/query-tests/results.html.tmpl index 0ac1e923251..63c31a43677 100644 --- a/test/integration/query-tests/results.html.tmpl +++ b/test/integration/query-tests/results.html.tmpl @@ -11,7 +11,7 @@ -

<%- r.group %>/<%- r.test %>

+

<%- r.group %>/<%- r.test %> <% if (!r.ok) { %>(failed)<% } %>