diff --git a/.circleci/config.yml b/.circleci/config.yml index eee41c49067..3a7a3a8b256 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,12 +22,6 @@ workflows: filters: tags: only: /.*/ - - check-size: - requires: - - build - filters: - tags: - only: /.*/ - collect-stats: requires: - build @@ -157,6 +151,10 @@ jobs: - run: yarn run build-style-spec - run: yarn run build-flow-types - run: yarn run test-build + - run: + name: Check bundle size + command: | + node build/check-bundle-size.js - deploy: name: Trigger memory metrics when merging to main command: | @@ -174,17 +172,6 @@ jobs: paths: - dist - check-size: - <<: *defaults - steps: - - attach_workspace: - at: . - - run: - name: Check bundle size - command: | - node build/check-bundle-size.js "dist/mapbox-gl.js" "JS" - node build/check-bundle-size.js "dist/mapbox-gl.css" "CSS" - collect-stats: <<: *defaults steps: @@ -217,7 +204,10 @@ jobs: steps: - attach_workspace: at: . + - run: yarn run build-token - run: yarn run test-render + - store_test_results: + path: test/integration/render-tests - store_artifacts: path: "test/integration/render-tests/index.html" diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a0193f147..ff72653d228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +## 2.0.0 + +### ⚠️ Breaking changes + +- **mapbox-gl-js is no longer under the 3-Clause BSD license. By upgrading to this release, you are agreeing to [Mapbox terms of service](https://www.mapbox.com/legal/tos/).** Refer to LICENSE.txt for the new licensing terms and details. For questions, contact our team at [https://support.mapbox.com](https://support.mapbox.com). +- Beginning with v2.0.0, a billable map load occurs whenever a Map object is initialized. Before updating an existing implementation from v1.x.x to v2.x.x, please review the pricing documentation to estimate expected costs. +- Deprecate Internet Explorer 11, no longer supported from this release. ([#8283](https://github.com/mapbox/mapbox-gl-js/issues/8283), [#6391](https://github.com/mapbox/mapbox-gl-js/issues/6391)) +- Support for unlocked pitch up to 85°. The default `maxPitch` is increased from 60° to 85° which can result in viewing above the horizon line. By default, this area will be drawn transparent but a new sky layer can be added to the map in order to fill this space. The legacy behavior can be achieved by simply adding `maxPitch: 60` to the map options when instantiating your map. + +### ✨ Features and improvements + +- Add 3D terrain feature. All layer types and markers can now be extruded using the new `terrain` root level style-spec property or with the function `map.setTerrain()`. ([#1489](https://github.com/mapbox/mapbox-gl-js/issues/1489)) +- Add support for unlocked pitch up to 85° (previously 60°). ([#3731](https://github.com/mapbox/mapbox-gl-js/issues/3731)) +- Add a new sky layer acting as an infinite background above the horizon line. This layer can be used from the style-spec and has two types: `atmospheric` and `gradient`. +- Add a free form camera API, allowing for more complex camera manipulation in 3D, accessible using `map.getFreeCameraOptions()` and `map.setFreeCameraOptions()`. +- Improve performance by adopting a two-phase tile loading strategy, prioritizing rendering of non-symbol layers first. +- Improve performance by avoiding parsing vector tiles that were already aborted. +- Improve performance by adopting a preemptive shader compilation strategy. ([#9384](https://github.com/mapbox/mapbox-gl-js/issues/9384)) +- Improve performance by disabling fade-in animation for symbols and raster tiles on initial map load. +- Improve performance by defaulting to 2 workers on all platforms. ([#3153](https://github.com/mapbox/mapbox-gl-js/issues/3153)) +- Improve performance by loading tiles on the main thread at initial map load. +- Improve performance by using better worker task scheduling. + +### 🐞 Bug fixes + +- Avoid reloading `raster` and `raster-dem` tiles when the RTLTextPlugin loads. +- Add runtime evaluation of label collision boxes for more accurate symbol placement at fractional zoom levels and tilted views. +- Fix tile cache size for terrain DEM sources. +- Prevent holding on to DEM memory on the worker. +- Reduce memory used by `fill-extrusion`s. + +### 🛠️ Workflow + +- Run render tests in browser. + ## 1.13.0 ### ✨ Features and improvements diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b917c3c1de..caab09ab654 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,7 +111,7 @@ See [`bench/README.md`](./bench/README.md). * We use [`assert`](https://nodejs.org/api/assert.html) to check invariants that are not likely to be caused by user error. These `assert` statements are stripped out of production builds. * We use the following ES6 features: * `let`/`const` - * `for...of` loops (for arraylike iteration only, i.e. what is supported by [Bublé's `dangerousForOf` transform](https://buble.surge.sh/guide/#dangerous-transforms)) + * `for...of` loops * Arrow functions * Classes * Template strings @@ -120,8 +120,7 @@ See [`bench/README.md`](./bench/README.md). * Rest parameters * Destructuring * Modules -* 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) + * Spread (`...`) operator * Iterators and generators * "Library" features such as `Map`, `Set`, `array.find`, etc. diff --git a/LICENSE.txt b/LICENSE.txt index a5aa63e13bd..1054bedc9f8 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,18 +1,42 @@ +mapbox-gl-js v2.0 + +Mapbox Web SDK + Copyright (c) 2020, Mapbox All rights reserved. +Mapbox gl-js version 2.0 or higher (“Mapbox Web SDK”) must be used according to +the Mapbox Terms of Service. This license allows developers with a current active +Mapbox account to use and modify the Mapbox Web SDK. Developers may modify the +Mapbox Web SDK code so long as the modifications do not change or interfere with +marked portions of the code related to billing, accounting, and anonymized data +collection. The Mapbox Web SDK only sends anonymized usage data, which Mapbox uses +for fixing bugs and errors, accounting, and generating aggregated anonymized +statistics. This license terminates automatically if a user no longer has an +active Mapbox account. + +For the full license terms, please see the Mapbox Terms of Service at +https://www.mapbox.com/legal/tos/. + +------------------------------------------------------------------------------- + +Contains code from mapbox-gl-js v1.13 and earlier + +Version v1.13 of mapbox-gl-js and earlier are licensed under a BSD-3-Clause license + +Copyright (c) 2020, Mapbox Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of Mapbox GL JS nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of Mapbox GL JS nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT @@ -23,33 +47,8 @@ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -------------------------------------------------------------------------------- - -Contains code from glfx.js - -Copyright (C) 2011 by Evan Wallace - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- @@ -82,3 +81,27 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------- + +Contains code from glfx.js + +Copyright (C) 2011 by Evan Wallace + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 1593b86dced..cf95eca8bc5 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,10 @@ native SDKs. For code and issues specific to the native SDKs, see the ## License -Mapbox GL JS is licensed under the [3-Clause BSD license](./LICENSE.txt). -The licenses of its dependencies are tracked via [FOSSA](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fmapbox%2Fmapbox-gl-js): +Copyright © 2020 Mapbox -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fmapbox%2Fmapbox-gl-js.svg?type=large)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fmapbox%2Fmapbox-gl-js?ref=badge_large) +All rights reserved. + +Mapbox gl-js version 2.0 or higher (“Mapbox Web SDK”) must be used according to the Mapbox Terms of Service. This license allows developers with a current active Mapbox account to use and modify the Mapbox Web SDK. Developers may modify the Mapbox Web SDK code so long as the modifications do not change or interfere with marked portions of the code related to billing, accounting, and anonymized data collection. The Mapbox Web SDK only sends anonymized usage data, which Mapbox uses for fixing bugs and errors, accounting, and generating aggregated anonymized statistics. This license terminates automatically if a user no longer has an active Mapbox account. + +For the full license terms, please see the [Mapbox Terms of Service](https://www.mapbox.com/legal/tos/). diff --git a/bench/benchmarks/symbol_layout.js b/bench/benchmarks/symbol_layout.js index 69397653456..1152fc735bc 100644 --- a/bench/benchmarks/symbol_layout.js +++ b/bench/benchmarks/symbol_layout.js @@ -38,7 +38,8 @@ export default class SymbolLayout extends Layout { tileResult.iconMap, tileResult.imageAtlas.iconPositions, false, - tileResult.tileID.canonical); + tileResult.tileID.canonical, + tileResult.tileZoom); } } }); diff --git a/bench/lib/tile_parser.js b/bench/lib/tile_parser.js index e28afd39f2f..4370fcdad1e 100644 --- a/bench/lib/tile_parser.js +++ b/bench/lib/tile_parser.js @@ -116,12 +116,13 @@ export default class TileParser { parseTile(tile: {tileID: OverscaledTileID, buffer: ArrayBuffer}, returnDependencies?: boolean): Promise { const workerTile = new WorkerTile({ tileID: tile.tileID, + tileZoom: tile.tileID.overscaledZ, zoom: tile.tileID.overscaledZ, tileSize: 512, overscaling: 1, showCollisionBoxes: false, source: this.sourceID, - uid: '0', + uid: 0, maxZoom: 22, pixelRatio: 1, request: {url: ''}, @@ -130,7 +131,8 @@ export default class TileParser { cameraToCenterDistance: 0, cameraToTileDistance: 0, returnDependencies, - promoteId: undefined + promoteId: undefined, + isSymbolTile: false }); const vectorTile = new VT.VectorTile(new Protobuf(tile.buffer)); diff --git a/bench/rollup_config_benchmarks.js b/bench/rollup_config_benchmarks.js index 8ef5437de9a..b4ae0e99512 100644 --- a/bench/rollup_config_benchmarks.js +++ b/bench/rollup_config_benchmarks.js @@ -2,7 +2,6 @@ import fs from 'fs'; import sourcemaps from 'rollup-plugin-sourcemaps'; import replace from 'rollup-plugin-replace'; import {plugins} from '../build/rollup_plugins'; -import buble from 'rollup-plugin-buble'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; @@ -58,7 +57,6 @@ const viewConfig = { sourcemap: false }, plugins: [ - buble({transforms: {dangerousForOf: true}, objectAssign: true}), resolve({browser: true, preferBuiltins: false}), commonjs(), replace(replaceConfig) diff --git a/build/banner.js b/build/banner.js index cb164daba95..be976cfc5d9 100644 --- a/build/banner.js +++ b/build/banner.js @@ -1,4 +1,4 @@ import fs from 'fs'; const version = JSON.parse(fs.readFileSync('package.json')).version; -export default `/* Mapbox GL JS is licensed under the 3-Clause BSD License. Full text of license: https://github.com/mapbox/mapbox-gl-js/blob/v${version}/LICENSE.txt */`; +export default `/* Mapbox GL JS is Copyright © 2020 Mapbox and subject to the Mapbox Terms of Service ((https://www.mapbox.com/legal/tos/). */`; diff --git a/build/check-bundle-size.js b/build/check-bundle-size.js index 92346aa4bbb..ed8c2ba30d3 100755 --- a/build/check-bundle-size.js +++ b/build/check-bundle-size.js @@ -1,29 +1,13 @@ #!/usr/bin/env node /* eslint-disable */ - -const jwt = require('jsonwebtoken'); -const github = require('@octokit/rest')(); +const { Octokit } = require("@octokit/rest"); +const { createAppAuth } = require("@octokit/auth-app"); const prettyBytes = require('pretty-bytes'); const fs = require('fs'); const {execSync} = require('child_process'); const zlib = require('zlib'); -const SIZE_CHECK_APP_ID = 14028; -const SIZE_CHECK_APP_INSTALLATION_ID = 229425; - -const file = process.argv[2]; -const label = process.argv[3]; -const name = `Size - ${label}`; - -if (!file || !label) { - console.log(`Usage: ${process.argv[0]} ${process.argv[1]} FILE LABEL`); - process.exit(1); -} - -const {size} = fs.statSync(file); -const gzipSize = zlib.gzipSync(fs.readFileSync(file)).length; - process.on('unhandledRejection', error => { // don't log `error` directly, because errors from child_process.execSync // contain an (undocumented) `envPairs` with environment variable values @@ -31,74 +15,83 @@ process.on('unhandledRejection', error => { process.exit(1) }); -const pk = process.env['SIZE_CHECK_APP_PRIVATE_KEY']; -if (!pk) { +const SIZE_CHECK_APP_ID = 14028; +const SIZE_CHECK_APP_INSTALLATION_ID = 229425; + +const FILES = [ + ['JS', "dist/mapbox-gl.js"], + ['CSS', "dist/mapbox-gl.css"] +]; +const PK = Buffer.from(process.env['SIZE_CHECK_APP_PRIVATE_KEY'], 'base64').toString('binary'); +if (!PK) { console.log('Fork PR; not computing size.'); process.exit(0); } - -const key = Buffer.from(pk, 'base64').toString('binary'); -const payload = { - exp: Math.floor(Date.now() / 1000) + 60, - iat: Math.floor(Date.now() / 1000), - iss: SIZE_CHECK_APP_ID -}; - -const token = jwt.sign(payload, key, {algorithm: 'RS256'}); -github.authenticate({type: 'app', token}); - -function getMergeBase() { - const pr = process.env['CIRCLE_PULL_REQUEST']; - if (pr) { - const number = +pr.match(/\/(\d+)\/?$/)[1]; - return github.pullRequests.get({ - owner: 'mapbox', - repo: 'mapbox-gl-js', - pull_number: number - }).then(({data}) => { - const base = data.base.ref; - const head = process.env['CIRCLE_SHA1']; - return execSync(`git merge-base origin/${base} ${head}`).toString().trim(); - }); - } else { - // Walk backward through the history (maximum of 10 commits) until - // finding a commit on either main or release-*; assume that's the - // base branch. - const head = process.env['CIRCLE_SHA1']; - for (const sha of execSync(`git rev-list --max-count=10 ${head}`).toString().trim().split('\n')) { - const base = execSync(`git branch -r --contains ${sha} origin/main origin/release-* origin/publisher-production`).toString().split('\n')[0].trim().replace(/^origin\//, ''); - if (base) { - return Promise.resolve(execSync(`git merge-base origin/${base} ${head}`).toString().trim()); - } +const owner = 'mapbox'; +const repo = 'mapbox-gl-js'; + +(async () => { + // Initialize github client + const github = new Octokit({ + authStrategy: createAppAuth, + auth: { + id: SIZE_CHECK_APP_ID, + privateKey: PK, + installationId: SIZE_CHECK_APP_INSTALLATION_ID } - } - - return Promise.resolve(null); -} + }); -function getPriorSize(mergeBase) { - if (!mergeBase) { - console.log('No merge base available.'); - return Promise.resolve(null); + //get current sizes + const currentSizes = FILES.map(([label, filePath]) => [label, getSize(filePath)]); + console.log(currentSizes); + // Why we need to add GitHub's public key to known_hosts: + // https://circleci.com/docs/2.0/gh-bb-integration/#establishing-the-authenticity-of-an-ssh-host + execSync(`mkdir -p ~/.ssh`); + execSync(`echo 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== + bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw== + ' >> ~/.ssh/known_hosts`); + execSync(`git reset --hard && git checkout origin/main`); + execSync('yarn install'); + execSync('yarn run build-prod-min'); + const priorSizes = FILES.map(([label, filePath]) => [label, getSize(filePath)]); + console.log(priorSizes); + + // Generate a github check for each filetype + for(let check_idx=0; check_idx { - const run = data.check_runs.find(run => run.name === name); - if (run) { - const match = run.output.summary.match(/`[^`]+` is (\d+) bytes \([^\)]+\) uncompressed, (\d+) bytes \([^\)]+\) gzipped\./); - if (match) { - const prior = { size: +match[1], gzipSize: +match[2] }; - console.log(`Prior size was ${prettyBytes(prior.size)}, gzipped ${prior.gzipSize}.`); - return prior; - } - } - console.log('No matching check found.'); - return Promise.resolve(null); - }); +function getSize(filePath) { + const {size} = fs.statSync(filePath); + const gzipSize = zlib.gzipSync(fs.readFileSync(filePath)).length; + return {size, gzipSize}; } function formatSize(size, priorSize) { @@ -110,31 +103,3 @@ function formatSize(size, priorSize) { return prettyBytes(size); } } - -github.apps.createInstallationToken({installation_id: SIZE_CHECK_APP_INSTALLATION_ID}) - .then(({data}) => { - github.authenticate({type: 'token', token: data.token}); - getMergeBase().then(getPriorSize).then(prior => { - const title = `${formatSize(size, prior ? prior.size : null)}, gzipped ${formatSize(gzipSize, prior ? prior.gzipSize : null)}`; - - const megabit = Math.pow(2, 12); - const downloadTime3G = (gzipSize / (3 * megabit)).toFixed(0); - const downloadTime4G = (gzipSize / (10 * megabit)).toFixed(0); - const summary = `\`${file}\` is ${size} bytes (${prettyBytes(size)}) uncompressed, ${gzipSize} bytes (${prettyBytes(gzipSize)}) gzipped. -That's **${downloadTime3G} seconds** over slow 3G (3 Mbps), **${downloadTime4G} seconds** over fast 4G (10 Mbps).`; - - console.log(`Posting check result:\n${title}\n${summary}`); - - return github.checks.create({ - owner: 'mapbox', - repo: 'mapbox-gl-js', - name, - head_branch: process.env['CIRCLE_BRANCH'], - head_sha: process.env['CIRCLE_SHA1'], - status: 'completed', - conclusion: 'success', - completed_at: new Date().toISOString(), - output: { title, summary } - }); - }) - }); diff --git a/build/generate-flow-typed-style-spec.js b/build/generate-flow-typed-style-spec.js index cbbbda16917..c0295bbf321 100644 --- a/build/generate-flow-typed-style-spec.js +++ b/build/generate-flow-typed-style-spec.js @@ -32,6 +32,8 @@ function flowType(property) { } case 'light': return 'LightSpecification'; + case 'terrain': + return 'TerrainSpecification'; case 'sources': return '{[_: string]: SourceSpecification}'; case '*': @@ -99,7 +101,7 @@ function flowLayer(key) { return flowObject(spec[`layout_${key}`], ' ', '|'); }; - if (key === 'background') { + if (key === 'background' || key === 'sky') { delete layer.source; delete layer['source-layer']; delete layer.filter; @@ -175,6 +177,8 @@ ${flowObjectDeclaration('StyleSpecification', spec.$root)} ${flowObjectDeclaration('LightSpecification', spec.light)} +${flowObjectDeclaration('TerrainSpecification', spec.terrain)} + ${spec.source.map(key => flowObjectDeclaration(flowSourceTypeName(key), spec[key])).join('\n\n')} export type SourceSpecification = diff --git a/build/generate-struct-arrays.js b/build/generate-struct-arrays.js index 439af9d65ba..82cc1c0211f 100644 --- a/build/generate-struct-arrays.js +++ b/build/generate-struct-arrays.js @@ -127,10 +127,11 @@ createStructArrayType('raster_bounds', rasterBoundsAttributes); const circleAttributes = require('../src/data/bucket/circle_attributes').default; const fillAttributes = require('../src/data/bucket/fill_attributes').default; -const fillExtrusionAttributes = require('../src/data/bucket/fill_extrusion_attributes').default; const lineAttributes = require('../src/data/bucket/line_attributes').default; const lineAttributesExt = require('../src/data/bucket/line_attributes_ext').default; const patternAttributes = require('../src/data/bucket/pattern_attributes').default; +const skyboxAttributes = require('../src/render/skybox_attributes').default; +const {fillExtrusionAttributes, centroidAttributes} = require('../src/data/bucket/fill_extrusion_attributes'); // layout vertex arrays const layoutAttributes = { @@ -155,6 +156,7 @@ const { collisionBoxLayout, collisionCircleLayout, collisionVertexAttributes, + collisionVertexAttributesExt, quadTriangle, placement, symbolInstance, @@ -169,6 +171,7 @@ createStructArrayType('collision_box', collisionBox, true); createStructArrayType(`collision_box_layout`, collisionBoxLayout); createStructArrayType(`collision_circle_layout`, collisionCircleLayout); createStructArrayType(`collision_vertex`, collisionVertexAttributes); +createStructArrayType(`collision_vertex_ext`, collisionVertexAttributesExt); createStructArrayType(`quad_triangle`, quadTriangle); createStructArrayType('placed_symbol', placement, true); createStructArrayType('symbol_instance', symbolInstance, true); @@ -200,6 +203,9 @@ createStructArrayType('line_strip_index', createLayout([ { type: 'Uint16', name: 'vertices', components: 1 } ])); +// skybox vertex array +createStructArrayType(`skybox_vertex`, skyboxAttributes); + // paint vertex arrays // used by SourceBinder for float properties @@ -223,6 +229,9 @@ createStructArrayLayoutType(createLayout([{ components: 4 }], 4)); +// Fill extrusion specific array +createStructArrayType(`fill_extrusion_centroid`, centroidAttributes); + const layouts = Object.keys(layoutCache).map(k => layoutCache[k]); fs.writeFileSync('src/data/array_types.js', diff --git a/build/rollup_plugins.js b/build/rollup_plugins.js index 5a8fe2e5e56..0b8825cac8f 100644 --- a/build/rollup_plugins.js +++ b/build/rollup_plugins.js @@ -1,6 +1,5 @@ import flowRemoveTypes from '@mapbox/flow-remove-types'; -import buble from 'rollup-plugin-buble'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import unassert from 'rollup-plugin-unassert'; @@ -9,27 +8,31 @@ import {terser} from 'rollup-plugin-terser'; import minifyStyleSpec from './rollup_plugin_minify_style_spec'; import {createFilter} from 'rollup-pluginutils'; import strip from '@rollup/plugin-strip'; +import replace from 'rollup-plugin-replace'; // Common set of plugins/transformations shared across different rollup // builds (main mapboxgl bundle, style-spec package, benchmarks bundle) -export const plugins = (minified, production) => [ +export const plugins = (minified, production, test, bench) => [ flow(), minifyStyleSpec(), json(), production ? strip({ sourceMap: true, - functions: ['PerformanceUtils.*', 'Debug.*'] + functions: ['PerformanceUtils.*', 'WorkerPerformanceUtils.*', 'Debug.*'] + }) : false, + production || bench ? unassert() : false, + test ? replace({ + 'process.env.CI': JSON.stringify(process.env.CI), + 'process.env.UPDATE': JSON.stringify(process.env.UPDATE) }) : false, glsl('./src/shaders/*.glsl', production), - buble({transforms: {dangerousForOf: true}, objectAssign: "Object.assign"}), minified ? terser({ compress: { pure_getters: true, passes: 3 } }) : false, - production ? unassert() : false, resolve({ browser: true, preferBuiltins: false @@ -68,7 +71,7 @@ function glsl(include, minify) { .replace(/\n+/g, '\n') // collapse multi line breaks .replace(/\n\s+/g, '\n') // strip identation .replace(/\s?([+-\/*=,])\s?/g, '$1') // strip whitespace around operators - .replace(/([;\(\),\{\}])\n(?=[^#])/g, '$1'); // strip more line breaks + .replace(/([;,\{\}])\n(?=[^#])/g, '$1'); // strip more line breaks } return { diff --git a/build/test/build-tape.js b/build/test/build-tape.js index 78f11d8d1e4..cd558c12ce3 100644 --- a/build/test/build-tape.js +++ b/build/test/build-tape.js @@ -6,7 +6,7 @@ const fs = require('fs'); module.exports = function() { return new Promise((resolve, reject) => { browserify(require.resolve('../../test/util/tape_config.js'), { standalone: 'tape' }) - .transform("babelify", {presets: ["@babel/preset-env"], global: true}) + .transform("babelify", {presets: ["@babel/preset-env"], global: true, compact: true}) .bundle((err, buff) => { if (err) { throw err; } diff --git a/debug/3d-playground.html b/debug/3d-playground.html new file mode 100644 index 00000000000..c0c6a9f7994 --- /dev/null +++ b/debug/3d-playground.html @@ -0,0 +1,296 @@ + + + + Mapbox GL JS debug page + + + + + + + + +
+ + + + + + + diff --git a/debug/auth.html b/debug/auth.html new file mode 100644 index 00000000000..4fa5be668c4 --- /dev/null +++ b/debug/auth.html @@ -0,0 +1,45 @@ + + + + Mapbox GL JS debug page + + + + + + + +
+ + + + + diff --git a/debug/bounds.html b/debug/bounds.html index eb92005c89e..3cf8059ebc9 100644 --- a/debug/bounds.html +++ b/debug/bounds.html @@ -8,11 +8,16 @@
+
+
+
+ @@ -58,6 +63,21 @@ map.getSource('bounds').setData(geojson()); }); }); + + map.on('load', function() { + map.addSource('mapbox-dem', { + "type": "raster-dem", + "url": "mapbox://mapbox.terrain-rgb", + "tileSize": 512, + "maxzoom": 14 + }); + document.getElementById('terrain-checkbox').onclick(); + }); + + document.getElementById('terrain-checkbox').onclick = function() { + map.setTerrain(this.checked ? {"source": "mapbox-dem", "exaggeration": 1.5} : null); + }; + diff --git a/debug/circles.html b/debug/circles.html index 126ecf47b89..cdf00285e85 100644 --- a/debug/circles.html +++ b/debug/circles.html @@ -8,16 +8,29 @@
+
+
+
+
+
+
diff --git a/debug/debug-empty.html b/debug/debug-empty.html new file mode 100644 index 00000000000..e92859c6cfa --- /dev/null +++ b/debug/debug-empty.html @@ -0,0 +1,168 @@ + + + + Debug page + + + + + + + +
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/debug/free_camera.html b/debug/free_camera.html new file mode 100644 index 00000000000..51d4f9519af --- /dev/null +++ b/debug/free_camera.html @@ -0,0 +1,164 @@ + + + + Mapbox GL JS debug page + + + + + + + +
+
+ +
+ + + + + + diff --git a/debug/geojson_update.html b/debug/geojson_update.html new file mode 100644 index 00000000000..c8590617c14 --- /dev/null +++ b/debug/geojson_update.html @@ -0,0 +1,126 @@ + + + + + + + Mapbox GL JS debug page + + + + + + + + +
+ + + + + + + + + + diff --git a/debug/heatmap.html b/debug/heatmap.html index 61922291b82..eeb98d48fcf 100644 --- a/debug/heatmap.html +++ b/debug/heatmap.html @@ -8,11 +8,21 @@
+
+ +
@@ -60,8 +70,19 @@ ] } }, 'waterway-label'); + + map.addSource('mapbox-dem', { + "type": "raster-dem", + "url": "mapbox://mapbox.terrain-rgb", + "tileSize": 512, + "maxzoom": 14 + }); }); +document.getElementById('terrain').onclick = function() { + map.setTerrain(this.checked ? {"source": "mapbox-dem"} : undefined); +}; + diff --git a/debug/hillshade.html b/debug/hillshade.html index bea5071f649..5574964581e 100644 --- a/debug/hillshade.html +++ b/debug/hillshade.html @@ -8,6 +8,12 @@ @@ -59,6 +65,9 @@
+
+
+
@@ -78,6 +87,12 @@ "url": "mapbox://mapbox.terrain-rgb", "tileSize": 256, }); + map.addSource('mapbox-terrain', { + "type": "raster-dem", + "url": "mapbox://mapbox.terrain-rgb", + "tileSize": 512, + "maxzoom": 14 + }); map.addLayer({ "id": "Mapbox data", "source": "mapbox-dem", @@ -122,6 +137,7 @@ "visibility": "none" } }, 'waterway-river-canal-shadow'); + document.getElementById('terrain-checkbox').onclick(); }); var toggleableLayerIds = ['Mapbox data', 'Terrarium data', 'Mapbox data to Z8']; @@ -152,6 +168,10 @@ layers.appendChild(link); } +document.getElementById('terrain-checkbox').onclick = function() { + map.setTerrain(this.checked ? {"source": 'mapbox-terrain'} : null); +}; + diff --git a/debug/image.html b/debug/image.html index 7d225d4fef5..1b662f65be5 100644 --- a/debug/image.html +++ b/debug/image.html @@ -8,12 +8,20 @@
- +
+
+
diff --git a/debug/index.html b/debug/index.html index 04198d03ec9..cffdd3eef64 100644 --- a/debug/index.html +++ b/debug/index.html @@ -21,7 +21,7 @@ var map = window.map = new mapboxgl.Map({ container: 'map', zoom: 12.5, - center: [-77.01866, 38.888], + center: [-122.4194, 37.7749], style: 'mapbox://styles/mapbox/streets-v10', hash: true }); diff --git a/debug/markers.html b/debug/markers.html index 3f125d141f9..4b5fdb52c93 100644 --- a/debug/markers.html +++ b/debug/markers.html @@ -8,13 +8,16 @@
- +
+
+
+
diff --git a/debug/query_features.html b/debug/query_features.html index 2b3e482c603..87f6b81a16a 100644 --- a/debug/query_features.html +++ b/debug/query_features.html @@ -8,11 +8,15 @@
+
+
+
@@ -96,6 +100,18 @@ } }); +document.getElementById('terrain-checkbox').onclick = function() { + if (!map.getSource('mapbox-dem')) { + map.addSource('mapbox-dem', { + "type": "raster-dem", + "url": "mapbox://mapbox.terrain-rgb", + "tileSize": 512, + "maxzoom": 14 + }); + } + map.setTerrain(this.checked ? {"source": "mapbox-dem", "exaggeration": 1.5} : null); +}; + diff --git a/debug/satellite.html b/debug/satellite.html index c142fa8ab5d..0f87778892e 100644 --- a/debug/satellite.html +++ b/debug/satellite.html @@ -8,11 +8,21 @@
+
+ +
@@ -25,6 +35,19 @@ style: 'mapbox://styles/mapbox/satellite-v9', hash: true }); +map.on('load', function () { + map.addSource('mapbox-dem', { + "type": "raster-dem", + "url": "mapbox://mapbox.terrain-rgb", + "tileSize": 512, + "maxzoom": 14 + }); + document.getElementById('terrain').onclick(); +}); + +document.getElementById('terrain').onclick = function() { + map.setTerrain(this.checked ? {"source": "mapbox-dem"} : undefined); +}; diff --git a/debug/skybox-gradient.html b/debug/skybox-gradient.html new file mode 100644 index 00000000000..ac97938dfd5 --- /dev/null +++ b/debug/skybox-gradient.html @@ -0,0 +1,75 @@ + + + + + cubemap demo + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/debug/skybox.html b/debug/skybox.html new file mode 100644 index 00000000000..118d9a22011 --- /dev/null +++ b/debug/skybox.html @@ -0,0 +1,123 @@ + + + + + Mapbox GL JS Sky Demo + + + + + + + + +
+
+ + + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/debug/terrain-debug.html b/debug/terrain-debug.html new file mode 100644 index 00000000000..ed83c56b49f --- /dev/null +++ b/debug/terrain-debug.html @@ -0,0 +1,270 @@ + + + + Mapbox GL JS debug page + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + diff --git a/debug/terrain-hike.html b/debug/terrain-hike.html new file mode 100644 index 00000000000..11fd2c5ac95 --- /dev/null +++ b/debug/terrain-hike.html @@ -0,0 +1,126 @@ + + + + + + + Mapbox GL JS debug page + + + + + + + + +
+ + + + + + + + + + diff --git a/debug/video.html b/debug/video.html index 14aac3e329d..f595b1f8bfb 100644 --- a/debug/video.html +++ b/debug/video.html @@ -15,6 +15,9 @@
Play/pause
+
+
+
@@ -39,6 +42,12 @@ [-122.51309394836426, 37.563391708549425], [-122.51423120498657, 37.56161849366671] ] + }, + 'mapbox-dem': { + "type": "raster-dem", + "url": "mapbox://mapbox.terrain-rgb", + "tileSize": 512, + "maxzoom": 14 } }, "layers": [{ @@ -67,6 +76,10 @@ hash: false }); +document.getElementById('terrain-checkbox').onclick = function() { + map.setTerrain(this.checked ? {"source": "mapbox-dem"} : null); +}; + document.getElementById('timeslider') .addEventListener('change', function() { map.getSource('video').seek(parseFloat(this.value)); diff --git a/package.json b/package.json index 32b6484ba0b..4c7ffda71a5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mapbox-gl", "description": "A WebGL interactive maps library", - "version": "1.14.0-dev", + "version": "2.0.0-dev", "main": "dist/mapbox-gl.js", "style": "dist/mapbox-gl.css", "license": "SEE LICENSE IN LICENSE.txt", @@ -43,7 +43,8 @@ "@mapbox/gazetteer": "^4.0.4", "@mapbox/mapbox-gl-rtl-text": "^0.2.1", "@mapbox/mvt-fixtures": "^3.6.0", - "@octokit/rest": "^16.30.1", + "@octokit/auth-app": "^2.4.7", + "@octokit/rest": "^18.0.0", "@rollup/plugin-strip": "^1.3.1", "address": "^1.1.2", "babel-eslint": "^10.0.1", @@ -84,7 +85,6 @@ "nyc": "^13.3.0", "pirates": "^4.0.1", "pixelmatch": "^5.1.0", - "pngjs": "^3.4.0", "postcss-cli": "^6.1.2", "postcss-inline-svg": "^3.1.1", "pretty-bytes": "^5.1.0", @@ -94,7 +94,6 @@ "react-dom": "^16.8.6", "request": "^2.88.0", "rollup": "^1.23.1", - "rollup-plugin-buble": "^0.19.8", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-json": "^4.0.0", "rollup-plugin-node-resolve": "^5.2.0", @@ -109,7 +108,6 @@ "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", "tap": "~12.4.1", - "tap-parser": "^10.0.1", "tape": "^4.13.2", "tape-filter": "^1.0.4", "testem": "^3.0.0" @@ -123,10 +121,11 @@ "scripts": { "build-dev": "rollup -c --environment BUILD:dev", "watch-dev": "rollup -c --environment BUILD:dev --watch", + "build-bench": "rollup -c --environment BUILD:bench,MINIFY:true", "build-prod": "rollup -c --environment BUILD:production", "build-prod-min": "rollup -c --environment BUILD:production,MINIFY:true", "build-csp": "rollup -c rollup.config.csp.js", - "build-query-suite": "rollup -c test/integration/rollup.config.test.js", + "build-test-suite": "rollup -c test/integration/rollup.config.test.js", "build-flow-types": "mkdir -p dist && cp build/mapbox-gl.js.flow dist/mapbox-gl.js.flow && cp build/mapbox-gl.js.flow dist/mapbox-gl-dev.js.flow", "build-css": "postcss -o dist/mapbox-gl.css src/css/mapbox-gl.css", "build-style-spec": "cd src/style-spec && npm run build && cd ../.. && mkdir -p dist/style-spec && cp src/style-spec/dist/* dist/style-spec", @@ -135,9 +134,8 @@ "build-benchmarks": "BENCHMARK_VERSION=${BENCHMARK_VERSION:-\"$(git rev-parse --abbrev-ref HEAD) $(git rev-parse --short=7 HEAD)\"} rollup -c bench/versions/rollup_config_benchmarks.js", "watch-benchmarks": "BENCHMARK_VERSION=${BENCHMARK_VERSION:-\"$(git rev-parse --abbrev-ref HEAD) $(git rev-parse --short=7 HEAD)\"} rollup -c bench/rollup_config_benchmarks.js -w", "start-server": "st --no-cache -H 0.0.0.0 --port 9966 --index index.html .", - "start": "run-p build-token watch-css watch-query watch-benchmarks start-server", + "start": "run-p build-token watch-css watch-dev watch-benchmarks start-server", "start-debug": "run-p build-token watch-css watch-dev start-server", - "start-tests": "run-p build-token watch-css watch-query start-server", "start-bench": "run-p build-token watch-benchmarks start-server", "start-release": "run-s build-token build-prod-min build-css print-release-url start-server", "diff-tarball": "build/run-node build/diff-tarball && echo \"Please confirm the above is correct [y/n]? \"; read answer; if [ \"$answer\" = \"${answer#[Yy]}\" ]; then false; fi", @@ -151,10 +149,10 @@ "test-unit": "build/run-tap --reporter classic --no-coverage test/unit", "test-build": "build/run-tap --no-coverage test/build/**/*.test.js", "test-browser": "build/run-tap --reporter spec --no-coverage test/browser/**/*.test.js", - "test-render": "node --max-old-space-size=2048 test/render.test.js", - "test-query-node": "node test/query.test.js", - "watch-query": "testem -f test/integration/testem.js", - "test-query": "testem ci -f test/integration/testem.js -R xunit > test/integration/query-tests/test-results.xml", + "watch-render": "SUITE_NAME=render testem -f test/integration/testem.js", + "watch-query": "SUITE_NAME=query testem -f test/integration/testem.js", + "test-render": "CI=true SUITE_NAME=render testem ci -f test/integration/testem.js", + "test-query": "CI=true SUITE_NAME=query testem ci -f test/integration/testem.js", "test-expressions": "build/run-node test/expression.test.js", "test-flow": "build/run-node build/generate-flow-typed-style-spec && flow .", "test-cov": "nyc --require=@mapbox/flow-remove-types/register --reporter=text-summary --reporter=lcov --cache run-s test-unit test-expressions test-query test-render", @@ -168,6 +166,7 @@ "dist/style-spec/", "flow-typed/*.js", "src/", - ".flowconfig" + ".flowconfig", + "LICENSE.txt" ] } diff --git a/rollup.config.csp.js b/rollup.config.csp.js index 8e1aee8e5e2..9ce142fac68 100644 --- a/rollup.config.csp.js +++ b/rollup.config.csp.js @@ -15,7 +15,7 @@ const config = (input, file, format) => ({ banner }, treeshake: true, - plugins: plugins(true, true) + plugins: plugins(true, true, false) }); export default [ diff --git a/rollup.config.js b/rollup.config.js index b103700d52c..a0185e77841 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -6,9 +6,22 @@ import banner from './build/banner'; const {BUILD, MINIFY} = process.env; const minified = MINIFY === 'true'; const production = BUILD === 'production'; -const outputFile = - !production ? 'dist/mapbox-gl-dev.js' : - minified ? 'dist/mapbox-gl.js' : 'dist/mapbox-gl-unminified.js'; +const bench = BUILD === 'bench'; + +function buildType(build, minified) { + switch (build) { + case 'production': + if (minified) return 'dist/mapbox-gl.js'; + return 'dist/mapbox-gl-unminified.js'; + case 'bench': + return 'dist/mapbox-gl-bench.js'; + case 'dev': + return 'dist/mapbox-gl-dev.js'; + default: + return 'dist/mapbox-gl-dev.js'; + } +} +const outputFile = buildType(BUILD, MINIFY); export default [{ // First, use code splitting to bundle GL JS into three "chunks": @@ -28,7 +41,7 @@ export default [{ chunkFileNames: 'shared.js' }, treeshake: production, - plugins: plugins(minified, production) + plugins: plugins(minified, production, false, bench) }, { // Next, bundle together the three "chunks" produced in the previous pass // into a single, final bundle. See rollup/bundle_prelude.js and diff --git a/src/css/mapbox-gl.css b/src/css/mapbox-gl.css index 17e5ed318b3..31a1006617a 100644 --- a/src/css/mapbox-gl.css +++ b/src/css/mapbox-gl.css @@ -719,6 +719,12 @@ a.mapboxgl-ctrl-logo.mapboxgl-compact { top: 0; left: 0; will-change: transform; + opacity: 1; + transition: opacity 0.2s; +} + +.mapboxgl-marker-occluded { + opacity: 0.2; } .mapboxgl-user-location-dot { diff --git a/src/data/array_types.js b/src/data/array_types.js index bc15e318b2d..b9f500deaa3 100644 --- a/src/data/array_types.js +++ b/src/data/array_types.js @@ -72,80 +72,48 @@ class StructArrayLayout4i8 extends StructArray { StructArrayLayout4i8.prototype.bytesPerElement = 8; register('StructArrayLayout4i8', StructArrayLayout4i8); -/** - * Implementation of the StructArray layout: - * [0]: Int16[2] - * [4]: Int16[4] - * - * @private - */ -class StructArrayLayout2i4i12 extends StructArray { - uint8: Uint8Array; - int16: Int16Array; - - _refreshViews() { - this.uint8 = new Uint8Array(this.arrayBuffer); - this.int16 = new Int16Array(this.arrayBuffer); - } - - emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number) { - const i = this.length; - this.resize(i + 1); - return this.emplace(i, v0, v1, v2, v3, v4, v5); - } - - emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number) { - const o2 = i * 6; - this.int16[o2 + 0] = v0; - this.int16[o2 + 1] = v1; - this.int16[o2 + 2] = v2; - this.int16[o2 + 3] = v3; - this.int16[o2 + 4] = v4; - this.int16[o2 + 5] = v5; - return i; - } -} - -StructArrayLayout2i4i12.prototype.bytesPerElement = 12; -register('StructArrayLayout2i4i12', StructArrayLayout2i4i12); - /** * Implementation of the StructArray layout: * [0]: Int16[2] * [4]: Uint8[4] + * [8]: Float32[1] * * @private */ -class StructArrayLayout2i4ub8 extends StructArray { +class StructArrayLayout2i4ub1f12 extends StructArray { uint8: Uint8Array; int16: Int16Array; + float32: Float32Array; _refreshViews() { this.uint8 = new Uint8Array(this.arrayBuffer); this.int16 = new Int16Array(this.arrayBuffer); + this.float32 = new Float32Array(this.arrayBuffer); } - emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number) { + emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number) { const i = this.length; this.resize(i + 1); - return this.emplace(i, v0, v1, v2, v3, v4, v5); + return this.emplace(i, v0, v1, v2, v3, v4, v5, v6); } - emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number) { - const o2 = i * 4; - const o1 = i * 8; + emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number) { + const o2 = i * 6; + const o1 = i * 12; + const o4 = i * 3; this.int16[o2 + 0] = v0; this.int16[o2 + 1] = v1; this.uint8[o1 + 4] = v2; this.uint8[o1 + 5] = v3; this.uint8[o1 + 6] = v4; this.uint8[o1 + 7] = v5; + this.float32[o4 + 2] = v6; return i; } } -StructArrayLayout2i4ub8.prototype.bytesPerElement = 8; -register('StructArrayLayout2i4ub8', StructArrayLayout2i4ub8); +StructArrayLayout2i4ub1f12.prototype.bytesPerElement = 12; +register('StructArrayLayout2i4ub1f12', StructArrayLayout2i4ub1f12); /** * Implementation of the StructArray layout: @@ -331,49 +299,54 @@ register('StructArrayLayout1ul4', StructArrayLayout1ul4); /** * Implementation of the StructArray layout: - * [0]: Int16[6] - * [12]: Uint32[1] - * [16]: Uint16[2] + * [0]: Int16[2] + * [4]: Float32[4] + * [20]: Int16[1] + * [24]: Uint32[1] + * [28]: Uint16[2] * * @private */ -class StructArrayLayout6i1ul2ui20 extends StructArray { +class StructArrayLayout2i4f1i1ul2ui32 extends StructArray { uint8: Uint8Array; int16: Int16Array; + float32: Float32Array; uint32: Uint32Array; uint16: Uint16Array; _refreshViews() { this.uint8 = new Uint8Array(this.arrayBuffer); this.int16 = new Int16Array(this.arrayBuffer); + this.float32 = new Float32Array(this.arrayBuffer); this.uint32 = new Uint32Array(this.arrayBuffer); this.uint16 = new Uint16Array(this.arrayBuffer); } - emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number) { + emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number) { const i = this.length; this.resize(i + 1); - return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7, v8); + return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); } - emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number) { - const o2 = i * 10; - const o4 = i * 5; + emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number) { + const o2 = i * 16; + const o4 = i * 8; this.int16[o2 + 0] = v0; this.int16[o2 + 1] = v1; - this.int16[o2 + 2] = v2; - this.int16[o2 + 3] = v3; - this.int16[o2 + 4] = v4; - this.int16[o2 + 5] = v5; - this.uint32[o4 + 3] = v6; - this.uint16[o2 + 8] = v7; - this.uint16[o2 + 9] = v8; + this.float32[o4 + 1] = v2; + this.float32[o4 + 2] = v3; + this.float32[o4 + 3] = v4; + this.float32[o4 + 4] = v5; + this.int16[o2 + 10] = v6; + this.uint32[o4 + 6] = v7; + this.uint16[o2 + 14] = v8; + this.uint16[o2 + 15] = v9; return i; } } -StructArrayLayout6i1ul2ui20.prototype.bytesPerElement = 20; -register('StructArrayLayout6i1ul2ui20', StructArrayLayout6i1ul2ui20); +StructArrayLayout2i4f1i1ul2ui32.prototype.bytesPerElement = 32; +register('StructArrayLayout2i4f1i1ul2ui32', StructArrayLayout2i4f1i1ul2ui32); /** * Implementation of the StructArray layout: @@ -858,30 +831,32 @@ class CollisionBoxStruct extends Struct { y1: number; x2: number; y2: number; + padding: number; featureIndex: number; sourceLayerIndex: number; bucketIndex: number; anchorPoint: Point; get anchorPointX() { return this._structArray.int16[this._pos2 + 0]; } get anchorPointY() { return this._structArray.int16[this._pos2 + 1]; } - get x1() { return this._structArray.int16[this._pos2 + 2]; } - get y1() { return this._structArray.int16[this._pos2 + 3]; } - get x2() { return this._structArray.int16[this._pos2 + 4]; } - get y2() { return this._structArray.int16[this._pos2 + 5]; } - get featureIndex() { return this._structArray.uint32[this._pos4 + 3]; } - get sourceLayerIndex() { return this._structArray.uint16[this._pos2 + 8]; } - get bucketIndex() { return this._structArray.uint16[this._pos2 + 9]; } + get x1() { return this._structArray.float32[this._pos4 + 1]; } + get y1() { return this._structArray.float32[this._pos4 + 2]; } + get x2() { return this._structArray.float32[this._pos4 + 3]; } + get y2() { return this._structArray.float32[this._pos4 + 4]; } + get padding() { return this._structArray.int16[this._pos2 + 10]; } + get featureIndex() { return this._structArray.uint32[this._pos4 + 6]; } + get sourceLayerIndex() { return this._structArray.uint16[this._pos2 + 14]; } + get bucketIndex() { return this._structArray.uint16[this._pos2 + 15]; } get anchorPoint() { return new Point(this.anchorPointX, this.anchorPointY); } } -CollisionBoxStruct.prototype.size = 20; +CollisionBoxStruct.prototype.size = 32; export type CollisionBox = CollisionBoxStruct; /** * @private */ -export class CollisionBoxArray extends StructArrayLayout6i1ul2ui20 { +export class CollisionBoxArray extends StructArrayLayout2i4f1i1ul2ui32 { /** * Return the CollisionBoxStruct at the given location in the array. * @param {number} index The index of the element. @@ -1093,14 +1068,13 @@ register('FeatureIndexArray', FeatureIndexArray); export { StructArrayLayout2i4, StructArrayLayout4i8, - StructArrayLayout2i4i12, - StructArrayLayout2i4ub8, + StructArrayLayout2i4ub1f12, StructArrayLayout2f8, StructArrayLayout10ui20, StructArrayLayout4i4ui4i24, StructArrayLayout3f12, StructArrayLayout1ul4, - StructArrayLayout6i1ul2ui20, + StructArrayLayout2i4f1i1ul2ui32, StructArrayLayout2i2i2i12, StructArrayLayout2f1f2i16, StructArrayLayout2ub2f12, @@ -1117,9 +1091,9 @@ export { StructArrayLayout4i8 as RasterBoundsArray, StructArrayLayout2i4 as CircleLayoutArray, StructArrayLayout2i4 as FillLayoutArray, - StructArrayLayout2i4i12 as FillExtrusionLayoutArray, + StructArrayLayout4i8 as FillExtrusionLayoutArray, StructArrayLayout2i4 as HeatmapLayoutArray, - StructArrayLayout2i4ub8 as LineLayoutArray, + StructArrayLayout2i4ub1f12 as LineLayoutArray, StructArrayLayout2f8 as LineExtLayoutArray, StructArrayLayout10ui20 as PatternLayoutArray, StructArrayLayout4i4ui4i24 as SymbolLayoutArray, @@ -1128,8 +1102,11 @@ export { StructArrayLayout2i2i2i12 as CollisionBoxLayoutArray, StructArrayLayout2f1f2i16 as CollisionCircleLayoutArray, StructArrayLayout2ub2f12 as CollisionVertexArray, + StructArrayLayout3f12 as CollisionVertexExtArray, StructArrayLayout3ui6 as QuadTriangleArray, StructArrayLayout3ui6 as TriangleIndexArray, StructArrayLayout2ui4 as LineIndexArray, - StructArrayLayout1ui2 as LineStripIndexArray + StructArrayLayout1ui2 as LineStripIndexArray, + StructArrayLayout3f12 as SkyboxVertexArray, + StructArrayLayout2ui4 as FillExtrusionCentroidArray }; diff --git a/src/data/bucket.js b/src/data/bucket.js index 5bbe276d4b2..00abec2335b 100644 --- a/src/data/bucket.js +++ b/src/data/bucket.js @@ -17,7 +17,8 @@ export type BucketParameters = { overscaling: number, collisionBoxArray: CollisionBoxArray, sourceLayerIndex: number, - sourceID: string + sourceID: string, + enableTerrain: boolean } export type PopulateParameters = { diff --git a/src/data/bucket/fill_extrusion_attributes.js b/src/data/bucket/fill_extrusion_attributes.js index f8c6d70afd0..a1ad511442b 100644 --- a/src/data/bucket/fill_extrusion_attributes.js +++ b/src/data/bucket/fill_extrusion_attributes.js @@ -1,10 +1,12 @@ // @flow import {createLayout} from '../../util/struct_array'; -const layout = createLayout([ - {name: 'a_pos', components: 2, type: 'Int16'}, - {name: 'a_normal_ed', components: 4, type: 'Int16'}, -], 4); +export const fillExtrusionAttributes = createLayout([ + {name: 'a_pos_normal_ed', components: 4, type: 'Int16'} +]); -export default layout; -export const {members, size, alignment} = layout; +export const centroidAttributes = createLayout([ + {name: 'a_centroid_pos', components: 2, type: 'Uint16'} +]); + +export const {members, size, alignment} = fillExtrusionAttributes; diff --git a/src/data/bucket/fill_extrusion_bucket.js b/src/data/bucket/fill_extrusion_bucket.js index b45ba4b2ce9..cf1451ee018 100644 --- a/src/data/bucket/fill_extrusion_bucket.js +++ b/src/data/bucket/fill_extrusion_bucket.js @@ -1,8 +1,8 @@ // @flow -import {FillExtrusionLayoutArray} from '../array_types'; +import {FillExtrusionLayoutArray, FillExtrusionCentroidArray} from '../array_types'; -import {members as layoutAttributes} from './fill_extrusion_attributes'; +import {members as layoutAttributes, centroidAttributes} from './fill_extrusion_attributes'; import SegmentVector from '../segment'; import {ProgramConfigurationSet} from '../program_configuration'; import {TriangleIndexArray} from '../index_array_type'; @@ -18,6 +18,8 @@ import {hasPattern, addPatternDependencies} from './pattern_bucket_features'; import loadGeometry from '../load_geometry'; import toEvaluationFeature from '../evaluation_feature'; import EvaluationParameters from '../../style/evaluation_parameters'; +import Point from '@mapbox/point-geometry'; +import {number as interpolate} from '../../style-spec/util/interpolate'; import type {CanonicalTileID} from '../../source/tile_id'; import type { @@ -32,30 +34,137 @@ import type FillExtrusionStyleLayer from '../../style/style_layer/fill_extrusion import type Context from '../../gl/context'; import type IndexBuffer from '../../gl/index_buffer'; import type VertexBuffer from '../../gl/vertex_buffer'; -import type Point from '@mapbox/point-geometry'; import type {FeatureStates} from '../../source/source_state'; import type {ImagePosition} from '../../render/image_atlas'; const FACTOR = Math.pow(2, 13); -function addVertex(vertexArray, x, y, nx, ny, nz, t, e) { +function addVertex(vertexArray, x, y, nxRatio, nySign, normalUp, top, e) { vertexArray.emplaceBack( - // a_pos - x, - y, - // a_normal_ed: 3-component normal and 1-component edgedistance - Math.floor(nx * FACTOR) * 2 + t, - ny * FACTOR * 2, - nz * FACTOR * 2, + // a_pos_normal_ed: + // Encode top and side/up normal using the least significant bits + (x << 1) + top, + (y << 1) + normalUp, + // dxdy is signed, encode quadrant info using the least significant bit + (Math.floor(nxRatio * FACTOR) << 1) + nySign, // edgedistance (used for wrapping patterns around extrusion sides) Math.round(e) ); } +class PartMetadata { + acc: Point; + min: Point; + max: Point; + polyCount: Array<{edges: number, top: number}>; + currentPolyCount: {edges: number, top: number}; + borders: Array<[number, number]>; // Array<[min, max]> + vertexArrayOffset: number; + + constructor() { + this.acc = new Point(0, 0); + this.polyCount = []; + } + + startRing(p: Point) { + this.currentPolyCount = {edges: 0, top: 0}; + this.polyCount.push(this.currentPolyCount); + if (this.min) return; + this.min = new Point(p.x, p.y); + this.max = new Point(p.x, p.y); + } + + append(p: Point, prev: Point) { + this.currentPolyCount.edges++; + + this.acc._add(p); + let checkBorders = !!this.borders; + + const min = this.min, max = this.max; + if (p.x < min.x) { + min.x = p.x; + checkBorders = true; + } else if (p.x > max.x) { + max.x = p.x; + checkBorders = true; + } + if (p.y < min.y) { + min.y = p.y; + checkBorders = true; + } else if (p.y > max.y) { + max.y = p.y; + checkBorders = true; + } + if (((p.x === 0 || p.x === EXTENT) && p.x === prev.x) !== ((p.y === 0 || p.y === EXTENT) && p.y === prev.y)) { + // Custom defined geojson buildings are cut on borders. Points are + // repeated when edge cuts tile corner (reason for using xor). + this.processBorderOverlap(p, prev); + } + if (checkBorders) this.checkBorderIntersection(p, prev); + } + + checkBorderIntersection(p: Point, prev: Point) { + if ((prev.x < 0) !== (p.x < 0)) { + this.addBorderIntersection(0, interpolate(prev.y, p.y, (0 - prev.x) / (p.x - prev.x))); + } + if ((prev.x > EXTENT) !== (p.x > EXTENT)) { + this.addBorderIntersection(1, interpolate(prev.y, p.y, (EXTENT - prev.x) / (p.x - prev.x))); + } + if ((prev.y < 0) !== (p.y < 0)) { + this.addBorderIntersection(2, interpolate(prev.x, p.x, (0 - prev.y) / (p.y - prev.y))); + } + if ((prev.y > EXTENT) !== (p.y > EXTENT)) { + this.addBorderIntersection(3, interpolate(prev.x, p.x, (EXTENT - prev.y) / (p.y - prev.y))); + } + } + + addBorderIntersection(index: 0 | 1 | 2 | 3, i: number) { + if (!this.borders) { + this.borders = [ + [Number.MAX_VALUE, -Number.MAX_VALUE], + [Number.MAX_VALUE, -Number.MAX_VALUE], + [Number.MAX_VALUE, -Number.MAX_VALUE], + [Number.MAX_VALUE, -Number.MAX_VALUE] + ]; + } + const b = this.borders[index]; + if (i < b[0]) b[0] = i; + if (i > b[1]) b[1] = i; + } + + processBorderOverlap(p: Point, prev: Point) { + if (p.x === prev.x) { + if (p.y === prev.y) return; // custom defined geojson could have points repeated. + const index = p.x === 0 ? 0 : 1; + this.addBorderIntersection(index, prev.y); + this.addBorderIntersection(index, p.y); + } else { + assert(p.y === prev.y); + const index = p.y === 0 ? 2 : 3; + this.addBorderIntersection(index, prev.x); + this.addBorderIntersection(index, p.x); + } + } + + centroid(): Point { + const count = this.polyCount.reduce((acc, p) => acc + p.edges, 0); + return count !== 0 ? this.acc.div(count)._round() : new Point(0, 0); + } + + span(): Point { + return new Point(this.max.x - this.min.x, this.max.y - this.min.y); + } + + intersectsCount(): number { + return this.borders.reduce((acc, p) => acc + +(p[0] !== Number.MAX_VALUE), 0); + } +} + class FillExtrusionBucket implements Bucket { index: number; zoom: number; overscaling: number; + enableTerrain: boolean; layers: Array; layerIds: Array; stateDependentLayers: Array; @@ -64,6 +173,9 @@ class FillExtrusionBucket implements Bucket { layoutVertexArray: FillExtrusionLayoutArray; layoutVertexBuffer: VertexBuffer; + centroidVertexArray: FillExtrusionCentroidArray; + centroidVertexBuffer: VertexBuffer; + indexArray: TriangleIndexArray; indexBuffer: IndexBuffer; @@ -73,6 +185,13 @@ class FillExtrusionBucket implements Bucket { uploaded: boolean; features: Array; + featuresOnBorder: Array; + // borders / borderDone: 0 - left, 1, right, 2 - top, 3 - bottom + borders: Array>; // For each side, indices into featuresOnBorder array. + borderDone: Array; + needsCentroidUpdate: boolean; + tileToMeter: number; // cache conversion. + constructor(options: BucketParameters) { this.zoom = options.zoom; this.overscaling = options.overscaling; @@ -82,16 +201,21 @@ class FillExtrusionBucket implements Bucket { this.hasPattern = false; this.layoutVertexArray = new FillExtrusionLayoutArray(); + this.centroidVertexArray = new FillExtrusionCentroidArray(); this.indexArray = new TriangleIndexArray(); this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom); this.segments = new SegmentVector(); this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); - + this.enableTerrain = options.enableTerrain; } populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { this.features = []; this.hasPattern = hasPattern('fill-extrusion', this.layers, options); + this.featuresOnBorder = []; + this.borders = [[], [], [], []]; + this.borderDone = [false, false, false, false]; + this.tileToMeter = tileToMeter(canonical); for (const {feature, id, index, sourceLayerIndex} of features) { const needGeometry = this.layers[0]._featureFilter.needGeometry; @@ -115,8 +239,9 @@ class FillExtrusionBucket implements Bucket { this.addFeature(bucketFeature, bucketFeature.geometry, index, canonical, {}); } - options.featureIndex.insert(feature, bucketFeature.geometry, index, sourceLayerIndex, this.index, true); + options.featureIndex.insert(feature, bucketFeature.geometry, index, sourceLayerIndex, this.index); } + this.sortBorders(); } addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { @@ -124,6 +249,7 @@ class FillExtrusionBucket implements Bucket { const {geometry} = feature; this.addFeature(feature, geometry, feature.index, canonical, imagePositions); } + this.sortBorders(); } update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) { @@ -148,32 +274,48 @@ class FillExtrusionBucket implements Bucket { this.uploaded = true; } + uploadCentroid(context: Context) { + if (this.centroidVertexArray.length === 0) return; + if (!this.centroidVertexBuffer) { + this.centroidVertexBuffer = context.createVertexBuffer(this.centroidVertexArray, centroidAttributes.members, true); + } else if (this.needsCentroidUpdate) { + this.centroidVertexBuffer.updateData(this.centroidVertexArray); + } + this.needsCentroidUpdate = false; + } + destroy() { if (!this.layoutVertexBuffer) return; this.layoutVertexBuffer.destroy(); + if (this.centroidVertexBuffer) this.centroidVertexBuffer.destroy(); this.indexBuffer.destroy(); this.programConfigurations.destroy(); this.segments.destroy(); } addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { + const flatRoof = this.enableTerrain && feature.properties && feature.properties.hasOwnProperty('type') && + feature.properties.hasOwnProperty('height') && vectorTileFeatureTypes[feature.type] === 'Polygon'; + + const metadata = flatRoof ? new PartMetadata() : null; + for (const polygon of classifyRings(geometry, EARCUT_MAX_RINGS)) { let numVertices = 0; - for (const ring of polygon) { - numVertices += ring.length; - } let segment = this.segments.prepareSegment(4, this.layoutVertexArray, this.indexArray); - for (const ring of polygon) { - if (ring.length === 0) { - continue; - } + if (polygon.length === 0 || isEntirelyOutside(polygon[0])) { + continue; + } - if (isEntirelyOutside(ring)) { + for (let i = 0; i < polygon.length; i++) { + const ring = polygon[i]; + if (ring.length === 0) { continue; } + numVertices += ring.length; let edgeDistance = 0; + if (metadata) metadata.startRing(ring[0]); for (let p = 0; p < ring.length; p++) { const p1 = ring[p]; @@ -182,21 +324,26 @@ class FillExtrusionBucket implements Bucket { const p2 = ring[p - 1]; if (!isBoundaryEdge(p1, p2)) { + if (metadata) metadata.append(p1, p2); if (segment.vertexLength + 4 > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) { segment = this.segments.prepareSegment(4, this.layoutVertexArray, this.indexArray); } - const perp = p1.sub(p2)._perp()._unit(); + const d = p1.sub(p2)._perp(); + // Given that nz === 0, encode nx / (abs(nx) + abs(ny)) and signs. + // This information is sufficient to reconstruct normal vector in vertex shader. + const nxRatio = d.x / (Math.abs(d.x) + Math.abs(d.y)); + const nySign = d.y > 0 ? 1 : 0; const dist = p2.dist(p1); if (edgeDistance + dist > 32768) edgeDistance = 0; - addVertex(this.layoutVertexArray, p1.x, p1.y, perp.x, perp.y, 0, 0, edgeDistance); - addVertex(this.layoutVertexArray, p1.x, p1.y, perp.x, perp.y, 0, 1, edgeDistance); + addVertex(this.layoutVertexArray, p1.x, p1.y, nxRatio, nySign, 0, 0, edgeDistance); + addVertex(this.layoutVertexArray, p1.x, p1.y, nxRatio, nySign, 0, 1, edgeDistance); edgeDistance += dist; - addVertex(this.layoutVertexArray, p2.x, p2.y, perp.x, perp.y, 0, 0, edgeDistance); - addVertex(this.layoutVertexArray, p2.x, p2.y, perp.x, perp.y, 0, 1, edgeDistance); + addVertex(this.layoutVertexArray, p2.x, p2.y, nxRatio, nySign, 0, 0, edgeDistance); + addVertex(this.layoutVertexArray, p2.x, p2.y, nxRatio, nySign, 0, 1, edgeDistance); const bottomRight = segment.vertexLength; @@ -228,7 +375,8 @@ class FillExtrusionBucket implements Bucket { const holeIndices = []; const triangleIndex = segment.vertexLength; - for (const ring of polygon) { + for (let i = 0; i < polygon.length; i++) { + const ring = polygon[i]; if (ring.length === 0) { continue; } @@ -244,6 +392,7 @@ class FillExtrusionBucket implements Bucket { flattened.push(p.x); flattened.push(p.y); + if (metadata) metadata.currentPolyCount.top++; } } @@ -262,11 +411,78 @@ class FillExtrusionBucket implements Bucket { segment.vertexLength += numVertices; } + if (metadata && metadata.polyCount.length > 0) { + // When building is split between tiles, don't handle flat roofs here. + if (metadata.borders) { + // Store to the bucket. Flat roofs are handled in flatRoofsUpdate, + // after joining parts that lay in different buckets. + metadata.vertexArrayOffset = this.centroidVertexArray.length; + const borders = metadata.borders; + const index = this.featuresOnBorder.push(metadata) - 1; + for (let i = 0; i < 4; i++) { + if (borders[i][0] !== Number.MAX_VALUE) { this.borders[i].push(index); } + } + } + this.encodeCentroid(metadata.borders ? undefined : metadata.centroid(), metadata); + assert(!this.centroidVertexArray.length || this.centroidVertexArray.length === this.layoutVertexArray.length); + } + this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, canonical); } + + sortBorders() { + for (let i = 0; i < 4; i++) { + // Sort by border intersection area minimums, ascending. + this.borders[i].sort((a, b) => this.featuresOnBorder[a].borders[i][0] - this.featuresOnBorder[b].borders[i][0]); + } + } + + encodeCentroid(c: Point, metadata: PartMetadata, append: boolean = true) { + let x, y; + // Encoded centroid x and y: + // x y + // --------------------------------------------- + // 0 0 Default, no flat roof. + // 0 1 Hide, used to hide parts of buildings on border while expecting the other side to get loaded + // >0 0 Elevation encoded to uint16 word + // >0 >0 Encoded centroid position and x & y span + if (c) { + if (c.y !== 0) { + const span = metadata.span()._mult(this.tileToMeter); + x = (Math.max(c.x, 1) << 3) + Math.min(7, Math.round(span.x / 10)); + y = (Math.max(c.y, 1) << 3) + Math.min(7, Math.round(span.y / 10)); + } else { // encode height: + x = Math.ceil(c.x * 7.3); + y = 0; + } + } else { + // Use the impossible situation (building that has width and doesn't cross border cannot have centroid + // at border) to encode unprocessed border building: it is initially (append === true) hidden until + // computing centroid for joined building parts in rendering thread (flatRoofsUpdate). If it intersects more than + // two borders, flat roof approach is not applied. + x = 0; + y = +append; // Hide (1) initially when creating - visibility is changed in draw_fill_extrusion as soon as neighbor tile gets loaded. + } + + assert(append || metadata.vertexArrayOffset !== undefined); + let offset = append ? this.centroidVertexArray.length : metadata.vertexArrayOffset; + for (const polyInfo of metadata.polyCount) { + if (append) { + this.centroidVertexArray.resize(this.centroidVertexArray.length + polyInfo.edges * 4 + polyInfo.top); + } + for (let i = 0; i < polyInfo.edges * 2; i++) { + this.centroidVertexArray.emplace(offset++, 0, y); + this.centroidVertexArray.emplace(offset++, x, y); + } + for (let i = 0; i < polyInfo.top; i++) { + this.centroidVertexArray.emplace(offset++, x, y); + } + } + } } register('FillExtrusionBucket', FillExtrusionBucket, {omit: ['layers', 'features']}); +register('PartMetadata', PartMetadata); export default FillExtrusionBucket; @@ -276,8 +492,19 @@ function isBoundaryEdge(p1, p2) { } function isEntirelyOutside(ring) { - return ring.every(p => p.x < 0) || - ring.every(p => p.x > EXTENT) || - ring.every(p => p.y < 0) || - ring.every(p => p.y > EXTENT); + // Discard rings with corners on border if all other vertices are outside: they get defined + // also in the tile across the border. Eventual zero area rings at border are discarded by classifyRings + // and there is no need to handle that case here. + return ring.every(p => p.x <= 0) || + ring.every(p => p.x >= EXTENT) || + ring.every(p => p.y <= 0) || + ring.every(p => p.y >= EXTENT); +} + +function tileToMeter(canonical: CanonicalTileID) { + const circumferenceAtEquator = 40075017; + const mercatorY = canonical.y / (1 << canonical.z); + const exp = Math.exp(Math.PI * (1 - 2 * mercatorY)); + // simplify cos(2 * atan(e) - PI/2) from mercator_coordinate.js, remove trigonometrics. + return circumferenceAtEquator * 2 * exp / (exp * exp + 1) / EXTENT / (1 << canonical.z); } diff --git a/src/data/bucket/line_attributes.js b/src/data/bucket/line_attributes.js index 51678b408e1..6e308f6d29c 100644 --- a/src/data/bucket/line_attributes.js +++ b/src/data/bucket/line_attributes.js @@ -3,7 +3,8 @@ import {createLayout} from '../../util/struct_array'; const lineLayoutAttributes = createLayout([ {name: 'a_pos_normal', components: 2, type: 'Int16'}, - {name: 'a_data', components: 4, type: 'Uint8'} + {name: 'a_data', components: 4, type: 'Uint8'}, + {name: 'a_linesofar', components: 1, type: 'Float32'} ], 4); export default lineLayoutAttributes; diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index 64a029b3dd0..48bc6f852e3 100644 --- a/src/data/bucket/line_bucket.js +++ b/src/data/bucket/line_bucket.js @@ -60,17 +60,6 @@ const SHARP_CORNER_OFFSET = 15; // Angle per triangle for approximating round line joins. const DEG_PER_TRIANGLE = 20; -// The number of bits that is used to store the line distance in the buffer. -const LINE_DISTANCE_BUFFER_BITS = 15; - -// We don't have enough bits for the line distance as we'd like to have, so -// use this value to scale the line distance (in tile units) down to a smaller -// value. This lets us store longer distances while sacrificing precision. -const LINE_DISTANCE_SCALE = 1 / 2; - -// The maximum line distance, in tile units, that fits in the buffer. -const MAX_LINE_DISTANCE = Math.pow(2, LINE_DISTANCE_BUFFER_BITS - 1) / LINE_DISTANCE_SCALE; - type LineClips = { start: number; end: number; @@ -90,6 +79,7 @@ class LineBucket implements Bucket { totalDistance: number; maxLineLength: number; scaledDistance: number; + lineSoFar: number; lineClips: ?LineClips; e1: number; @@ -262,6 +252,7 @@ class LineBucket implements Bucket { this.distance = 0; this.scaledDistance = 0; this.totalDistance = 0; + this.lineSoFar = 0; if (this.lineClips) { this.lineClipsArray.push(this.lineClips); @@ -521,22 +512,9 @@ class LineBucket implements Bucket { this.addHalfVertex(p, leftX, leftY, round, false, endLeft, segment); this.addHalfVertex(p, rightX, rightY, round, true, -endRight, segment); - - // There is a maximum "distance along the line" that we can store in the buffers. - // When we get close to the distance, reset it to zero and add the vertex again with - // a distance of zero. The max distance is determined by the number of bits we allocate - // to `linesofar`. - if (this.distance > MAX_LINE_DISTANCE / 2 && this.totalDistance === 0) { - this.distance = 0; - this.addCurrentVertex(p, normal, endLeft, endRight, segment, round); - } } addHalfVertex({x, y}: Point, extrudeX: number, extrudeY: number, round: boolean, up: boolean, dir: number, segment: Segment) { - const totalDistance = this.lineClips ? this.scaledDistance * (MAX_LINE_DISTANCE - 1) : this.scaledDistance; - // scale down so that we can store longer distances while sacrificing precision. - const linesofarScaled = totalDistance * LINE_DISTANCE_SCALE; - this.layoutVertexArray.emplaceBack( // a_pos_normal // Encode round/up the least significant bits @@ -546,19 +524,14 @@ class LineBucket implements Bucket { // add 128 to store a byte in an unsigned byte Math.round(EXTRUDE_SCALE * extrudeX) + 128, Math.round(EXTRUDE_SCALE * extrudeY) + 128, - // Encode the -1/0/1 direction value into the first two bits of .z of a_data. - // Combine it with the lower 6 bits of `linesofarScaled` (shifted by 2 bits to make - // room for the direction value). The upper 8 bits of `linesofarScaled` are placed in - // the `w` component. - ((dir === 0 ? 0 : (dir < 0 ? -1 : 1)) + 1) | ((linesofarScaled & 0x3F) << 2), - linesofarScaled >> 6); + ((dir === 0 ? 0 : (dir < 0 ? -1 : 1)) + 1), + 0, // unused + // a_linesofar + this.lineSoFar); // Constructs a second vertex buffer with higher precision line progress if (this.lineClips) { - const progressRealigned = this.scaledDistance - this.lineClips.start; - const endClipRealigned = this.lineClips.end - this.lineClips.start; - const uvX = progressRealigned / endClipRealigned; - this.layoutVertexArray2.emplaceBack(uvX, this.lineClipsArray.length); + this.layoutVertexArray2.emplaceBack(this.scaledDistance, this.lineClipsArray.length); } const e = segment.vertexLength++; @@ -577,10 +550,15 @@ class LineBucket implements Bucket { // Knowing the ratio of the full linestring covered by this tiled feature, as well // as the total distance (in tile units) of this tiled feature, and the distance // (in tile units) of the current vertex, we can determine the relative distance - // of this vertex along the full linestring feature and scale it to [0, 2^15) - this.scaledDistance = this.lineClips ? - this.lineClips.start + (this.lineClips.end - this.lineClips.start) * this.distance / this.totalDistance : - this.distance; + // of this vertex along the full linestring feature. + if (this.lineClips) { + const featureShare = this.lineClips.end - this.lineClips.start; + const totalFeatureLength = this.totalDistance / featureShare; + this.scaledDistance = this.distance / this.totalDistance; + this.lineSoFar = totalFeatureLength * this.lineClips.start + this.distance; + } else { + this.lineSoFar = this.distance; + } } updateDistance(prev: Point, next: Point) { diff --git a/src/data/bucket/symbol_attributes.js b/src/data/bucket/symbol_attributes.js index 5d307d2ada6..fb948a0acc1 100644 --- a/src/data/bucket/symbol_attributes.js +++ b/src/data/bucket/symbol_attributes.js @@ -18,7 +18,12 @@ export const placementOpacityAttributes = createLayout([ export const collisionVertexAttributes = createLayout([ {name: 'a_placed', components: 2, type: 'Uint8'}, - {name: 'a_shift', components: 2, type: 'Float32'} + {name: 'a_shift', components: 2, type: 'Float32'}, +]); + +export const collisionVertexAttributesExt = createLayout([ + {name: 'a_size_scale', components: 1, type: 'Float32'}, + {name: 'a_padding', components: 2, type: 'Float32'}, ]); export const collisionBox = createLayout([ @@ -27,10 +32,12 @@ export const collisionBox = createLayout([ {type: 'Int16', name: 'anchorPointY'}, // distances to the edges from the anchor - {type: 'Int16', name: 'x1'}, - {type: 'Int16', name: 'y1'}, - {type: 'Int16', name: 'x2'}, - {type: 'Int16', name: 'y2'}, + {type: 'Float32', name: 'x1'}, + {type: 'Float32', name: 'y1'}, + {type: 'Float32', name: 'x2'}, + {type: 'Float32', name: 'y2'}, + + {type: 'Int16', name: 'padding'}, // the index of the feature in the original vectortile {type: 'Uint32', name: 'featureIndex'}, @@ -47,7 +54,7 @@ export const collisionBoxLayout = createLayout([ // used to render collision box ], 4); export const collisionCircleLayout = createLayout([ // used to render collision circles for debugging purposes - {name: 'a_pos', components: 2, type: 'Float32'}, + {name: 'a_pos_2f', components: 2, type: 'Float32'}, {name: 'a_radius', components: 1, type: 'Float32'}, {name: 'a_flags', components: 2, type: 'Int16'} ], 4); diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 5065d83f50d..3808d5a3731 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -2,6 +2,7 @@ import {symbolLayoutAttributes, collisionVertexAttributes, + collisionVertexAttributesExt, collisionBoxLayout, dynamicLayoutAttributes } from './symbol_attributes'; @@ -10,6 +11,7 @@ import {SymbolLayoutArray, SymbolDynamicLayoutArray, SymbolOpacityArray, CollisionBoxLayoutArray, + CollisionVertexExtArray, CollisionVertexArray, PlacedSymbolArray, SymbolInstanceArray, @@ -17,6 +19,8 @@ import {SymbolLayoutArray, SymbolLineVertexArray } from '../array_types'; +import ONE_EM from '../../symbol/one_em'; +import * as symbolSize from '../../symbol/symbol_size'; import Point from '@mapbox/point-geometry'; import SegmentVector from '../segment'; import {ProgramConfigurationSet} from '../program_configuration'; @@ -63,8 +67,10 @@ export type SingleCollisionBox = { y1: number; x2: number; y2: number; + padding: number; anchorPointX: number; anchorPointY: number; + elevation?: number; }; export type CollisionArrays = { @@ -162,9 +168,6 @@ export class SymbolBuffers { opacityVertexArray: SymbolOpacityArray; opacityVertexBuffer: VertexBuffer; - collisionVertexArray: CollisionVertexArray; - collisionVertexBuffer: VertexBuffer; - placedSymbolArray: PlacedSymbolArray; constructor(programConfigurations: ProgramConfigurationSet) { @@ -229,6 +232,9 @@ class CollisionBuffers { collisionVertexArray: CollisionVertexArray; collisionVertexBuffer: VertexBuffer; + collisionVertexArrayExt: CollisionVertexExtArray; + collisionVertexBufferExt: VertexBuffer; + constructor(LayoutArray: Class, layoutAttributes: Array, IndexArray: Class) { @@ -237,12 +243,14 @@ class CollisionBuffers { this.indexArray = new IndexArray(); this.segments = new SegmentVector(); this.collisionVertexArray = new CollisionVertexArray(); + this.collisionVertexArrayExt = new CollisionVertexExtArray(); } upload(context: Context) { this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, this.layoutAttributes); this.indexBuffer = context.createIndexBuffer(this.indexArray); this.collisionVertexBuffer = context.createVertexBuffer(this.collisionVertexArray, collisionVertexAttributes.members, true); + this.collisionVertexBufferExt = context.createVertexBuffer(this.collisionVertexArrayExt, collisionVertexAttributesExt.members, true); } destroy() { @@ -251,6 +259,7 @@ class CollisionBuffers { this.indexBuffer.destroy(); this.segments.destroy(); this.collisionVertexBuffer.destroy(); + this.collisionVertexBufferExt.destroy(); } } @@ -274,7 +283,7 @@ register('CollisionBuffers', CollisionBuffers); * 3. performSymbolLayout(bucket, stacks, icons) perform texts shaping and * layout on a Symbol Bucket. This step populates: * `this.symbolInstances`: metadata on generated symbols - * `this.collisionBoxArray`: collision data for use by foreground + * `collisionBoxArray`: collision data for use by foreground * `this.text`: SymbolBuffers for text symbols * `this.icons`: SymbolBuffers for icons * `this.iconCollisionBox`: Debug SymbolBuffers for icon collision boxes @@ -668,9 +677,8 @@ class SymbolBucket implements Bucket { ); } - _addCollisionDebugVertex(layoutVertexArray: StructArray, collisionVertexArray: StructArray, point: Point, anchorX: number, anchorY: number, extrude: Point) { - collisionVertexArray.emplaceBack(0, 0); - return layoutVertexArray.emplaceBack( + _commitLayoutVertex(array: StructArray, point: Point, anchorX: number, anchorY: number, extrude: Point) { + array.emplaceBack( // pos point.x, point.y, @@ -682,20 +690,25 @@ class SymbolBucket implements Bucket { Math.round(extrude.y)); } - addCollisionDebugVertices(x1: number, y1: number, x2: number, y2: number, arrays: CollisionBuffers, boxAnchorPoint: Point, symbolInstance: SymbolInstance) { + _addCollisionDebugVertices(box: CollisionBox, scale: number, arrays: CollisionBuffers, boxAnchorPoint: Point, symbolInstance: SymbolInstance) { const segment = arrays.segments.prepareSegment(4, arrays.layoutVertexArray, arrays.indexArray); const index = segment.vertexLength; - - const layoutVertexArray = arrays.layoutVertexArray; - const collisionVertexArray = arrays.collisionVertexArray; - const anchorX = symbolInstance.anchorX; const anchorY = symbolInstance.anchorY; - this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x1, y1)); - this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x2, y1)); - this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x2, y2)); - this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x1, y2)); + for (let i = 0; i < 4; i++) { + arrays.collisionVertexArray.emplaceBack(0, 0, 0, 0); + } + + arrays.collisionVertexArrayExt.emplaceBack(scale, -box.padding, -box.padding); + arrays.collisionVertexArrayExt.emplaceBack(scale, box.padding, -box.padding); + arrays.collisionVertexArrayExt.emplaceBack(scale, box.padding, box.padding); + arrays.collisionVertexArrayExt.emplaceBack(scale, -box.padding, box.padding); + + this._commitLayoutVertex(arrays.layoutVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(box.x1, box.y1)); + this._commitLayoutVertex(arrays.layoutVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(box.x2, box.y1)); + this._commitLayoutVertex(arrays.layoutVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(box.x2, box.y2)); + this._commitLayoutVertex(arrays.layoutVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(box.x1, box.y2)); segment.vertexLength += 4; @@ -708,21 +721,25 @@ class SymbolBucket implements Bucket { segment.primitiveLength += 4; } - addDebugCollisionBoxes(startIndex: number, endIndex: number, symbolInstance: SymbolInstance, isText: boolean) { + _addTextDebugCollisionBoxes(size: any, zoom: number, collisionBoxArray: CollisionBoxArray, startIndex: number, endIndex: number, instance: SymbolInstance) { for (let b = startIndex; b < endIndex; b++) { - const box: CollisionBox = (this.collisionBoxArray.get(b): any); - const x1 = box.x1; - const y1 = box.y1; - const x2 = box.x2; - const y2 = box.y2; + const box: CollisionBox = (collisionBoxArray.get(b): any); + const scale = this.getSymbolInstanceTextSize(size, instance, zoom, b); - this.addCollisionDebugVertices(x1, y1, x2, y2, - isText ? this.textCollisionBox : this.iconCollisionBox, - box.anchorPoint, symbolInstance); + this._addCollisionDebugVertices(box, scale, this.textCollisionBox, box.anchorPoint, instance); } } - generateCollisionDebugBuffers() { + _addIconDebugCollisionBoxes(size: any, zoom: number, collisionBoxArray: CollisionBoxArray, startIndex: number, endIndex: number, instance: SymbolInstance) { + for (let b = startIndex; b < endIndex; b++) { + const box: CollisionBox = (collisionBoxArray.get(b): any); + const scale = this.getSymbolInstanceIconSize(size, zoom, b); + + this._addCollisionDebugVertices(box, scale, this.iconCollisionBox, box.anchorPoint, instance); + } + } + + generateCollisionDebugBuffers(zoom: number, collisionBoxArray: CollisionBoxArray) { if (this.hasDebugData()) { this.destroyDebugData(); } @@ -730,12 +747,87 @@ class SymbolBucket implements Bucket { this.textCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray); this.iconCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray); + const iconSize = symbolSize.evaluateSizeForZoom(this.iconSizeData, zoom); + const textSize = symbolSize.evaluateSizeForZoom(this.textSizeData, zoom); + + for (let i = 0; i < this.symbolInstances.length; i++) { + const symbolInstance = this.symbolInstances.get(i); + this._addTextDebugCollisionBoxes(textSize, zoom, collisionBoxArray, symbolInstance.textBoxStartIndex, symbolInstance.textBoxEndIndex, symbolInstance); + this._addTextDebugCollisionBoxes(textSize, zoom, collisionBoxArray, symbolInstance.verticalTextBoxStartIndex, symbolInstance.verticalTextBoxEndIndex, symbolInstance); + this._addIconDebugCollisionBoxes(iconSize, zoom, collisionBoxArray, symbolInstance.iconBoxStartIndex, symbolInstance.iconBoxEndIndex, symbolInstance); + this._addIconDebugCollisionBoxes(iconSize, zoom, collisionBoxArray, symbolInstance.verticalIconBoxStartIndex, symbolInstance.verticalIconBoxEndIndex, symbolInstance); + } + } + + getSymbolInstanceTextSize(textSize: any, instance: SymbolInstance, zoom: number, boxIndex: number) { + const symbolIndex = instance.rightJustifiedTextSymbolIndex >= 0 ? + instance.rightJustifiedTextSymbolIndex : instance.centerJustifiedTextSymbolIndex >= 0 ? + instance.centerJustifiedTextSymbolIndex : instance.leftJustifiedTextSymbolIndex >= 0 ? + instance.leftJustifiedTextSymbolIndex : instance.verticalPlacedTextSymbolIndex >= 0 ? + instance.verticalPlacedTextSymbolIndex : boxIndex; + + const symbol: any = this.text.placedSymbolArray.get(symbolIndex); + const featureSize = symbolSize.evaluateSizeForFeature(this.textSizeData, textSize, symbol) / ONE_EM; + + return this.tilePixelRatio * featureSize; + } + + getSymbolInstanceIconSize(iconSize: any, zoom: number, index: number) { + const symbol: any = this.icon.placedSymbolArray.get(index); + const featureSize = symbolSize.evaluateSizeForFeature(this.iconSizeData, iconSize, symbol); + + return this.tilePixelRatio * featureSize; + } + + _commitDebugCollisionVertexUpdate(array: StructArray, scale: number, padding: number) { + array.emplaceBack(scale, -padding, -padding); + array.emplaceBack(scale, padding, -padding); + array.emplaceBack(scale, padding, padding); + array.emplaceBack(scale, -padding, padding); + } + + _updateTextDebugCollisionBoxes(size: any, zoom: number, collisionBoxArray: CollisionBoxArray, startIndex: number, endIndex: number, instance: SymbolInstance) { + for (let b = startIndex; b < endIndex; b++) { + const box: CollisionBox = (collisionBoxArray.get(b): any); + const scale = this.getSymbolInstanceTextSize(size, instance, zoom, b); + const array = this.textCollisionBox.collisionVertexArrayExt; + this._commitDebugCollisionVertexUpdate(array, scale, box.padding); + } + } + + _updateIconDebugCollisionBoxes(size: any, zoom: number, collisionBoxArray: CollisionBoxArray, startIndex: number, endIndex: number) { + for (let b = startIndex; b < endIndex; b++) { + const box: CollisionBox = (collisionBoxArray.get(b): any); + const scale = this.getSymbolInstanceIconSize(size, zoom, b); + const array = this.iconCollisionBox.collisionVertexArrayExt; + this._commitDebugCollisionVertexUpdate(array, scale, box.padding); + } + } + + updateCollisionDebugBuffers(zoom: number, collisionBoxArray: CollisionBoxArray) { + if (!this.hasDebugData()) { + return; + } + + if (this.hasTextCollisionBoxData()) this.textCollisionBox.collisionVertexArrayExt.clear(); + if (this.hasIconCollisionBoxData()) this.iconCollisionBox.collisionVertexArrayExt.clear(); + + const iconSize = symbolSize.evaluateSizeForZoom(this.iconSizeData, zoom); + const textSize = symbolSize.evaluateSizeForZoom(this.textSizeData, zoom); + for (let i = 0; i < this.symbolInstances.length; i++) { const symbolInstance = this.symbolInstances.get(i); - this.addDebugCollisionBoxes(symbolInstance.textBoxStartIndex, symbolInstance.textBoxEndIndex, symbolInstance, true); - this.addDebugCollisionBoxes(symbolInstance.verticalTextBoxStartIndex, symbolInstance.verticalTextBoxEndIndex, symbolInstance, true); - this.addDebugCollisionBoxes(symbolInstance.iconBoxStartIndex, symbolInstance.iconBoxEndIndex, symbolInstance, false); - this.addDebugCollisionBoxes(symbolInstance.verticalIconBoxStartIndex, symbolInstance.verticalIconBoxEndIndex, symbolInstance, false); + this._updateTextDebugCollisionBoxes(textSize, zoom, collisionBoxArray, symbolInstance.textBoxStartIndex, symbolInstance.textBoxEndIndex, symbolInstance); + this._updateTextDebugCollisionBoxes(textSize, zoom, collisionBoxArray, symbolInstance.verticalTextBoxStartIndex, symbolInstance.verticalTextBoxEndIndex, symbolInstance); + this._updateIconDebugCollisionBoxes(iconSize, zoom, collisionBoxArray, symbolInstance.iconBoxStartIndex, symbolInstance.iconBoxEndIndex); + this._updateIconDebugCollisionBoxes(iconSize, zoom, collisionBoxArray, symbolInstance.verticalIconBoxStartIndex, symbolInstance.verticalIconBoxEndIndex); + } + + if (this.hasTextCollisionBoxData() && this.textCollisionBox.collisionVertexBufferExt) { + this.textCollisionBox.collisionVertexBufferExt.updateData(this.textCollisionBox.collisionVertexArrayExt); + } + if (this.hasIconCollisionBoxData() && this.iconCollisionBox.collisionVertexBufferExt) { + this.iconCollisionBox.collisionVertexBufferExt.updateData(this.iconCollisionBox.collisionVertexArrayExt); } } @@ -750,27 +842,27 @@ class SymbolBucket implements Bucket { const collisionArrays = {}; for (let k = textStartIndex; k < textEndIndex; k++) { const box: CollisionBox = (collisionBoxArray.get(k): any); - collisionArrays.textBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; + collisionArrays.textBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, padding: box.padding, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; collisionArrays.textFeatureIndex = box.featureIndex; break; // Only one box allowed per instance } for (let k = verticalTextStartIndex; k < verticalTextEndIndex; k++) { const box: CollisionBox = (collisionBoxArray.get(k): any); - collisionArrays.verticalTextBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; + collisionArrays.verticalTextBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, padding: box.padding, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; collisionArrays.verticalTextFeatureIndex = box.featureIndex; break; // Only one box allowed per instance } for (let k = iconStartIndex; k < iconEndIndex; k++) { // An icon can only have one box now, so this indexing is a bit vestigial... const box: CollisionBox = (collisionBoxArray.get(k): any); - collisionArrays.iconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; + collisionArrays.iconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, padding: box.padding, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; collisionArrays.iconFeatureIndex = box.featureIndex; break; // Only one box allowed per instance } for (let k = verticalIconStartIndex; k < verticalIconEndIndex; k++) { // An icon can only have one box now, so this indexing is a bit vestigial... const box: CollisionBox = (collisionBoxArray.get(k): any); - collisionArrays.verticalIconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; + collisionArrays.verticalIconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, padding: box.padding, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; collisionArrays.verticalIconFeatureIndex = box.featureIndex; break; // Only one box allowed per instance } diff --git a/src/data/debug_viz.js b/src/data/debug_viz.js new file mode 100644 index 00000000000..757a82fbb28 --- /dev/null +++ b/src/data/debug_viz.js @@ -0,0 +1,99 @@ +// @flow +import {PosArray, LineStripIndexArray} from "./array_types"; +import SegmentVector from "./segment"; +import posAttributes from './pos_attributes'; +import Color from "../style-spec/util/color"; + +import type VertexBuffer from "../gl/vertex_buffer"; +import type IndexBuffer from "../gl/index_buffer"; +import type Point from '@mapbox/point-geometry'; +import type Context from "../gl/context"; + +/** + * Helper class that can be used to draw debug geometry in tile-space + * + * @class TileSpaceDebugBuffer + * @private + */ +export class TileSpaceDebugBuffer { + vertices: PosArray; + indices: LineStripIndexArray; + tileSize: number; + needsUpload: boolean; + color: Color; + + vertexBuffer: ?VertexBuffer; + indexBuffer: ?IndexBuffer; + segments: ?SegmentVector; + + constructor(tileSize: number, color: Color = Color.red) { + this.vertices = new PosArray(); + this.indices = new LineStripIndexArray(); + this.tileSize = tileSize; + this.needsUpload = true; + this.color = color; + } + + addPoints(points: Point[]) { + this.clearPoints(); + for (const point of points) { + this.addPoint(point); + } + this.addPoint(points[0]); + } + + addPoint(p: Point) { + // Add a bowtie shape + const crosshairSize = 80; + const currLineLineLength = this.vertices.length; + this.vertices.emplaceBack(p.x, p.y); + this.vertices.emplaceBack(p.x + crosshairSize / 2, p.y); + this.vertices.emplaceBack(p.x, p.y - crosshairSize / 2); + this.vertices.emplaceBack(p.x, p.y + crosshairSize / 2); + this.vertices.emplaceBack(p.x - crosshairSize / 2, p.y); + this.indices.emplaceBack(currLineLineLength); + this.indices.emplaceBack(currLineLineLength + 1); + this.indices.emplaceBack(currLineLineLength + 2); + this.indices.emplaceBack(currLineLineLength + 3); + this.indices.emplaceBack(currLineLineLength + 4); + this.indices.emplaceBack(currLineLineLength); + + this.needsUpload = true; + } + + clearPoints() { + this.vertices.clear(); + this.indices.clear(); + this.needsUpload = true; + } + + lazyUpload(context: Context) { + if (this.needsUpload && this.hasVertices()) { + this.unload(); + + this.vertexBuffer = context.createVertexBuffer(this.vertices, posAttributes.members, true); + this.indexBuffer = context.createIndexBuffer(this.indices, true); + this.segments = SegmentVector.simpleSegment(0, 0, this.vertices.length, this.indices.length); + this.needsUpload = false; + } + } + + hasVertices(): boolean { + return this.vertices.length > 1; + } + + unload() { + if (this.vertexBuffer) { + this.vertexBuffer.destroy(); + delete this.vertexBuffer; + } + if (this.indexBuffer) { + this.indexBuffer.destroy(); + delete this.indexBuffer; + } + if (this.segments) { + this.segments.destroy(); + delete this.segments; + } + } +} diff --git a/src/data/dem_data.js b/src/data/dem_data.js index fcbf98ad830..01b065e5859 100644 --- a/src/data/dem_data.js +++ b/src/data/dem_data.js @@ -3,6 +3,8 @@ import {RGBAImage} from '../util/image'; import {warnOnce} from '../util/util'; import {register} from '../util/web_worker_transfer'; +import DemMinMaxQuadTree from './dem_tree'; +import assert from 'assert'; // DEMData is a data structure for decoding, backfilling, and storing elevation data for processing in the hillshade shaders // data can be populated either from a pngraw image tile or from serliazed data sent back from a worker. When data is initially @@ -14,16 +16,29 @@ import {register} from '../util/web_worker_transfer'; // surrounding pixel values to compute the slope at that pixel, and we cannot accurately calculate the slope at pixels on a // tile's edge without backfilling from neighboring tiles. +export type DEMEncoding = "mapbox" | "terrarium"; + +const unpackVectors = { + mapbox: [6553.6, 25.6, 0.1, 10000.0], + terrarium: [256.0, 1.0, 1.0 / 256.0, 32768.0] +}; + export default class DEMData { - uid: string; + uid: number; data: Uint32Array; stride: number; dim: number; - encoding: "mapbox" | "terrarium"; + encoding: DEMEncoding; + borderReady: boolean; + _tree: DemMinMaxQuadTree; + get tree(): DemMinMaxQuadTree { + if (!this._tree) this._buildQuadTree(); + return this._tree; + } // RGBAImage data has uniform 1px padding on all sides: square tile edge size defines stride // and dim is calculated as stride - 2. - constructor(uid: string, data: RGBAImage, encoding: "mapbox" | "terrarium") { + constructor(uid: number, data: RGBAImage, encoding: DEMEncoding, borderReady: boolean = false, buildQuadTree: boolean = false) { this.uid = uid; if (data.height !== data.width) throw new RangeError('DEM tiles must be square'); if (encoding && encoding !== "mapbox" && encoding !== "terrarium") return warnOnce( @@ -33,6 +48,9 @@ export default class DEMData { const dim = this.dim = data.height - 2; this.data = new Uint32Array(data.data.buffer); this.encoding = encoding || 'mapbox'; + this.borderReady = borderReady; + + if (borderReady) return; // in order to avoid flashing seams between tiles, here we are initially populating a 1px border of pixels around the image // with the data of the nearest pixel from the image. this data is eventually replaced when the tile's neighboring @@ -52,6 +70,13 @@ export default class DEMData { this.data[this._idx(dim, -1)] = this.data[this._idx(dim - 1, 0)]; this.data[this._idx(-1, dim)] = this.data[this._idx(0, dim - 1)]; this.data[this._idx(dim, dim)] = this.data[this._idx(dim - 1, dim - 1)]; + if (buildQuadTree) this._buildQuadTree(); + } + + _buildQuadTree() { + assert(!this._tree); + // Construct the implicit sparse quad tree by traversing mips from top to down + this._tree = new DemMinMaxQuadTree(this); } get(x: number, y: number) { @@ -61,8 +86,12 @@ export default class DEMData { return unpack(pixels[index], pixels[index + 1], pixels[index + 2]); } - getUnpackVector() { - return this.encoding === "terrarium" ? [256.0, 1.0, 1.0 / 256.0, 32768.0] : [6553.6, 25.6, 0.1, 10000.0]; + static getUnpackVector(encoding: DEMEncoding): [number, number, number, number] { + return unpackVectors[encoding]; + } + + get unpackVector(): [number, number, number, number] { + return unpackVectors[this.encoding]; } _idx(x: number, y: number) { @@ -82,6 +111,18 @@ export default class DEMData { return ((r * 256 + g + b / 256) - 32768.0); } + static pack(altitude: number, encoding: DEMEncoding): [number, number, number, number] { + const color = [0, 0, 0, 0]; + const vector = DEMData.getUnpackVector(encoding); + let v = Math.floor((altitude + vector[3]) / vector[2]); + color[2] = v % 256; + v = Math.floor(v / 256); + color[1] = v % 256; + v = Math.floor(v / 256); + color[0] = v; + return color; + } + getPixels() { return new RGBAImage({width: this.stride, height: this.stride}, new Uint8Array(this.data.buffer)); } @@ -120,6 +161,11 @@ export default class DEMData { } } } + + onDeserialize() { + if (this._tree) this._tree.dem = this; + } } register('DEMData', DEMData); +register('DemMinMaxQuadTree', DemMinMaxQuadTree, {omit: ['dem']}); diff --git a/src/data/dem_tree.js b/src/data/dem_tree.js new file mode 100644 index 00000000000..c6493c52d23 --- /dev/null +++ b/src/data/dem_tree.js @@ -0,0 +1,475 @@ +// @flow + +import DEMData from "./dem_data"; +import {vec3} from 'gl-matrix'; +import {number as interpolate} from '../style-spec/util/interpolate'; +import {clamp} from '../util/util'; + +type vec3Like = vec3 | [number, number, number]; + +class MipLevel { + size: number; + minimums: Array; + maximums: Array; + leaves: Array; + + constructor(size_: number) { + this.size = size_; + this.minimums = []; + this.maximums = []; + this.leaves = []; + } + + getElevation(x: number, y: number): { min: number, max: number} { + const idx = this.toIdx(x, y); + return { + min: this.minimums[idx], + max: this.maximums[idx] + }; + } + + isLeaf(x: number, y: number): number { + return this.leaves[this.toIdx(x, y)]; + } + + toIdx(x: number, y: number): number { + return y * this.size + x; + } +} + +function aabbRayIntersect(min: vec3Like, max: vec3Like, pos: vec3Like, dir: vec3Like): ?number { + let tMin = 0; + let tMax = Number.MAX_VALUE; + + const epsilon = 1e-15; + + for (let i = 0; i < 3; i++) { + if (Math.abs(dir[i]) < epsilon) { + // Parallel ray + if (pos[i] < min[i] || pos[i] > max[i]) + return null; + } else { + const ood = 1.0 / dir[i]; + let t1 = (min[i] - pos[i]) * ood; + let t2 = (max[i] - pos[i]) * ood; + if (t1 > t2) { + const temp = t1; + t1 = t2; + t2 = temp; + } + if (t1 > tMin) + tMin = t1; + if (t2 < tMax) + tMax = t2; + if (tMin > tMax) + return null; + } + } + + return tMin; +} + +function triangleRayIntersect(ax, ay, az, bx, by, bz, cx, cy, cz, pos: vec3Like, dir: vec3Like): ?number { + // Compute barycentric coordinates u and v to find the intersection + const abX = bx - ax; + const abY = by - ay; + const abZ = bz - az; + + const acX = cx - ax; + const acY = cy - ay; + const acZ = cz - az; + + // pvec = cross(dir, a), det = dot(ab, pvec) + const pvecX = dir[1] * acZ - dir[2] * acY; + const pvecY = dir[2] * acX - dir[0] * acZ; + const pvecZ = dir[0] * acY - dir[1] * acX; + const det = abX * pvecX + abY * pvecY + abZ * pvecZ; + + if (Math.abs(det) < 1e-15) + return null; + + const invDet = 1.0 / det; + const tvecX = pos[0] - ax; + const tvecY = pos[1] - ay; + const tvecZ = pos[2] - az; + const u = (tvecX * pvecX + tvecY * pvecY + tvecZ * pvecZ) * invDet; + + if (u < 0.0 || u > 1.0) + return null; + + // qvec = cross(tvec, ab) + const qvecX = tvecY * abZ - tvecZ * abY; + const qvecY = tvecZ * abX - tvecX * abZ; + const qvecZ = tvecX * abY - tvecY * abX; + const v = (dir[0] * qvecX + dir[1] * qvecY + dir[2] * qvecZ) * invDet; + + if (v < 0.0 || u + v > 1.0) + return null; + + return (acX * qvecX + acY * qvecY + acZ * qvecZ) * invDet; +} + +function frac(v, lo, hi) { + return (v - lo) / (hi - lo); +} + +function decodeBounds(x, y, depth, boundsMinx, boundsMiny, boundsMaxx, boundsMaxy, outMin, outMax) { + const scale = 1 << depth; + const rangex = boundsMaxx - boundsMinx; + const rangey = boundsMaxy - boundsMiny; + + const minX = (x + 0) / scale * rangex + boundsMinx; + const maxX = (x + 1) / scale * rangex + boundsMinx; + const minY = (y + 0) / scale * rangey + boundsMiny; + const maxY = (y + 1) / scale * rangey + boundsMiny; + + outMin[0] = minX; + outMin[1] = minY; + outMax[0] = maxX; + outMax[1] = maxY; +} + +// A small padding value is used with bounding boxes to extend the bottom below sea level +const aabbSkirtPadding = 100; + +// A sparse min max quad tree for performing accelerated queries against dem elevation data. +// Each tree node stores the minimum and maximum elevation of its children nodes and a flag whether the node is a leaf. +// Node data is stored in non-interleaved arrays where the root is at index 0. +export default class DemMinMaxQuadTree { + maximums: Array; + minimums: Array; + leaves: Array; + childOffsets: Array; + nodeCount: number; + dem: DEMData; + _siblingOffset: Array>; + + constructor(dem_: DEMData) { + this.maximums = []; + this.minimums = []; + this.leaves = []; + this.childOffsets = []; + this.nodeCount = 0; + this.dem = dem_; + + // Precompute the order of 4 sibling nodes in the memory. Top-left, top-right, bottom-left, bottom-right + this._siblingOffset = [ + [0, 0], + [1, 0], + [0, 1], + [1, 1] + ]; + + if (!this.dem) + return; + + const mips = buildDemMipmap(this.dem); + const maxLvl = mips.length - 1; + + // Create the root node + const rootMip = mips[maxLvl]; + const min = rootMip.minimums; + const max = rootMip.maximums; + const leaves = rootMip.leaves; + this._addNode(min[0], max[0], leaves[0]); + + // Construct the rest of the tree recursively + this._construct(mips, 0, 0, maxLvl, 0); + } + + // Performs raycast against the tree root only. Min and max coordinates defines the size of the root node + raycastRoot(minx: number, miny: number, maxx: number, maxy: number, p: vec3Like, d: vec3Like, exaggeration: number = 1): ?number { + const min = [minx, miny, -aabbSkirtPadding]; + const max = [maxx, maxy, this.maximums[0] * exaggeration]; + return aabbRayIntersect(min, max, p, d); + } + + raycast(rootMinx: number, rootMiny: number, rootMaxx: number, rootMaxy: number, p: vec3Like, d: vec3Like, exaggeration: number = 1): ?number { + if (!this.nodeCount) + return null; + + const t = this.raycastRoot(rootMinx, rootMiny, rootMaxx, rootMaxy, p, d, exaggeration); + if (t == null) + return null; + + const tHits = []; + const sortedHits = []; + const boundsMin = []; + const boundsMax = []; + + const stack = [{ + idx: 0, + t, + nodex: 0, + nodey: 0, + depth: 0 + }]; + + // Traverse the tree until something is hit or the ray escapes + while (stack.length > 0) { + const {idx, t, nodex, nodey, depth} = stack.pop(); + + if (this.leaves[idx]) { + // Create 2 triangles to approximate the surface plane for more precise tests + decodeBounds(nodex, nodey, depth, rootMinx, rootMiny, rootMaxx, rootMaxy, boundsMin, boundsMax); + + const scale = 1 << depth; + const minxUv = (nodex + 0) / scale; + const maxxUv = (nodex + 1) / scale; + const minyUv = (nodey + 0) / scale; + const maxyUv = (nodey + 1) / scale; + + // 4 corner points A, B, C and D defines the (quad) area covered by this node + const az = sampleElevation(minxUv, minyUv, this.dem) * exaggeration; + const bz = sampleElevation(maxxUv, minyUv, this.dem) * exaggeration; + const cz = sampleElevation(maxxUv, maxyUv, this.dem) * exaggeration; + const dz = sampleElevation(minxUv, maxyUv, this.dem) * exaggeration; + + const t0: any = triangleRayIntersect( + boundsMin[0], boundsMin[1], az, // A + boundsMax[0], boundsMin[1], bz, // B + boundsMax[0], boundsMax[1], cz, // C + p, d); + + const t1: any = triangleRayIntersect( + boundsMax[0], boundsMax[1], cz, + boundsMin[0], boundsMax[1], dz, + boundsMin[0], boundsMin[1], az, + p, d); + + const tMin = Math.min( + t0 !== null ? t0 : Number.MAX_VALUE, + t1 !== null ? t1 : Number.MAX_VALUE); + + // The ray might go below the two surface triangles but hit one of the sides. + // This covers the case of skirt geometry between two dem tiles of different zoom level + if (tMin === Number.MAX_VALUE) { + const hitPos = vec3.scaleAndAdd([], p, d, t); + const fracx = frac(hitPos[0], boundsMin[0], boundsMax[0]); + const fracy = frac(hitPos[1], boundsMin[1], boundsMax[1]); + + if (bilinearLerp(az, bz, dz, cz, fracx, fracy) >= hitPos[2]) + return t; + } else { + return tMin; + } + + continue; + } + + // Perform intersection tests agains each of the 4 child nodes and store results from closest to furthest. + let hitCount = 0; + + for (let i = 0; i < this._siblingOffset.length; i++) { + + const childNodeX = (nodex << 1) + this._siblingOffset[i][0]; + const childNodeY = (nodey << 1) + this._siblingOffset[i][1]; + + // Decode node aabb from the morton code + decodeBounds(childNodeX, childNodeY, depth + 1, rootMinx, rootMiny, rootMaxx, rootMaxy, boundsMin, boundsMax); + + boundsMin[2] = -aabbSkirtPadding; + boundsMax[2] = this.maximums[this.childOffsets[idx] + i] * exaggeration; + + const result = aabbRayIntersect(boundsMin, boundsMax, p, d); + if (result != null) { + // Build the result list from furthest to closest hit. + // The order will be inversed when building the stack + const tHit: number = result; + tHits[i] = tHit; + + let added = false; + for (let j = 0; j < hitCount && !added; j++) { + if (tHit >= tHits[sortedHits[j]]) { + sortedHits.splice(j, 0, i); + added = true; + } + } + if (!added) + sortedHits[hitCount] = i; + hitCount++; + } + } + + // Continue recursion from closest to furthest + for (let i = 0; i < hitCount; i++) { + const hitIdx = sortedHits[i]; + stack.push({ + idx: this.childOffsets[idx] + hitIdx, + t: tHits[hitIdx], + nodex: (nodex << 1) + this._siblingOffset[hitIdx][0], + nodey: (nodey << 1) + this._siblingOffset[hitIdx][1], + depth: depth + 1 + }); + } + } + + return null; + } + + _addNode(min: number, max: number, leaf: number) { + this.minimums.push(min); + this.maximums.push(max); + this.leaves.push(leaf); + this.childOffsets.push(0); + return this.nodeCount++; + } + + _construct(mips: Array, x: number, y: number, lvl: number, parentIdx: number) { + if (mips[lvl].isLeaf(x, y) === 1) { + return; + } + + // Update parent offset + if (!this.childOffsets[parentIdx]) + this.childOffsets[parentIdx] = this.nodeCount; + + // Construct all 4 children and place them next to each other in memory + const childLvl = lvl - 1; + const childMip = mips[childLvl]; + + let leafMask = 0; + let firstNodeIdx; + + for (let i = 0; i < this._siblingOffset.length; i++) { + const childX = x * 2 + this._siblingOffset[i][0]; + const childY = y * 2 + this._siblingOffset[i][1]; + + const elevation = childMip.getElevation(childX, childY); + const leaf = childMip.isLeaf(childX, childY); + const nodeIdx = this._addNode(elevation.min, elevation.max, leaf); + + if (leaf) + leafMask |= 1 << i; + if (!firstNodeIdx) + firstNodeIdx = nodeIdx; + } + + // Continue construction of the tree recursively to non-leaf nodes. + for (let i = 0; i < this._siblingOffset.length; i++) { + if (!(leafMask & (1 << i))) { + this._construct(mips, x * 2 + this._siblingOffset[i][0], y * 2 + this._siblingOffset[i][1], childLvl, firstNodeIdx + i); + } + } + } +} + +function bilinearLerp(p00: any, p10: any, p01: any, p11: any, x: number, y: number): any { + return interpolate( + interpolate(p00, p01, y), + interpolate(p10, p11, y), + x); +} + +// Sample elevation in normalized uv-space ([0, 0] is the top left) +// This function does not account for exaggeration +export function sampleElevation(fx: number, fy: number, dem: DEMData): number { + // Sample position in texels + const demSize = dem.dim; + const x = clamp(fx * demSize - 0.5, 0, demSize - 1); + const y = clamp(fy * demSize - 0.5, 0, demSize - 1); + + // Compute 4 corner points for bilinear interpolation + const ixMin = Math.floor(x); + const iyMin = Math.floor(y); + const ixMax = Math.min(ixMin + 1, demSize - 1); + const iyMax = Math.min(iyMin + 1, demSize - 1); + + const e00 = dem.get(ixMin, iyMin); + const e10 = dem.get(ixMax, iyMin); + const e01 = dem.get(ixMin, iyMax); + const e11 = dem.get(ixMax, iyMax); + + return bilinearLerp(e00, e10, e01, e11, x - ixMin, y - iyMin); +} + +export function buildDemMipmap(dem: DEMData): Array { + const demSize = dem.dim; + + const elevationDiffThreshold = 5; + const texelSizeOfMip0 = 8; + const levelCount = Math.ceil(Math.log2(demSize / texelSizeOfMip0)); + const mips: Array = []; + + let blockCount = Math.ceil(Math.pow(2, levelCount)); + const blockSize = 1 / blockCount; + + const blockSamples = (x, y, size, exclusive, outBounds) => { + const padding = exclusive ? 1 : 0; + const minx = x * size; + const maxx = (x + 1) * size - padding; + const miny = y * size; + const maxy = (y + 1) * size - padding; + + outBounds[0] = minx; + outBounds[1] = miny; + outBounds[2] = maxx; + outBounds[3] = maxy; + }; + + // The first mip (0) is built by sampling 4 corner points of each 8x8 texel block + let mip = new MipLevel(blockCount); + const blockBounds = []; + + for (let idx = 0; idx < blockCount * blockCount; idx++) { + const y = Math.floor(idx / blockCount); + const x = idx % blockCount; + + blockSamples(x, y, blockSize, false, blockBounds); + + const e0 = sampleElevation(blockBounds[0], blockBounds[1], dem); // minx, miny + const e1 = sampleElevation(blockBounds[2], blockBounds[1], dem); // maxx, miny + const e2 = sampleElevation(blockBounds[2], blockBounds[3], dem); // maxx, maxy + const e3 = sampleElevation(blockBounds[0], blockBounds[3], dem); // minx, maxy + + mip.minimums.push(Math.min(e0, e1, e2, e3)); + mip.maximums.push(Math.max(e0, e1, e2, e3)); + mip.leaves.push(1); + } + + mips.push(mip); + + // Construct the rest of the mip levels from bottom to up + for (blockCount /= 2; blockCount >= 1; blockCount /= 2) { + const prevMip = mips[mips.length - 1]; + + mip = new MipLevel(blockCount); + + for (let idx = 0; idx < blockCount * blockCount; idx++) { + const y = Math.floor(idx / blockCount); + const x = idx % blockCount; + + // Sample elevation of all 4 children mip texels. 4 leaf nodes can be concatenated into a single + // leaf if the total elevation difference is below the threshold value + blockSamples(x, y, 2, true, blockBounds); + + const e0 = prevMip.getElevation(blockBounds[0], blockBounds[1]); + const e1 = prevMip.getElevation(blockBounds[2], blockBounds[1]); + const e2 = prevMip.getElevation(blockBounds[2], blockBounds[3]); + const e3 = prevMip.getElevation(blockBounds[0], blockBounds[3]); + + const l0 = prevMip.isLeaf(blockBounds[0], blockBounds[1]); + const l1 = prevMip.isLeaf(blockBounds[2], blockBounds[1]); + const l2 = prevMip.isLeaf(blockBounds[2], blockBounds[3]); + const l3 = prevMip.isLeaf(blockBounds[0], blockBounds[3]); + + const minElevation = Math.min(e0.min, e1.min, e2.min, e3.min); + const maxElevation = Math.max(e0.max, e1.max, e2.max, e3.max); + const canConcatenate = l0 && l1 && l2 && l3; + + mip.maximums.push(maxElevation); + mip.minimums.push(minElevation); + + if (maxElevation - minElevation <= elevationDiffThreshold && canConcatenate) { + // All samples have uniform elevation. Mark this as a leaf + mip.leaves.push(1); + } else { + mip.leaves.push(0); + } + } + + mips.push(mip); + } + + return mips; +} diff --git a/src/data/feature_index.js b/src/data/feature_index.js index 739b3f966d5..67c490e6bba 100644 --- a/src/data/feature_index.js +++ b/src/data/feature_index.js @@ -18,22 +18,19 @@ import EvaluationParameters from '../style/evaluation_parameters'; import SourceFeatureState from '../source/source_state'; import {polygonIntersectsBox} from '../util/intersection_tests'; import {PossiblyEvaluated} from '../style/properties'; +import {FeatureIndexArray} from './array_types'; +import {DEMSampler} from '../terrain/elevation'; import type StyleLayer from '../style/style_layer'; import type {FeatureFilter} from '../style-spec/feature_filter'; import type Transform from '../geo/transform'; import type {FilterSpecification, PromoteIdSpecification} from '../style-spec/types'; - -import {FeatureIndexArray} from './array_types'; +import type {TilespaceQueryGeometry} from '../style/query_geometry'; type QueryParameters = { - scale: number, pixelPosMatrix: Float32Array, transform: Transform, - tileSize: number, - queryGeometry: Array, - cameraQueryGeometry: Array, - queryPadding: number, + tileResult: TilespaceQueryGeometry, params: { filter: FilterSpecification, layers: Array, @@ -47,7 +44,6 @@ class FeatureIndex { y: number; z: number; grid: Grid; - grid3D: Grid; featureIndexArray: FeatureIndexArray; promoteId: ?PromoteIdSpecification; @@ -63,16 +59,15 @@ class FeatureIndex { this.y = tileID.canonical.y; this.z = tileID.canonical.z; this.grid = new Grid(EXTENT, 16, 0); - this.grid3D = new Grid(EXTENT, 16, 0); this.featureIndexArray = new FeatureIndexArray(); this.promoteId = promoteId; } - insert(feature: VectorTileFeature, geometry: Array>, featureIndex: number, sourceLayerIndex: number, bucketIndex: number, is3D?: boolean) { + insert(feature: VectorTileFeature, geometry: Array>, featureIndex: number, sourceLayerIndex: number, bucketIndex: number) { const key = this.featureIndexArray.length; this.featureIndexArray.emplaceBack(featureIndex, sourceLayerIndex, bucketIndex); - const grid = is3D ? this.grid3D : this.grid; + const grid = this.grid; for (let r = 0; r < geometry.length; r++) { const ring = geometry[r]; @@ -106,30 +101,23 @@ class FeatureIndex { // Finds non-symbol features in this tile at a particular position. query(args: QueryParameters, styleLayers: {[_: string]: StyleLayer}, serializedLayers: {[_: string]: Object}, sourceFeatureState: SourceFeatureState): {[_: string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>} { this.loadVTLayers(); - const params = args.params || {}, - pixelsToTileUnits = EXTENT / args.tileSize / args.scale, filter = featureFilter(params.filter); + const tilespaceGeometry = args.tileResult; + const transform = args.transform; + + const bounds = tilespaceGeometry.bufferedTilespaceBounds; + const queryPredicate = (bx1, by1, bx2, by2) => { + return polygonIntersectsBox(tilespaceGeometry.bufferedTilespaceGeometry, bx1, by1, bx2, by2); + }; + const matching = this.grid.query(bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y, queryPredicate); + matching.sort(topDownFeatureComparator); - const queryGeometry = args.queryGeometry; - const queryPadding = args.queryPadding * pixelsToTileUnits; - - const bounds = getBounds(queryGeometry); - const matching = this.grid.query(bounds.minX - queryPadding, bounds.minY - queryPadding, bounds.maxX + queryPadding, bounds.maxY + queryPadding); - - const cameraBounds = getBounds(args.cameraQueryGeometry); - const matching3D = this.grid3D.query( - cameraBounds.minX - queryPadding, cameraBounds.minY - queryPadding, cameraBounds.maxX + queryPadding, cameraBounds.maxY + queryPadding, - (bx1, by1, bx2, by2) => { - return polygonIntersectsBox(args.cameraQueryGeometry, bx1 - queryPadding, by1 - queryPadding, bx2 + queryPadding, by2 + queryPadding); - }); - - for (const key of matching3D) { - matching.push(key); + let elevationHelper = null; + if (transform.elevation && matching.length > 0) { + elevationHelper = DEMSampler.create(transform.elevation, this.tileID); } - matching.sort(topDownFeatureComparator); - const result = {}; let previousIndex; for (let k = 0; k < matching.length; k++) { @@ -157,7 +145,7 @@ class FeatureIndex { featureGeometry = loadGeometry(feature); } - return styleLayer.queryIntersectsFeature(queryGeometry, feature, featureState, featureGeometry, this.z, args.transform, pixelsToTileUnits, args.pixelPosMatrix); + return styleLayer.queryIntersectsFeature(tilespaceGeometry, feature, featureState, featureGeometry, this.z, args.transform, args.pixelPosMatrix, elevationHelper); } ); } @@ -303,20 +291,6 @@ function evaluateProperties(serializedProperties, styleLayerProperties, feature, }); } -function getBounds(geometry: Array) { - let minX = Infinity; - let minY = Infinity; - let maxX = -Infinity; - let maxY = -Infinity; - for (const p of geometry) { - minX = Math.min(minX, p.x); - minY = Math.min(minY, p.y); - maxX = Math.max(maxX, p.x); - maxY = Math.max(maxY, p.y); - } - return {minX, minY, maxX, maxY}; -} - function topDownFeatureComparator(a, b) { return b - a; } diff --git a/src/data/feature_position_map.js b/src/data/feature_position_map.js index 15137377a39..2d76881aae0 100644 --- a/src/data/feature_position_map.js +++ b/src/data/feature_position_map.js @@ -2,6 +2,7 @@ import murmur3 from 'murmurhash-js'; import {register} from '../util/web_worker_transfer'; +import {MAX_SAFE_INTEGER} from '../util/util'; import assert from 'assert'; type SerializedFeaturePositionMap = { @@ -84,8 +85,6 @@ export default class FeaturePositionMap { } } -const MAX_SAFE_INTEGER = Math.pow(2, 53) - 1; - function getNumericId(value: mixed) { const numValue = +value; if (!isNaN(numValue) && numValue <= MAX_SAFE_INTEGER) { diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js index 0ea6ee93895..a1d754c4089 100644 --- a/src/data/program_configuration.js +++ b/src/data/program_configuration.js @@ -401,7 +401,7 @@ export default class ProgramConfiguration { _buffers: Array; - constructor(layer: TypedStyleLayer, zoom: number, filterProperties: (_: string) => boolean) { + constructor(layer: TypedStyleLayer, zoom: number, filterProperties: (_: string) => boolean = () => true) { this.binders = {}; this._buffers = []; diff --git a/src/geo/transform.js b/src/geo/transform.js index 997e3f00386..4ed321d1d38 100644 --- a/src/geo/transform.js +++ b/src/geo/transform.js @@ -2,18 +2,27 @@ import LngLat from './lng_lat'; import LngLatBounds from './lng_lat_bounds'; -import MercatorCoordinate, {mercatorXfromLng, mercatorYfromLat, mercatorZfromAltitude} from './mercator_coordinate'; +import MercatorCoordinate, {mercatorXfromLng, mercatorYfromLat, mercatorZfromAltitude, latFromMercatorY} from './mercator_coordinate'; import Point from '@mapbox/point-geometry'; -import {wrap, clamp} from '../util/util'; +import {wrap, clamp, radToDeg, degToRad} from '../util/util'; import {number as interpolate} from '../style-spec/util/interpolate'; import EXTENT from '../data/extent'; -import {vec4, mat4, mat2, vec2} from 'gl-matrix'; -import {Aabb, Frustum} from '../util/primitives.js'; +import {vec4, mat4, mat2, vec3, quat} from 'gl-matrix'; +import {Aabb, Frustum, Ray} from '../util/primitives.js'; import EdgeInsets from './edge_insets'; +import {FreeCamera, FreeCameraOptions, orientationFromFrame} from '../ui/free_camera'; +import assert from 'assert'; import {UnwrappedTileID, OverscaledTileID, CanonicalTileID} from '../source/tile_id'; +import type {Elevation} from '../terrain/elevation'; import type {PaddingOptions} from './edge_insets'; +const NUM_WORLD_COPIES = 3; +const DEFAULT_MIN_ZOOM = 0; + +type RayIntersectionResult = { p0: vec4, p1: vec4, t: number }; +type ElevationReference = "sea" | "ground"; + /** * A single transform, generally used for a single tile to be * scaled, rotated, and zoomed. @@ -39,11 +48,16 @@ class Transform { alignedProjMatrix: Float64Array; pixelMatrix: Float64Array; pixelMatrixInverse: Float64Array; + skyboxMatrix: Float32Array; glCoordMatrix: Float32Array; labelPlaneMatrix: Float32Array; + freezeTileCoverage: boolean; + cameraElevationReference: ElevationReference; + _elevation: ?Elevation; _fov: number; _pitch: number; _zoom: number; + _cameraZoom: ?number; _unmodified: boolean; _renderWorldCopies: boolean; _minZoom: number; @@ -53,15 +67,17 @@ class Transform { _center: LngLat; _edgeInsets: EdgeInsets; _constraining: boolean; - _posMatrixCache: {[_: string]: Float32Array}; - _alignedPosMatrixCache: {[_: string]: Float32Array}; + _posMatrixCache: {[_: number]: Float32Array}; + _alignedPosMatrixCache: {[_: number]: Float32Array}; + _camera: FreeCamera; + _centerAltitude: number; constructor(minZoom: ?number, maxZoom: ?number, minPitch: ?number, maxPitch: ?number, renderWorldCopies: boolean | void) { this.tileSize = 512; // constant this.maxValidLatitude = 85.051129; // constant this._renderWorldCopies = renderWorldCopies === undefined ? true : renderWorldCopies; - this._minZoom = minZoom || 0; + this._minZoom = minZoom || DEFAULT_MIN_ZOOM; this._maxZoom = maxZoom || 22; this._minPitch = (minPitch === undefined || minPitch === null) ? 0 : minPitch; @@ -80,25 +96,58 @@ class Transform { this._edgeInsets = new EdgeInsets(); this._posMatrixCache = {}; this._alignedPosMatrixCache = {}; + this._camera = new FreeCamera(); + this._centerAltitude = 0; + this.cameraElevationReference = "ground"; } clone(): Transform { const clone = new Transform(this._minZoom, this._maxZoom, this._minPitch, this.maxPitch, this._renderWorldCopies); + clone._elevation = this._elevation; + clone._centerAltitude = this._centerAltitude; clone.tileSize = this.tileSize; clone.latRange = this.latRange; clone.width = this.width; clone.height = this.height; + clone.cameraElevationReference = this.cameraElevationReference; clone._center = this._center; - clone.zoom = this.zoom; + clone._setZoom(this.zoom); + clone._cameraZoom = this._cameraZoom; clone.angle = this.angle; clone._fov = this._fov; clone._pitch = this._pitch; clone._unmodified = this._unmodified; clone._edgeInsets = this._edgeInsets.clone(); + clone._camera = this._camera.clone(); clone._calcMatrices(); + clone.freezeTileCoverage = this.freezeTileCoverage; return clone; } + get elevation(): ?Elevation { return this._elevation; } + set elevation(elevation: ?Elevation) { + if (this._elevation === elevation) return; + this._elevation = elevation; + if (!elevation) { + this._cameraZoom = null; + this._centerAltitude = 0; + } else { + if (this._updateCenterElevation()) + this._updateCameraOnTerrain(); + } + this._calcMatrices(); + } + updateElevation(constrainCameraOverTerrain: boolean) { // On render, no need for higher granularity on update reasons. + if (this._terrainEnabled() && this._cameraZoom == null) { + if (this._updateCenterElevation()) + this._updateCameraOnTerrain(); + } + if (constrainCameraOverTerrain) { + this._constrainCameraAltitude(); + } + this._calcMatrices(); + } + get minZoom(): number { return this._minZoom; } set minZoom(zoom: number) { if (this._minZoom === zoom) return; @@ -192,23 +241,85 @@ class Transform { const z = Math.min(Math.max(zoom, this.minZoom), this.maxZoom); if (this._zoom === z) return; this._unmodified = false; + this._setZoom(z); + if (this._terrainEnabled()) { + this._updateCameraOnTerrain(); + } + this._constrain(); + this._calcMatrices(); + } + _setZoom(z: number) { this._zoom = z; this.scale = this.zoomScale(z); this.tileZoom = Math.floor(z); this.zoomFraction = z - this.tileZoom; - this._constrain(); - this._calcMatrices(); + } + + _updateCenterElevation(): boolean { + if (!this._elevation) + return false; + + // Camera zoom describes the distance of the camera to the sea level (altitude). It is used only for manipulating the camera location. + // The standard zoom (this._zoom) defines the camera distance to the terrain (height). Its behavior and conceptual meaning in determining + // which tiles to stream is same with or without the terrain. + const elevationAtCenter = this._elevation.getAtPoint(MercatorCoordinate.fromLngLat(this.center), -1); + + if (elevationAtCenter === -1) { + // Elevation data not loaded yet + this._cameraZoom = null; + return false; + } + + this._centerAltitude = elevationAtCenter; + return true; + } + + // Places the camera above terrain so that the current zoom value is respected at the center. + // In other words, camera height in relative to ground elevation remains constant. + // Returns false if the elevation data is not available (yet) at the center point. + _updateCameraOnTerrain() { + const height = this.cameraToCenterDistance / this.worldSize; + const terrainElevation = mercatorZfromAltitude(this._centerAltitude, this.center.lat); + + this._cameraZoom = this._zoomFromMercatorZ(terrainElevation + height); } get center(): LngLat { return this._center; } set center(center: LngLat) { if (center.lat === this._center.lat && center.lng === this._center.lng) return; + this._unmodified = false; this._center = center; + if (this._terrainEnabled()) { + if (this.cameraElevationReference === "ground") { + // Check that the elevation data is available at the new location. + if (this._updateCenterElevation()) + this._updateCameraOnTerrain(); + else + this._cameraZoom = null; + } else { + this._updateZoomFromElevation(); + } + } this._constrain(); this._calcMatrices(); } + _updateZoomFromElevation() { + if (this._cameraZoom == null || !this._elevation) + return; + + // Compute zoom level from the height of the camera relative to the terrain + const cameraZoom: number = this._cameraZoom; + const elevationAtCenter = this._elevation.getAtPoint(MercatorCoordinate.fromLngLat(this.center)); + const mercatorElevation = mercatorZfromAltitude(elevationAtCenter, this.center.lat); + const altitude = this._mercatorZfromZoom(cameraZoom); + const minHeight = this._mercatorZfromZoom(this._maxZoom); + const height = Math.max(altitude - mercatorElevation, minHeight); + + this._setZoom(this._zoomFromMercatorZ(height)); + } + get padding(): PaddingOptions { return this._edgeInsets.toJSON(); } set padding(padding: PaddingOptions) { if (this._edgeInsets.equals(padding)) return; @@ -218,6 +329,100 @@ class Transform { this._calcMatrices(); } + /** + * Computes a zoom value relative to a map plane that goes through the provided mercator position. + * @param {*} position A position defining the altitude of the the map plane + */ + computeZoomRelativeTo(position: MercatorCoordinate): number { + // Find map center position on the target plane by casting a ray from screen center towards the plane. + // Direct distance to the target position is used if the target position is above camera position. + const centerOnTargetAltitude = this.rayIntersectionCoordinate(this.pointRayIntersection(this.centerPoint, position.toAltitude())); + + let targetPosition: ?vec3; + if (position.z < this._camera.position[2]) { + targetPosition = [centerOnTargetAltitude.x, centerOnTargetAltitude.y, centerOnTargetAltitude.z]; + } else { + targetPosition = [position.x, position.y, position.z]; + } + + const distToTarget = vec3.length(vec3.sub([], this._camera.position, targetPosition)); + return clamp(this._zoomFromMercatorZ(distToTarget), this._minZoom, this._maxZoom); + } + + setFreeCameraOptions(options: FreeCameraOptions) { + if (!this.height) + return; + + if (!options.position && !options.orientation) + return; + + // Camera state must be up-to-date before accessing its getters + this._updateCameraState(); + + let changed = false; + if (options.orientation && !quat.exactEquals(options.orientation, this._camera.orientation)) { + changed = this._setCameraOrientation(options.orientation); + } + + if (options.position) { + const newPosition = [options.position.x, options.position.y, options.position.z]; + if (!vec3.exactEquals(newPosition, this._camera.position)) { + this._setCameraPosition(newPosition); + changed = true; + } + } + + if (changed) { + this._updateStateFromCamera(); + this.recenterOnTerrain(); + } + } + + getFreeCameraOptions(): FreeCameraOptions { + this._updateCameraState(); + const pos = this._camera.position; + const options = new FreeCameraOptions(); + options.position = new MercatorCoordinate(pos[0], pos[1], pos[2]); + options.orientation = this._camera.orientation; + options._elevation = this.elevation; + options._renderWorldCopies = this._renderWorldCopies; + + return options; + } + + _setCameraOrientation(orientation: quat): boolean { + // zero-length quaternions are not valid + if (!quat.length(orientation)) + return false; + + quat.normalize(orientation, orientation); + + // The new orientation must be sanitized by making sure it can be represented + // with a pitch and bearing. Roll-component must be removed and the camera can't be upside down + const forward = vec3.transformQuat([], [0, 0, -1], orientation); + const up = vec3.transformQuat([], [0, -1, 0], orientation); + + if (up[2] < 0.0) + return false; + + const updatedOrientation = orientationFromFrame(forward, up); + if (!updatedOrientation) + return false; + + this._camera.orientation = updatedOrientation; + return true; + } + + _setCameraPosition(position: vec3) { + // Altitude must be clamped to respect min and max zoom + const minWorldSize = this.zoomScale(this.minZoom) * this.tileSize; + const maxWorldSize = this.zoomScale(this.maxZoom) * this.tileSize; + const distToCenter = this.cameraToCenterDistance; + + position[2] = clamp(position[2], distToCenter / maxWorldSize, distToCenter / minWorldSize); + this._camera.position = position; + } + /** * The center of the screen in pixels with the top-left corner being (0,0) * and +y axis pointing downwards. This accounts for padding. @@ -230,6 +435,17 @@ class Transform { return this._edgeInsets.getCenter(this.width, this.height); } + /** + * Returns the vertical half-fov, accounting for padding, in radians. + * + * @readonly + * @type {number} + * @private + */ + get fovAboveCenter(): number { + return this._fov * (0.5 + this.centerOffset.y / this.height); + } + /** * Returns if the padding params match * @@ -308,7 +524,6 @@ class Transform { * @param {number} options.maxzoom * @param {boolean} options.roundZoom * @param {boolean} options.reparseOverscaled - * @param {boolean} options.renderWorldCopies * @returns {Array} OverscaledTileIDs * @private */ @@ -319,33 +534,43 @@ class Transform { maxzoom?: number, roundZoom?: boolean, reparseOverscaled?: boolean, - renderWorldCopies?: boolean + renderWorldCopies?: boolean, + useElevationData?: boolean } ): Array { let z = this.coveringZoomLevel(options); const actualZ = z; + const useElevationData = !!options.useElevationData; + if (options.minzoom !== undefined && z < options.minzoom) return []; if (options.maxzoom !== undefined && z > options.maxzoom) z = options.maxzoom; const centerCoord = MercatorCoordinate.fromLngLat(this.center); - const numTiles = Math.pow(2, z); + const numTiles = 1 << z; const centerPoint = [numTiles * centerCoord.x, numTiles * centerCoord.y, 0]; const cameraFrustum = Frustum.fromInvProjectionMatrix(this.invProjMatrix, this.worldSize, z); + const cameraCoord = this.pointCoordinate(this.getCameraPoint()); + const meterToTile = numTiles * mercatorZfromAltitude(1, this.center.lat); + const cameraAltitude = this._camera.position[2] / mercatorZfromAltitude(1, this.center.lat); + const cameraPoint = [numTiles * cameraCoord.x, numTiles * cameraCoord.y, cameraAltitude]; + // Let's consider an example for !roundZoom: e.g. tileZoom 16 is used from zoom 16 all the way to zoom 16.99. + // This would mean that the minimal distance to split would be based on distance from camera to center of 16.99 zoom. + // The same is already incorporated in logic behind roundZoom for raster (so there is no adjustment needed in following line). + // 0.02 added to compensate for precision errors, see "coveringTiles for terrain" test in transform.test.js. + const zoomSplitDistance = this.cameraToCenterDistance / options.tileSize * (options.roundZoom ? 1 : 0.502); // No change of LOD behavior for pitch lower than 60 and when there is no top padding: return only tile ids from the requested zoom level - let minZoom = options.minzoom || 0; - // Use 0.1 as an epsilon to avoid for explicit == 0.0 floating point checks - if (this.pitch <= 60.0 && this._edgeInsets.top < 0.1) - minZoom = z; - - // There should always be a certain number of maximum zoom level tiles surrounding the center location - const radiusOfMaxLvlLodInTiles = 3; + const minZoom = this.pitch <= 60.0 && this._edgeInsets.top <= this._edgeInsets.bottom && !this._elevation ? z : 0; + const maxRange = this.elevation ? this.elevation.exaggeration() * 10000 : 0; const newRootTile = (wrap: number): any => { + const max = maxRange; + const min = -maxRange; return { + // With elevation, this._elevation provides z coordinate values. For 2D: // All tiles are on zero elevation plane => z difference is zero - aabb: new Aabb([wrap * numTiles, 0, 0], [(wrap + 1) * numTiles, numTiles, 0]), + aabb: new Aabb([wrap * numTiles, 0, min], [(wrap + 1) * numTiles, numTiles, max]), zoom: 0, x: 0, y: 0, @@ -360,9 +585,47 @@ class Transform { const maxZoom = z; const overscaledZ = options.reparseOverscaled ? actualZ : z; + const getAABBFromElevation = (aabb, tileID) => { + assert(this._elevation); + if (!this._elevation) return; // To silence flow. + const minmax = this._elevation.getMinMaxForTile(tileID); + if (minmax) { + aabb.min[2] = minmax.min; + aabb.max[2] = minmax.max; + aabb.center[2] = (aabb.min[2] + aabb.max[2]) / 2; + } + }; + const square = a => a * a; + const cameraHeightSqr = square((cameraAltitude - this._centerAltitude) * meterToTile); // in tile coordinates. + + // Scale distance to split for acute angles. + // dzSqr: z component of camera to tile distance, square. + // dSqr: 3D distance of camera to tile, square. + const distToSplitScale = (dzSqr, dSqr) => { + // When the angle between camera to tile ray and tile plane is smaller + // than acuteAngleThreshold, scale the distance to split. Scaling is adaptive: smaller + // the angle, the scale gets lower value. Although it seems early to start at 45, + // it is not: scaling kicks in around 60 degrees pitch. + const acuteAngleThresholdSin = 0.707; // Math.sin(45) + const stretchTile = 1.1; + // Distances longer than 'dz / acuteAngleThresholdSin' gets scaled + // following geometric series sum: every next dz length in distance can be + // 'stretchTile times' longer. It is further, the angle is sharper. Total, + // adjusted, distance would then be: + // = dz / acuteAngleThresholdSin + (dz * stretchTile + dz * stretchTile ^ 2 + ... + dz * stretchTile ^ k), + // where k = (d - dz / acuteAngleThresholdSin) / dz = d / dz - 1 / acuteAngleThresholdSin; + // = dz / acuteAngleThresholdSin + dz * ((stretchTile ^ (k + 1) - 1) / (stretchTile - 1) - 1) + // or put differently, given that k is based on d and dz, tile on distance d could be used on distance scaled by: + // 1 / acuteAngleThresholdSin + (stretchTile ^ (k + 1) - 1) / (stretchTile - 1) - 1 + if (dSqr * square(acuteAngleThresholdSin) < dzSqr) return 1.0; // Early return, no scale. + const r = Math.sqrt(dSqr / dzSqr); + const k = r - 1 / acuteAngleThresholdSin; + return r / (1 / acuteAngleThresholdSin + (Math.pow(stretchTile, k + 1) - 1) / (stretchTile - 1) - 1); + }; + if (this._renderWorldCopies) { // Render copy of the globe thrice on both sides - for (let i = 1; i <= 3; i++) { + for (let i = 1; i <= NUM_WORLD_COPIES; i++) { stack.push(newRootTile(-i)); stack.push(newRootTile(i)); } @@ -386,23 +649,36 @@ class Transform { fullyVisible = intersectResult === 2; } - const distanceX = it.aabb.distanceX(centerPoint); - const distanceY = it.aabb.distanceY(centerPoint); - const longestDim = Math.max(Math.abs(distanceX), Math.abs(distanceY)); + let shouldSplit = true; + if (minZoom <= it.zoom && it.zoom < maxZoom) { + const dx = it.aabb.distanceX(cameraPoint); + const dy = it.aabb.distanceY(cameraPoint); + let dzSqr = cameraHeightSqr; + + if (useElevationData) { + dzSqr = square(it.aabb.distanceZ(cameraPoint) * meterToTile); + } - // We're using distance based heuristics to determine if a tile should be split into quadrants or not. - // radiusOfMaxLvlLodInTiles defines that there's always a certain number of maxLevel tiles next to the map center. - // Using the fact that a parent node in quadtree is twice the size of its children (per dimension) - // we can define distance thresholds for each relative level: - // f(k) = offset + 2 + 4 + 8 + 16 + ... + 2^k. This is the same as "offset+2^(k+1)-2" - const distToSplit = radiusOfMaxLvlLodInTiles + (1 << (maxZoom - it.zoom)) - 2; + const distanceSqr = dx * dx + dy * dy + dzSqr; + const distToSplit = (1 << maxZoom - it.zoom) * zoomSplitDistance; + const distToSplitSqr = square(distToSplit * distToSplitScale(Math.max(dzSqr, cameraHeightSqr), distanceSqr)); + + shouldSplit = distanceSqr < distToSplitSqr; + } // Have we reached the target depth or is the tile too far away to be any split further? - if (it.zoom === maxZoom || (longestDim > distToSplit && it.zoom >= minZoom)) { - result.push({ - tileID: new OverscaledTileID(it.zoom === maxZoom ? overscaledZ : it.zoom, it.wrap, it.zoom, x, y), - distanceSq: vec2.sqrLen([centerPoint[0] - 0.5 - x, centerPoint[1] - 0.5 - y]) - }); + if (it.zoom === maxZoom || !shouldSplit) { + const tileZoom = it.zoom === maxZoom ? overscaledZ : it.zoom; + if (!!options.minzoom && options.minzoom > tileZoom) { + // Not within source tile range. + continue; + } + + const dx = centerPoint[0] - ((0.5 + x + (it.wrap << it.zoom)) * (1 << (z - it.zoom))); + const dy = centerPoint[1] - 0.5 - y; + const id = it.tileID ? it.tileID : new OverscaledTileID(tileZoom, it.wrap, it.zoom, x, y); + + result.push({tileID: id, distanceSq: dx * dx + dy * dy}); continue; } @@ -410,11 +686,23 @@ class Transform { const childX = (x << 1) + (i % 2); const childY = (y << 1) + (i >> 1); - stack.push({aabb: it.aabb.quadrant(i), zoom: it.zoom + 1, x: childX, y: childY, wrap: it.wrap, fullyVisible}); + const aabb = it.aabb.quadrant(i); + let tileID = null; + if (useElevationData && it.zoom > maxZoom - 6) { + // Using elevation data for tiles helps clipping out tiles that are not visible and + // precise distance calculation. it.zoom > maxZoom - 6 is an optimization as those before get subdivided + // or they are so far at horizon that it doesn't matter. + tileID = new OverscaledTileID(it.zoom + 1 === maxZoom ? overscaledZ : it.zoom + 1, it.wrap, it.zoom + 1, childX, childY); + getAABBFromElevation(aabb, tileID); + } + stack.push({aabb, zoom: it.zoom + 1, x: childX, y: childY, wrap: it.wrap, fullyVisible, tileID}); } } - - return result.sort((a, b) => a.distanceSq - b.distanceSq).map(a => a.tileID); + const cover = result.sort((a, b) => a.distanceSq - b.distanceSq).map(a => a.tileID); + // Relax the assertion on terrain, on high zoom we use distance to center of tile + // while camera might be closer to selected center of map. + assert(!cover.length || this.elevation || cover[0].overscaledZ === overscaledZ); + return cover; } resize(width: number, height: number) { @@ -457,14 +745,36 @@ class Transform { } } + setLocation(location: MercatorCoordinate) { + this.center = this.coordinateLocation(location); + if (this._renderWorldCopies) { + this.center = this.center.wrap(); + } + } + /** - * Given a location, return the screen point that corresponds to it + * Given a location, return the screen point that corresponds to it. In 3D mode + * (with terrain) this behaves the same as in 2D mode. + * This method is coupled with {@see pointLocation} in 3D mode to model map manipulation + * using flat plane approach to keep constant elevation above ground. * @param {LngLat} lnglat location * @returns {Point} screen point * @private */ locationPoint(lnglat: LngLat) { - return this.coordinatePoint(this.locationCoordinate(lnglat)); + return this._coordinatePoint(this.locationCoordinate(lnglat), false); + } + + /** + * Given a location, return the screen point that corresponds to it + * In 3D mode (when terrain is enabled) elevation is sampled for the point before + * projecting it. In 2D mode, behaves the same locationPoint. + * @param {LngLat} lnglat location + * @returns {Point} screen point + * @private + */ + locationPoint3D(lnglat: LngLat) { + return this._coordinatePoint(this.locationCoordinate(lnglat), true); } /** @@ -477,6 +787,18 @@ class Transform { return this.coordinateLocation(this.pointCoordinate(p)); } + /** + * Given a point on screen, return its lnglat + * In 3D mode (map with terrain) returns location of terrain raycast point. + * In 2D mode, behaves the same as {@see pointLocation}. + * @param {Point} p screen point + * @returns {LngLat} lnglat location + * @private + */ + pointLocation3D(p: Point) { + return this.coordinateLocation(this.pointCoordinate3D(p)); + } + /** * Given a geographical lnglat, return an unrounded * coordinate that represents it at this transform's zoom level. @@ -498,44 +820,155 @@ class Transform { return coord.toLngLat(); } - pointCoordinate(p: Point) { - const targetZ = 0; + /** + * Casts a ray from a point on screen and returns the Ray, + * and the extent along it, at which it intersects the map plane. + * + * @param {Point} p viewport pixel co-ordinates + * @param {number} z optional altitude of the map plane + * @returns {{ p0: vec4, p1: vec4, t: number }} p0,p1 are two points on the ray + * t is the fractional extent along the ray at which the ray intersects the map plane + * @private + */ + pointRayIntersection(p: Point, z: ?number): RayIntersectionResult { + const targetZ = (z !== undefined && z !== null) ? z : this._centerAltitude; // since we don't know the correct projected z value for the point, // unproject two points to get a line and then find the point on that // line with z=0 - const coord0 = [p.x, p.y, 0, 1]; - const coord1 = [p.x, p.y, 1, 1]; + const p0 = [p.x, p.y, 0, 1]; + const p1 = [p.x, p.y, 1, 1]; + + vec4.transformMat4(p0, p0, this.pixelMatrixInverse); + vec4.transformMat4(p1, p1, this.pixelMatrixInverse); - vec4.transformMat4(coord0, coord0, this.pixelMatrixInverse); - vec4.transformMat4(coord1, coord1, this.pixelMatrixInverse); + const w0 = p0[3]; + const w1 = p1[3]; + vec4.scale(p0, p0, 1 / w0); + vec4.scale(p1, p1, 1 / w1); - const w0 = coord0[3]; - const w1 = coord1[3]; - const x0 = coord0[0] / w0; - const x1 = coord1[0] / w1; - const y0 = coord0[1] / w0; - const y1 = coord1[1] / w1; - const z0 = coord0[2] / w0; - const z1 = coord1[2] / w1; + const z0 = p0[2]; + const z1 = p1[2]; const t = z0 === z1 ? 0 : (targetZ - z0) / (z1 - z0); + return {p0, p1, t}; + } + + screenPointToMercatorRay(p: Point): Ray { + const p0 = [p.x, p.y, 0, 1]; + const p1 = [p.x, p.y, 1, 1]; + + vec4.transformMat4(p0, p0, this.pixelMatrixInverse); + vec4.transformMat4(p1, p1, this.pixelMatrixInverse); + + vec4.scale(p0, p0, 1 / p0[3]); + vec4.scale(p1, p1, 1 / p1[3]); + + // Convert altitude from meters to pixels + p0[2] = mercatorZfromAltitude(p0[2], this._center.lat) * this.worldSize; + p1[2] = mercatorZfromAltitude(p1[2], this._center.lat) * this.worldSize; + + vec4.scale(p0, p0, 1 / this.worldSize); + vec4.scale(p1, p1, 1 / this.worldSize); + + return new Ray([p0[0], p0[1], p0[2]], vec3.normalize([], vec3.sub([], p1, p0))); + } + + /** + * Helper method to convert the ray intersection with the map plane to MercatorCoordinate + * + * @param {RayIntersectionResult} rayIntersection + * @returns {MercatorCoordinate} + * @private + */ + rayIntersectionCoordinate(rayIntersection: RayIntersectionResult): MercatorCoordinate { + const {p0, p1, t} = rayIntersection; + + const z0 = mercatorZfromAltitude(p0[2], this._center.lat); + const z1 = mercatorZfromAltitude(p1[2], this._center.lat); + return new MercatorCoordinate( - interpolate(x0, x1, t) / this.worldSize, - interpolate(y0, y1, t) / this.worldSize); + interpolate(p0[0], p1[0], t) / this.worldSize, + interpolate(p0[1], p1[1], t) / this.worldSize, + interpolate(z0, z1, t)); + } + + /** + * Given a point on screen, returns MercatorCoordinate. + * @param {Point} p top left origin screen point, in pixels. + * @private + */ + pointCoordinate(p: Point): MercatorCoordinate { + // For p above horizon, don't return point behind camera but clamp p.y at horizon line. + const horizonOffset = this.horizonLineFromTop(); + const clamped = horizonOffset > p.y ? new Point(p.x, horizonOffset) : p; + + return this.rayIntersectionCoordinate(this.pointRayIntersection(clamped)); + } + + /** + * Given a point on screen, returns MercatorCoordinate. + * In 3D mode, raycast to terrain. In 2D mode, behaves the same as {@see pointCoordinate}. + * For p above terrain, don't return point behind camera but clamp p.y at the top of terrain. + * @param {Point} p top left origin screen point, in pixels. + * @private + */ + pointCoordinate3D(p: Point): MercatorCoordinate { + if (!this.elevation) return this.pointCoordinate(p); + const elevation = this.elevation; + let raycast = this.elevation.pointCoordinate(p); + if (raycast) return new MercatorCoordinate(raycast[0], raycast[1], raycast[2]); + let start = 0, end = this.horizonLineFromTop(); + if (p.y > end) return this.pointCoordinate(p); // holes between tiles below horizon line or below bottom. + const samples = 10; + const threshold = 0.02 * end; + const r = p.clone(); + + for (let i = 0; i < samples && end - start > threshold; i++) { + r.y = interpolate(start, end, 0.66); // non uniform binary search favoring points closer to horizon. + const rCast = elevation.pointCoordinate(r); + if (rCast) { + end = r.y; + raycast = rCast; + } else { + start = r.y; + } + } + return raycast ? new MercatorCoordinate(raycast[0], raycast[1], raycast[2]) : this.pointCoordinate(p); + } + + /** + * Returns true if a screenspace Point p, is above the horizon. + * + * @param {Point} p + * @returns {boolean} + * @private + */ + isPointAboveHorizon(p: Point): boolean { + if (!this.elevation) { + const horizon = this.horizonLineFromTop(); + return p.y < horizon; + } else { + return !this.elevation.pointCoordinate(p); + } } /** * Given a coordinate, return the screen point that corresponds to it * @param {Coordinate} coord + * @param {boolean} sampleTerrainIn3D in 3D mode (terrain enabled), sample elevation for the point. + * If false, do the same as in 2D mode, assume flat camera elevation plane for all points. * @returns {Point} screen point * @private */ - coordinatePoint(coord: MercatorCoordinate) { - const p = [coord.x * this.worldSize, coord.y * this.worldSize, 0, 1]; + _coordinatePoint(coord: MercatorCoordinate, sampleTerrainIn3D: boolean) { + const elevation = sampleTerrainIn3D && this.elevation ? this.elevation.getAtPoint(coord, this._centerAltitude) : this._centerAltitude; + const p = [coord.x * this.worldSize, coord.y * this.worldSize, elevation + coord.toAltitude(), 1]; vec4.transformMat4(p, p, this.pixelMatrix); - return new Point(p[0] / p[3], p[1] / p[3]); + return p[3] > 0 ? + new Point(p[0] / p[3], p[1] / p[3]) : + new Point(Number.MAX_VALUE, Number.MAX_VALUE); } /** @@ -544,6 +977,7 @@ class Transform { * @returns {LngLatBounds} Returns a {@link LngLatBounds} object describing the map's geographical bounds. */ getBounds(): LngLatBounds { + if (this._terrainEnabled()) return this._getBounds3D(); return new LngLatBounds() .extend(this.pointLocation(new Point(0, 0))) .extend(this.pointLocation(new Point(this.width, 0))) @@ -551,6 +985,46 @@ class Transform { .extend(this.pointLocation(new Point(0, this.height))); } + _getBounds3D(): LngLatBounds { + assert(this.elevation); + const elevation = ((this.elevation: any): Elevation); + const minmax = elevation.visibleDemTiles.reduce((acc, t) => { + if (t.dem) { + const tree = t.dem.tree; + acc.min = Math.min(acc.min, tree.minimums[0]); + acc.max = Math.max(acc.max, tree.maximums[0]); + } + return acc; + }, {min: Number.MAX_VALUE, max: 0}); + minmax.min *= elevation.exaggeration(); + minmax.max *= elevation.exaggeration(); + const top = this.horizonLineFromTop(); + return [ + new Point(0, top), + new Point(this.width, top), + new Point(this.width, this.height), + new Point(0, this.height) + ].reduce((acc, p) => { + return acc + .extend(this.coordinateLocation(this.rayIntersectionCoordinate(this.pointRayIntersection(p, minmax.min)))) + .extend(this.coordinateLocation(this.rayIntersectionCoordinate(this.pointRayIntersection(p, minmax.max)))); + }, new LngLatBounds()); + } + + /** + * Returns position oh horizon line, from the top, in pixels. If horizon is not + * visible, returns 0. + * @private + */ + horizonLineFromTop(): number { + // h is height of space above map center to horizon. + const h = this.height / 2 / Math.tan(this._fov / 2) / Math.tan(Math.max(this._pitch, 0.1)) + this.centerOffset.y; + // incorporate 3% of the area above center to account for reduced precision. + const horizonEpsilon = 0.03; + const offset = this.height / 2 - h * (1 - horizonEpsilon); + return Math.max(0, offset); + } + /** * Returns the maximum geographical bounds the map is constrained to, or `null` if none set. * @returns {LngLatBounds} {@link LngLatBounds} @@ -606,6 +1080,79 @@ class Transform { return this.mercatorMatrix.slice(); } + recenterOnTerrain() { + if (!this._elevation) + return; + + const elevation: Elevation = this._elevation; + this._updateCameraState(); + + // Cast a ray towards the sea level and find the intersection point with the terrain. + const start = this._camera.position; + const dir = this._camera.forward(); + + if (start.z <= 0 || dir[2] >= 0) + return; + + // The raycast function expects z-component to be in meters + const metersToMerc = mercatorZfromAltitude(1.0, this._center.lat); + start[2] /= metersToMerc; + dir[2] /= metersToMerc; + vec3.normalize(dir, dir); + + const t = elevation.raycast(start, dir, elevation.exaggeration()); + + if (t) { + const point = vec3.scaleAndAdd([], start, dir, t); + const newCenter = new MercatorCoordinate(point[0], point[1], mercatorZfromAltitude(point[2], latFromMercatorY(point[1]))); + + const pos = this._camera.position; + const camToNew = [newCenter.x - pos[0], newCenter.y - pos[1], newCenter.z - pos[2]]; + const maxAltitude = newCenter.z + vec3.length(camToNew); + + // Camera zoom has to be updated as the orbit distance might have changed + this._cameraZoom = this._zoomFromMercatorZ(maxAltitude); + this._centerAltitude = newCenter.toAltitude(); + this._center = newCenter.toLngLat(); + this._updateZoomFromElevation(); + this._constrain(); + this._calcMatrices(); + } + } + + _constrainCameraAltitude() { + if (!this._elevation) + return; + + const elevation: Elevation = this._elevation; + this._updateCameraState(); + const elevationAtCamera = elevation.getAtPoint(this._camera.mercatorPosition); + + const minHeight = this._minimumHeightOverTerrain() * Math.cos(degToRad(this._maxPitch)); + const terrainElevation = mercatorZfromAltitude(elevationAtCamera, this._center.lat); + const cameraHeight = this._camera.position[2] - terrainElevation; + + if (cameraHeight < minHeight) { + const center = MercatorCoordinate.fromLngLat(this._center, this._centerAltitude); + const cameraPos = this._camera.mercatorPosition; + const cameraToCenter = [center.x - cameraPos.x, center.y - cameraPos.y, center.z - cameraPos.z]; + const prevDistToCamera = vec3.length(cameraToCenter); + + // Adjust the camera vector so that the camera is placed above the terrain. + // Distance between the camera and the center point is kept constant. + cameraToCenter[2] -= minHeight - cameraHeight; + + const newDistToCamera = vec3.length(cameraToCenter); + if (newDistToCamera === 0) + return; + + vec3.scale(cameraToCenter, cameraToCenter, prevDistToCamera / newDistToCamera); + this._camera.position = [center.x - cameraToCenter[0], center.y - cameraToCenter[1], center.z - cameraToCenter[2]]; + this._camera.orientation = orientationFromFrame(cameraToCenter, this._camera.up()); + this._updateStateFromCamera(); + } + } + _constrain() { if (!this.center || !this.width || !this.height || this._constraining) return; @@ -671,29 +1218,71 @@ class Transform { y2 !== undefined ? y2 : point.y)); } + this._constrainCameraAltitude(); + this._unmodified = unmodified; this._constraining = false; } + /** + * Returns the minimum zoom at which `this.width` can fit `this.lngRange` + * and `this.height` can fit `this.latRange` + * + * @returns {number} + */ + _minZoomForBounds(): number { + const minZoomForDim = (dim: number, range: [number, number]): number => { + return Math.log2(dim / (this.tileSize * Math.abs(range[1] - range[0]))); + }; + let minLatZoom = DEFAULT_MIN_ZOOM; + if (this.latRange) { + const latRange = this.latRange; + minLatZoom = minZoomForDim(this.height, [mercatorYfromLat(latRange[0]), mercatorYfromLat(latRange[1])]); + } + let minLngZoom = DEFAULT_MIN_ZOOM; + if (this.lngRange) { + const lngRange = this.lngRange; + minLngZoom = minZoomForDim(this.width, [mercatorXfromLng(lngRange[0]), mercatorXfromLng(lngRange[1])]); + } + + return Math.max(minLatZoom, minLngZoom); + } + + /** + * Returns the maximum distance of the camera from the center of the bounds, such that + * `this.width` can fit `this.lngRange` and `this.height` can fit `this.latRange`. + * In mercator units. + * + * @returns {number} + */ + _maxCameraBoundsDistance(): number { + return this._mercatorZfromZoom(this._minZoomForBounds()); + } + _calcMatrices() { if (!this.height) return; const halfFov = this._fov / 2; const offset = this.centerOffset; this.cameraToCenterDistance = 0.5 / Math.tan(halfFov) * this.height; + const pixelsPerMeter = mercatorZfromAltitude(1, this.center.lat) * this.worldSize; + + this._updateCameraState(); // Find the distance from the center point [width/2 + offset.x, height/2 + offset.y] to the // center top point [width/2 + offset.x, 0] in Z units, using the law of sines. // 1 Z unit is equivalent to 1 horizontal px at the center of the map // (the distance between[width/2, height/2] and [width/2 + 1, height/2]) const groundAngle = Math.PI / 2 + this._pitch; - const fovAboveCenter = this._fov * (0.5 + offset.y / this.height); - const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * this.cameraToCenterDistance / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, 0.01, Math.PI - 0.01)); + const fovAboveCenter = this.fovAboveCenter; + + const cameraToSeaLevelDistance = this._camera.position[2] * this.worldSize / Math.cos(this._pitch); + const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * cameraToSeaLevelDistance / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, 0.01, Math.PI - 0.01)); const point = this.point; const x = point.x, y = point.y; // Calculate z distance of the farthest fragment that should be rendered. - const furthestDistance = Math.cos(Math.PI / 2 - this._pitch) * topHalfSurfaceDistance + this.cameraToCenterDistance; + const furthestDistance = Math.cos(Math.PI / 2 - this._pitch) * topHalfSurfaceDistance + cameraToSeaLevelDistance; // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` const farZ = furthestDistance * 1.01; @@ -706,29 +1295,32 @@ class Transform { // seems to solve z-fighting issues in deckgl while not clipping buildings too close to the camera. const nearZ = this.height / 50; - // matrix for conversion from location to GL coordinates (-1 .. 1) - let m = new Float64Array(16); - mat4.perspective(m, this._fov, this.width / this.height, nearZ, farZ); + const worldToCamera = this._camera.getWorldToCamera(this.worldSize, pixelsPerMeter); + const cameraToClip = this._camera.getCameraToClipPerspective(this._fov, this.width / this.height, nearZ, farZ); - //Apply center of perspective offset - m[8] = -offset.x * 2 / this.width; - m[9] = offset.y * 2 / this.height; + // Apply center of perspective offset + cameraToClip[8] = -offset.x * 2 / this.width; + cameraToClip[9] = offset.y * 2 / this.height; - mat4.scale(m, m, [1, -1, 1]); - mat4.translate(m, m, [0, 0, -this.cameraToCenterDistance]); - mat4.rotateX(m, m, this._pitch); - mat4.rotateZ(m, m, this.angle); - mat4.translate(m, m, [-x, -y, 0]); + let m = mat4.mul([], cameraToClip, worldToCamera); // The mercatorMatrix can be used to transform points from mercator coordinates // ([0, 0] nw, [1, 1] se) to GL coordinates. - this.mercatorMatrix = mat4.scale([], m, [this.worldSize, this.worldSize, this.worldSize]); - - // scale vertically to meters per pixel (inverse of ground resolution): - mat4.scale(m, m, [1, 1, mercatorZfromAltitude(1, this.center.lat) * this.worldSize, 1]); + this.mercatorMatrix = mat4.scale([], m, [this.worldSize, this.worldSize, this.worldSize / pixelsPerMeter]); this.projMatrix = m; - this.invProjMatrix = mat4.invert([], this.projMatrix); + // For tile cover calculation, use inverted of base (non elevated) matrix + // as tile elevations are in tile coordinates and relative to center elevation. + this.invProjMatrix = mat4.invert(new Float64Array(16), this.projMatrix); + + const view = new Float32Array(16); + mat4.identity(view); + mat4.scale(view, view, [1, -1, 1]); + mat4.rotateX(view, view, this._pitch); + mat4.rotateZ(view, view, this.angle); + + const projection = mat4.perspective(new Float32Array(16), this._fov, this.width / this.height, nearZ, farZ); + this.skyboxMatrix = mat4.multiply(view, projection, view); // Make a second projection matrix that is aligned to a pixel grid for rendering raster tiles. // We're rounding the (floating point) x/y values to achieve to avoid rendering raster images to fractional @@ -758,7 +1350,7 @@ class Transform { // matrix for conversion from location to screen coordinates this.pixelMatrix = mat4.multiply(new Float64Array(16), this.labelPlaneMatrix, this.projMatrix); - // inverse matrix for conversion from screen coordinaes to location + // inverse matrix for conversion from screen coordinates to location m = mat4.invert(new Float64Array(16), this.pixelMatrix); if (!m) throw new Error("failed to invert matrix"); this.pixelMatrixInverse = m; @@ -767,14 +1359,162 @@ class Transform { this._alignedPosMatrixCache = {}; } - maxPitchScaleFactor() { - // calcMatrices hasn't run yet - if (!this.pixelMatrixInverse) return 1; + _updateCameraState() { + if (!this.height) return; + + // Set camera orientation and move it to a proper distance from the map + this._camera.setPitchBearing(this._pitch, this.angle); + + const dir = this._camera.forward(); + const distance = this.cameraToCenterDistance; + const center = this.point; - const coord = this.pointCoordinate(new Point(0, 0)); - const p = [coord.x * this.worldSize, coord.y * this.worldSize, 0, 1]; - const topPoint = vec4.transformMat4(p, p, this.pixelMatrix); - return topPoint[3] / this.cameraToCenterDistance; + // Use camera zoom (if terrain is enabled) to maintain constant altitude to sea level + const zoom = this._cameraZoom ? this._cameraZoom : this._zoom; + const altitude = this._mercatorZfromZoom(zoom); + const height = altitude - mercatorZfromAltitude(this._centerAltitude, this.center.lat); + + // simplified version of: this._worldSizeFromZoom(this._zoomFromMercatorZ(height)) + const updatedWorldSize = this.cameraToCenterDistance / height; + + this._camera.position = [ + center.x / this.worldSize - (dir[0] * distance) / updatedWorldSize, + center.y / this.worldSize - (dir[1] * distance) / updatedWorldSize, + mercatorZfromAltitude(this._centerAltitude, this._center.lat) + (-dir[2] * distance) / updatedWorldSize + ]; + } + + /** + * Apply a 3d translation to the camera position, but clamping it so that + * it respects the bounds set by `this.latRange` and `this.lngRange`. + * + * @param {vec3} translation + */ + _translateCameraConstrained(translation: vec3) { + const maxDistance = this._maxCameraBoundsDistance(); + // Define a ceiling in mercator Z + const maxZ = maxDistance * Math.cos(this._pitch); + const z = this._camera.position[2]; + const deltaZ = translation[2]; + let t = 1; + // we only need to clamp if the camera is moving upwards + if (deltaZ > 0) { + t = Math.min((maxZ - z) / deltaZ, 1); + } + + this._camera.position = vec3.scaleAndAdd([], this._camera.position, translation, t); + this._updateStateFromCamera(); + } + + _updateStateFromCamera() { + const position = this._camera.position; + const dir = this._camera.forward(); + const {pitch, bearing} = this._camera.getPitchBearing(); + + // Compute zoom from the distance between camera and terrain + const centerAltitude = mercatorZfromAltitude(this._centerAltitude, this.center.lat); + const minHeight = this._mercatorZfromZoom(this._maxZoom) * Math.cos(degToRad(this._maxPitch)); + const height = Math.max((position[2] - centerAltitude) / Math.cos(pitch), minHeight); + const zoom = this._zoomFromMercatorZ(height); + + // Cast a ray towards the ground to find the center point + vec3.scaleAndAdd(position, position, dir, height); + + this._pitch = clamp(pitch, degToRad(this.minPitch), degToRad(this.maxPitch)); + this.angle = wrap(bearing, -Math.PI, Math.PI); + this._setZoom(clamp(zoom, this._minZoom, this._maxZoom)); + + if (this._terrainEnabled()) + this._updateCameraOnTerrain(); + + this._center = new MercatorCoordinate(position[0], position[1], position[2]).toLngLat(); + this._unmodified = false; + this._constrain(); + this._calcMatrices(); + } + + _worldSizeFromZoom(zoom: number): number { + return Math.pow(2.0, zoom) * this.tileSize; + } + + _mercatorZfromZoom(zoom: number): number { + return this.cameraToCenterDistance / this._worldSizeFromZoom(zoom); + } + + _minimumHeightOverTerrain() { + // Determine minimum height for the camera over the terrain related to current zoom. + // Values above than 2 allow max-pitch camera closer to e.g. top of the hill, exposing + // drape raster overscale artifacts or cut terrain (see under it) as it gets clipped on + // near plane. Returned value is in mercator coordinates. + const MAX_DRAPE_OVERZOOM = 2; + const zoom = Math.min((this._cameraZoom != null ? this._cameraZoom : this._zoom) + MAX_DRAPE_OVERZOOM, this._maxZoom); + return this._mercatorZfromZoom(zoom); + } + + _zoomFromMercatorZ(z: number): number { + return this.scaleZoom(this.cameraToCenterDistance / (z * this.tileSize)); + } + + _terrainEnabled(): boolean { + return !!this._elevation; + } + + isHorizonVisibleForPoints(p0: Point, p1: Point): boolean { + const minX = Math.min(p0.x, p1.x); + const maxX = Math.max(p0.x, p1.x); + const minY = Math.min(p0.y, p1.y); + const maxY = Math.max(p0.y, p1.y); + + const min = new Point(minX, minY); + const max = new Point(maxX, maxY); + + const corners = [ + min, max, + new Point(minX, maxY), + new Point(maxX, minY), + ]; + + const minWX = (this._renderWorldCopies) ? -NUM_WORLD_COPIES : 0; + const maxWX = (this._renderWorldCopies) ? 1 + NUM_WORLD_COPIES : 1; + const minWY = 0; + const maxWY = 1; + + for (const corner of corners) { + const rayIntersection = this.pointRayIntersection(corner); + if (rayIntersection.t < 0) { + return true; + } + const coordinate = this.rayIntersectionCoordinate(rayIntersection); + if (coordinate.x < minWX || coordinate.y < minWY || + coordinate.x > maxWX || coordinate.y > maxWY) { + return true; + } + } + + return false; + } + + // Checks the four corners of the frustum to see if they lie in the map's quad. + isHorizonVisible(): boolean { + // we consider the horizon as visible if the angle between + // a the top plane of the frustum and the map plane is smaller than this threshold. + const horizonAngleEpsilon = 2; + if (this.pitch + radToDeg(this.fovAboveCenter) > (90 - horizonAngleEpsilon)) { + return true; + } + + return this.isHorizonVisibleForPoints(new Point(0, 0), new Point(this.width, this.height)); + } + + /** + * Converts a zoom delta value into a physical distance travelled in web mercator coordinates + * @param {vec3} center Destination mercator point of the movement. + * @param {number} zoomDelta Change in the zoom value + */ + zoomDeltaToMovement(center: vec3, zoomDelta: number): number { + const distance = vec3.length(vec3.sub([], this._camera.position, center)); + const relativeZoom = this._zoomFromMercatorZ(distance) + zoomDelta; + return distance - this._mercatorZfromZoom(relativeZoom); } /* @@ -793,42 +1533,6 @@ class Transform { const yOffset = Math.tan(pitch) * (this.cameraToCenterDistance || 1); return this.centerPoint.add(new Point(0, yOffset)); } - - /* - * When the map is pitched, some of the 3D features that intersect a query will not intersect - * the query at the surface of the earth. Instead the feature may be closer and only intersect - * the query because it extrudes into the air. - * - * This returns a geometry that includes all of the original query as well as all possible ares of the - * screen where the *base* of a visible extrusion could be. - * - For point queries, the line from the query point to the "camera point" - * - For other geometries, the envelope of the query geometry and the "camera point" - */ - getCameraQueryGeometry(queryGeometry: Array): Array { - const c = this.getCameraPoint(); - - if (queryGeometry.length === 1) { - return [queryGeometry[0], c]; - } else { - let minX = c.x; - let minY = c.y; - let maxX = c.x; - let maxY = c.y; - for (const p of queryGeometry) { - minX = Math.min(minX, p.x); - minY = Math.min(minY, p.y); - maxX = Math.max(maxX, p.x); - maxY = Math.max(maxY, p.y); - } - return [ - new Point(minX, minY), - new Point(maxX, minY), - new Point(maxX, maxY), - new Point(minX, maxY), - new Point(minX, minY) - ]; - } - } } export default Transform; diff --git a/src/gl/context.js b/src/gl/context.js index 5950d58a597..072b123a920 100644 --- a/src/gl/context.js +++ b/src/gl/context.js @@ -67,6 +67,8 @@ class Context { extRenderToTextureHalfFloat: any; extTimerQuery: any; + extTextureFilterAnisotropicForceOff: boolean; + constructor(gl: WebGLRenderingContext) { this.gl = gl; this.extVertexArrayObject = this.gl.getExtension('OES_vertex_array_object'); @@ -111,6 +113,7 @@ class Context { if (this.extTextureFilterAnisotropic) { this.extTextureFilterAnisotropicMax = gl.getParameter(this.extTextureFilterAnisotropic.MAX_TEXTURE_MAX_ANISOTROPY_EXT); } + this.extTextureFilterAnisotropicForceOff = false; this.extTextureHalfFloat = gl.getExtension('OES_texture_half_float'); if (this.extTextureHalfFloat) { @@ -211,7 +214,7 @@ class Context { return new Framebuffer(this, width, height, hasDepth); } - clear({color, depth}: ClearArgs) { + clear({color, depth, stencil}: ClearArgs) { const gl = this.gl; let mask = 0; @@ -232,12 +235,11 @@ class Context { this.depthMask.set(true); } - // See note in Painter#clearStencil: implement this the easy way once GPU bug/workaround is fixed upstream - // if (typeof stencil !== 'undefined') { - // mask |= gl.STENCIL_BUFFER_BIT; - // this.clearStencil.set(stencil); - // this.stencilMask.set(0xFF); - // } + if (typeof stencil !== 'undefined') { + mask |= gl.STENCIL_BUFFER_BIT; + this.clearStencil.set(stencil); + this.stencilMask.set(0xFF); + } gl.clear(mask); } diff --git a/src/gl/cull_face_mode.js b/src/gl/cull_face_mode.js index c6088208354..dab72978ad8 100644 --- a/src/gl/cull_face_mode.js +++ b/src/gl/cull_face_mode.js @@ -3,7 +3,9 @@ import type {CullFaceModeType, FrontFaceType} from './types'; const BACK = 0x0405; +const FRONT = 0x0404; const CCW = 0x0901; +const CW = 0x0900; class CullFaceMode { enable: boolean; @@ -18,9 +20,15 @@ class CullFaceMode { static disabled: $ReadOnly; static backCCW: $ReadOnly; + static backCW: $ReadOnly; + static frontCW: $ReadOnly; + static frontCCW: $ReadOnly; } CullFaceMode.disabled = new CullFaceMode(false, BACK, CCW); CullFaceMode.backCCW = new CullFaceMode(true, BACK, CCW); +CullFaceMode.backCW = new CullFaceMode(true, BACK, CW); +CullFaceMode.frontCW = new CullFaceMode(true, FRONT, CW); +CullFaceMode.frontCCW = new CullFaceMode(true, FRONT, CCW); export default CullFaceMode; diff --git a/src/gl/framebuffer.js b/src/gl/framebuffer.js index 11cac48f756..74b31e2effa 100644 --- a/src/gl/framebuffer.js +++ b/src/gl/framebuffer.js @@ -1,8 +1,8 @@ // @flow import {ColorAttachment, DepthAttachment} from './value'; -import assert from 'assert'; import type Context from './context'; +import assert from 'assert'; class Framebuffer { context: Context; diff --git a/src/gl/value.js b/src/gl/value.js index 3faabc3c24e..91f5c514942 100644 --- a/src/gl/value.js +++ b/src/gl/value.js @@ -1,6 +1,7 @@ // @flow import Color from '../style-spec/util/color'; +import assert from 'assert'; import type Context from './context'; import type { @@ -140,6 +141,9 @@ export class StencilFunc extends BaseValue { set(v: StencilFuncType): void { const c = this.current; if (v.func === c.func && v.ref === c.ref && v.mask === c.mask && !this.dirty) return; + // Assume UNSIGNED_INT_24_8 storage, with 8 bits dedicated to stencil. + // Please revise your stencil values if this threshold is triggered. + assert(v.ref >= 0 && v.ref <= 255); this.gl.stencilFunc(v.func, v.ref, v.mask); this.current = v; this.dirty = false; @@ -507,14 +511,19 @@ export class ColorAttachment extends FramebufferAttachment { } export class DepthAttachment extends FramebufferAttachment { + attachment(): number { return this.gl.DEPTH_ATTACHMENT; } set(v: ?WebGLRenderbuffer): void { if (v === this.current && !this.dirty) return; this.context.bindFramebuffer.set(this.parent); // note: it's possible to attach a texture to the depth attachment // point, but thus far MBGL only uses renderbuffers for depth const gl = this.gl; - gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, v); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, this.attachment(), gl.RENDERBUFFER, v); this.current = v; this.dirty = false; } } + +export class DepthStencilAttachment extends DepthAttachment { + attachment(): number { return this.gl.DEPTH_STENCIL_ATTACHMENT; } +} diff --git a/src/index.js b/src/index.js index 7928a81485f..36e55255d08 100644 --- a/src/index.js +++ b/src/index.js @@ -25,7 +25,10 @@ import {setRTLTextPlugin, getRTLTextPluginStatus} from './source/rtl_text_plugin import WorkerPool from './util/worker_pool'; import {prewarm, clearPrewarmedResources} from './util/global_worker_pool'; import {clearTileCache} from './util/tile_request_cache'; +import {WorkerPerformanceUtils} from './util/worker_performance_utils'; import {PerformanceUtils} from './util/performance'; +import {FreeCameraOptions} from './ui/free_camera'; +import browser from './util/browser'; const exported = { version, @@ -45,6 +48,7 @@ const exported = { LngLatBounds, Point, MercatorCoordinate, + FreeCameraOptions, Evented, config, /** @@ -174,7 +178,7 @@ const exported = { }; //This gets automatically stripped out in production builds. -Debug.extend(exported, {isSafari, getPerformanceMetrics: PerformanceUtils.getPerformanceMetrics}); +Debug.extend(exported, {isSafari, getPerformanceMetrics: PerformanceUtils.getPerformanceMetrics, getPerformanceMetricsAsync: WorkerPerformanceUtils.getPerformanceMetricsAsync, setNow: browser.setNow, restoreNow: browser.restoreNow}); /** * The version of Mapbox GL JS in use as specified in `package.json`, diff --git a/src/render/draw_background.js b/src/render/draw_background.js index 3855b519dab..a93e53d3604 100644 --- a/src/render/draw_background.js +++ b/src/render/draw_background.js @@ -7,6 +7,7 @@ import { backgroundUniformValues, backgroundPatternUniformValues } from './program/background_program'; +import {OverscaledTileID} from '../source/tile_id'; import type Painter from './painter'; import type SourceCache from '../source/source_cache'; @@ -14,7 +15,7 @@ import type BackgroundStyleLayer from '../style/style_layer/background_style_lay export default drawBackground; -function drawBackground(painter: Painter, sourceCache: SourceCache, layer: BackgroundStyleLayer) { +function drawBackground(painter: Painter, sourceCache: SourceCache, layer: BackgroundStyleLayer, coords: Array) { const color = layer.paint.get('background-color'); const opacity = layer.paint.get('background-opacity'); @@ -36,7 +37,7 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Backg const program = painter.useProgram(image ? 'backgroundPattern' : 'background'); - const tileIDs = transform.coveringTiles({tileSize}); + const tileIDs = coords ? coords : transform.coveringTiles({tileSize}); if (image) { context.activeTexture.set(gl.TEXTURE0); @@ -45,7 +46,9 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Backg const crossfade = layer.getCrossfadeParameters(); for (const tileID of tileIDs) { - const matrix = painter.transform.calculatePosMatrix(tileID.toUnwrapped()); + const matrix = coords ? tileID.posMatrix : painter.transform.calculatePosMatrix(tileID.toUnwrapped()); + painter.prepareDrawTile(tileID); + const uniformValues = image ? backgroundPatternUniformValues(matrix, opacity, painter, image, {tileID, tileSize}, crossfade) : backgroundUniformValues(matrix, opacity, color); diff --git a/src/render/draw_circle.js b/src/render/draw_circle.js index 6b11ca12971..5fe136fd499 100644 --- a/src/render/draw_circle.js +++ b/src/render/draw_circle.js @@ -4,7 +4,7 @@ import StencilMode from '../gl/stencil_mode'; import DepthMode from '../gl/depth_mode'; import CullFaceMode from '../gl/cull_face_mode'; import Program from './program'; -import {circleUniformValues} from './program/circle_program'; +import {circleUniformValues, circleDefinesValues} from './program/circle_program'; import SegmentVector from '../data/segment'; import {OverscaledTileID} from '../source/tile_id'; @@ -17,6 +17,8 @@ import type VertexBuffer from '../gl/vertex_buffer'; import type IndexBuffer from '../gl/index_buffer'; import type {UniformValues} from './uniform_binding'; import type {CircleUniformsType} from './program/circle_program'; +import type Tile from '../source/tile'; +import type {DynamicDefinesType} from './program/program_uniforms'; export default drawCircles; @@ -25,7 +27,8 @@ type TileRenderState = { program: Program<*>, layoutVertexBuffer: VertexBuffer, indexBuffer: IndexBuffer, - uniformValues: UniformValues + uniformValues: UniformValues, + tile: Tile }; type SegmentsTileRenderState = { @@ -65,7 +68,8 @@ function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleSt if (!bucket) continue; const programConfiguration = bucket.programConfigurations.get(layer.id); - const program = painter.useProgram('circle', programConfiguration); + const definesValues = circleDefinesValues(layer); + const program = painter.useProgram('circle', programConfiguration, ((definesValues: any): DynamicDefinesType[])); const layoutVertexBuffer = bucket.layoutVertexBuffer; const indexBuffer = bucket.indexBuffer; const uniformValues = circleUniformValues(painter, coord, tile, layer); @@ -76,6 +80,7 @@ function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleSt layoutVertexBuffer, indexBuffer, uniformValues, + tile }; if (sortFeaturesByKey) { @@ -102,8 +107,9 @@ function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleSt } for (const segmentsState of segmentsRenderStates) { - const {programConfiguration, program, layoutVertexBuffer, indexBuffer, uniformValues} = segmentsState.state; + const {programConfiguration, program, layoutVertexBuffer, indexBuffer, uniformValues, tile} = segmentsState.state; const segments = segmentsState.segments; + if (painter.terrain) painter.terrain.setupElevationDraw(tile, program, {useDepthForOcclusion: true}); program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled, uniformValues, layer.id, diff --git a/src/render/draw_collision_debug.js b/src/render/draw_collision_debug.js index 473238be026..b0c94d23842 100644 --- a/src/render/draw_collision_debug.js +++ b/src/render/draw_collision_debug.js @@ -69,6 +69,7 @@ function drawCollisionDebug(painter: Painter, sourceCache: SourceCache, layer: S circleOffset = circleCount; } if (!buffers) continue; + if (painter.terrain) painter.terrain.setupElevationDraw(tile, program); program.draw(context, gl.LINES, DepthMode.disabled, StencilMode.disabled, painter.colorModeForRenderPass(), @@ -78,8 +79,9 @@ function drawCollisionDebug(painter: Painter, sourceCache: SourceCache, layer: S painter.transform, tile), layer.id, buffers.layoutVertexBuffer, buffers.indexBuffer, - buffers.segments, null, painter.transform.zoom, null, null, - buffers.collisionVertexBuffer); + buffers.segments, null, painter.transform.zoom, null, + buffers.collisionVertexBuffer, + buffers.collisionVertexBufferExt); } if (!isText || !tileBatches.length) { diff --git a/src/render/draw_debug.js b/src/render/draw_debug.js index bedb06a5731..b9c34e23366 100644 --- a/src/render/draw_debug.js +++ b/src/render/draw_debug.js @@ -36,6 +36,12 @@ export function drawDebugPadding(painter: Painter) { drawCrosshair(painter, center.x, painter.transform.height - center.y, centerColor); } +export function drawDebugQueryGeometry(painter: Painter, sourceCache: SourceCache, coords: Array) { + for (let i = 0; i < coords.length; i++) { + drawTileQueryGeometry(painter, sourceCache, coords[i]); + } +} + function drawCrosshair(painter: Painter, x: number, y: number, color: Color) { const size = 20; const lineWidth = 2; @@ -69,12 +75,57 @@ function drawDebug(painter: Painter, sourceCache: SourceCache, coords: Array 0) { + tile.queryGeometryDebugViz.lazyUpload(context); + const vertexBuffer = tile.queryGeometryDebugViz.vertexBuffer; + const indexBuffer = tile.queryGeometryDebugViz.indexBuffer; + const segments = tile.queryGeometryDebugViz.segments; + if (vertexBuffer != null && indexBuffer != null && segments != null) { + program.draw(context, gl.LINE_STRIP, depthMode, stencilMode, colorMode, CullFaceMode.disabled, + debugUniformValues(posMatrix, tile.queryGeometryDebugViz.color), id, + vertexBuffer, indexBuffer, segments); + } + } + + if (tile.queryBoundsDebugViz && tile.queryBoundsDebugViz.vertices.length > 0) { + tile.queryBoundsDebugViz.lazyUpload(context); + const vertexBuffer = tile.queryBoundsDebugViz.vertexBuffer; + const indexBuffer = tile.queryBoundsDebugViz.indexBuffer; + const segments = tile.queryBoundsDebugViz.segments; + if (vertexBuffer != null && indexBuffer != null && segments != null) { + program.draw(context, gl.LINE_STRIP, depthMode, stencilMode, colorMode, CullFaceMode.disabled, + debugUniformValues(posMatrix, tile.queryBoundsDebugViz.color), id, + vertexBuffer, indexBuffer, segments); + } + } +} + function drawDebugTile(painter, sourceCache, coord: OverscaledTileID) { const context = painter.context; const gl = context.gl; const posMatrix = coord.posMatrix; const program = painter.useProgram('debug'); + const tile = sourceCache.getTileByID(coord.key); + if (painter.terrain) painter.terrain.setupElevationDraw(tile, program); const depthMode = DepthMode.disabled; const stencilMode = StencilMode.disabled; @@ -89,7 +140,7 @@ function drawDebugTile(painter, sourceCache, coord: OverscaledTileID) { debugUniformValues(posMatrix, Color.red), id, painter.debugBuffer, painter.tileBorderIndexBuffer, painter.debugSegments); - const tileRawData = sourceCache.getTileByID(coord.key).latestRawTileData; + const tileRawData = tile.latestRawTileData; const tileByteLength = (tileRawData && tileRawData.byteLength) || 0; const tileSizeKb = Math.floor(tileByteLength / 1024); const tileSize = sourceCache.getTile(coord).tileSize; diff --git a/src/render/draw_fill.js b/src/render/draw_fill.js index 7f673f944d4..b8c9827aff6 100644 --- a/src/render/draw_fill.js +++ b/src/render/draw_fill.js @@ -80,6 +80,7 @@ function drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode const bucket: ?FillBucket = (tile.getBucket(layer): any); if (!bucket) continue; + painter.prepareDrawTile(coord); const programConfiguration = bucket.programConfigurations.get(layer.id); const program = painter.useProgram(programName, programConfiguration); @@ -110,7 +111,7 @@ function drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode } else { indexBuffer = bucket.indexBuffer2; segments = bucket.segments2; - const drawingBufferSize = [gl.drawingBufferWidth, gl.drawingBufferHeight]; + const drawingBufferSize = (painter.terrain && painter.terrain.renderingToTexture) ? painter.terrain.drapeBufferSize : [gl.drawingBufferWidth, gl.drawingBufferHeight]; uniformValues = (programName === 'fillOutlinePattern' && image) ? fillOutlinePatternUniformValues(tileMatrix, painter, crossfade, tile, drawingBufferSize) : fillOutlineUniformValues(tileMatrix, drawingBufferSize); diff --git a/src/render/draw_fill_extrusion.js b/src/render/draw_fill_extrusion.js index b9a0dde384f..42628a49b62 100644 --- a/src/render/draw_fill_extrusion.js +++ b/src/render/draw_fill_extrusion.js @@ -4,16 +4,19 @@ import DepthMode from '../gl/depth_mode'; import StencilMode from '../gl/stencil_mode'; import ColorMode from '../gl/color_mode'; import CullFaceMode from '../gl/cull_face_mode'; +import EXTENT from '../data/extent'; import { fillExtrusionUniformValues, fillExtrusionPatternUniformValues, } from './program/fill_extrusion_program'; +import Point from '@mapbox/point-geometry'; +import {OverscaledTileID} from '../source/tile_id'; +import assert from 'assert'; import type Painter from './painter'; import type SourceCache from '../source/source_cache'; import type FillExtrusionStyleLayer from '../style/style_layer/fill_extrusion_style_layer'; import type FillExtrusionBucket from '../data/bucket/fill_extrusion_bucket'; -import type {OverscaledTileID} from '../source/tile_id'; export default draw; @@ -63,6 +66,17 @@ function drawExtrusionTiles(painter, source, layer, coords, depthMode, stencilMo const programConfiguration = bucket.programConfigurations.get(layer.id); const program = painter.useProgram(image ? 'fillExtrusionPattern' : 'fillExtrusion', programConfiguration); + if (painter.terrain) { + const terrain = painter.terrain; + if (!bucket.enableTerrain) continue; + terrain.setupElevationDraw(tile, program, {useMeterToDem: true}); + flatRoofsUpdate(context, source, coord, bucket, layer, terrain); + if (!bucket.centroidVertexBuffer) { + const attrIndex: number | void = program.attributes['a_centroid_pos']; + if (attrIndex !== undefined) gl.vertexAttrib2f(attrIndex, 0, 0); + } + } + if (image) { painter.context.activeTexture.set(gl.TEXTURE0); tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); @@ -90,6 +104,176 @@ function drawExtrusionTiles(painter, source, layer, coords, depthMode, stencilMo program.draw(context, context.gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW, uniformValues, layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer, bucket.segments, layer.paint, painter.transform.zoom, - programConfiguration); + programConfiguration, painter.terrain ? bucket.centroidVertexBuffer : null); + } +} + +// Flat roofs array is prepared in the bucket, except for buildings that are on tile borders. +// For them, join pieces, calculate joined size here, and then upload data. +function flatRoofsUpdate(context, source, coord, bucket, layer, terrain) { + // For all four borders: 0 - left, 1, right, 2 - top, 3 - bottom + const neighborCoord = [ + coord => { + let x = coord.canonical.x - 1; + let w = coord.wrap; + if (x < 0) { + x = (1 << coord.canonical.z) - 1; + w--; + } + return new OverscaledTileID(coord.overscaledZ, w, coord.canonical.z, x, coord.canonical.y); + }, + coord => { + let x = coord.canonical.x + 1; + let w = coord.wrap; + if (x === 1 << coord.canonical.z) { + x = 0; + w++; + } + return new OverscaledTileID(coord.overscaledZ, w, coord.canonical.z, x, coord.canonical.y); + }, + coord => new OverscaledTileID(coord.overscaledZ, coord.wrap, coord.canonical.z, coord.canonical.x, + (coord.canonical.y === 0 ? 1 << coord.canonical.z : coord.canonical.y) - 1), + coord => new OverscaledTileID(coord.overscaledZ, coord.wrap, coord.canonical.z, coord.canonical.x, + coord.canonical.y === (1 << coord.canonical.z) - 1 ? 0 : coord.canonical.y + 1) + ]; + + const getLoadedBucket = (nid) => { + const maxzoom = source.getSource().maxzoom; + // In overscale range, look one tile zoom above and under. We do this to + // avoid flickering and use the content in Z-1 and Z+1 buckets until Z bucket is loaded. + for (const j of [0, -1, 1]) { + if (nid.overscaledZ + j < maxzoom) continue; + if (j > 0 && nid.overscaledZ < maxzoom) continue; + const n = source.getTileByID(nid.calculateScaledKey(nid.overscaledZ + j)); + if (n && n.hasData()) { + const nBucket: ?FillExtrusionBucket = (n.getBucket(layer): any); + if (nBucket) return nBucket; + } + } + }; + + const projectedToBorder = [0, 0, 0]; // [min, max, maxOffsetFromBorder] + const xjoin = (a, b) => { + projectedToBorder[0] = Math.min(a.min.y, b.min.y); + projectedToBorder[1] = Math.max(a.max.y, b.max.y); + projectedToBorder[2] = EXTENT - b.min.x > a.max.x ? b.min.x - EXTENT : a.max.x; + return projectedToBorder; + }; + const yjoin = (a, b) => { + projectedToBorder[0] = Math.min(a.min.x, b.min.x); + projectedToBorder[1] = Math.max(a.max.x, b.max.x); + projectedToBorder[2] = EXTENT - b.min.y > a.max.y ? b.min.y - EXTENT : a.max.y; + return projectedToBorder; + }; + const projectCombinedSpanToBorder = [ + (a, b) => xjoin(a, b), + (a, b) => xjoin(b, a), + (a, b) => yjoin(a, b), + (a, b) => yjoin(b, a) + ]; + + const centroid = new Point(0, 0); + const error = 3; // Allow intrusion of a building to the building with adjacent wall. + + let demTile, neighborDEMTile, neighborTileID; + + const flatBase = (min, max, edge, verticalEdge, maxOffsetFromBorder) => { + const points = [[verticalEdge ? edge : min, verticalEdge ? min : edge, 0], [verticalEdge ? edge : max, verticalEdge ? max : edge, 0]]; + + const coord3 = maxOffsetFromBorder < 0 ? EXTENT + maxOffsetFromBorder : maxOffsetFromBorder; + const thirdPoint = [verticalEdge ? coord3 : (min + max) / 2, verticalEdge ? (min + max) / 2 : coord3, 0]; + if ((edge === 0 && maxOffsetFromBorder < 0) || (edge !== 0 && maxOffsetFromBorder > 0)) { + // Third point is inside neighbor tile, not in the |coord| tile. + terrain.getForTilePoints(neighborTileID, [thirdPoint], true, neighborDEMTile); + } else { + points.push(thirdPoint); + } + terrain.getForTilePoints(coord, points, true, demTile); + return Math.max(points[0][2], points[1][2], thirdPoint[2]) / terrain.exaggeration(); + }; + + // Process all four borders: get neighboring tile + for (let i = 0; i < 4; i++) { + // Sort by border intersection area minimums, ascending. + const a = bucket.borders[i]; + if (a.length === 0) { bucket.borderDone[i] = true; } + if (bucket.borderDone[i]) continue; + const nid = neighborTileID = neighborCoord[i](coord); + const nBucket: ?FillExtrusionBucket = getLoadedBucket(nid); + if (!nBucket || !nBucket.enableTerrain) continue; + + neighborDEMTile = terrain.findDEMTileFor(nid); + if (!neighborDEMTile || !neighborDEMTile.dem) continue; + if (!demTile) { + const dem = terrain.findDEMTileFor(coord); + if (!(dem && dem.dem)) return; // defer update until an elevation tile is available. + demTile = dem; + } + const j = (i < 2 ? 1 : 5) - i; + const b = nBucket.borders[j]; + let ib = 0; + for (let ia = 0; ia < a.length; ia++) { + const parta = bucket.featuresOnBorder[a[ia]]; + const partABorderRange = parta.borders[i]; + // Find all nBucket parts that share the border overlap. + let partb; + while (ib < b.length) { + // Pass all that are before the overlap. + partb = nBucket.featuresOnBorder[b[ib]]; + const partBBorderRange = partb.borders[j]; + if (partBBorderRange[1] > partABorderRange[0] + error) break; + if (!nBucket.borderDone[j]) nBucket.encodeCentroid(undefined, partb, false); + ib++; + } + if (partb && ib < b.length) { + const saveIb = ib; + let count = 0; + while (true) { + // Collect all parts overlapping parta on the edge, to make sure it is only one. + const partBBorderRange = partb.borders[j]; + if (partBBorderRange[0] > partABorderRange[1] - error) break; + count++; + if (++ib === b.length) break; + partb = nBucket.featuresOnBorder[b[ib]]; + } + partb = nBucket.featuresOnBorder[b[saveIb]]; + + // If any of a or b crosses more than one tile edge, don't support flat roof. + if (parta.intersectsCount() > 1 || partb.intersectsCount() > 1 || count !== 1) { + if (count !== 1) { + ib = saveIb; // rewind unprocessed ib so that it is processed again for the next ia. + } + + bucket.encodeCentroid(undefined, parta, false); + if (!nBucket.borderDone[j]) nBucket.encodeCentroid(undefined, partb, false); + continue; + } + + // Now we have 1-1 matching of parts in both tiles that share the edge. Calculate flat base elevation + // as average of three points: 2 are edge points (combined span projected to border) and one is point of + // span that has maximum offset to border. + const span = projectCombinedSpanToBorder[i](parta, partb); + const edge = (i % 2) ? EXTENT - 1 : 0; + centroid.x = flatBase(span[0], Math.min(EXTENT - 1, span[1]), edge, i < 2, span[2]); + centroid.y = 0; + assert(parta.vertexArrayOffset !== undefined && parta.vertexArrayOffset < bucket.layoutVertexArray.length); + bucket.encodeCentroid(centroid, parta, false); + + assert(partb.vertexArrayOffset !== undefined && partb.vertexArrayOffset < nBucket.layoutVertexArray.length); + if (!nBucket.borderDone[j]) nBucket.encodeCentroid(centroid, partb, false); + } else { + assert(parta.intersectsCount() > 1 || (partb && partb.intersectsCount() > 1)); // expected at the end of border, when buildings cover corner (show building w/o flat roof). + bucket.encodeCentroid(undefined, parta, false); + } + } + + bucket.borderDone[i] = bucket.needsCentroidUpdate = true; + if (!nBucket.borderDone[j]) { + nBucket.borderDone[j] = nBucket.needsCentroidUpdate = true; + } + } + + if (bucket.needsCentroidUpdate || (!bucket.centroidVertexBuffer && bucket.centroidVertexArray.length !== 0)) { + bucket.uploadCentroid(context); } } diff --git a/src/render/draw_heatmap.js b/src/render/draw_heatmap.js index 4b60a48e83b..5661b1b0ba4 100644 --- a/src/render/draw_heatmap.js +++ b/src/render/draw_heatmap.js @@ -53,6 +53,7 @@ function drawHeatmap(painter: Painter, sourceCache: SourceCache, layer: HeatmapS const programConfiguration = bucket.programConfigurations.get(layer.id); const program = painter.useProgram('heatmap', programConfiguration); const {zoom} = painter.transform; + if (painter.terrain) painter.terrain.setupElevationDraw(tile, program); program.draw(context, gl.TRIANGLES, DepthMode.disabled, stencilMode, colorMode, CullFaceMode.disabled, heatmapUniformValues(coord.posMatrix, diff --git a/src/render/draw_hillshade.js b/src/render/draw_hillshade.js index 25519fce4b8..58d571ccc2e 100644 --- a/src/render/draw_hillshade.js +++ b/src/render/draw_hillshade.js @@ -11,8 +11,11 @@ import { import type Painter from './painter'; import type SourceCache from '../source/source_cache'; +import type Tile from '../source/tile'; import type HillshadeStyleLayer from '../style/style_layer/hillshade_style_layer'; import type {OverscaledTileID} from '../source/tile_id'; +import assert from 'assert'; +import DEMData from '../data/dem_data'; export default drawHillshade; @@ -24,7 +27,10 @@ function drawHillshade(painter: Painter, sourceCache: SourceCache, layer: Hillsh const depthMode = painter.depthModeForSublayer(0, DepthMode.ReadOnly); const colorMode = painter.colorModeForRenderPass(); - const [stencilModes, coords] = painter.renderPass === 'translucent' ? + // When rendering to texture, coordinates are already sorted: primary by + // proxy id and secondary sort is by Z. + const renderingToTexture = painter.terrain && painter.terrain.renderingToTexture; + const [stencilModes, coords] = painter.renderPass === 'translucent' && !renderingToTexture ? painter.stencilConfigForOverlap(tileIDs) : [{}, tileIDs]; for (const coord of coords) { @@ -32,76 +38,85 @@ function drawHillshade(painter: Painter, sourceCache: SourceCache, layer: Hillsh if (tile.needsHillshadePrepare && painter.renderPass === 'offscreen') { prepareHillshade(painter, tile, layer, depthMode, StencilMode.disabled, colorMode); } else if (painter.renderPass === 'translucent') { - renderHillshade(painter, tile, layer, depthMode, stencilModes[coord.overscaledZ], colorMode); + const stencilMode = renderingToTexture && painter.terrain ? + painter.terrain.stencilModeForRTTOverlap(coord) : stencilModes[coord.overscaledZ]; + renderHillshade(painter, coord, tile, layer, depthMode, stencilMode, colorMode); } } context.viewport.set([0, 0, painter.width, painter.height]); } -function renderHillshade(painter, tile, layer, depthMode, stencilMode, colorMode) { +function renderHillshade(painter, coord, tile, layer, depthMode, stencilMode, colorMode) { const context = painter.context; const gl = context.gl; const fbo = tile.fbo; if (!fbo) return; + painter.prepareDrawTile(coord); const program = painter.useProgram('hillshade'); context.activeTexture.set(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, fbo.colorAttachment.get()); - const uniformValues = hillshadeUniformValues(painter, tile, layer); + const uniformValues = hillshadeUniformValues(painter, tile, layer, painter.terrain ? coord.posMatrix : null); program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled, uniformValues, layer.id, painter.rasterBoundsBuffer, painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments); } +export function prepareDEMTexture(painter: Painter, tile: Tile, dem: DEMData) { + if (!tile.needsDEMTextureUpload) return; + + const context = painter.context; + const gl = context.gl; + + context.pixelStoreUnpackPremultiplyAlpha.set(false); + const textureStride = dem.stride; + tile.demTexture = tile.demTexture || painter.getTileTexture(textureStride); + const pixelData = dem.getPixels(); + if (tile.demTexture) { + tile.demTexture.update(pixelData, {premultiply: false}); + } else { + tile.demTexture = new Texture(context, pixelData, gl.RGBA, {premultiply: false}); + } + tile.needsDEMTextureUpload = false; +} + // hillshade rendering is done in two steps. the prepare step first calculates the slope of the terrain in the x and y // directions for each pixel, and saves those values to a framebuffer texture in the r and g channels. function prepareHillshade(painter, tile, layer, depthMode, stencilMode, colorMode) { const context = painter.context; const gl = context.gl; - const dem = tile.dem; - if (dem && dem.data) { - const tileSize = dem.dim; - const textureStride = dem.stride; - - const pixelData = dem.getPixels(); - context.activeTexture.set(gl.TEXTURE1); - - context.pixelStoreUnpackPremultiplyAlpha.set(false); - tile.demTexture = tile.demTexture || painter.getTileTexture(textureStride); - if (tile.demTexture) { - const demTexture = tile.demTexture; - demTexture.update(pixelData, {premultiply: false}); - demTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); - } else { - tile.demTexture = new Texture(context, pixelData, gl.RGBA, {premultiply: false}); - tile.demTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); - } - - context.activeTexture.set(gl.TEXTURE0); + if (!tile.dem) return; + const dem: DEMData = tile.dem; - let fbo = tile.fbo; + context.activeTexture.set(gl.TEXTURE1); + prepareDEMTexture(painter, tile, dem); + assert(tile.demTexture); + if (!tile.demTexture) return; // Silence flow. + tile.demTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); + const tileSize = dem.dim; - if (!fbo) { - const renderTexture = new Texture(context, {width: tileSize, height: tileSize, data: null}, gl.RGBA); - renderTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + context.activeTexture.set(gl.TEXTURE0); + let fbo = tile.fbo; + if (!fbo) { + const renderTexture = new Texture(context, {width: tileSize, height: tileSize, data: null}, gl.RGBA); + renderTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); - fbo = tile.fbo = context.createFramebuffer(tileSize, tileSize, true); - fbo.colorAttachment.set(renderTexture.texture); - } + fbo = tile.fbo = context.createFramebuffer(tileSize, tileSize, true); + fbo.colorAttachment.set(renderTexture.texture); + } - context.bindFramebuffer.set(fbo.framebuffer); - context.viewport.set([0, 0, tileSize, tileSize]); + context.bindFramebuffer.set(fbo.framebuffer); + context.viewport.set([0, 0, tileSize, tileSize]); - painter.useProgram('hillshadePrepare').draw(context, gl.TRIANGLES, - depthMode, stencilMode, colorMode, CullFaceMode.disabled, - hillshadeUniformPrepareValues(tile.tileID, dem), - layer.id, painter.rasterBoundsBuffer, - painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments); + painter.useProgram('hillshadePrepare').draw(context, gl.TRIANGLES, + depthMode, stencilMode, colorMode, CullFaceMode.disabled, + hillshadeUniformPrepareValues(tile.tileID, dem), + layer.id, painter.rasterBoundsBuffer, + painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments); - tile.needsHillshadePrepare = false; - } + tile.needsHillshadePrepare = false; } diff --git a/src/render/draw_line.js b/src/render/draw_line.js index 48c2813b01c..5585ff92573 100644 --- a/src/render/draw_line.js +++ b/src/render/draw_line.js @@ -48,11 +48,11 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay for (const coord of coords) { const tile = sourceCache.getTile(coord); - if (image && !tile.patternsLoaded()) continue; const bucket: ?LineBucket = (tile.getBucket(layer): any); if (!bucket) continue; + painter.prepareDrawTile(coord); const programConfiguration = bucket.programConfigurations.get(layer.id); const prevProgram = painter.context.program.get(); @@ -67,10 +67,11 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom); } - const uniformValues = image ? linePatternUniformValues(painter, tile, layer, crossfade) : - dasharray ? lineSDFUniformValues(painter, tile, layer, dasharray, crossfade) : - gradient ? lineGradientUniformValues(painter, tile, layer, bucket.lineClipsArray.length) : - lineUniformValues(painter, tile, layer); + const matrix = painter.terrain ? coord.posMatrix : null; + const uniformValues = image ? linePatternUniformValues(painter, tile, layer, crossfade, matrix) : + dasharray ? lineSDFUniformValues(painter, tile, layer, dasharray, crossfade, matrix) : + gradient ? lineGradientUniformValues(painter, tile, layer, matrix, bucket.lineClipsArray.length) : + lineUniformValues(painter, tile, layer, matrix); if (image) { context.activeTexture.set(gl.TEXTURE0); diff --git a/src/render/draw_raster.js b/src/render/draw_raster.js index d65cffaa900..ccda6f0e6fe 100644 --- a/src/render/draw_raster.js +++ b/src/render/draw_raster.js @@ -1,9 +1,6 @@ // @flow -import {clamp} from '../util/util'; - import ImageSource from '../source/image_source'; -import browser from '../util/browser'; import StencilMode from '../gl/stencil_mode'; import DepthMode from '../gl/depth_mode'; import CullFaceMode from '../gl/cull_face_mode'; @@ -13,10 +10,11 @@ import type Painter from './painter'; import type SourceCache from '../source/source_cache'; import type RasterStyleLayer from '../style/style_layer/raster_style_layer'; import type {OverscaledTileID} from '../source/tile_id'; +import rasterFade from './raster_fade'; export default drawRaster; -function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterStyleLayer, tileIDs: Array) { +function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterStyleLayer, tileIDs: Array, variableOffsets: any, isInitialLoad: boolean) { if (painter.renderPass !== 'translucent') return; if (layer.paint.get('raster-opacity') === 0) return; if (!tileIDs.length) return; @@ -28,7 +26,11 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty const colorMode = painter.colorModeForRenderPass(); - const [stencilModes, coords] = source instanceof ImageSource ? [{}, tileIDs] : + // When rendering to texture, coordinates are already sorted: primary by + // proxy id and secondary sort is by Z. + const renderingToTexture = painter.terrain && painter.terrain.renderingToTexture; + + const [stencilModes, coords] = source instanceof ImageSource || renderingToTexture ? [{}, tileIDs] : painter.stencilConfigForOverlap(tileIDs); const minTileZ = coords[coords.length - 1].overscaledZ; @@ -37,16 +39,25 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty for (const coord of coords) { // Set the lower zoom level to sublayer 0, and higher zoom levels to higher sublayers // Use gl.LESS to prevent double drawing in areas where tiles overlap. - const depthMode = painter.depthModeForSublayer(coord.overscaledZ - minTileZ, + const depthMode = renderingToTexture ? DepthMode.disabled : painter.depthModeForSublayer(coord.overscaledZ - minTileZ, layer.paint.get('raster-opacity') === 1 ? DepthMode.ReadWrite : DepthMode.ReadOnly, gl.LESS); const tile = sourceCache.getTile(coord); - const posMatrix = painter.transform.calculatePosMatrix(coord.toUnwrapped(), align); + if (renderingToTexture && !(tile && tile.hasData())) continue; + + const posMatrix = (renderingToTexture) ? coord.posMatrix : + painter.transform.calculatePosMatrix(coord.toUnwrapped(), align); + + const stencilMode = painter.terrain && renderingToTexture ? + painter.terrain.stencilModeForRTTOverlap(coord) : + stencilModes[coord.overscaledZ]; - tile.registerFadeDuration(layer.paint.get('raster-fade-duration')); + const rasterFadeDuration = isInitialLoad ? 0 : layer.paint.get('raster-fade-duration'); + tile.registerFadeDuration(rasterFadeDuration); - const parentTile = sourceCache.findLoadedParent(coord, 0), - fade = getFadeValues(tile, parentTile, sourceCache, layer, painter.transform); + const parentTile = sourceCache.findLoadedParent(coord, 0); + const fade = rasterFade(tile, parentTile, sourceCache, painter.transform, rasterFadeDuration); + if (painter.terrain) painter.terrain.prepareDrawTile(coord); let parentScaleBy, parentTL; @@ -73,53 +84,10 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty uniformValues, layer.id, source.boundsBuffer, painter.quadTriangleIndexBuffer, source.boundsSegments); } else { - program.draw(context, gl.TRIANGLES, depthMode, stencilModes[coord.overscaledZ], colorMode, CullFaceMode.disabled, + program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled, uniformValues, layer.id, painter.rasterBoundsBuffer, painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments); } } } -function getFadeValues(tile, parentTile, sourceCache, layer, transform) { - const fadeDuration = layer.paint.get('raster-fade-duration'); - - if (fadeDuration > 0) { - const now = browser.now(); - const sinceTile = (now - tile.timeAdded) / fadeDuration; - const sinceParent = parentTile ? (now - parentTile.timeAdded) / fadeDuration : -1; - - const source = sourceCache.getSource(); - const idealZ = transform.coveringZoomLevel({ - tileSize: source.tileSize, - roundZoom: source.roundZoom - }); - - // if no parent or parent is older, fade in; if parent is younger, fade out - const fadeIn = !parentTile || Math.abs(parentTile.tileID.overscaledZ - idealZ) > Math.abs(tile.tileID.overscaledZ - idealZ); - - const childOpacity = (fadeIn && tile.refreshedUponExpiration) ? 1 : clamp(fadeIn ? sinceTile : 1 - sinceParent, 0, 1); - - // we don't crossfade tiles that were just refreshed upon expiring: - // once they're old enough to pass the crossfading threshold - // (fadeDuration), unset the `refreshedUponExpiration` flag so we don't - // incorrectly fail to crossfade them when zooming - if (tile.refreshedUponExpiration && sinceTile >= 1) tile.refreshedUponExpiration = false; - - if (parentTile) { - return { - opacity: 1, - mix: 1 - childOpacity - }; - } else { - return { - opacity: childOpacity, - mix: 0 - }; - } - } else { - return { - opacity: 1, - mix: 0 - }; - } -} diff --git a/src/render/draw_sky.js b/src/render/draw_sky.js new file mode 100644 index 00000000000..cb1403c45f0 --- /dev/null +++ b/src/render/draw_sky.js @@ -0,0 +1,177 @@ +// @flow + +import StencilMode from '../gl/stencil_mode'; +import DepthMode from '../gl/depth_mode'; +import ColorMode from '../gl/color_mode'; +import CullFaceMode from '../gl/cull_face_mode'; +import Context from '../gl/context'; +import Texture from './texture'; +import Program from './program'; +import type SourceCache from '../source/source_cache'; +import SkyboxGeometry from './skybox_geometry'; +import {skyboxUniformValues, skyboxGradientUniformValues} from './program/skybox_program'; +import {skyboxCaptureUniformValues} from './program/skybox_capture_program'; +import SkyLayer from '../style/style_layer/sky_style_layer'; +import type Painter from './painter'; +import {vec3, mat3, mat4} from 'gl-matrix'; +import assert from 'assert'; + +export default drawSky; + +function drawSky(painter: Painter, sourceCache: SourceCache, layer: SkyLayer) { + const opacity = layer.paint.get('sky-opacity'); + if (opacity === 0) { + return; + } + + const context = painter.context; + const type = layer.paint.get('sky-type'); + const depthMode = new DepthMode(context.gl.LEQUAL, DepthMode.ReadOnly, [0, 1]); + const temporalOffset = (painter.frameCounter / 1000.0) % 1; + + if (type === 'atmosphere') { + if (painter.renderPass === 'offscreen') { + if (layer.needsSkyboxCapture(painter)) { + captureSkybox(painter, layer, 32, 32); + layer.markSkyboxValid(painter); + } + } else if (painter.renderPass === 'sky') { + drawSkyboxFromCapture(painter, layer, depthMode, opacity, temporalOffset); + } + } else if (type === 'gradient') { + if (painter.renderPass === 'sky') { + drawSkyboxGradient(painter, layer, depthMode, opacity, temporalOffset); + } + } else { + assert(false, `${type} is unsupported sky-type`); + } +} + +function drawSkyboxGradient(painter: Painter, layer: SkyLayer, depthMode: DepthMode, opacity: number, temporalOffset: number) { + const context = painter.context; + const gl = context.gl; + const transform = painter.transform; + const program = painter.useProgram('skyboxGradient'); + + // Lazily initialize geometry and texture if they havent been created yet. + if (!layer.skyboxGeometry) { + layer.skyboxGeometry = new SkyboxGeometry(context); + } + context.activeTexture.set(gl.TEXTURE0); + let colorRampTexture = layer.colorRampTexture; + if (!colorRampTexture) { + colorRampTexture = layer.colorRampTexture = new Texture(context, layer.colorRamp, gl.RGBA); + } + colorRampTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + const uniformValues = skyboxGradientUniformValues( + transform.skyboxMatrix, + layer.getCenter(painter, false), + layer.paint.get('sky-gradient-radius'), + opacity, + temporalOffset + ); + + program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, + painter.colorModeForRenderPass(), CullFaceMode.backCW, + uniformValues, 'skyboxGradient', layer.skyboxGeometry.vertexBuffer, + layer.skyboxGeometry.indexBuffer, layer.skyboxGeometry.segment); +} + +function drawSkyboxFromCapture(painter: Painter, layer: SkyLayer, depthMode: DepthMode, opacity: number, temporalOffset: number) { + const context = painter.context; + const gl = context.gl; + const transform = painter.transform; + const program = painter.useProgram('skybox'); + + context.activeTexture.set(gl.TEXTURE0); + + gl.bindTexture(gl.TEXTURE_CUBE_MAP, layer.skyboxTexture); + + const uniformValues = skyboxUniformValues(transform.skyboxMatrix, layer.getCenter(painter, false), 0, opacity, temporalOffset); + + program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, + painter.colorModeForRenderPass(), CullFaceMode.backCW, + uniformValues, 'skybox', layer.skyboxGeometry.vertexBuffer, + layer.skyboxGeometry.indexBuffer, layer.skyboxGeometry.segment); +} + +function drawSkyboxFace(context: Context, layer: SkyLayer, program: Program<*>, faceRotate: mat4, sunDirection: vec3, i: number) { + const gl = context.gl; + + const atmosphereColor = layer.paint.get('sky-atmosphere-color'); + const atmosphereHaloColor = layer.paint.get('sky-atmosphere-halo-color'); + const sunIntensity = layer.paint.get('sky-atmosphere-sun-intensity'); + + const uniformValues = skyboxCaptureUniformValues( + mat3.fromMat4([], faceRotate), + sunDirection, + sunIntensity, + atmosphereColor, + atmosphereHaloColor); + + const glFace = gl.TEXTURE_CUBE_MAP_POSITIVE_X + i; + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, glFace, layer.skyboxTexture, 0); + + program.draw(context, gl.TRIANGLES, DepthMode.disabled, StencilMode.disabled, ColorMode.unblended, CullFaceMode.frontCW, + uniformValues, 'skyboxCapture', layer.skyboxGeometry.vertexBuffer, + layer.skyboxGeometry.indexBuffer, layer.skyboxGeometry.segment); +} + +function captureSkybox(painter: Painter, layer: SkyLayer, width: number, height: number) { + const context = painter.context; + const gl = context.gl; + let fbo = layer.skyboxFbo; + + // Using absence of fbo as a signal for lazy initialization of all resources, cache resources in layer object + if (!fbo) { + fbo = layer.skyboxFbo = context.createFramebuffer(width, height, false); + layer.skyboxGeometry = new SkyboxGeometry(context); + layer.skyboxTexture = context.gl.createTexture(); + + gl.bindTexture(gl.TEXTURE_CUBE_MAP, layer.skyboxTexture); + gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + + for (let i = 0; i < 6; ++i) { + const glFace = gl.TEXTURE_CUBE_MAP_POSITIVE_X + i; + + // The format here could be RGB, but render tests are not happy with rendering to such a format + gl.texImage2D(glFace, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + } + } + + context.bindFramebuffer.set(fbo.framebuffer); + context.viewport.set([0, 0, width, height]); + + const sunDirection = layer.getCenter(painter, true); + const program = painter.useProgram('skyboxCapture'); + const faceRotate = new Float64Array(16); + + // +x; + mat4.identity(faceRotate); + mat4.rotateY(faceRotate, faceRotate, -Math.PI * 0.5); + drawSkyboxFace(context, layer, program, faceRotate, sunDirection, 0); + // -x + mat4.identity(faceRotate); + mat4.rotateY(faceRotate, faceRotate, Math.PI * 0.5); + drawSkyboxFace(context, layer, program, faceRotate, sunDirection, 1); + // +y + mat4.identity(faceRotate); + mat4.rotateX(faceRotate, faceRotate, -Math.PI * 0.5); + drawSkyboxFace(context, layer, program, faceRotate, sunDirection, 2); + // -y + mat4.identity(faceRotate); + mat4.rotateX(faceRotate, faceRotate, Math.PI * 0.5); + drawSkyboxFace(context, layer, program, faceRotate, sunDirection, 3); + // +z + mat4.identity(faceRotate); + drawSkyboxFace(context, layer, program, faceRotate, sunDirection, 4); + // -z + mat4.identity(faceRotate); + mat4.rotateY(faceRotate, faceRotate, Math.PI); + drawSkyboxFace(context, layer, program, faceRotate, sunDirection, 5); + + context.viewport.set([0, 0, painter.width, painter.height]); +} diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index 1dd5c4acff0..222615f007e 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -13,10 +13,10 @@ import StencilMode from '../gl/stencil_mode'; import DepthMode from '../gl/depth_mode'; import CullFaceMode from '../gl/cull_face_mode'; import {addDynamicAttributes} from '../data/bucket/symbol_bucket'; - import {getAnchorAlignment, WritingMode} from '../symbol/shaping'; import ONE_EM from '../symbol/one_em'; import {evaluateVariableOffset} from '../symbol/symbol_layout'; +import Tile from '../source/tile'; import { symbolIconUniformValues, @@ -48,7 +48,9 @@ type SymbolTileRenderState = { atlasInterpolation: any, atlasInterpolationIcon: any, isSDF: boolean, - hasHalo: boolean + hasHalo: boolean, + tile: Tile, + labelPlaneMatrixInv: ?Float32Array } }; @@ -130,14 +132,16 @@ function updateVariableAnchors(coords, painter, layer, sourceCache, rotationAlig if (size) { const tileScale = Math.pow(2, tr.zoom - tile.tileID.overscaledZ); + const elevation = tr.elevation; + const getElevation = elevation ? (p => elevation.getAtTileOffset(coord, p.x, p.y)) : (_ => 0); updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, variableOffsets, symbolSize, - tr, labelPlaneMatrix, coord.posMatrix, tileScale, size, updateTextFitIcon); + tr, labelPlaneMatrix, coord.posMatrix, tileScale, size, updateTextFitIcon, getElevation); } } } function updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, variableOffsets, symbolSize, - transform, labelPlaneMatrix, posMatrix, tileScale, size, updateTextFitIcon) { + transform, labelPlaneMatrix, posMatrix, tileScale, size, updateTextFitIcon, getElevation) { const placedSymbols = bucket.text.placedSymbolArray; const dynamicTextLayoutVertexArray = bucket.text.dynamicLayoutVertexArray; const dynamicIconLayoutVertexArray = bucket.icon.dynamicLayoutVertexArray; @@ -155,7 +159,8 @@ function updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, var symbolProjection.hideGlyphs(symbol.numGlyphs, dynamicTextLayoutVertexArray); } else { const tileAnchor = new Point(symbol.anchorX, symbol.anchorY); - const projectedAnchor = symbolProjection.project(tileAnchor, pitchWithMap ? posMatrix : labelPlaneMatrix); + const elevation = getElevation(tileAnchor); + const projectedAnchor = symbolProjection.project(tileAnchor, pitchWithMap ? posMatrix : labelPlaneMatrix, elevation); const perspectiveRatio = symbolProjection.getPerspectiveRatio(transform.cameraToCenterDistance, projectedAnchor.signedDistanceFromCamera); let renderTextSize = symbolSize.evaluateSizeForFeature(bucket.textSizeData, size, symbol) * perspectiveRatio / ONE_EM; if (pitchWithMap) { @@ -172,7 +177,7 @@ function updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, var // calculated above. In the (somewhat weird) case of pitch-aligned text, we add an equivalent // tile-unit based shift to the anchor before projecting to the label plane. const shiftedAnchor = pitchWithMap ? - symbolProjection.project(tileAnchor.add(shift), labelPlaneMatrix).point : + symbolProjection.project(tileAnchor.add(shift), labelPlaneMatrix, elevation).point : projectedAnchor.point.add(rotateWithMap ? shift.rotate(-transform.angle) : shift); @@ -223,7 +228,6 @@ function getSymbolProgramName(isSDF: boolean, isText: boolean, bucket: SymbolBuc function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate, translateAnchor, rotationAlignment, pitchAlignment, keepUpright, stencilMode, colorMode) { - const context = painter.context; const gl = context.gl; const tr = painter.transform; @@ -244,6 +248,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate const variablePlacement = layer.layout.get('text-variable-anchor'); const tileRenderState: Array = []; + const defines = painter.terrain && pitchWithMap ? ['PITCH_WITH_MAP_TERRAIN'] : null; for (const coord of coords) { const tile = sourceCache.getTile(coord); @@ -258,7 +263,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData; const transformed = pitchWithMap || tr.pitch !== 0; - const program = painter.useProgram(getSymbolProgramName(isSDF, isText, bucket), programConfiguration); + const program = painter.useProgram(getSymbolProgramName(isSDF, isText, bucket), programConfiguration, defines); const size = symbolSize.evaluateSizeForZoom(sizeData, tr.zoom); let texSize: [number, number]; @@ -288,6 +293,8 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate const s = pixelsToTileUnits(tile, 1, painter.transform.zoom); const labelPlaneMatrix = symbolProjection.getLabelPlaneMatrix(coord.posMatrix, pitchWithMap, rotateWithMap, painter.transform, s); + // labelPlaneMatrixInv is used for converting vertex pos to tile coordinates needed for sampling elevation. + const labelPlaneMatrixInv = painter.terrain && pitchWithMap && alongLine ? mat4.invert(new Float32Array(16), labelPlaneMatrix) : identityMat4; const glCoordMatrix = symbolProjection.getGlCoordMatrix(coord.posMatrix, pitchWithMap, rotateWithMap, painter.transform, s); const hasVariableAnchors = variablePlacement && bucket.hasTextData(); @@ -296,7 +303,9 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate bucket.hasIconData(); if (alongLine) { - symbolProjection.updateLineLabels(bucket, coord.posMatrix, painter, isText, labelPlaneMatrix, glCoordMatrix, pitchWithMap, keepUpright); + const elevation = tr.elevation; + const getElevation = elevation ? (p => elevation.getAtTileOffset(coord, p.x, p.y)) : null; + symbolProjection.updateLineLabels(bucket, coord.posMatrix, painter, isText, labelPlaneMatrix, glCoordMatrix, pitchWithMap, keepUpright, getElevation); } const matrix = painter.translatePosMatrix(coord.posMatrix, tile, translate, translateAnchor), @@ -331,7 +340,9 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate atlasInterpolation, atlasInterpolationIcon, isSDF, - hasHalo + hasHalo, + tile, + labelPlaneMatrixInv }; if (hasSortKey && bucket.canOverlap) { @@ -360,6 +371,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate for (const segmentState of tileRenderState) { const state = segmentState.state; + if (painter.terrain) painter.terrain.setupElevationDraw(state.tile, state.program, {useDepthForOcclusion: true, labelPlaneMatrixInv: state.labelPlaneMatrixInv}); context.activeTexture.set(gl.TEXTURE0); state.atlasTexture.bind(state.atlasInterpolation, gl.CLAMP_TO_EDGE); if (state.atlasTextureIcon) { diff --git a/src/render/line_atlas.js b/src/render/line_atlas.js index c755ea8990e..317e6c0ead0 100644 --- a/src/render/line_atlas.js +++ b/src/render/line_atlas.js @@ -159,8 +159,20 @@ class LineAtlas { return null; } + // dasharray is empty, draws a full line (no dash or no gap length represented, default behavior) + if (dasharray.length === 0) { + // insert a single dash range in order to draw a full line + dasharray.push(1); + } + let length = 0; - for (let i = 0; i < dasharray.length; i++) { length += dasharray[i]; } + for (let i = 0; i < dasharray.length; i++) { + if (dasharray[i] < 0) { + warnOnce('Negative value is found in line dasharray, replacing values with 0'); + dasharray[i] = 0; + } + length += dasharray[i]; + } if (length !== 0) { const stretch = this.width / length; diff --git a/src/render/painter.js b/src/render/painter.js index 56507434800..280ee596e55 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -9,7 +9,7 @@ import EXTENT from '../data/extent'; import pixelsToTileUnits from '../source/pixels_to_tile_units'; import SegmentVector from '../data/segment'; import {RasterBoundsArray, PosArray, TriangleIndexArray, LineStripIndexArray} from '../data/array_types'; -import {values} from '../util/util'; +import {values, MAX_SAFE_INTEGER} from '../util/util'; import rasterBoundsAttributes from '../data/raster_bounds_attributes'; import posAttributes from '../data/pos_attributes'; import ProgramConfiguration from '../data/program_configuration'; @@ -34,8 +34,11 @@ import fillExtrusion from './draw_fill_extrusion'; import hillshade from './draw_hillshade'; import raster from './draw_raster'; import background from './draw_background'; -import debug, {drawDebugPadding} from './draw_debug'; +import debug, {drawDebugPadding, drawDebugQueryGeometry} from './draw_debug'; import custom from './draw_custom'; +import sky from './draw_sky'; +import {Terrain} from '../terrain/terrain'; +import {Debug} from '../util/debug'; const draw = { symbol, @@ -47,6 +50,7 @@ const draw = { hillshade, raster, background, + sky, debug, custom }; @@ -64,18 +68,26 @@ import type VertexBuffer from '../gl/vertex_buffer'; import type IndexBuffer from '../gl/index_buffer'; import type {DepthRangeType, DepthMaskType, DepthFuncType} from '../gl/types'; import type ResolvedImage from '../style-spec/expression/types/resolved_image'; +import type {DynamicDefinesType} from './program/program_uniforms'; -export type RenderPass = 'offscreen' | 'opaque' | 'translucent'; +export type RenderPass = 'offscreen' | 'opaque' | 'translucent' | 'sky'; +export type CanvasCopyInstances = { + canvasCopies: WebGLTexture[], + timeStamps: number[] +} type PainterOptions = { showOverdrawInspector: boolean, showTileBoundaries: boolean, + showQueryGeometry: boolean, showPadding: boolean, rotating: boolean, zooming: boolean, moving: boolean, gpuTiming: boolean, - fadeDuration: number + fadeDuration: number, + isInitialLoad: boolean, + speedIndexTiming: boolean } /** @@ -103,7 +115,7 @@ class Painter { viewportSegments: SegmentVector; quadTriangleIndexBuffer: IndexBuffer; tileBorderIndexBuffer: IndexBuffer; - _tileClippingMaskIDs: {[_: string]: number }; + _tileClippingMaskIDs: {[_: number]: number }; stencilClearMode: StencilMode; style: Style; options: PainterOptions; @@ -112,6 +124,7 @@ class Painter { glyphManager: GlyphManager; depthRangeFor3D: DepthRangeType; opaquePassCutoff: number; + frameCounter: number; renderPass: RenderPass; currentLayer: number; currentStencilSource: ?string; @@ -125,11 +138,17 @@ class Painter { emptyTexture: Texture; debugOverlayTexture: Texture; debugOverlayCanvas: HTMLCanvasElement; + _terrain: ?Terrain; + tileLoaded: boolean; + frameCopies: Array; + loadTimeStamps: Array; constructor(gl: WebGLRenderingContext, transform: Transform) { this.context = new Context(gl); this.transform = transform; this._tileTextures = {}; + this.frameCopies = []; + this.loadTimeStamps = []; this.setup(); @@ -141,6 +160,22 @@ class Painter { this.crossTileSymbolIndex = new CrossTileSymbolIndex(); this.gpuTimers = {}; + this.frameCounter = 0; + } + + updateTerrain(style: Style, cameraChanging: boolean) { + const enabled = !!style && !!style.terrain; + if (!enabled && (!this._terrain || !this._terrain.enabled)) return; + if (!this._terrain) { + this._terrain = new Terrain(this, style); + } + const terrain: Terrain = this._terrain; + this.transform.elevation = enabled ? terrain : null; + terrain.update(style, this.transform, cameraChanging); + } + + get terrain(): ?Terrain { + return this._terrain && this._terrain.enabled ? this._terrain : null; } /* @@ -215,6 +250,7 @@ class Painter { const gl = this.context.gl; this.stencilClearMode = new StencilMode({func: gl.ALWAYS, mask: 0}, 0x0, 0xFF, gl.ZERO, gl.ZERO, gl.ZERO); + this.loadTimeStamps.push(window.performance.now()); } /* @@ -244,10 +280,10 @@ class Painter { this.quadTriangleIndexBuffer, this.viewportSegments); } - _renderTileClippingMasks(layer: StyleLayer, tileIDs: Array) { - if (this.currentStencilSource === layer.source || !layer.isTileClipped() || !tileIDs || !tileIDs.length) return; + _renderTileClippingMasks(layer: StyleLayer, sourceCache?: SourceCache, tileIDs?: Array) { + if (!sourceCache || this.currentStencilSource === sourceCache.id || !layer.isTileClipped() || !tileIDs || !tileIDs.length) return; - this.currentStencilSource = layer.source; + this.currentStencilSource = sourceCache.id; const context = this.context; const gl = context.gl; @@ -288,7 +324,8 @@ class Painter { return new StencilMode({func: gl.NOTEQUAL, mask: 0xFF}, id, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE); } - stencilModeForClipping(tileID: OverscaledTileID): StencilMode { + stencilModeForClipping(tileID: OverscaledTileID): $ReadOnly { + if (this.terrain) return this.terrain.stencilModeForRTTOverlap(tileID); const gl = this.context.gl; return new StencilMode({func: gl.EQUAL, mask: 0xFF}, this._tileClippingMaskIDs[tileID.key], 0x00, gl.KEEP, gl.KEEP, gl.REPLACE); } @@ -367,7 +404,7 @@ class Painter { this.imageManager.beginFrame(); const layerIds = this.style._order; - const sourceCaches = this.style.sourceCaches; + const sourceCaches = this.style._sourceCaches; for (const id in sourceCaches) { const sourceCache = sourceCaches[id]; @@ -396,6 +433,13 @@ class Painter { } } + if (this.terrain) { + this.terrain.updateTileBinding(coordsDescendingSymbol); + // All render to texture is done in translucent pass to remove need + // for depth buffer allocation per tile. + this.opaquePassCutoff = 0; + } + // Offscreen pass =============================================== // We first do all rendering that requires rendering to a separate // framebuffer, and then save those for rendering back to the map @@ -404,12 +448,13 @@ class Painter { for (const layerId of layerIds) { const layer = this.style._layers[layerId]; + const sourceCache = style._getLayerSourceCache(layer); if (!layer.hasOffscreenPass() || layer.isHidden(this.transform.zoom)) continue; - const coords = coordsDescending[layer.source]; - if (layer.type !== 'custom' && !coords.length) continue; + const coords = sourceCache ? coordsDescending[sourceCache.id] : undefined; + if (!(layer.type === 'custom' || layer.isSky()) && !(coords && coords.length)) continue; - this.renderLayer(this, sourceCaches[layer.source], layer, coords); + this.renderLayer(this, sourceCache, layer, coords); } // Rebind the main framebuffer now that all offscreen layers have been rendered: @@ -428,47 +473,72 @@ class Painter { for (this.currentLayer = layerIds.length - 1; this.currentLayer >= 0; this.currentLayer--) { const layer = this.style._layers[layerIds[this.currentLayer]]; - const sourceCache = sourceCaches[layer.source]; - const coords = coordsAscending[layer.source]; + const sourceCache = style._getLayerSourceCache(layer); + if ((this.terrain && this.terrain.renderLayer(layer, sourceCache)) || layer.isSky()) continue; + const coords = sourceCache ? coordsDescending[sourceCache.id] : undefined; - this._renderTileClippingMasks(layer, coords); + this._renderTileClippingMasks(layer, sourceCache, coords); this.renderLayer(this, sourceCache, layer, coords); } + // Sky pass ====================================================== + // Draw all sky layers bottom to top. + // They are drawn at max depth, they are drawn after opaque and before + // translucent to fail depth testing and mix with translucent objects. + this.renderPass = 'sky'; + if (this.transform.isHorizonVisible()) { + for (this.currentLayer = 0; this.currentLayer < layerIds.length; this.currentLayer++) { + const layer = this.style._layers[layerIds[this.currentLayer]]; + const sourceCache = style._getLayerSourceCache(layer); + if (!layer.isSky()) continue; + const coords = sourceCache ? coordsDescending[sourceCache.id] : undefined; + + this.renderLayer(this, sourceCache, layer, coords); + } + } + // Translucent pass =============================================== // Draw all other layers bottom-to-top. this.renderPass = 'translucent'; for (this.currentLayer = 0; this.currentLayer < layerIds.length; this.currentLayer++) { const layer = this.style._layers[layerIds[this.currentLayer]]; - const sourceCache = sourceCaches[layer.source]; + const sourceCache = style._getLayerSourceCache(layer); + if ((this.terrain && this.terrain.renderLayer(layer, sourceCache)) || layer.isSky()) continue; // For symbol layers in the translucent pass, we add extra tiles to the renderable set // for cross-tile symbol fading. Symbol layers don't use tile clipping, so no need to render // separate clipping masks - const coords = (layer.type === 'symbol' ? coordsDescendingSymbol : coordsDescending)[layer.source]; + const coords = sourceCache ? + (layer.type === 'symbol' ? coordsDescendingSymbol : coordsDescending)[sourceCache.id] : + undefined; - this._renderTileClippingMasks(layer, coordsAscending[layer.source]); + this._renderTileClippingMasks(layer, sourceCache, sourceCache ? coordsAscending[sourceCache.id] : undefined); this.renderLayer(this, sourceCache, layer, coords); } - if (this.options.showTileBoundaries) { + if (this.options.showTileBoundaries || this.options.showQueryGeometry) { //Use source with highest maxzoom - let selectedSource; - let sourceCache; + let selectedSource = null; const layers = values(this.style._layers); layers.forEach((layer) => { - if (layer.source && !layer.isHidden(this.transform.zoom)) { - if (layer.source !== (sourceCache && sourceCache.id)) { - sourceCache = this.style.sourceCaches[layer.source]; - } + const sourceCache = style._getLayerSourceCache(layer); + if (sourceCache && !layer.isHidden(this.transform.zoom)) { if (!selectedSource || (selectedSource.getSource().maxzoom < sourceCache.getSource().maxzoom)) { selectedSource = sourceCache; } } }); if (selectedSource) { - draw.debug(this, selectedSource, selectedSource.getVisibleCoordinates()); + if (this.options.showTileBoundaries) { + draw.debug(this, selectedSource, selectedSource.getVisibleCoordinates()); + } + + Debug.run(() => { + if (this.options.showQueryGeometry && selectedSource) { + drawDebugQueryGeometry(this, selectedSource, selectedSource.getVisibleCoordinates()); + } + }); } } @@ -479,15 +549,21 @@ class Painter { // Set defaults for most GL values so that anyone using the state after the render // encounters more expected values. this.context.setDefault(); + this.frameCounter = (this.frameCounter + 1) % MAX_SAFE_INTEGER; + + if (this.tileLoaded && this.options.speedIndexTiming) { + this.loadTimeStamps.push(window.performance.now()); + this.saveCanvasCopy(); + } } - renderLayer(painter: Painter, sourceCache: SourceCache, layer: StyleLayer, coords: Array) { + renderLayer(painter: Painter, sourceCache?: SourceCache, layer: StyleLayer, coords?: Array) { if (layer.isHidden(this.transform.zoom)) return; - if (layer.type !== 'background' && layer.type !== 'custom' && !coords.length) return; + if (layer.type !== 'background' && layer.type !== 'sky' && layer.type !== 'custom' && !(coords && coords.length)) return; this.id = layer.id; this.gpuTimingStart(layer); - draw[layer.type](painter, sourceCache, layer, coords, this.style.placement.variableOffsets); + draw[layer.type](painter, sourceCache, layer, coords, this.style.placement.variableOffsets, this.options.isInitialLoad); this.gpuTimingEnd(); } @@ -584,7 +660,7 @@ class Painter { /** * Checks whether a pattern image is needed, and if it is, whether it is not loaded. * - * @returns true if a needed image is missing and rendering needs to be skipped. +* @returns true if a needed image is missing and rendering needs to be skipped. * @private */ isPatternMissing(image: ?CrossFaded): boolean { @@ -595,11 +671,34 @@ class Painter { return !imagePosA || !imagePosB; } - useProgram(name: string, programConfiguration: ?ProgramConfiguration): Program { + /** + * Returns #defines that would need to be injected into every Program + * based on the current state of Painter. + * + * @returns {string[]} + * @private + */ + currentGlobalDefines(): string[] { + const terrain = this.terrain && !this.terrain.renderingToTexture; // Enables elevation sampling in vertex shader. + const rtt = this.terrain && this.terrain.renderingToTexture; + + const defines = []; + if (terrain) defines.push('TERRAIN'); + if (rtt) defines.push('RENDER_TO_TEXTURE'); + if (this._showOverdrawInspector) defines.push('OVERDRAW_INSPECTOR'); + return defines; + } + + useProgram(name: string, programConfiguration: ?ProgramConfiguration, fixedDefines: ?DynamicDefinesType[]): Program { this.cache = this.cache || {}; - const key = `${name}${programConfiguration ? programConfiguration.cacheKey : ''}${this._showOverdrawInspector ? '/overdraw' : ''}`; + const defines = (((fixedDefines || []): any): string[]); + + const globalDefines = this.currentGlobalDefines(); + const allDefines = globalDefines.concat(defines); + const key = Program.cacheKey(name, allDefines, programConfiguration); + if (!this.cache[key]) { - this.cache[key] = new Program(this.context, name, shaders[name], programConfiguration, programUniforms[name], this._showOverdrawInspector); + this.cache[key] = new Program(this.context, name, shaders[name], programConfiguration, programUniforms[name], allDefines); } return this.cache[key]; } @@ -644,11 +743,44 @@ class Painter { } destroy() { + if (this._terrain) { + this._terrain.destroy(); + } this.emptyTexture.destroy(); if (this.debugOverlayTexture) { this.debugOverlayTexture.destroy(); } } + + prepareDrawTile(tileID: OverscaledTileID) { + if (this.terrain) { + this.terrain.prepareDrawTile(tileID); + } + } + + setTileLoadedFlag(flag: boolean) { + this.tileLoaded = flag; + } + + saveCanvasCopy() { + this.frameCopies.push(this.canvasCopy()); + this.tileLoaded = false; + } + + canvasCopy() { + const gl = this.context.gl; + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, 0); + return texture; + } + + getCanvasCopiesAndTimestamps(): CanvasCopyInstances { + return { + canvasCopies: this.frameCopies, + timeStamps: this.loadTimeStamps + }; + } } export default Painter; diff --git a/src/render/program.js b/src/render/program.js index 7e273c0a339..47359046e56 100644 --- a/src/render/program.js +++ b/src/render/program.js @@ -1,10 +1,12 @@ // @flow -import {prelude} from '../shaders'; +import {prelude, preludeTerrain} from '../shaders'; import assert from 'assert'; import ProgramConfiguration from '../data/program_configuration'; import VertexArrayObject from './vertex_array_object'; import Context from '../gl/context'; +import {terrainUniforms} from '../terrain/terrain'; +import type {TerrainUniformsType} from '../terrain/terrain'; import type SegmentVector from '../data/segment'; import type VertexBuffer from '../gl/vertex_buffer'; @@ -38,13 +40,22 @@ class Program { fixedUniforms: Us; binderUniforms: Array; failedToCreate: boolean; + terrainUniforms: ?TerrainUniformsType; + + static cacheKey(name: string, defines: string[], programConfiguration: ?ProgramConfiguration): string { + let key = `${name}${programConfiguration ? programConfiguration.cacheKey : ''}`; + for (const define of defines) { + key += `/${define}`; + } + return key; + } constructor(context: Context, - name: string, - source: {fragmentSource: string, vertexSource: string, staticAttributes: Array, staticUniforms: Array}, - configuration: ?ProgramConfiguration, - fixedUniforms: (Context, UniformLocations) => Us, - showOverdrawInspector: boolean) { + name: string, + source: {fragmentSource: string, vertexSource: string, staticAttributes: Array, staticUniforms: Array}, + configuration: ?ProgramConfiguration, + fixedUniforms: (Context, UniformLocations) => Us, + fixedDefines: string[]) { const gl = context.gl; this.program = gl.createProgram(); @@ -61,13 +72,11 @@ class Program { if (allUniformsInfo.indexOf(uniform) < 0) allUniformsInfo.push(uniform); } - const defines = configuration ? configuration.defines() : []; - if (showOverdrawInspector) { - defines.push('#define OVERDRAW_INSPECTOR;'); - } + let defines = configuration ? configuration.defines() : []; + defines = defines.concat(fixedDefines.map((define) => `#define ${define}`)); const fragmentSource = defines.concat(prelude.fragmentSource, source.fragmentSource).join('\n'); - const vertexSource = defines.concat(prelude.vertexSource, source.vertexSource).join('\n'); + const vertexSource = defines.concat(prelude.vertexSource, preludeTerrain.vertexSource, source.vertexSource).join('\n'); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); if (gl.isContextLost()) { this.failedToCreate = true; @@ -118,6 +127,19 @@ class Program { this.fixedUniforms = fixedUniforms(context, uniformLocations); this.binderUniforms = configuration ? configuration.getUniforms(context, uniformLocations) : []; + if (fixedDefines.indexOf('TERRAIN') !== -1) { this.terrainUniforms = terrainUniforms(context, uniformLocations); } + } + + setTerrainUniformValues(context: Context, terrainUnformValues: UniformValues) { + if (!this.terrainUniforms) return; + const uniforms: TerrainUniformsType = this.terrainUniforms; + + if (this.failedToCreate) return; + context.program.set(this.program); + + for (const name in terrainUnformValues) { + uniforms[name].set(terrainUnformValues[name]); + } } draw(context: Context, diff --git a/src/render/program/circle_program.js b/src/render/program/circle_program.js index afde54051ee..6dfedc2acbe 100644 --- a/src/render/program/circle_program.js +++ b/src/render/program/circle_program.js @@ -1,7 +1,6 @@ // @flow import { - Uniform1i, Uniform1f, Uniform2f, UniformMatrix4f @@ -18,17 +17,15 @@ import browser from '../../util/browser'; export type CircleUniformsType = {| 'u_camera_to_center_distance': Uniform1f, - 'u_scale_with_map': Uniform1i, - 'u_pitch_with_map': Uniform1i, 'u_extrude_scale': Uniform2f, 'u_device_pixel_ratio': Uniform1f, 'u_matrix': UniformMatrix4f |}; +export type CircleDefinesType = 'PITCH_WITH_MAP' | 'SCALE_WITH_MAP'; + const circleUniforms = (context: Context, locations: UniformLocations): CircleUniformsType => ({ 'u_camera_to_center_distance': new Uniform1f(context, locations.u_camera_to_center_distance), - 'u_scale_with_map': new Uniform1i(context, locations.u_scale_with_map), - 'u_pitch_with_map': new Uniform1i(context, locations.u_pitch_with_map), 'u_extrude_scale': new Uniform2f(context, locations.u_extrude_scale), 'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio), 'u_matrix': new UniformMatrix4f(context, locations.u_matrix) @@ -42,28 +39,32 @@ const circleUniformValues = ( ): UniformValues => { const transform = painter.transform; - let pitchWithMap: boolean, extrudeScale: [number, number]; + let extrudeScale: [number, number]; if (layer.paint.get('circle-pitch-alignment') === 'map') { const pixelRatio = pixelsToTileUnits(tile, 1, transform.zoom); - pitchWithMap = true; extrudeScale = [pixelRatio, pixelRatio]; } else { - pitchWithMap = false; extrudeScale = transform.pixelsToGLUnits; } return { 'u_camera_to_center_distance': transform.cameraToCenterDistance, - 'u_scale_with_map': +(layer.paint.get('circle-pitch-scale') === 'map'), 'u_matrix': painter.translatePosMatrix( coord.posMatrix, tile, layer.paint.get('circle-translate'), layer.paint.get('circle-translate-anchor')), - 'u_pitch_with_map': +(pitchWithMap), 'u_device_pixel_ratio': browser.devicePixelRatio, 'u_extrude_scale': extrudeScale }; }; -export {circleUniforms, circleUniformValues}; +const circleDefinesValues = (layer: CircleStyleLayer): CircleDefinesType[] => { + const values = []; + if (layer.paint.get('circle-pitch-alignment') === 'map') values.push('PITCH_WITH_MAP'); + if (layer.paint.get('circle-pitch-scale') === 'map') values.push('SCALE_WITH_MAP'); + + return values; +}; + +export {circleUniforms, circleUniformValues, circleDefinesValues}; diff --git a/src/render/program/collision_program.js b/src/render/program/collision_program.js index 82a5372b84d..3a36ec3826a 100644 --- a/src/render/program/collision_program.js +++ b/src/render/program/collision_program.js @@ -5,8 +5,7 @@ import { Uniform2f, UniformMatrix4f } from '../uniform_binding'; -import pixelsToTileUnits from '../../source/pixels_to_tile_units'; - +import EXTENT from '../../data/extent'; import type Context from '../../gl/context'; import type {UniformValues, UniformLocations} from '../uniform_binding'; import type Transform from '../../geo/transform'; @@ -15,9 +14,7 @@ import type Tile from '../../source/tile'; export type CollisionUniformsType = {| 'u_matrix': UniformMatrix4f, 'u_camera_to_center_distance': Uniform1f, - 'u_pixels_to_tile_units': Uniform1f, - 'u_extrude_scale': Uniform2f, - 'u_overscale_factor': Uniform1f + 'u_extrude_scale': Uniform2f |}; export type CollisionCircleUniformsType = {| @@ -30,9 +27,7 @@ export type CollisionCircleUniformsType = {| const collisionUniforms = (context: Context, locations: UniformLocations): CollisionUniformsType => ({ 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), 'u_camera_to_center_distance': new Uniform1f(context, locations.u_camera_to_center_distance), - 'u_pixels_to_tile_units': new Uniform1f(context, locations.u_pixels_to_tile_units), - 'u_extrude_scale': new Uniform2f(context, locations.u_extrude_scale), - 'u_overscale_factor': new Uniform1f(context, locations.u_overscale_factor) + 'u_extrude_scale': new Uniform2f(context, locations.u_extrude_scale) }); const collisionCircleUniforms = (context: Context, locations: UniformLocations): CollisionCircleUniformsType => ({ @@ -47,16 +42,12 @@ const collisionUniformValues = ( transform: Transform, tile: Tile ): UniformValues => { - const pixelRatio = pixelsToTileUnits(tile, 1, transform.zoom); - const scale = Math.pow(2, transform.zoom - tile.tileID.overscaledZ); - const overscaleFactor = tile.tileID.overscaleFactor(); + const pixelRatio = EXTENT / tile.tileSize; return { 'u_matrix': matrix, 'u_camera_to_center_distance': transform.cameraToCenterDistance, - 'u_pixels_to_tile_units': pixelRatio, - 'u_extrude_scale': [transform.pixelsToGLUnits[0] / (pixelRatio * scale), - transform.pixelsToGLUnits[1] / (pixelRatio * scale)], - 'u_overscale_factor': overscaleFactor + 'u_extrude_scale': [transform.pixelsToGLUnits[0] / pixelRatio, + transform.pixelsToGLUnits[1] / pixelRatio] }; }; diff --git a/src/render/program/fill_extrusion_program.js b/src/render/program/fill_extrusion_program.js index 39dec5cf055..d5e2c50eefa 100644 --- a/src/render/program/fill_extrusion_program.js +++ b/src/render/program/fill_extrusion_program.js @@ -81,10 +81,11 @@ const fillExtrusionUniformValues = ( const _lp = light.properties.get('position'); const lightPos = [_lp.x, _lp.y, _lp.z]; const lightMat = mat3.create(); - if (light.properties.get('anchor') === 'viewport') { + const anchor = light.properties.get('anchor'); + if (anchor === 'viewport') { mat3.fromRotation(lightMat, -painter.transform.angle); + vec3.transformMat3(lightPos, lightPos, lightMat); } - vec3.transformMat3(lightPos, lightPos, lightMat); const lightColor = light.properties.get('color'); diff --git a/src/render/program/hillshade_program.js b/src/render/program/hillshade_program.js index b843970ff15..9d0e22e60c0 100644 --- a/src/render/program/hillshade_program.js +++ b/src/render/program/hillshade_program.js @@ -60,7 +60,8 @@ const hillshadePrepareUniforms = (context: Context, locations: UniformLocations) const hillshadeUniformValues = ( painter: Painter, tile: Tile, - layer: HillshadeStyleLayer + layer: HillshadeStyleLayer, + matrix: ?Float32Array ): UniformValues => { const shadow = layer.paint.get("hillshade-shadow-color"); const highlight = layer.paint.get("hillshade-highlight-color"); @@ -73,7 +74,7 @@ const hillshadeUniformValues = ( } const align = !painter.options.moving; return { - 'u_matrix': painter.transform.calculatePosMatrix(tile.tileID.toUnwrapped(), align), + 'u_matrix': matrix ? matrix : painter.transform.calculatePosMatrix(tile.tileID.toUnwrapped(), align), 'u_image': 0, 'u_latrange': getTileLatRange(painter, tile.tileID), 'u_light': [layer.paint.get('hillshade-exaggeration'), azimuthal], @@ -98,7 +99,7 @@ const hillshadeUniformPrepareValues = ( 'u_image': 1, 'u_dimension': [stride, stride], 'u_zoom': tileID.overscaledZ, - 'u_unpack': dem.getUnpackVector() + 'u_unpack': dem.unpackVector }; }; diff --git a/src/render/program/line_program.js b/src/render/program/line_program.js index 79fa83c2340..1725067feba 100644 --- a/src/render/program/line_program.js +++ b/src/render/program/line_program.js @@ -105,12 +105,13 @@ const lineSDFUniforms = (context: Context, locations: UniformLocations): LineSDF const lineUniformValues = ( painter: Painter, tile: Tile, - layer: LineStyleLayer + layer: LineStyleLayer, + matrix: ?Float32Array ): UniformValues => { const transform = painter.transform; return { - 'u_matrix': calculateMatrix(painter, tile, layer), + 'u_matrix': calculateMatrix(painter, tile, layer, matrix), 'u_ratio': 1 / pixelsToTileUnits(tile, 1, transform.zoom), 'u_device_pixel_ratio': browser.devicePixelRatio, 'u_units_to_pixels': [ @@ -124,9 +125,10 @@ const lineGradientUniformValues = ( painter: Painter, tile: Tile, layer: LineStyleLayer, + matrix: ?Float32Array, imageHeight: number ): UniformValues => { - return extend(lineUniformValues(painter, tile, layer), { + return extend(lineUniformValues(painter, tile, layer, matrix), { 'u_image': 0, 'u_image_height': imageHeight, }); @@ -136,12 +138,13 @@ const linePatternUniformValues = ( painter: Painter, tile: Tile, layer: LineStyleLayer, - crossfade: CrossfadeParameters + crossfade: CrossfadeParameters, + matrix: ?Float32Array ): UniformValues => { const transform = painter.transform; const tileZoomRatio = calculateTileRatio(tile, transform); return { - 'u_matrix': calculateMatrix(painter, tile, layer), + 'u_matrix': calculateMatrix(painter, tile, layer, matrix), 'u_texsize': tile.imageAtlasTexture.size, // camera zoom ratio 'u_ratio': 1 / pixelsToTileUnits(tile, 1, transform.zoom), @@ -161,7 +164,8 @@ const lineSDFUniformValues = ( tile: Tile, layer: LineStyleLayer, dasharray: CrossFaded>, - crossfade: CrossfadeParameters + crossfade: CrossfadeParameters, + matrix: ?Float32Array ): UniformValues => { const transform = painter.transform; const lineAtlas = painter.lineAtlas; @@ -175,7 +179,7 @@ const lineSDFUniformValues = ( const widthA = posA.width * crossfade.fromScale; const widthB = posB.width * crossfade.toScale; - return extend(lineUniformValues(painter, tile, layer), { + return extend(lineUniformValues(painter, tile, layer, matrix), { 'u_patternscale_a': [tileRatio / widthA, -posA.height / 2], 'u_patternscale_b': [tileRatio / widthB, -posB.height / 2], 'u_sdfgamma': lineAtlas.width / (Math.min(widthA, widthB) * 256 * browser.devicePixelRatio) / 2, @@ -190,9 +194,9 @@ function calculateTileRatio(tile: Tile, transform: Transform) { return 1 / pixelsToTileUnits(tile, 1, transform.tileZoom); } -function calculateMatrix(painter, tile, layer) { +function calculateMatrix(painter, tile, layer, matrix) { return painter.translatePosMatrix( - tile.tileID.posMatrix, + matrix ? matrix : tile.tileID.posMatrix, tile, layer.paint.get('line-translate'), layer.paint.get('line-translate-anchor') diff --git a/src/render/program/program_uniforms.js b/src/render/program/program_uniforms.js index e36ec6f6e83..c9c0da146de 100644 --- a/src/render/program/program_uniforms.js +++ b/src/render/program/program_uniforms.js @@ -1,5 +1,7 @@ // @flow +import type {CircleDefinesType} from './circle_program'; +import type {SymbolDefinesType} from './symbol_program'; import {fillExtrusionUniforms, fillExtrusionPatternUniforms} from './fill_extrusion_program'; import {fillUniforms, fillPatternUniforms, fillOutlineUniforms, fillOutlinePatternUniforms} from './fill_program'; import {circleUniforms} from './circle_program'; @@ -12,6 +14,11 @@ import {lineUniforms, lineGradientUniforms, linePatternUniforms, lineSDFUniforms import {rasterUniforms} from './raster_program'; import {symbolIconUniforms, symbolSDFUniforms, symbolTextAndIconUniforms} from './symbol_program'; import {backgroundUniforms, backgroundPatternUniforms} from './background_program'; +import {terrainRasterUniforms} from '../../terrain/terrain_raster_program'; +import {skyboxUniforms, skyboxGradientUniforms} from './skybox_program'; +import {skyboxCaptureUniforms} from './skybox_capture_program'; + +export type DynamicDefinesType = CircleDefinesType | SymbolDefinesType; export const programUniforms = { fillExtrusion: fillExtrusionUniforms, @@ -38,5 +45,10 @@ export const programUniforms = { symbolSDF: symbolSDFUniforms, symbolTextAndIcon: symbolTextAndIconUniforms, background: backgroundUniforms, - backgroundPattern: backgroundPatternUniforms + backgroundPattern: backgroundPatternUniforms, + terrainRaster: terrainRasterUniforms, + terrainDepth: terrainRasterUniforms, + skybox: skyboxUniforms, + skyboxGradient: skyboxGradientUniforms, + skyboxCapture: skyboxCaptureUniforms }; diff --git a/src/render/program/skybox_capture_program.js b/src/render/program/skybox_capture_program.js new file mode 100644 index 00000000000..df0dd38229d --- /dev/null +++ b/src/render/program/skybox_capture_program.js @@ -0,0 +1,64 @@ +// @flow + +import {vec3} from 'gl-matrix'; +import type Color from '../../style-spec/util/color'; + +import { + UniformMatrix3f, + Uniform1f, + Uniform3f, + Uniform4f, +} from '../uniform_binding'; +import type { + UniformValues, + UniformLocations, +} from '../uniform_binding'; +import type Context from '../../gl/context'; + +export type SkyboxCaptureUniformsType = {| + 'u_matrix_3f': UniformMatrix3f, + 'u_sun_direction': Uniform3f, + 'u_sun_intensity': Uniform1f, + 'u_color_tint_r': Uniform4f, + 'u_color_tint_m': Uniform4f, + 'u_luminance': Uniform1f, +|}; + +const skyboxCaptureUniforms = (context: Context, locations: UniformLocations): SkyboxCaptureUniformsType => ({ + 'u_matrix_3f': new UniformMatrix3f(context, locations.u_matrix_3f), + 'u_sun_direction': new Uniform3f(context, locations.u_sun_direction), + 'u_sun_intensity': new Uniform1f(context, locations.u_sun_intensity), + 'u_color_tint_r': new Uniform4f(context, locations.u_color_tint_r), + 'u_color_tint_m': new Uniform4f(context, locations.u_color_tint_m), + 'u_luminance': new Uniform1f(context, locations.u_luminance), +}); + +const skyboxCaptureUniformValues = ( + matrix: Float32Array, + sunDirection: vec3, + sunIntensity: number, + atmosphereColor: Color, + atmosphereHaloColor: Color +): UniformValues => ({ + 'u_matrix_3f': matrix, + 'u_sun_direction': sunDirection, + 'u_sun_intensity': sunIntensity, + 'u_color_tint_r': [ + atmosphereColor.r, + atmosphereColor.g, + atmosphereColor.b, + atmosphereColor.a + ], + 'u_color_tint_m': [ + atmosphereHaloColor.r, + atmosphereHaloColor.g, + atmosphereHaloColor.b, + atmosphereHaloColor.a + ], + 'u_luminance': 5e-5, +}); + +export { + skyboxCaptureUniforms, + skyboxCaptureUniformValues, +}; diff --git a/src/render/program/skybox_program.js b/src/render/program/skybox_program.js new file mode 100644 index 00000000000..35da2f968de --- /dev/null +++ b/src/render/program/skybox_program.js @@ -0,0 +1,87 @@ +// @flow + +import { + UniformMatrix4f, + Uniform1i, + Uniform3f, + Uniform1f +} from '../uniform_binding'; +import {vec3} from 'gl-matrix'; +import {degToRad} from '../../util/util'; + +import type {UniformValues, UniformLocations} from '../uniform_binding'; +import type Context from '../../gl/context'; + +export type SkyboxUniformsType = {| + 'u_matrix': UniformMatrix4f, + 'u_sun_direction': Uniform3f, + 'u_cubemap': Uniform1i, + 'u_opacity': Uniform1f, + 'u_temporal_offset': Uniform1f +|}; + +export type SkyboxGradientlUniformsType = {| + 'u_matrix': UniformMatrix4f, + 'u_color_ramp': Uniform1i, + 'u_center_direction': Uniform3f, + 'u_radius': Uniform1f, + 'u_opacity': Uniform1f, + 'u_temporal_offset': Uniform1f, +|}; + +const skyboxUniforms = (context: Context, locations: UniformLocations): SkyboxUniformsType => ({ + 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), + 'u_sun_direction': new Uniform3f(context, locations.u_sun_direction), + 'u_cubemap': new Uniform1i(context, locations.u_cubemap), + 'u_opacity': new Uniform1f(context, locations.u_opacity), + 'u_temporal_offset': new Uniform1f(context, locations.u_temporal_offset) + +}); + +const skyboxUniformValues = ( + matrix: Float32Array, + sunDirection: vec3, + cubemap: number, + opacity: number, + temporalOffset: number +): UniformValues => ({ + 'u_matrix': matrix, + 'u_sun_direction': sunDirection, + 'u_cubemap': cubemap, + 'u_opacity': opacity, + 'u_temporal_offset': temporalOffset +}); + +const skyboxGradientUniforms = (context: Context, locations: UniformLocations): SkyboxGradientlUniformsType => ({ + 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), + 'u_color_ramp': new Uniform1i(context, locations.u_color_ramp), + // radial gradient uniforms + 'u_center_direction': new Uniform3f(context, locations.u_center_direction), + 'u_radius': new Uniform1f(context, locations.u_radius), + 'u_opacity': new Uniform1f(context, locations.u_opacity), + 'u_temporal_offset': new Uniform1f(context, locations.u_temporal_offset) +}); + +const skyboxGradientUniformValues = ( + matrix: Float32Array, + centerDirection: vec3, + radius: number, //degrees + opacity: number, + temporalOffset: number +): UniformValues => { + return { + 'u_matrix': matrix, + 'u_color_ramp': 0, + 'u_center_direction': centerDirection, + 'u_radius': degToRad(radius), + 'u_opacity': opacity, + 'u_temporal_offset': temporalOffset + }; +}; + +export { + skyboxUniforms, + skyboxUniformValues, + skyboxGradientUniforms, + skyboxGradientUniformValues +}; diff --git a/src/render/program/symbol_program.js b/src/render/program/symbol_program.js index 6b082119205..be27d61c4a2 100644 --- a/src/render/program/symbol_program.js +++ b/src/render/program/symbol_program.js @@ -78,6 +78,8 @@ export type symbolTextAndIconUniformsType = {| 'u_is_halo': Uniform1i |}; +export type SymbolDefinesType = 'PITCH_WITH_MAP_TERRAIN'; + const symbolIconUniforms = (context: Context, locations: UniformLocations): SymbolIconUniformsType => ({ 'u_is_size_zoom_constant': new Uniform1i(context, locations.u_is_size_zoom_constant), 'u_is_size_feature_constant': new Uniform1i(context, locations.u_is_size_feature_constant), diff --git a/src/render/raster_fade.js b/src/render/raster_fade.js new file mode 100644 index 00000000000..a10a879886d --- /dev/null +++ b/src/render/raster_fade.js @@ -0,0 +1,56 @@ +// @flow + +import type SourceCache from '../source/source_cache'; +import {clamp} from '../util/util'; +import browser from '../util/browser'; +import Tile from '../source/tile'; +import type Transform from '../geo/transform'; + +export type RasterFade = {| + opacity: number, + mix: number, +|}; + +function rasterFade(tile: Tile, parentTile: ?Tile, sourceCache: SourceCache, transform: Transform, fadeDuration: number): RasterFade { + if (fadeDuration > 0) { + const now = browser.now(); + const sinceTile = (now - tile.timeAdded) / fadeDuration; + const sinceParent = parentTile ? (now - parentTile.timeAdded) / fadeDuration : -1; + + const source = sourceCache.getSource(); + const idealZ = transform.coveringZoomLevel({ + tileSize: source.tileSize, + roundZoom: source.roundZoom + }); + + // if no parent or parent is older, fade in; if parent is younger, fade out + const fadeIn = !parentTile || Math.abs(parentTile.tileID.overscaledZ - idealZ) > Math.abs(tile.tileID.overscaledZ - idealZ); + + const childOpacity = (fadeIn && tile.refreshedUponExpiration) ? 1 : clamp(fadeIn ? sinceTile : 1 - sinceParent, 0, 1); + + // we don't crossfade tiles that were just refreshed upon expiring: + // once they're old enough to pass the crossfading threshold + // (fadeDuration), unset the `refreshedUponExpiration` flag so we don't + // incorrectly fail to crossfade them when zooming + if (tile.refreshedUponExpiration && sinceTile >= 1) tile.refreshedUponExpiration = false; + + if (parentTile) { + return { + opacity: 1, + mix: 1 - childOpacity + }; + } else { + return { + opacity: childOpacity, + mix: 0 + }; + } + } else { + return { + opacity: 1, + mix: 0 + }; + } +} + +export default rasterFade; diff --git a/src/render/skybox_attributes.js b/src/render/skybox_attributes.js new file mode 100644 index 00000000000..4df774a4b03 --- /dev/null +++ b/src/render/skybox_attributes.js @@ -0,0 +1,9 @@ +// @flow +import {createLayout} from '../util/struct_array'; + +export const skyboxAttributes = createLayout([ + {name: 'a_pos_3f', components: 3, type: 'Float32'} +]); + +export default skyboxAttributes; +export const {members, size, alignment} = skyboxAttributes; diff --git a/src/render/skybox_geometry.js b/src/render/skybox_geometry.js new file mode 100644 index 00000000000..c98bd870e9b --- /dev/null +++ b/src/render/skybox_geometry.js @@ -0,0 +1,65 @@ +// @flow + +import {members as skyboxAttributes} from './skybox_attributes'; +import {SkyboxVertexArray, TriangleIndexArray} from '../data/array_types'; +import SegmentVector from '../data/segment'; +import type IndexBuffer from '../gl/index_buffer'; +import type VertexBuffer from '../gl/vertex_buffer'; +import type Context from '../gl/context'; + +function addVertex(vertexArray, x, y, z) { + vertexArray.emplaceBack( + // a_pos + x, + y, + z + ); +} + +class SkyboxGeometry { + vertexArray: SkyboxVertexArray; + vertexBuffer: VertexBuffer; + indices: TriangleIndexArray; + indexBuffer: IndexBuffer; + segment: SegmentVector; + + constructor(context: Context) { + this.vertexArray = new SkyboxVertexArray(); + this.indices = new TriangleIndexArray(); + + addVertex(this.vertexArray, -1.0, -1.0, 1.0); + addVertex(this.vertexArray, 1.0, -1.0, 1.0); + addVertex(this.vertexArray, -1.0, 1.0, 1.0); + addVertex(this.vertexArray, 1.0, 1.0, 1.0); + addVertex(this.vertexArray, -1.0, -1.0, -1.0); + addVertex(this.vertexArray, 1.0, -1.0, -1.0); + addVertex(this.vertexArray, -1.0, 1.0, -1.0); + addVertex(this.vertexArray, 1.0, 1.0, -1.0); + + // +x + this.indices.emplaceBack(5, 1, 3); + this.indices.emplaceBack(3, 7, 5); + // -x + this.indices.emplaceBack(6, 2, 0); + this.indices.emplaceBack(0, 4, 6); + // +y + this.indices.emplaceBack(2, 6, 7); + this.indices.emplaceBack(7, 3, 2); + // -y + this.indices.emplaceBack(5, 4, 0); + this.indices.emplaceBack(0, 1, 5); + // +z + this.indices.emplaceBack(0, 2, 3); + this.indices.emplaceBack(3, 1, 0); + // -z + this.indices.emplaceBack(7, 6, 4); + this.indices.emplaceBack(4, 5, 7); + + this.vertexBuffer = context.createVertexBuffer(this.vertexArray, skyboxAttributes); + this.indexBuffer = context.createIndexBuffer(this.indices); + + this.segment = SegmentVector.simpleSegment(0, 0, 36, 12); + } +} + +export default SkyboxGeometry; diff --git a/src/render/uniform_binding.js b/src/render/uniform_binding.js index 4cb0c5f32ee..6c619c0c01c 100644 --- a/src/render/uniform_binding.js +++ b/src/render/uniform_binding.js @@ -133,6 +133,24 @@ class UniformMatrix4f extends Uniform { } } +const emptyMat3 = new Float32Array(9); +class UniformMatrix3f extends Uniform { + constructor(context: Context, location: WebGLUniformLocation) { + super(context, location); + this.current = emptyMat3; + } + + set(v: Float32Array): void { + for (let i = 0; i < 9; i++) { + if (v[i] !== this.current[i]) { + this.current = v; + this.gl.uniformMatrix3fv(this.location, false, v); + break; + } + } + } +} + export { Uniform, Uniform1i, @@ -141,6 +159,7 @@ export { Uniform3f, Uniform4f, UniformColor, + UniformMatrix3f, UniformMatrix4f }; diff --git a/src/shaders/_prelude.fragment.glsl b/src/shaders/_prelude.fragment.glsl index e98fb22d587..8a4f2a685e8 100644 --- a/src/shaders/_prelude.fragment.glsl +++ b/src/shaders/_prelude.fragment.glsl @@ -15,3 +15,5 @@ precision mediump float; #endif #endif + +const float PI = 3.141592653589793; diff --git a/src/shaders/_prelude.vertex.glsl b/src/shaders/_prelude.vertex.glsl index 9cd030e4eb9..f42df4978e7 100644 --- a/src/shaders/_prelude.vertex.glsl +++ b/src/shaders/_prelude.vertex.glsl @@ -71,3 +71,7 @@ vec2 get_pattern_pos(const vec2 pixel_coord_upper, const vec2 pixel_coord_lower, vec2 offset = mod(mod(mod(pixel_coord_upper, pattern_size) * 256.0, pattern_size) * 256.0 + pixel_coord_lower, pattern_size); return (tile_units_to_pixels * pos + offset) / pattern_size; } + +const float PI = 3.141592653589793; + +const vec4 AWAY = vec4(-1000.0, -1000.0, -1000.0, 1); // Normalized device coordinate that is not rendered. diff --git a/src/shaders/_prelude_terrain.vertex.glsl b/src/shaders/_prelude_terrain.vertex.glsl new file mode 100644 index 00000000000..69ffe4c3850 --- /dev/null +++ b/src/shaders/_prelude_terrain.vertex.glsl @@ -0,0 +1,149 @@ +#ifdef TERRAIN + +uniform sampler2D u_dem; +uniform sampler2D u_dem_prev; +uniform vec4 u_dem_unpack; +uniform vec2 u_dem_tl; +uniform vec2 u_dem_tl_prev; +uniform float u_dem_scale; +uniform float u_dem_scale_prev; +uniform float u_dem_size; +uniform float u_dem_lerp; +uniform float u_exaggeration; +uniform float u_meter_to_dem; +uniform mat4 u_label_plane_matrix_inv; + +uniform sampler2D u_depth; +uniform vec2 u_depth_size_inv; + +vec4 tileUvToDemSample(vec2 uv, float dem_size, float dem_scale, vec2 dem_tl) { + vec2 pos = dem_size * (uv * dem_scale + dem_tl) + 1.0; + vec2 f = fract(pos); + return vec4((pos - f + 0.5) / (dem_size + 2.0), f); +} + +float decodeElevation(vec4 v) { + return dot(vec4(v.xyz * 255.0, -1.0), u_dem_unpack); +} + +float currentElevation(vec2 apos) { + float dd = 1.0 / (u_dem_size + 2.0); + vec4 r = tileUvToDemSample(apos / 8192.0, u_dem_size, u_dem_scale, u_dem_tl); + vec2 pos = r.xy; + vec2 f = r.zw; + + float tl = decodeElevation(texture2D(u_dem, pos)); + float tr = decodeElevation(texture2D(u_dem, pos + vec2(dd, 0.0))); + float bl = decodeElevation(texture2D(u_dem, pos + vec2(0.0, dd))); + float br = decodeElevation(texture2D(u_dem, pos + vec2(dd, dd))); + + return u_exaggeration * mix(mix(tl, tr, f.x), mix(bl, br, f.x), f.y); +} + +float prevElevation(vec2 apos) { + float dd = 1.0 / (u_dem_size + 2.0); + vec4 r = tileUvToDemSample(apos / 8192.0, u_dem_size, u_dem_scale_prev, u_dem_tl_prev); + vec2 pos = r.xy; + vec2 f = r.zw; + + float tl = decodeElevation(texture2D(u_dem_prev, pos)); + float tr = decodeElevation(texture2D(u_dem_prev, pos + vec2(dd, 0.0))); + float bl = decodeElevation(texture2D(u_dem_prev, pos + vec2(0.0, dd))); + float br = decodeElevation(texture2D(u_dem_prev, pos + vec2(dd, dd))); + + return u_exaggeration * mix(mix(tl, tr, f.x), mix(bl, br, f.x), f.y); +} + +#ifdef TERRAIN_VERTEX_MORPHING +float elevation(vec2 apos) { + float nextElevation = currentElevation(apos); + float prevElevation = prevElevation(apos); + return mix(prevElevation, nextElevation, u_dem_lerp); +} +#else +float elevation(vec2 apos) { + return currentElevation(apos); +} +#endif + +// Unpack depth from RGBA. A piece of code copied in various libraries and WebGL +// shadow mapping examples. +float unpack_depth(vec4 rgba_depth) +{ + const vec4 bit_shift = vec4(1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0); + return dot(rgba_depth, bit_shift) * 2.0 - 1.0; +} + +bool isOccluded(vec4 frag) { + vec3 coord = frag.xyz / frag.w; + float depth = unpack_depth(texture2D(u_depth, (coord.xy + 1.0) * 0.5)); + return coord.z > depth + 0.0005; +} + +float occlusionFade(vec4 frag) { + vec3 coord = frag.xyz / frag.w; + + vec3 df = vec3(5.0 * u_depth_size_inv, 0.0); + vec2 uv = 0.5 * coord.xy + 0.5; + vec4 depth = vec4( + unpack_depth(texture2D(u_depth, uv - df.xz)), + unpack_depth(texture2D(u_depth, uv + df.xz)), + unpack_depth(texture2D(u_depth, uv - df.zy)), + unpack_depth(texture2D(u_depth, uv + df.zy)) + ); + return dot(vec4(0.25), vec4(1.0) - clamp(300.0 * (vec4(coord.z - 0.001) - depth), 0.0, 1.0)); +} + +vec4 fourSample(vec2 pos, vec2 off) { + vec4 demtl = vec4(texture2D(u_dem, pos).xyz * 255.0, -1.0); + float tl = dot(demtl, u_dem_unpack); + vec4 demtr = vec4(texture2D(u_dem, pos + vec2(off.x, 0.0)).xyz * 255.0, -1.0); + float tr = dot(demtr, u_dem_unpack); + vec4 dembl = vec4(texture2D(u_dem, pos + vec2(0.0, off.y)).xyz * 255.0, -1.0); + float bl = dot(dembl, u_dem_unpack); + vec4 dembr = vec4(texture2D(u_dem, pos + off).xyz * 255.0, -1.0); + float br = dot(dembr, u_dem_unpack); + return vec4(tl, tr, bl, br); +} + +float flatElevation(vec2 pack) { + vec2 apos = floor(pack / 8.0); + vec2 span = 10.0 * (pack - apos * 8.0); + + vec2 uvTex = (apos - vec2(1.0, 1.0)) / 8190.0; + float size = u_dem_size + 2.0; + float dd = 1.0 / size; + + vec2 pos = u_dem_size * (uvTex * u_dem_scale + u_dem_tl) + 1.0; + vec2 f = fract(pos); + pos = (pos - f + 0.5) * dd; + + // Get elevation of centroid. + vec4 h = fourSample(pos, vec2(dd)); + float z = mix(mix(h.x, h.y, f.x), mix(h.z, h.w, f.x), f.y); + + vec2 w = floor(0.5 * (span * u_meter_to_dem - 1.0)); + vec2 d = dd * w; + vec4 bounds = vec4(d, vec2(1.0) - d); + + // Get building wide sample, to get better slope estimate. + h = fourSample(pos - d, 2.0 * d + vec2(dd)); + + vec4 diff = abs(h.xzxy - h.ywzw); + vec2 slope = min(vec2(0.25), u_meter_to_dem * 0.5 * (diff.xz + diff.yw) / (2.0 * w + vec2(1.0))); + vec2 fix = slope * span; + float base = z + max(fix.x, fix.y); + return u_exaggeration * base; +} + +float elevationFromUint16(float word) { + return u_exaggeration * word / 7.3; +} + +#else + +float elevation(vec2 pos) { return 0.0; } +bool isOccluded(vec4 frag) { return false; } +float occlusionFade(vec4 frag) { return 1.0; } + +#endif \ No newline at end of file diff --git a/src/shaders/circle.fragment.glsl b/src/shaders/circle.fragment.glsl index 14081450b09..a9f972539fb 100644 --- a/src/shaders/circle.fragment.glsl +++ b/src/shaders/circle.fragment.glsl @@ -1,4 +1,5 @@ varying vec3 v_data; +varying float v_visibility; #pragma mapbox: define highp vec4 color #pragma mapbox: define mediump float radius @@ -31,7 +32,7 @@ void main() { extrude_length - radius / (radius + stroke_width) ); - gl_FragColor = opacity_t * mix(color * opacity, stroke_color * stroke_opacity, color_t); + gl_FragColor = v_visibility * opacity_t * mix(color * opacity, stroke_color * stroke_opacity, color_t); #ifdef OVERDRAW_INSPECTOR gl_FragColor = vec4(1.0); diff --git a/src/shaders/circle.vertex.glsl b/src/shaders/circle.vertex.glsl index 1f084104174..6ec4503eff5 100644 --- a/src/shaders/circle.vertex.glsl +++ b/src/shaders/circle.vertex.glsl @@ -1,6 +1,10 @@ +#define NUM_VISIBILITY_RINGS 2 +#define INV_SQRT2 0.70710678 +#define ELEVATION_BIAS 0.0001 + +#define NUM_SAMPLES_PER_RING 16 + uniform mat4 u_matrix; -uniform bool u_scale_with_map; -uniform bool u_pitch_with_map; uniform vec2 u_extrude_scale; uniform lowp float u_device_pixel_ratio; uniform highp float u_camera_to_center_distance; @@ -8,6 +12,7 @@ uniform highp float u_camera_to_center_distance; attribute vec2 a_pos; varying vec3 v_data; +varying float v_visibility; #pragma mapbox: define highp vec4 color #pragma mapbox: define mediump float radius @@ -17,6 +22,49 @@ varying vec3 v_data; #pragma mapbox: define mediump float stroke_width #pragma mapbox: define lowp float stroke_opacity +vec2 calc_offset(vec2 extrusion, float radius, float stroke_width, float view_scale) { + return extrusion * (radius + stroke_width) * u_extrude_scale * view_scale; +} + +float cantilevered_elevation(vec2 pos, float radius, float stroke_width, float view_scale) { + vec2 c1 = pos + calc_offset(vec2(-1,-1), radius, stroke_width, view_scale); + vec2 c2 = pos + calc_offset(vec2(1,-1), radius, stroke_width, view_scale); + vec2 c3 = pos + calc_offset(vec2(1,1), radius, stroke_width, view_scale); + vec2 c4 = pos + calc_offset(vec2(-1,1), radius, stroke_width, view_scale); + float h1 = elevation(c1) + ELEVATION_BIAS; + float h2 = elevation(c2) + ELEVATION_BIAS; + float h3 = elevation(c3) + ELEVATION_BIAS; + float h4 = elevation(c4) + ELEVATION_BIAS; + return max(h4, max(h3, max(h1,h2))); +} + +float circle_elevation(vec2 pos) { +#if defined(TERRAIN) + return elevation(pos) + ELEVATION_BIAS; +#else + return 0.0; +#endif +} + +vec4 project_vertex(vec2 extrusion, vec4 world_center, vec4 projected_center, float radius, float stroke_width, float view_scale) { + vec2 sample_offset = calc_offset(extrusion, radius, stroke_width, view_scale); +#ifdef PITCH_WITH_MAP + return u_matrix * ( world_center + vec4(sample_offset, 0, 0) ); +#else + return projected_center + vec4(sample_offset, 0, 0); +#endif +} + +float get_sample_step() { +#ifdef PITCH_WITH_MAP + return 2.0 * PI / float(NUM_SAMPLES_PER_RING); +#else + // We want to only sample the top half of the circle when it is viewport-aligned. + // This is to prevent the circle from intersecting with the ground plane below it at high pitch. + return PI / float(NUM_SAMPLES_PER_RING); +#endif +} + void main(void) { #pragma mapbox: initialize highp vec4 color #pragma mapbox: initialize mediump float radius @@ -32,28 +80,56 @@ void main(void) { // multiply a_pos by 0.5, since we had it * 2 in order to sneak // in extrusion data vec2 circle_center = floor(a_pos * 0.5); - if (u_pitch_with_map) { - vec2 corner_position = circle_center; - if (u_scale_with_map) { - corner_position += extrude * (radius + stroke_width) * u_extrude_scale; - } else { + // extract height offset for terrain, this returns 0 if terrain is not active + float height = circle_elevation(circle_center); + vec4 world_center = vec4(circle_center, height, 1); + vec4 projected_center = u_matrix * world_center; + + float view_scale = 0.0; + #ifdef PITCH_WITH_MAP + #ifdef SCALE_WITH_MAP + view_scale = 1.0; + #else // Pitching the circle with the map effectively scales it with the map // To counteract the effect for pitch-scale: viewport, we rescale the // whole circle based on the pitch scaling effect at its central point - vec4 projected_center = u_matrix * vec4(circle_center, 0, 1); - corner_position += extrude * (radius + stroke_width) * u_extrude_scale * (projected_center.w / u_camera_to_center_distance); - } - - gl_Position = u_matrix * vec4(corner_position, 0, 1); - } else { - gl_Position = u_matrix * vec4(circle_center, 0, 1); + view_scale = projected_center.w / u_camera_to_center_distance; + #endif + #else + #ifdef SCALE_WITH_MAP + view_scale = u_camera_to_center_distance; + #else + view_scale = projected_center.w; + #endif + #endif + gl_Position = project_vertex(extrude, world_center, projected_center, radius, stroke_width, view_scale); - if (u_scale_with_map) { - gl_Position.xy += extrude * (radius + stroke_width) * u_extrude_scale * u_camera_to_center_distance; - } else { - gl_Position.xy += extrude * (radius + stroke_width) * u_extrude_scale * gl_Position.w; + float visibility = 0.0; + #ifdef TERRAIN + float step = get_sample_step(); + #ifdef PITCH_WITH_MAP + // to prevent the circle from self-intersecting with the terrain underneath on a sloped hill, + // we calculate the elevation at each corner and pick the highest one when computing visibility. + float cantilevered_height = cantilevered_elevation(circle_center, radius, stroke_width, view_scale); + vec4 occlusion_world_center = vec4(circle_center, cantilevered_height, 1); + vec4 occlusion_projected_center = u_matrix * occlusion_world_center; + #else + vec4 occlusion_world_center = world_center; + vec4 occlusion_projected_center = projected_center; + #endif + for(int ring = 0; ring < NUM_VISIBILITY_RINGS; ring++) { + float scale = (float(ring) + 1.0)/float(NUM_VISIBILITY_RINGS); + for(int i = 0; i < NUM_SAMPLES_PER_RING; i++) { + vec2 extrusion = vec2(cos(step * float(i)), -sin(step * float(i))) * scale; + vec4 frag_pos = project_vertex(extrusion, occlusion_world_center, occlusion_projected_center, radius, stroke_width, view_scale); + visibility += float(!isOccluded(frag_pos)); + } } - } + visibility /= float(NUM_VISIBILITY_RINGS) * float(NUM_SAMPLES_PER_RING); + #else + visibility = 1.0; + #endif + v_visibility = visibility; // This is a minimum blur distance that serves as a faux-antialiasing for // the circle. since blur is a ratio of the circle's size and the intent is diff --git a/src/shaders/collision_box.fragment.glsl b/src/shaders/collision_box.fragment.glsl index 751c412ded9..f53471da06d 100644 --- a/src/shaders/collision_box.fragment.glsl +++ b/src/shaders/collision_box.fragment.glsl @@ -1,21 +1,10 @@ - varying float v_placed; varying float v_notUsed; void main() { + vec4 red = vec4(1.0, 0.0, 0.0, 1.0); // Red = collision, hide label + vec4 blue = vec4(0.0, 0.0, 1.0, 0.5); // Blue = no collision, label is showing - float alpha = 0.5; - - // Red = collision, hide label - gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0) * alpha; - - // Blue = no collision, label is showing - if (v_placed > 0.5) { - gl_FragColor = vec4(0.0, 0.0, 1.0, 0.5) * alpha; - } - - if (v_notUsed > 0.5) { - // This box not used, fade it out - gl_FragColor *= .1; - } + gl_FragColor = mix(red, blue, step(0.5, v_placed)) * 0.5; + gl_FragColor *= mix(1.0, 0.1, step(0.5, v_notUsed)); } \ No newline at end of file diff --git a/src/shaders/collision_box.vertex.glsl b/src/shaders/collision_box.vertex.glsl index 8fa4bfc025d..dfdd231254c 100644 --- a/src/shaders/collision_box.vertex.glsl +++ b/src/shaders/collision_box.vertex.glsl @@ -3,6 +3,8 @@ attribute vec2 a_anchor_pos; attribute vec2 a_extrude; attribute vec2 a_placed; attribute vec2 a_shift; +attribute float a_size_scale; +attribute vec2 a_padding; uniform mat4 u_matrix; uniform vec2 u_extrude_scale; @@ -12,15 +14,15 @@ varying float v_placed; varying float v_notUsed; void main() { - vec4 projectedPoint = u_matrix * vec4(a_anchor_pos, 0, 1); + vec4 projectedPoint = u_matrix * vec4(a_anchor_pos, elevation(a_anchor_pos), 1); highp float camera_to_anchor_distance = projectedPoint.w; highp float collision_perspective_ratio = clamp( 0.5 + 0.5 * (u_camera_to_center_distance / camera_to_anchor_distance), 0.0, // Prevents oversized near-field boxes in pitched/overzoomed tiles - 4.0); + 1.5); - gl_Position = u_matrix * vec4(a_pos, 0.0, 1.0); - gl_Position.xy += (a_extrude + a_shift) * u_extrude_scale * gl_Position.w * collision_perspective_ratio; + gl_Position = u_matrix * vec4(a_pos, elevation(a_pos), 1.0); + gl_Position.xy += (a_extrude * a_size_scale + a_shift + a_padding) * u_extrude_scale * gl_Position.w * collision_perspective_ratio; v_placed = a_placed.x; v_notUsed = a_placed.y; diff --git a/src/shaders/collision_circle.vertex.glsl b/src/shaders/collision_circle.vertex.glsl index e64f1728297..764dd900aa6 100644 --- a/src/shaders/collision_circle.vertex.glsl +++ b/src/shaders/collision_circle.vertex.glsl @@ -1,4 +1,4 @@ -attribute vec2 a_pos; +attribute vec2 a_pos_2f; attribute float a_radius; attribute vec2 a_flags; @@ -25,7 +25,7 @@ vec3 toTilePosition(vec2 screenPos) { } void main() { - vec2 quadCenterPos = a_pos; + vec2 quadCenterPos = a_pos_2f; float radius = a_radius; float collision = a_flags.x; float vertexIdx = a_flags.y; diff --git a/src/shaders/debug.vertex.glsl b/src/shaders/debug.vertex.glsl index c872e7fa981..27168b95458 100644 --- a/src/shaders/debug.vertex.glsl +++ b/src/shaders/debug.vertex.glsl @@ -7,6 +7,7 @@ uniform float u_overlay_scale; void main() { // This vertex shader expects a EXTENT x EXTENT quad, // The UV co-ordinates for the overlay texture can be calculated using that knowledge + float h = elevation(a_pos); v_uv = a_pos / 8192.0; - gl_Position = u_matrix * vec4(a_pos * u_overlay_scale, 0, 1); + gl_Position = u_matrix * vec4(a_pos * u_overlay_scale, h, 1); } diff --git a/src/shaders/fill_extrusion.vertex.glsl b/src/shaders/fill_extrusion.vertex.glsl index 7a771b6dc97..f160d7ef223 100644 --- a/src/shaders/fill_extrusion.vertex.glsl +++ b/src/shaders/fill_extrusion.vertex.glsl @@ -5,8 +5,8 @@ uniform lowp float u_lightintensity; uniform float u_vertical_gradient; uniform lowp float u_opacity; -attribute vec2 a_pos; -attribute vec4 a_normal_ed; +attribute vec4 a_pos_normal_ed; +attribute vec2 a_centroid_pos; varying vec4 v_color; @@ -20,14 +20,33 @@ void main() { #pragma mapbox: initialize highp float height #pragma mapbox: initialize highp vec4 color - vec3 normal = a_normal_ed.xyz; + vec3 pos_nx = floor(a_pos_normal_ed.xyz * 0.5); + // The least significant bits of a_pos_normal_ed.xy hold: + // x is 1 if it's on top, 0 for ground. + // y is 1 if the normal points up, and 0 if it points to side. + // z is sign of ny: 1 for positive, 0 for values <= 0. + mediump vec3 top_up_ny = a_pos_normal_ed.xyz - 2.0 * pos_nx; + + float x_normal = pos_nx.z / 8192.0; + vec3 normal = top_up_ny.y == 1.0 ? vec3(0.0, 0.0, 1.0) : normalize(vec3(x_normal, (2.0 * top_up_ny.z - 1.0) * (1.0 - abs(x_normal)), 0.0)); base = max(0.0, base); height = max(0.0, height); - float t = mod(normal.x, 2.0); - - gl_Position = u_matrix * vec4(a_pos, t > 0.0 ? height : base, 1); + float t = top_up_ny.x; + +#ifdef TERRAIN + vec2 centroid_pos = a_centroid_pos; + bool flat_roof = centroid_pos.x != 0.0; + float ele = elevation(pos_nx.xy); + float hidden = float(centroid_pos.x == 0.0 && centroid_pos.y == 1.0); + float c_ele = flat_roof ? centroid_pos.y == 0.0 ? elevationFromUint16(centroid_pos.x) : flatElevation(centroid_pos) : ele; + // If centroid elevation lower than vertex elevation, roof at least 2 meters height above base. + float h = flat_roof ? max(c_ele + height, ele + base + 2.0) : ele + (t > 0.0 ? height : base == 0.0 ? -5.0 : base); + gl_Position = mix(u_matrix * vec4(pos_nx.xy, h, 1), AWAY, hidden); +#else + gl_Position = u_matrix * vec4(pos_nx.xy, t > 0.0 ? height : base, 1); +#endif // Relative luminance (how dark/bright is the surface color?) float colorvalue = color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722; @@ -39,7 +58,7 @@ void main() { color += ambientlight; // Calculate cos(theta), where theta is the angle between surface normal and diffuse light ray - float directional = clamp(dot(normal / 16384.0, u_lightpos), 0.0, 1.0); + float directional = clamp(dot(normal, u_lightpos), 0.0, 1.0); // Adjust directional so that // the range of values for highlight/shading is narrower diff --git a/src/shaders/fill_extrusion_pattern.vertex.glsl b/src/shaders/fill_extrusion_pattern.vertex.glsl index 2428580f9ca..76e61121a06 100644 --- a/src/shaders/fill_extrusion_pattern.vertex.glsl +++ b/src/shaders/fill_extrusion_pattern.vertex.glsl @@ -10,8 +10,8 @@ uniform vec3 u_lightcolor; uniform lowp vec3 u_lightpos; uniform lowp float u_lightintensity; -attribute vec2 a_pos; -attribute vec4 a_normal_ed; +attribute vec4 a_pos_normal_ed; +attribute vec2 a_centroid_pos; varying vec2 v_pos_a; varying vec2 v_pos_b; @@ -41,8 +41,16 @@ void main() { float fromScale = u_scale.y; float toScale = u_scale.z; - vec3 normal = a_normal_ed.xyz; - float edgedistance = a_normal_ed.w; + vec3 pos_nx = floor(a_pos_normal_ed.xyz * 0.5); + // The least significant bits of a_pos_normal_ed.xy hold: + // x is 1 if it's on top, 0 for ground. + // y is 1 if the normal points up, and 0 if it points to side. + // z is sign of ny: 1 for positive, 0 for values <= 0. + mediump vec3 top_up_ny = a_pos_normal_ed.xyz - 2.0 * pos_nx; + + float x_normal = pos_nx.z / 8192.0; + vec3 normal = top_up_ny.y == 1.0 ? vec3(0.0, 0.0, 1.0) : normalize(vec3(x_normal, (2.0 * top_up_ny.z - 1.0) * (1.0 - abs(x_normal)), 0.0)); + float edgedistance = a_pos_normal_ed.w; vec2 display_size_a = (pattern_br_a - pattern_tl_a) / pixel_ratio_from; vec2 display_size_b = (pattern_br_b - pattern_tl_b) / pixel_ratio_to; @@ -50,20 +58,31 @@ void main() { base = max(0.0, base); height = max(0.0, height); - float t = mod(normal.x, 2.0); + float t = top_up_ny.x; float z = t > 0.0 ? height : base; - gl_Position = u_matrix * vec4(a_pos, z, 1); - - vec2 pos = normal.x == 1.0 && normal.y == 0.0 && normal.z == 16384.0 - ? a_pos // extrusion top +#ifdef TERRAIN + vec2 centroid_pos = a_centroid_pos; + bool flat_roof = centroid_pos.x != 0.0; + float ele = elevation(pos_nx.xy); + float hidden = float(centroid_pos.x == 0.0 && centroid_pos.y == 1.0); + float c_ele = flat_roof ? centroid_pos.y == 0.0 ? elevationFromUint16(centroid_pos.x) : flatElevation(centroid_pos) : ele; + // If centroid elevation lower than vertex elevation, roof at least 2 meters height above base. + float h = flat_roof ? max(c_ele + height, ele + base + 2.0) : ele + (t > 0.0 ? height : base == 0.0 ? -5.0 : base); + gl_Position = mix(u_matrix * vec4(pos_nx.xy, h, 1), AWAY, hidden); +#else + gl_Position = u_matrix * vec4(pos_nx.xy, z, 1); +#endif + + vec2 pos = normal.z == 1.0 + ? pos_nx.xy // extrusion top : vec2(edgedistance, z * u_height_factor); // extrusion side v_pos_a = get_pattern_pos(u_pixel_coord_upper, u_pixel_coord_lower, fromScale * display_size_a, tileRatio, pos); v_pos_b = get_pattern_pos(u_pixel_coord_upper, u_pixel_coord_lower, toScale * display_size_b, tileRatio, pos); v_lighting = vec4(0.0, 0.0, 0.0, 1.0); - float directional = clamp(dot(normal / 16383.0, u_lightpos), 0.0, 1.0); + float directional = clamp(dot(normal, u_lightpos), 0.0, 1.0); directional = mix((1.0 - u_lightintensity), max((0.5 + u_lightintensity), 1.0), directional); if (normal.y != 0.0) { diff --git a/src/shaders/heatmap.vertex.glsl b/src/shaders/heatmap.vertex.glsl index 3712e4f2b0f..66931d36946 100644 --- a/src/shaders/heatmap.vertex.glsl +++ b/src/shaders/heatmap.vertex.glsl @@ -48,7 +48,7 @@ void main(void) { // multiply a_pos by 0.5, since we had it * 2 in order to sneak // in extrusion data - vec4 pos = vec4(floor(a_pos * 0.5) + extrude, 0, 1); + vec4 pos = vec4(floor(a_pos * 0.5) + extrude, elevation(floor(a_pos * 0.5)), 1); gl_Position = u_matrix * pos; } diff --git a/src/shaders/hillshade.fragment.glsl b/src/shaders/hillshade.fragment.glsl index b459f79a511..c1c1d11f740 100644 --- a/src/shaders/hillshade.fragment.glsl +++ b/src/shaders/hillshade.fragment.glsl @@ -7,8 +7,6 @@ uniform vec4 u_shadow; uniform vec4 u_highlight; uniform vec4 u_accent; -#define PI 3.141592653589793 - void main() { vec4 pixel = texture2D(u_image, v_pos); diff --git a/src/shaders/line.vertex.glsl b/src/shaders/line.vertex.glsl index 9334e2b72e1..5b6d0b0d71b 100644 --- a/src/shaders/line.vertex.glsl +++ b/src/shaders/line.vertex.glsl @@ -17,7 +17,6 @@ uniform lowp float u_device_pixel_ratio; varying vec2 v_normal; varying vec2 v_width2; varying float v_gamma_scale; -varying highp float v_linesofar; #pragma mapbox: define highp vec4 color #pragma mapbox: define lowp float blur @@ -40,9 +39,6 @@ void main() { vec2 a_extrude = a_data.xy - 128.0; float a_direction = mod(a_data.z, 4.0) - 1.0; - - v_linesofar = (floor(a_data.z / 4.0) + a_data.w * 64.0) * 2.0; - vec2 pos = floor(a_pos_normal * 0.5); // x is 1 if it's a round cap, 0 otherwise @@ -76,10 +72,13 @@ void main() { vec4 projected_extrude = u_matrix * vec4(dist / u_ratio, 0.0, 0.0); gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude; +#ifndef RENDER_TO_TEXTURE // calculate how much the perspective view squishes or stretches the extrude float extrude_length_without_perspective = length(dist); float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels); v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective; - +#else + v_gamma_scale = 1.0; +#endif v_width2 = vec2(outset, inset); } diff --git a/src/shaders/line_gradient.vertex.glsl b/src/shaders/line_gradient.vertex.glsl index 4b02ba5337e..a46a0178e12 100644 --- a/src/shaders/line_gradient.vertex.glsl +++ b/src/shaders/line_gradient.vertex.glsl @@ -79,10 +79,13 @@ void main() { vec4 projected_extrude = u_matrix * vec4(dist / u_ratio, 0.0, 0.0); gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude; +#ifndef RENDER_TO_TEXTURE // calculate how much the perspective view squishes or stretches the extrude float extrude_length_without_perspective = length(dist); float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels); v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective; - +#else + v_gamma_scale = 1.0; +#endif v_width2 = vec2(outset, inset); } diff --git a/src/shaders/line_pattern.vertex.glsl b/src/shaders/line_pattern.vertex.glsl index fff2daa110a..2d3fbca338f 100644 --- a/src/shaders/line_pattern.vertex.glsl +++ b/src/shaders/line_pattern.vertex.glsl @@ -6,12 +6,9 @@ // #define scale 63.0 #define scale 0.015873016 -// We scale the distance before adding it to the buffers so that we can store -// long distances for long segments. Use this value to unscale the distance. -#define LINE_DISTANCE_SCALE 2.0 - attribute vec2 a_pos_normal; attribute vec4 a_data; +attribute float a_linesofar; uniform mat4 u_matrix; uniform vec2 u_units_to_pixels; @@ -53,7 +50,7 @@ void main() { vec2 a_extrude = a_data.xy - 128.0; float a_direction = mod(a_data.z, 4.0) - 1.0; - float a_linesofar = (floor(a_data.z / 4.0) + a_data.w * 64.0) * LINE_DISTANCE_SCALE; + // float tileRatio = u_scale.x; vec2 pos = floor(a_pos_normal * 0.5); @@ -88,11 +85,14 @@ void main() { vec4 projected_extrude = u_matrix * vec4(dist / u_ratio, 0.0, 0.0); gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude; +#ifndef RENDER_TO_TEXTURE // calculate how much the perspective view squishes or stretches the extrude float extrude_length_without_perspective = length(dist); float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels); v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective; - +#else + v_gamma_scale = 1.0; +#endif v_linesofar = a_linesofar; v_width2 = vec2(outset, inset); v_width = floorwidth; diff --git a/src/shaders/line_sdf.vertex.glsl b/src/shaders/line_sdf.vertex.glsl index c85140ef7c4..7dc1d739c00 100644 --- a/src/shaders/line_sdf.vertex.glsl +++ b/src/shaders/line_sdf.vertex.glsl @@ -6,12 +6,9 @@ // #define scale 63.0 #define scale 0.015873016 -// We scale the distance before adding it to the buffers so that we can store -// long distances for long segments. Use this value to unscale the distance. -#define LINE_DISTANCE_SCALE 2.0 - attribute vec2 a_pos_normal; attribute vec4 a_data; +attribute float a_linesofar; uniform mat4 u_matrix; uniform mediump float u_ratio; @@ -51,7 +48,6 @@ void main() { vec2 a_extrude = a_data.xy - 128.0; float a_direction = mod(a_data.z, 4.0) - 1.0; - float a_linesofar = (floor(a_data.z / 4.0) + a_data.w * 64.0) * LINE_DISTANCE_SCALE; vec2 pos = floor(a_pos_normal * 0.5); @@ -73,7 +69,7 @@ void main() { // Scale the extrusion vector down to a normal and then up by the line width // of this vertex. - mediump vec2 dist =outset * a_extrude * scale; + mediump vec2 dist = outset * a_extrude * scale; // Calculate the offset when drawing a line that is to the side of the actual line. // We do this by creating a vector that points towards the extrude, but rotate @@ -86,10 +82,14 @@ void main() { vec4 projected_extrude = u_matrix * vec4(dist / u_ratio, 0.0, 0.0); gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude; +#ifndef RENDER_TO_TEXTURE // calculate how much the perspective view squishes or stretches the extrude float extrude_length_without_perspective = length(dist); float extrude_length_with_perspective = length(projected_extrude.xy / gl_Position.w * u_units_to_pixels); v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective; +#else + v_gamma_scale = 1.0; +#endif v_tex_a = vec2(a_linesofar * u_patternscale_a.x / floorwidth, normal.y * u_patternscale_a.y + u_tex_y_a); v_tex_b = vec2(a_linesofar * u_patternscale_b.x / floorwidth, normal.y * u_patternscale_b.y + u_tex_y_b); diff --git a/src/shaders/shaders.js b/src/shaders/shaders.js index 25f1fabdbcb..d8aa42e33aa 100644 --- a/src/shaders/shaders.js +++ b/src/shaders/shaders.js @@ -54,7 +54,19 @@ import symbolSDFFrag from './symbol_sdf.fragment.glsl'; import symbolSDFVert from './symbol_sdf.vertex.glsl'; import symbolTextAndIconFrag from './symbol_text_and_icon.fragment.glsl'; import symbolTextAndIconVert from './symbol_text_and_icon.vertex.glsl'; +import skyboxFrag from './skybox.fragment.glsl'; +import skyboxGradientFrag from './skybox_gradient.fragment.glsl'; +import skyboxVert from './skybox.vertex.glsl'; +import terrainRasterFrag from './terrain_raster.fragment.glsl'; +import terrainRasterVert from './terrain_raster.vertex.glsl'; +import terrainDepthFrag from './terrain_depth.fragment.glsl'; +import terrainDepthVert from './terrain_depth.vertex.glsl'; +import preludeTerrainVert from './_prelude_terrain.vertex.glsl'; +import skyboxCaptureFrag from './skybox_capture.fragment.glsl'; +import skyboxCaptureVert from './skybox_capture.vertex.glsl'; +export let preludeTerrain = {}; +preludeTerrain = compile('', preludeTerrainVert, true); export const prelude = compile(preludeFrag, preludeVert); export const background = compile(backgroundFrag, backgroundVert); export const backgroundPattern = compile(backgroundPatternFrag, backgroundPatternVert); @@ -81,16 +93,24 @@ export const raster = compile(rasterFrag, rasterVert); export const symbolIcon = compile(symbolIconFrag, symbolIconVert); export const symbolSDF = compile(symbolSDFFrag, symbolSDFVert); export const symbolTextAndIcon = compile(symbolTextAndIconFrag, symbolTextAndIconVert); +export const terrainRaster = compile(terrainRasterFrag, terrainRasterVert); +export const terrainDepth = compile(terrainDepthFrag, terrainDepthVert); +export const skybox = compile(skyboxFrag, skyboxVert); +export const skyboxGradient = compile(skyboxGradientFrag, skyboxVert); +export const skyboxCapture = compile(skyboxCaptureFrag, skyboxCaptureVert); // Expand #pragmas to #ifdefs. -function compile(fragmentSource, vertexSource) { +function compile(fragmentSource, vertexSource, isPreludeTerrainShader) { const re = /#pragma mapbox: ([\w]+) ([\w]+) ([\w]+) ([\w]+)/g; - const staticAttributes = vertexSource.match(/attribute ([\w]+) ([\w]+)/g); - const fragmentUniforms = fragmentSource.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g); - const vertexUniforms = vertexSource.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g); - const staticUniforms = vertexUniforms ? vertexUniforms.concat(fragmentUniforms) : fragmentUniforms; + const staticAttributes = vertexSource.match(/attribute (highp |mediump |lowp )?([\w]+) ([\w]+)/g); + const fragmentUniforms = fragmentSource.match(/uniform (highp |mediump |lowp )?([\w]+) ([\w]+)([\s]*)([\w]*)/g); + const vertexUniforms = vertexSource.match(/uniform (highp |mediump |lowp )?([\w]+) ([\w]+)([\s]*)([\w]*)/g); + let staticUniforms = vertexUniforms ? vertexUniforms.concat(fragmentUniforms) : fragmentUniforms; + if (!isPreludeTerrainShader) { + staticUniforms = preludeTerrain.staticUniforms.concat(staticUniforms); + } const fragmentPragmas = {}; diff --git a/src/shaders/skybox.fragment.glsl b/src/shaders/skybox.fragment.glsl new file mode 100644 index 00000000000..93e7820add3 --- /dev/null +++ b/src/shaders/skybox.fragment.glsl @@ -0,0 +1,67 @@ +// [1] Banding in games http://loopit.dk/banding_in_games.pdf + +varying lowp vec3 v_uv; + +uniform lowp samplerCube u_cubemap; +uniform lowp float u_opacity; +uniform highp float u_temporal_offset; +uniform highp vec3 u_sun_direction; + +highp vec3 hash(highp vec2 p) { + highp vec3 p3 = fract(vec3(p.xyx) * vec3(443.8975, 397.2973, 491.1871)); + p3 += dot(p3, p3.yxz + 19.19); + return fract(vec3((p3.x + p3.y) * p3.z, (p3.x + p3.z) * p3.y, (p3.y + p3.z) * p3.x)); +} + +vec3 dither(vec3 color, highp vec2 seed) { + vec3 rnd = hash(seed) + hash(seed + 0.59374) - 0.5; + color.rgb += rnd / 255.0; + return color; +} + +float sun_disk(highp vec3 ray_direction, highp vec3 sun_direction) { + highp float cos_angle = dot(normalize(ray_direction), sun_direction); + + // Sun angular angle is ~0.5° + const highp float cos_sun_angular_diameter = 0.99996192306; + const highp float smoothstep_delta = 1e-5; + + return smoothstep( + cos_sun_angular_diameter - smoothstep_delta, + cos_sun_angular_diameter + smoothstep_delta, + cos_angle); +} + +float map(float value, float start, float end, float new_start, float new_end) { + return ((value - start) * (new_end - new_start)) / (end - start) + new_start; +} + +void main() { + vec3 uv = v_uv; + + // Add a small offset to prevent black bands around areas where + // the scattering algorithm does not manage to gather lighting + const float y_bias = 0.015; + uv.y += y_bias; + + // Inverse of the operation applied for non-linear UV parameterization + uv.y = pow(abs(uv.y), 1.0 / 5.0); + + // To make better utilization of the visible range (e.g. over the horizon, UVs + // from 0.0 to 1.0 on the Y-axis in cubemap space), the UV range is remapped from + // (0.0,1.0) to (-1.0,1.0) on y. The inverse operation is applied when generating. + uv.y = map(uv.y, 0.0, 1.0, -1.0, 1.0); + + vec3 sky_color = textureCube(u_cubemap, uv).rgb; + + // Dither [1] + sky_color.rgb = dither(sky_color.rgb, gl_FragCoord.xy + u_temporal_offset); + // Add sun disk + sky_color += 0.1 * sun_disk(v_uv, u_sun_direction); + + gl_FragColor = vec4(sky_color * u_opacity, u_opacity); + +#ifdef OVERDRAW_INSPECTOR + gl_FragColor = vec4(1.0); +#endif +} diff --git a/src/shaders/skybox.vertex.glsl b/src/shaders/skybox.vertex.glsl new file mode 100644 index 00000000000..b4d4cfb8932 --- /dev/null +++ b/src/shaders/skybox.vertex.glsl @@ -0,0 +1,17 @@ +attribute highp vec3 a_pos_3f; + +uniform lowp mat4 u_matrix; + +varying highp vec3 v_uv; + +void main() { + const mat3 half_neg_pi_around_x = mat3(1.0, 0.0, 0.0, + 0.0, 0.0, -1.0, + 0.0, 1.0, 0.0); + + v_uv = half_neg_pi_around_x * a_pos_3f; + vec4 pos = u_matrix * vec4(a_pos_3f, 1.0); + + // Enforce depth to be 1.0 + gl_Position = pos.xyww; +} diff --git a/src/shaders/skybox_capture.fragment.glsl b/src/shaders/skybox_capture.fragment.glsl new file mode 100644 index 00000000000..da483aff7b5 --- /dev/null +++ b/src/shaders/skybox_capture.fragment.glsl @@ -0,0 +1,141 @@ +// [1] Precomputed Atmospheric Scattering: https://hal.inria.fr/inria-00288758/document +// [2] Earth Fact Sheet https://nssdc.gsfc.nasa.gov/planetary/factsheet/earthfact.html +// [3] Tonemapping Operators http://filmicworlds.com/blog/filmic-tonemapping-operators + +varying highp vec3 v_position; + +uniform highp float u_sun_intensity; +uniform highp float u_luminance; +uniform lowp vec3 u_sun_direction; +uniform highp vec4 u_color_tint_r; +uniform highp vec4 u_color_tint_m; + +#ifdef GL_ES +precision highp float; +#endif + +// [1] equation (1) section 2.1. for λ = (680, 550, 440) nm, +// which corresponds to scattering coefficients at sea level +#define BETA_R vec3(5.5e-6, 13.0e-6, 22.4e-6) +// The following constants are from [1] Figure 6 and section 2.1 +#define BETA_M vec3(21e-6, 21e-6, 21e-6) +#define MIE_G 0.76 +#define DENSITY_HEIGHT_SCALE_R 8000.0 // m +#define DENSITY_HEIGHT_SCALE_M 1200.0 // m +// [1] and [2] section 2.1 +#define PLANET_RADIUS 6360e3 // m +#define ATMOSPHERE_RADIUS 6420e3 // m +#define SAMPLE_STEPS 10 +#define DENSITY_STEPS 4 + +float ray_sphere_exit(vec3 orig, vec3 dir, float radius) { + float a = dot(dir, dir); + float b = 2.0 * dot(dir, orig); + float c = dot(orig, orig) - radius * radius; + float d = sqrt(b * b - 4.0 * a * c); + return (-b + d) / (2.0 * a); +} + +vec3 extinction(vec2 density) { + return exp(-vec3(BETA_R * u_color_tint_r.a * density.x + BETA_M * u_color_tint_m.a * density.y)); +} + +vec2 local_density(vec3 point) { + float height = max(length(point) - PLANET_RADIUS, 0.0); + // Explicitly split in two shader statements, exp(vec2) + // did not behave correctly on specific arm mali arch. + float exp_r = exp(-height / DENSITY_HEIGHT_SCALE_R); + float exp_m = exp(-height / DENSITY_HEIGHT_SCALE_M); + return vec2(exp_r, exp_m); +} + +float phase_ray(float cos_angle) { + return (3.0 / (16.0 * PI)) * (1.0 + cos_angle * cos_angle); +} + +float phase_mie(float cos_angle) { + return (3.0 / (8.0 * PI)) * ((1.0 - MIE_G * MIE_G) * (1.0 + cos_angle * cos_angle)) / + ((2.0 + MIE_G * MIE_G) * pow(1.0 + MIE_G * MIE_G - 2.0 * MIE_G * cos_angle, 1.5)); +} + +vec2 density_to_atmosphere(vec3 point, vec3 light_dir) { + float ray_len = ray_sphere_exit(point, light_dir, ATMOSPHERE_RADIUS); + float step_len = ray_len / float(DENSITY_STEPS); + + vec2 density_point_to_atmosphere = vec2(0.0); + for (int i = 0; i < DENSITY_STEPS; ++i) { + vec3 point_on_ray = point + light_dir * ((float(i) + 0.5) * step_len); + density_point_to_atmosphere += local_density(point_on_ray) * step_len;; + } + + return density_point_to_atmosphere; +} + +vec3 atmosphere(vec3 ray_dir, vec3 sun_direction, float sun_intensity) { + vec2 density_orig_to_point = vec2(0.0); + vec3 scatter_r = vec3(0.0); + vec3 scatter_m = vec3(0.0); + vec3 origin = vec3(0.0, PLANET_RADIUS, 0.0); + + float ray_len = ray_sphere_exit(origin, ray_dir, ATMOSPHERE_RADIUS); + float step_len = ray_len / float(SAMPLE_STEPS); + for (int i = 0; i < SAMPLE_STEPS; ++i) { + vec3 point_on_ray = origin + ray_dir * ((float(i) + 0.5) * step_len); + + // Local density + vec2 density = local_density(point_on_ray) * step_len; + density_orig_to_point += density; + + // Density from point to atmosphere + vec2 density_point_to_atmosphere = density_to_atmosphere(point_on_ray, sun_direction); + + // Scattering contribution + vec2 density_orig_to_atmosphere = density_orig_to_point + density_point_to_atmosphere; + vec3 extinction = extinction(density_orig_to_atmosphere); + scatter_r += density.x * extinction; + scatter_m += density.y * extinction; + } + + // The mie and rayleigh phase functions describe how much light + // is scattered towards the eye when colliding with particles + float cos_angle = dot(ray_dir, sun_direction); + float phase_r = phase_ray(cos_angle); + float phase_m = phase_mie(cos_angle); + + // Apply light color adjustments + vec3 beta_r = BETA_R * u_color_tint_r.rgb * u_color_tint_r.a; + vec3 beta_m = BETA_M * u_color_tint_m.rgb * u_color_tint_m.a; + + return (scatter_r * phase_r * beta_r + scatter_m * phase_m * beta_m) * sun_intensity; +} + +const float A = 0.15; +const float B = 0.50; +const float C = 0.10; +const float D = 0.20; +const float E = 0.02; +const float F = 0.30; + +vec3 uncharted2_tonemap(vec3 x) { + return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F; +} + +void main() { + vec3 ray_direction = v_position; + + // Non-linear UV parameterization to increase horizon events + ray_direction.y = pow(ray_direction.y, 5.0); + + // Add a small offset to prevent black bands around areas where + // the scattering algorithm does not manage to gather lighting + const float y_bias = 0.015; + ray_direction.y += y_bias; + + vec3 color = atmosphere(normalize(ray_direction), u_sun_direction, u_sun_intensity); + + // Apply exposure [3] + float white_scale = 1.0748724675633854; // 1.0 / uncharted2_tonemap(1000.0) + color = uncharted2_tonemap((log2(2.0 / pow(u_luminance, 4.0))) * color) * white_scale; + + gl_FragColor = vec4(color, 1.0); +} diff --git a/src/shaders/skybox_capture.vertex.glsl b/src/shaders/skybox_capture.vertex.glsl new file mode 100644 index 00000000000..6427e8c1951 --- /dev/null +++ b/src/shaders/skybox_capture.vertex.glsl @@ -0,0 +1,23 @@ +attribute highp vec3 a_pos_3f; + +uniform mat3 u_matrix_3f; + +varying highp vec3 v_position; + +float map(float value, float start, float end, float new_start, float new_end) { + return ((value - start) * (new_end - new_start)) / (end - start) + new_start; +} + +void main() { + vec4 pos = vec4(u_matrix_3f * a_pos_3f, 1.0); + + v_position = pos.xyz; + v_position.y *= -1.0; + + // To make better utilization of the visible range (e.g. over the horizon, UVs + // from 0.0 to 1.0 on the Y-axis in cubemap space), the UV range is remapped from + // (-1.0,1.0) to (0.0,1.0) on y. The inverse operation is applied when sampling. + v_position.y = map(v_position.y, -1.0, 1.0, 0.0, 1.0); + + gl_Position = vec4(a_pos_3f.xy, 0.0, 1.0); +} diff --git a/src/shaders/skybox_gradient.fragment.glsl b/src/shaders/skybox_gradient.fragment.glsl new file mode 100644 index 00000000000..bc99de367bd --- /dev/null +++ b/src/shaders/skybox_gradient.fragment.glsl @@ -0,0 +1,33 @@ +varying highp vec3 v_uv; + +uniform lowp sampler2D u_color_ramp; +uniform lowp vec3 u_center_direction; +uniform lowp float u_radius; +uniform lowp float u_opacity; +uniform highp float u_temporal_offset; + +highp vec3 hash(highp vec2 p) { + highp vec3 p3 = fract(vec3(p.xyx) * vec3(443.8975, 397.2973, 491.1871)); + p3 += dot(p3, p3.yxz + 19.19); + return fract(vec3((p3.x + p3.y) * p3.z, (p3.x + p3.z) * p3.y, (p3.y + p3.z) * p3.x)); +} + +vec3 dither(vec3 color, highp vec2 seed) { + vec3 rnd = hash(seed) + hash(seed + 0.59374) - 0.5; + color.rgb += rnd / 255.0; + return color; +} + +void main() { + float progress = acos(dot(normalize(v_uv), u_center_direction)) / u_radius; + vec4 color = texture2D(u_color_ramp, vec2(progress, 0.5)) * u_opacity; + + // Dither + color.rgb = dither(color.rgb, gl_FragCoord.xy + u_temporal_offset); + + gl_FragColor = color; + +#ifdef OVERDRAW_INSPECTOR + gl_FragColor = vec4(1.0); +#endif +} diff --git a/src/shaders/symbol_icon.vertex.glsl b/src/shaders/symbol_icon.vertex.glsl index 16ab111429c..cf8c26764f8 100644 --- a/src/shaders/symbol_icon.vertex.glsl +++ b/src/shaders/symbol_icon.vertex.glsl @@ -1,5 +1,3 @@ -const float PI = 3.141592653589793; - attribute vec4 a_pos_offset; attribute vec4 a_data; attribute vec4 a_pixeloffset; @@ -54,7 +52,9 @@ void main() { size = u_size; } - vec4 projectedPoint = u_matrix * vec4(a_pos, 0, 1); + float h = elevation(a_pos); + vec4 projectedPoint = u_matrix * vec4(a_pos, h, 1); + highp float camera_to_anchor_distance = projectedPoint.w; // See comments in symbol_sdf.vertex highp float distance_ratio = u_pitch_with_map ? @@ -63,7 +63,7 @@ void main() { highp float perspective_ratio = clamp( 0.5 + 0.5 * distance_ratio, 0.0, // Prevents oversized near-field symbols in pitched/overzoomed tiles - 4.0); + 1.5); size *= perspective_ratio; @@ -72,7 +72,7 @@ void main() { highp float symbol_rotation = 0.0; if (u_rotate_symbol) { // See comments in symbol_sdf.vertex - vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), 0, 1); + vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), h, 1); vec2 a = projectedPoint.xy / projectedPoint.w; vec2 b = offsetProjectedPoint.xy / offsetProjectedPoint.w; @@ -84,11 +84,19 @@ void main() { highp float angle_cos = cos(segment_angle + symbol_rotation); mat2 rotation_matrix = mat2(angle_cos, -1.0 * angle_sin, angle_sin, angle_cos); - vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, 0.0, 1.0); - gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * max(a_minFontScale, fontScale) + a_pxoffset / 16.0), 0.0, 1.0); + vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, h, 1.0); + float z = 0.0; + vec2 offset = rotation_matrix * (a_offset / 32.0 * max(a_minFontScale, fontScale) + a_pxoffset / 16.0); +#ifdef PITCH_WITH_MAP_TERRAIN + vec4 tile_pos = u_label_plane_matrix_inv * vec4(a_projected_pos.xy + offset, 0.0, 1.0); + z = elevation(tile_pos.xy); +#endif + // Symbols might end up being behind the camera. Move them AWAY. + float occlusion_fade = occlusionFade(projectedPoint); + gl_Position = mix(u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + offset, z, 1.0), AWAY, float(projectedPoint.w <= 0.0 || occlusion_fade == 0.0)); v_tex = a_tex / u_texsize; vec2 fade_opacity = unpack_opacity(a_fade_opacity); float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change; - v_fade_opacity = max(0.0, min(1.0, fade_opacity[0] + fade_change)); + v_fade_opacity = max(0.0, min(occlusion_fade, fade_opacity[0] + fade_change)); } diff --git a/src/shaders/symbol_sdf.vertex.glsl b/src/shaders/symbol_sdf.vertex.glsl index 71ccf3c81d7..144847f65ea 100644 --- a/src/shaders/symbol_sdf.vertex.glsl +++ b/src/shaders/symbol_sdf.vertex.glsl @@ -1,5 +1,3 @@ -const float PI = 3.141592653589793; - attribute vec4 a_pos_offset; attribute vec4 a_data; attribute vec4 a_pixeloffset; @@ -65,7 +63,9 @@ void main() { size = u_size; } - vec4 projectedPoint = u_matrix * vec4(a_pos, 0, 1); + float h = elevation(a_pos); + vec4 projectedPoint = u_matrix * vec4(a_pos, h, 1); + highp float camera_to_anchor_distance = projectedPoint.w; // If the label is pitched with the map, layout is done in pitched space, // which makes labels in the distance smaller relative to viewport space. @@ -79,7 +79,7 @@ void main() { highp float perspective_ratio = clamp( 0.5 + 0.5 * distance_ratio, 0.0, // Prevents oversized near-field symbols in pitched/overzoomed tiles - 4.0); + 1.5); size *= perspective_ratio; @@ -90,7 +90,7 @@ void main() { // Point labels with 'rotation-alignment: map' are horizontal with respect to tile units // To figure out that angle in projected space, we draw a short horizontal line in tile // space, project it, and measure its angle in projected space. - vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), 0, 1); + vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), h, 1); vec2 a = projectedPoint.xy / projectedPoint.w; vec2 b = offsetProjectedPoint.xy / offsetProjectedPoint.w; @@ -102,13 +102,21 @@ void main() { highp float angle_cos = cos(segment_angle + symbol_rotation); mat2 rotation_matrix = mat2(angle_cos, -1.0 * angle_sin, angle_sin, angle_cos); - vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, 0.0, 1.0); - gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale + a_pxoffset), 0.0, 1.0); + vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, h, 1.0); + float z = 0.0; + vec2 offset = rotation_matrix * (a_offset / 32.0 * fontScale + a_pxoffset); +#ifdef PITCH_WITH_MAP_TERRAIN + vec4 tile_pos = u_label_plane_matrix_inv * vec4(a_projected_pos.xy + offset, 0.0, 1.0); + z = elevation(tile_pos.xy); +#endif + // Symbols might end up being behind the camera. Move them AWAY. + float occlusion_fade = occlusionFade(projectedPoint); + gl_Position = mix(u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + offset, z, 1.0), AWAY, float(projectedPoint.w <= 0.0 || occlusion_fade == 0.0)); float gamma_scale = gl_Position.w; vec2 fade_opacity = unpack_opacity(a_fade_opacity); float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change; - float interpolated_fade_opacity = max(0.0, min(1.0, fade_opacity[0] + fade_change)); + float interpolated_fade_opacity = max(0.0, min(occlusion_fade, fade_opacity[0] + fade_change)); v_data0 = a_tex / u_texsize; v_data1 = vec3(gamma_scale, size, interpolated_fade_opacity); diff --git a/src/shaders/symbol_text_and_icon.vertex.glsl b/src/shaders/symbol_text_and_icon.vertex.glsl index 647310fc9c9..3caf7a1242b 100644 --- a/src/shaders/symbol_text_and_icon.vertex.glsl +++ b/src/shaders/symbol_text_and_icon.vertex.glsl @@ -1,5 +1,3 @@ -const float PI = 3.141592653589793; - attribute vec4 a_pos_offset; attribute vec4 a_data; attribute vec3 a_projected_pos; @@ -65,7 +63,9 @@ void main() { size = u_size; } - vec4 projectedPoint = u_matrix * vec4(a_pos, 0, 1); + float h = elevation(a_pos); + vec4 projectedPoint = u_matrix * vec4(a_pos, h, 1); + highp float camera_to_anchor_distance = projectedPoint.w; // If the label is pitched with the map, layout is done in pitched space, // which makes labels in the distance smaller relative to viewport space. @@ -79,7 +79,7 @@ void main() { highp float perspective_ratio = clamp( 0.5 + 0.5 * distance_ratio, 0.0, // Prevents oversized near-field symbols in pitched/overzoomed tiles - 4.0); + 1.5); size *= perspective_ratio; @@ -90,7 +90,7 @@ void main() { // Point labels with 'rotation-alignment: map' are horizontal with respect to tile units // To figure out that angle in projected space, we draw a short horizontal line in tile // space, project it, and measure its angle in projected space. - vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), 0, 1); + vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), h, 1); vec2 a = projectedPoint.xy / projectedPoint.w; vec2 b = offsetProjectedPoint.xy / offsetProjectedPoint.w; @@ -102,13 +102,20 @@ void main() { highp float angle_cos = cos(segment_angle + symbol_rotation); mat2 rotation_matrix = mat2(angle_cos, -1.0 * angle_sin, angle_sin, angle_cos); - vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, 0.0, 1.0); - gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale), 0.0, 1.0); + vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, h, 1.0); + float z = 0.0; + vec2 offset = rotation_matrix * (a_offset / 32.0 * fontScale); +#ifdef PITCH_WITH_MAP_TERRAIN + vec4 tile_pos = u_label_plane_matrix_inv * vec4(a_projected_pos.xy + offset, 0.0, 1.0); + z = elevation(tile_pos.xy); +#endif + float occlusion_fade = occlusionFade(projectedPoint); + gl_Position = mix(u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + offset, z, 1.0), AWAY, float(projectedPoint.w <= 0.0 || occlusion_fade == 0.0)); float gamma_scale = gl_Position.w; vec2 fade_opacity = unpack_opacity(a_fade_opacity); float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change; - float interpolated_fade_opacity = max(0.0, min(1.0, fade_opacity[0] + fade_change)); + float interpolated_fade_opacity = max(0.0, min(occlusion_fade, fade_opacity[0] + fade_change)); v_data0.xy = a_tex / u_texsize; v_data0.zw = a_tex / u_texsize_icon; diff --git a/src/shaders/terrain_depth.fragment.glsl b/src/shaders/terrain_depth.fragment.glsl new file mode 100644 index 00000000000..f336772d62f --- /dev/null +++ b/src/shaders/terrain_depth.fragment.glsl @@ -0,0 +1,20 @@ +#ifdef GL_ES +precision highp float; +#endif + +// Pack depth to RGBA. A piece of code copied in various libraries and WebGL +// shadow mapping examples. +vec4 pack_depth(float ndc_z) { + float depth = ndc_z * 0.5 + 0.5; + const vec4 bit_shift = vec4(256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0); + const vec4 bit_mask = vec4(0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0); + vec4 res = fract(depth * bit_shift); + res -= res.xxyz * bit_mask; + return res; +} + +varying float v_depth; + +void main() { + gl_FragColor = pack_depth(v_depth); +} diff --git a/src/shaders/terrain_depth.vertex.glsl b/src/shaders/terrain_depth.vertex.glsl new file mode 100644 index 00000000000..f28d07dcf4f --- /dev/null +++ b/src/shaders/terrain_depth.vertex.glsl @@ -0,0 +1,11 @@ +uniform mat4 u_matrix; +attribute vec2 a_pos; +attribute vec2 a_texture_pos; + +varying float v_depth; + +void main() { + float elevation = elevation(a_texture_pos); + gl_Position = u_matrix * vec4(a_pos, elevation, 1.0); + v_depth = gl_Position.z / gl_Position.w; +} diff --git a/src/shaders/terrain_raster.fragment.glsl b/src/shaders/terrain_raster.fragment.glsl new file mode 100644 index 00000000000..10029f61cd8 --- /dev/null +++ b/src/shaders/terrain_raster.fragment.glsl @@ -0,0 +1,9 @@ +uniform sampler2D u_image0; +varying vec2 v_pos0; + +void main() { + gl_FragColor = texture2D(u_image0, v_pos0); +#ifdef OVERDRAW_INSPECTOR + gl_FragColor = vec4(1.0); +#endif +} diff --git a/src/shaders/terrain_raster.vertex.glsl b/src/shaders/terrain_raster.vertex.glsl new file mode 100644 index 00000000000..9ea9396c278 --- /dev/null +++ b/src/shaders/terrain_raster.vertex.glsl @@ -0,0 +1,17 @@ +uniform mat4 u_matrix; +uniform float u_skirt_height; + +attribute vec2 a_pos; +attribute vec2 a_texture_pos; + +varying vec2 v_pos0; + +const float skirtOffset = 24575.0; + +void main() { + v_pos0 = a_texture_pos / 8192.0; + float skirt = float(a_pos.x >= skirtOffset); + float elevation = elevation(a_texture_pos) - skirt * u_skirt_height; + vec2 decodedPos = a_pos - vec2(skirt * skirtOffset, 0.0); + gl_Position = u_matrix * vec4(decodedPos, elevation, 1.0); +} diff --git a/src/source/geojson_source.js b/src/source/geojson_source.js index b822900513d..889517eb30e 100644 --- a/src/source/geojson_source.js +++ b/src/source/geojson_source.js @@ -310,6 +310,7 @@ class GeoJSONSource extends Evented implements Source { type: this.type, uid: tile.uid, tileID: tile.tileID, + tileZoom: tile.tileZoom, zoom: tile.tileID.overscaledZ, maxZoom: this.maxzoom, tileSize: this.tileSize, diff --git a/src/source/geojson_worker_source.js b/src/source/geojson_worker_source.js index a395a6fbbed..f2f7cb8ba03 100644 --- a/src/source/geojson_worker_source.js +++ b/src/source/geojson_worker_source.js @@ -13,6 +13,7 @@ import VectorTileWorkerSource from './vector_tile_worker_source'; import {createExpression} from '../style-spec/expression'; import type { + RequestedTileParameters, WorkerTileParameters, WorkerTileCallback, } from '../source/worker_source'; @@ -47,7 +48,7 @@ export interface GeoJSONIndex { getLeaves(clusterId: number, limit: number, offset: number): Array; } -function loadGeoJSONTile(params: WorkerTileParameters, callback: LoadVectorDataCallback) { +function loadGeoJSONTile(params: RequestedTileParameters, callback: LoadVectorDataCallback) { const canonical = params.tileID.canonical; if (!this._geoJSONIndex) { diff --git a/src/source/query_features.js b/src/source/query_features.js index c7017b577be..96eebf10098 100644 --- a/src/source/query_features.js +++ b/src/source/query_features.js @@ -6,6 +6,7 @@ import type CollisionIndex from '../symbol/collision_index'; import type Transform from '../geo/transform'; import type {RetainedQueryData} from '../symbol/placement'; import type {FilterSpecification} from '../style-spec/types'; +import type {QueryGeometry} from '../style/query_geometry'; import assert from 'assert'; import {mat4} from 'gl-matrix'; @@ -14,57 +15,34 @@ import {mat4} from 'gl-matrix'; */ function getPixelPosMatrix(transform, tileID) { const t = mat4.identity([]); - mat4.translate(t, t, [1, 1, 0]); - mat4.scale(t, t, [transform.width * 0.5, transform.height * 0.5, 1]); + mat4.scale(t, t, [transform.width * 0.5, -transform.height * 0.5, 1]); + mat4.translate(t, t, [1, -1, 0]); return mat4.multiply(t, t, transform.calculatePosMatrix(tileID.toUnwrapped())); } -function queryIncludes3DLayer(layers?: Array, styleLayers: {[_: string]: StyleLayer}, sourceID: string) { - if (layers) { - for (const layerID of layers) { - const layer = styleLayers[layerID]; - if (layer && layer.source === sourceID && layer.type === 'fill-extrusion') { - return true; - } - } - } else { - for (const key in styleLayers) { - const layer = styleLayers[key]; - if (layer.source === sourceID && layer.type === 'fill-extrusion') { - return true; - } - } - } - return false; -} - export function queryRenderedFeatures(sourceCache: SourceCache, styleLayers: {[_: string]: StyleLayer}, serializedLayers: {[_: string]: Object}, - queryGeometry: Array, + queryGeometry: QueryGeometry, params: { filter: FilterSpecification, layers: Array, availableImages: Array }, - transform: Transform) { - - const has3DLayer = queryIncludes3DLayer(params && params.layers, styleLayers, sourceCache.id); - const maxPitchScaleFactor = transform.maxPitchScaleFactor(); - const tilesIn = sourceCache.tilesIn(queryGeometry, maxPitchScaleFactor, has3DLayer); - - tilesIn.sort(sortTilesIn); + transform: Transform, + use3DQuery: boolean, + visualizeQueryGeometry: boolean = false) { + const tileResults = sourceCache.tilesIn(queryGeometry, use3DQuery, visualizeQueryGeometry); + tileResults.sort(sortTilesIn); const renderedFeatureLayers = []; - for (const tileIn of tilesIn) { + for (const tileResult of tileResults) { renderedFeatureLayers.push({ - wrappedTileID: tileIn.tileID.wrapped().key, - queryResults: tileIn.tile.queryRenderedFeatures( + wrappedTileID: tileResult.tile.tileID.wrapped().key, + queryResults: tileResult.tile.queryRenderedFeatures( styleLayers, serializedLayers, sourceCache._state, - tileIn.queryGeometry, - tileIn.cameraQueryGeometry, - tileIn.scale, + tileResult, params, transform, - maxPitchScaleFactor, - getPixelPosMatrix(sourceCache.transform, tileIn.tileID)) + getPixelPosMatrix(sourceCache.transform, tileResult.tile.tileID), + visualizeQueryGeometry) }); } @@ -87,7 +65,7 @@ export function queryRenderedFeatures(sourceCache: SourceCache, export function queryRenderedSymbols(styleLayers: {[_: string]: StyleLayer}, serializedLayers: {[_: string]: StyleLayer}, - sourceCaches: {[_: string]: SourceCache}, + getLayerSourceCache: (layer: StyleLayer) => SourceCache, queryGeometry: Array, params: { filter: FilterSpecification, layers: Array, availableImages: Array }, collisionIndex: CollisionIndex, @@ -145,7 +123,7 @@ export function queryRenderedSymbols(styleLayers: {[_: string]: StyleLayer}, result[layerName].forEach((featureWrapper) => { const feature = featureWrapper.feature; const layer = styleLayers[layerName]; - const sourceCache = sourceCaches[layer.source]; + const sourceCache = getLayerSourceCache(layer); const state = sourceCache.getFeatureState(feature.layer['source-layer'], feature.id); feature.source = feature.layer.source; if (feature.layer['source-layer']) { diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index bd36edfdda7..2420f623295 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -1,7 +1,7 @@ // @flow import {getImage, ResourceType} from '../util/ajax'; -import {extend} from '../util/util'; +import {extend, prevPowerOfTwo} from '../util/util'; import {Evented} from '../util/evented'; import browser from '../util/browser'; import window from '../util/window'; @@ -40,10 +40,9 @@ class RasterDEMTileSource extends RasterTileSource implements Source { } loadTile(tile: Tile, callback: Callback) { - const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme), this.tileSize); + const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme), false, this.tileSize); tile.request = getImage(this.map._requestManager.transformRequest(url, ResourceType.Tile), imageLoaded.bind(this)); - tile.neighboringTiles = this._getNeighboringTiles(tile.tileID); function imageLoaded(err, img) { delete tile.request; if (tile.aborted) { @@ -57,13 +56,23 @@ class RasterDEMTileSource extends RasterTileSource implements Source { delete (img: any).cacheControl; delete (img: any).expires; const transfer = window.ImageBitmap && img instanceof window.ImageBitmap && offscreenCanvasSupported(); - const rawImageData = transfer ? img : browser.getImageData(img, 1); + // DEMData uses 1px padding. Handle cases with image buffer of 1 and 2 pxs, the rest assume default buffer 0 + // in order to keep the previous implementation working (no validation against tileSize). + const buffer = (img.width - prevPowerOfTwo(img.width)) / 2; + // padding is used in getImageData. As DEMData has 1px padding, if DEM tile buffer is 2px, discard outermost pixels. + const padding = 1 - buffer; + const borderReady = padding < 1; + if (!borderReady && !tile.neighboringTiles) { + tile.neighboringTiles = this._getNeighboringTiles(tile.tileID); + } + const rawImageData = transfer ? img : browser.getImageData(img, padding); const params = { uid: tile.uid, coord: tile.tileID, source: this.id, rawImageData, - encoding: this.encoding + encoding: this.encoding, + padding }; if (!tile.actor || tile.state === 'expired') { @@ -81,7 +90,9 @@ class RasterDEMTileSource extends RasterTileSource implements Source { if (dem) { tile.dem = dem; + tile.dem.onDeserialize(); tile.needsHillshadePrepare = true; + tile.needsDEMTextureUpload = true; tile.state = 'loaded'; callback(null); } @@ -128,9 +139,6 @@ class RasterDEMTileSource extends RasterTileSource implements Source { delete tile.neighboringTiles; tile.state = 'unloaded'; - if (tile.actor) { - tile.actor.send('removeDEMTile', {uid: tile.uid, source: this.id}); - } } } diff --git a/src/source/raster_dem_tile_worker_source.js b/src/source/raster_dem_tile_worker_source.js index 416462ea169..22257a1adda 100644 --- a/src/source/raster_dem_tile_worker_source.js +++ b/src/source/raster_dem_tile_worker_source.js @@ -5,34 +5,23 @@ import {RGBAImage} from '../util/image'; import window from '../util/window'; import type Actor from '../util/actor'; -import type { - WorkerDEMTileParameters, - WorkerDEMTileCallback, - TileParameters -} from './worker_source'; +import type {WorkerDEMTileParameters, WorkerDEMTileCallback} from './worker_source'; const {ImageBitmap} = window; class RasterDEMTileWorkerSource { actor: Actor; - loaded: {[_: string]: DEMData}; offscreenCanvas: OffscreenCanvas; offscreenCanvasContext: CanvasRenderingContext2D; - constructor() { - this.loaded = {}; - } - loadTile(params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { - const {uid, encoding, rawImageData} = params; + const {uid, encoding, rawImageData, padding, buildQuadTree} = params; // Main thread will transfer ImageBitmap if offscreen decode with OffscreenCanvas is supported, else it will transfer an already decoded image. - const imagePixels = (ImageBitmap && rawImageData instanceof ImageBitmap) ? this.getImageData(rawImageData) : rawImageData; - const dem = new DEMData(uid, imagePixels, encoding); - this.loaded = this.loaded || {}; - this.loaded[uid] = dem; + const imagePixels = (ImageBitmap && rawImageData instanceof ImageBitmap) ? this.getImageData(rawImageData, padding) : rawImageData; + const dem = new DEMData(uid, imagePixels, encoding, padding < 1, buildQuadTree); callback(null, dem); } - getImageData(imgBitmap: ImageBitmap): RGBAImage { + getImageData(imgBitmap: ImageBitmap, padding: number): RGBAImage { // Lazily initialize OffscreenCanvas if (!this.offscreenCanvas || !this.offscreenCanvasContext) { // Dem tiles are typically 256x256 @@ -44,19 +33,11 @@ class RasterDEMTileWorkerSource { this.offscreenCanvas.height = imgBitmap.height; this.offscreenCanvasContext.drawImage(imgBitmap, 0, 0, imgBitmap.width, imgBitmap.height); - // Insert an additional 1px padding around the image to allow backfilling for neighboring data. - const imgData = this.offscreenCanvasContext.getImageData(-1, -1, imgBitmap.width + 2, imgBitmap.height + 2); + // Insert or remove defined padding around the image to allow backfilling for neighboring data. + const imgData = this.offscreenCanvasContext.getImageData(-padding, -padding, imgBitmap.width + 2 * padding, imgBitmap.height + 2 * padding); this.offscreenCanvasContext.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height); return new RGBAImage({width: imgData.width, height: imgData.height}, imgData.data); } - - removeTile(params: TileParameters) { - const loaded = this.loaded, - uid = params.uid; - if (loaded && loaded[uid]) { - delete loaded[uid]; - } - } } export default RasterDEMTileWorkerSource; diff --git a/src/source/raster_tile_source.js b/src/source/raster_tile_source.js index 4aa5b71045c..e579d1cf006 100644 --- a/src/source/raster_tile_source.js +++ b/src/source/raster_tile_source.js @@ -5,9 +5,10 @@ import {extend, pick} from '../util/util'; import {getImage, ResourceType} from '../util/ajax'; import {Event, ErrorEvent, Evented} from '../util/evented'; import loadTileJSON from './load_tilejson'; -import {postTurnstileEvent, postMapLoadEvent} from '../util/mapbox'; +import {postTurnstileEvent} from '../util/mapbox'; import TileBounds from './tile_bounds'; import Texture from '../render/texture'; +import browser from '../util/browser'; import {cacheEntryPossiblyAdded} from '../util/tile_request_cache'; @@ -74,7 +75,6 @@ class RasterTileSource extends Evented implements Source { if (tileJSON.bounds) this.tileBounds = new TileBounds(tileJSON.bounds, this.minzoom, this.maxzoom); postTurnstileEvent(tileJSON.tiles); - postMapLoadEvent(tileJSON.tiles, this.map._getMapId(), this.map._requestManager._skuToken); // `content` is included here to prevent a race condition where `Style#_updateSources` is called // before the TileJSON arrives. this makes sure the tiles needed are loaded once TileJSON arrives @@ -110,7 +110,8 @@ class RasterTileSource extends Evented implements Source { } loadTile(tile: Tile, callback: Callback) { - const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme), this.tileSize); + const use2x = browser.devicePixelRatio >= 2; + const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme), use2x, this.tileSize); tile.request = getImage(this.map._requestManager.transformRequest(url, ResourceType.Tile), (err, img) => { delete tile.request; diff --git a/src/source/source.js b/src/source/source.js index 92f2279ee42..c53d5a7dcfc 100644 --- a/src/source/source.js +++ b/src/source/source.js @@ -53,11 +53,12 @@ export interface Source { loaded(): boolean; fire(event: Event): mixed; + on(type: *, listener: (Object) => any): Evented; +onAdd?: (map: Map) => void; +onRemove?: (map: Map) => void; - loadTile(tile: Tile, callback: Callback): void; + loadTile(tile: Tile, callback: Callback, tileWorkers?: {[string]: Actor}): void; +hasTile?: (tileID: OverscaledTileID) => boolean; +abortTile?: (tile: Tile, callback: Callback) => void; +unloadTile?: (tile: Tile, callback: Callback) => void; @@ -71,6 +72,8 @@ export interface Source { serialize(): Object; +prepare?: () => void; + + +afterUpdate?: () => void; } type SourceStatics = { diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 525df8ad006..486a5013a27 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -1,13 +1,9 @@ // @flow -import {create as createSource} from './source'; - import Tile from './tile'; import {Event, ErrorEvent, Evented} from '../util/evented'; import TileCache from './tile_cache'; -import MercatorCoordinate from '../geo/mercator_coordinate'; import {keysDifference, values} from '../util/util'; -import EXTENT from '../data/extent'; import Context from '../gl/context'; import Point from '@mapbox/point-geometry'; import browser from '../util/browser'; @@ -18,11 +14,10 @@ import SourceFeatureState from './source_state'; import type {Source} from './source'; import type Map from '../ui/map'; import type Style from '../style/style'; -import type Dispatcher from '../util/dispatcher'; import type Transform from '../geo/transform'; import type {TileState} from './tile'; import type {Callback} from '../types/callback'; -import type {SourceSpecification} from '../style-spec/types'; +import type {QueryGeometry, TilespaceQueryGeometry} from '../style/query_geometry'; /** * `SourceCache` is responsible for @@ -37,14 +32,13 @@ import type {SourceSpecification} from '../style-spec/types'; */ class SourceCache extends Evented { id: string; - dispatcher: Dispatcher; map: Map; style: Style; _source: Source; _sourceLoaded: boolean; _sourceErrored: boolean; - _tiles: {[_: string]: Tile}; + _tiles: {[_: string | number]: Tile}; _prevLng: number | void; _cache: TileCache; _timers: {[_: any]: TimeoutID}; @@ -52,22 +46,24 @@ class SourceCache extends Evented { _maxTileCacheSize: ?number; _paused: boolean; _shouldReloadOnResume: boolean; - _coveredTiles: {[_: string]: boolean}; + _coveredTiles: {[_: number | string]: boolean}; transform: Transform; - _isIdRenderable: (id: string, symbolLayer?: boolean) => boolean; + _isIdRenderable: (id: number, symbolLayer?: boolean) => boolean; used: boolean; + usedForTerrain: boolean; _state: SourceFeatureState; - _loadedParentTiles: {[_: string]: ?Tile}; + _loadedParentTiles: {[_: number | string]: ?Tile}; + _onlySymbols: ?boolean; static maxUnderzooming: number; static maxOverzooming: number; - constructor(id: string, options: SourceSpecification, dispatcher: Dispatcher) { + constructor(id: string, source: Source, onlySymbols?: boolean) { super(); this.id = id; - this.dispatcher = dispatcher; + this._onlySymbols = onlySymbols; - this.on('data', (e) => { + source.on('data', (e) => { // this._sourceLoaded signifies that the TileJSON is loaded if applicable. // if the source type does not come with a TileJSON, the flag signifies the // source data has loaded (i.e geojson has been tiled on the worker and is ready) @@ -83,12 +79,11 @@ class SourceCache extends Evented { } }); - this.on('error', () => { + source.on('error', () => { this._sourceErrored = true; }); - this._source = createSource(id, options, dispatcher, this); - + this._source = source; this._tiles = {}; this._cache = new TileCache(0, this._unloadTile.bind(this)); this._timers = {}; @@ -103,15 +98,6 @@ class SourceCache extends Evented { onAdd(map: Map) { this.map = map; this._maxTileCacheSize = map ? map._maxTileCacheSize : null; - if (this._source && this._source.onAdd) { - this._source.onAdd(map); - } - } - - onRemove(map: Map) { - if (this._source && this._source.onRemove) { - this._source.onRemove(map); - } } /** @@ -149,6 +135,7 @@ class SourceCache extends Evented { } _loadTile(tile: Tile, callback: Callback) { + tile.isSymbolTile = this._onlySymbols; return this._source.loadTile(tile, callback); } @@ -183,14 +170,14 @@ class SourceCache extends Evented { * Return all tile ids ordered with z-order, and cast to numbers * @private */ - getIds(): Array { - return (values(this._tiles): any).map((tile: Tile) => tile.tileID).sort(compareTileId).map(id => id.key); + getIds(): Array { + return values((this._tiles: any)).map((tile: Tile) => tile.tileID).sort(compareTileId).map(id => id.key); } - getRenderableIds(symbolLayer?: boolean): Array { + getRenderableIds(symbolLayer?: boolean): Array { const renderables: Array = []; for (const id in this._tiles) { - if (this._isIdRenderable(id, symbolLayer)) renderables.push(this._tiles[id]); + if (this._isIdRenderable(+id, symbolLayer)) renderables.push(this._tiles[id]); } if (symbolLayer) { return renderables.sort((a_: Tile, b_: Tile) => { @@ -212,7 +199,7 @@ class SourceCache extends Evented { return false; } - _isIdRenderable(id: string, symbolLayer?: boolean) { + _isIdRenderable(id: number, symbolLayer?: boolean) { return this._tiles[id] && this._tiles[id].hasData() && !this._coveredTiles[id] && (symbolLayer || !this._tiles[id].holdingForFade()); } @@ -226,11 +213,11 @@ class SourceCache extends Evented { this._cache.reset(); for (const i in this._tiles) { - if (this._tiles[i].state !== "errored") this._reloadTile(i, 'reloading'); + if (this._tiles[i].state !== "errored") this._reloadTile(+i, 'reloading'); } } - _reloadTile(id: string, state: TileState) { + _reloadTile(id: number, state: TileState) { const tile = this._tiles[id]; // this potentially does not address all underlying @@ -249,7 +236,7 @@ class SourceCache extends Evented { this._loadTile(tile, this._tileLoaded.bind(this, tile, id, state)); } - _tileLoaded(tile: Tile, id: string, previousState: TileState, err: ?Error) { + _tileLoaded(tile: Tile, id: number, previousState: TileState, err: ?Error) { if (err) { tile.state = 'errored'; if ((err: any).status !== 404) this._source.fire(new ErrorEvent(err, {tile})); @@ -264,7 +251,7 @@ class SourceCache extends Evented { if (this.getSource().type === 'raster-dem' && tile.dem) this._backfillDEM(tile); this._state.initializeTileState(tile, this.map ? this.map.painter : null); - this._source.fire(new Event('data', {dataType: 'source', tile, coord: tile.tileID})); + this._source.fire(new Event('data', {dataType: 'source', tile, coord: tile.tileID, 'sourceCacheId': this.id})); } /** @@ -283,7 +270,9 @@ class SourceCache extends Evented { } function fillBorder(tile, borderTile) { + if (!tile.dem || tile.dem.borderReady) return; tile.needsHillshadePrepare = true; + tile.needsDEMTextureUpload = true; let dx = borderTile.tileID.canonical.x - tile.tileID.canonical.x; const dy = borderTile.tileID.canonical.y - tile.tileID.canonical.y; const dim = Math.pow(2, tile.tileID.canonical.z); @@ -319,7 +308,7 @@ class SourceCache extends Evented { * Get a specific tile by id * @private */ - getTileByID(id: string): Tile { + getTileByID(id: number): Tile { return this._tiles[id]; } @@ -398,7 +387,7 @@ class SourceCache extends Evented { return tile; } // TileCache ignores wrap in lookup. - const cachedTile = this._cache.getByKey(tileID.wrapped().key); + const cachedTile = this._cache.getByKey(this._source.reparseOverscaled ? tileID.wrapped().key : tileID.canonical.key); return cachedTile; } @@ -411,9 +400,10 @@ class SourceCache extends Evented { * the map is more important. * @private */ - updateCacheSize(transform: Transform) { - const widthInTiles = Math.ceil(transform.width / this._source.tileSize) + 1; - const heightInTiles = Math.ceil(transform.height / this._source.tileSize) + 1; + updateCacheSize(transform: Transform, tileSize?: number) { + tileSize = tileSize || this._source.tileSize; + const widthInTiles = Math.ceil(transform.width / tileSize) + 1; + const heightInTiles = Math.ceil(transform.height / tileSize) + 1; const approxTilesInView = widthInTiles * heightInTiles; const commonZoomRange = 5; @@ -446,7 +436,7 @@ class SourceCache extends Evented { this._prevLng = lng; if (wrapDelta) { - const tiles: {[_: string]: Tile} = {}; + const tiles: {[_: string | number]: Tile} = {}; for (const key in this._tiles) { const tile = this._tiles[key]; tile.tileID = tile.tileID.unwrapTo(tile.tileID.wrap + wrapDelta); @@ -461,7 +451,7 @@ class SourceCache extends Evented { } for (const id in this._tiles) { const tile = this._tiles[id]; - this._setTileReloadTimer(id, tile); + this._setTileReloadTimer(+id, tile); } } } @@ -470,12 +460,21 @@ class SourceCache extends Evented { * Removes tiles that are outside the viewport and adds new tiles that * are inside the viewport. * @private + * @param {boolean} updateForTerrain Signals to update tiles even if the + * source is not used (this.used) by layers: it is used for terrain. + * @param {tileSize} tileSize If needed to get lower resolution ideal cover, + * override source.tileSize used in tile cover calculation. */ - update(transform: Transform) { + update(transform: Transform, tileSize?: number, updateForTerrain?: boolean) { this.transform = transform; - if (!this._sourceLoaded || this._paused) { return; } + if (!this._sourceLoaded || this._paused || this.transform.freezeTileCoverage) { return; } + assert(!(updateForTerrain && !this.usedForTerrain)); + if (this.usedForTerrain && !updateForTerrain) { + // If source is used for both terrain and hillshade, don't update it twice. + return; + } - this.updateCacheSize(transform); + this.updateCacheSize(transform, tileSize); this.handleWrapJump(this.transform.center.lng); // Covered is a list of retained tiles who's areas are fully covered by other, @@ -483,18 +482,19 @@ class SourceCache extends Evented { this._coveredTiles = {}; let idealTileIDs; - if (!this.used) { + if (!this.used && !this.usedForTerrain) { idealTileIDs = []; } else if (this._source.tileID) { idealTileIDs = transform.getVisibleUnwrappedCoordinates(this._source.tileID) .map((unwrapped) => new OverscaledTileID(unwrapped.canonical.z, unwrapped.wrap, unwrapped.canonical.z, unwrapped.canonical.x, unwrapped.canonical.y)); } else { idealTileIDs = transform.coveringTiles({ - tileSize: this._source.tileSize, + tileSize: tileSize || this._source.tileSize, minzoom: this._source.minzoom, maxzoom: this._source.maxzoom, - roundZoom: this._source.roundZoom, - reparseOverscaled: this._source.reparseOverscaled + roundZoom: this._source.roundZoom && !updateForTerrain, + reparseOverscaled: this._source.reparseOverscaled, + useElevationData: !!this.transform.elevation && !this.usedForTerrain }); if (this._source.hasTile) { @@ -502,29 +502,24 @@ class SourceCache extends Evented { } } - // Determine the overzooming/underzooming amounts. - const zoom = transform.coveringZoomLevel(this._source); - const minCoveringZoom = Math.max(zoom - SourceCache.maxOverzooming, this._source.minzoom); - const maxCoveringZoom = Math.max(zoom + SourceCache.maxUnderzooming, this._source.minzoom); - // Retain is a list of tiles that we shouldn't delete, even if they are not // the most ideal tile for the current viewport. This may include tiles like // parent or child tiles that are *already* loaded. - const retain = this._updateRetainedTiles(idealTileIDs, zoom); + const retain = this._updateRetainedTiles(idealTileIDs); - if (isRasterType(this._source.type)) { - const parentsForFading: {[_: string]: OverscaledTileID} = {}; + if (isRasterType(this._source.type) && idealTileIDs.length !== 0) { + const parentsForFading: {[_: string | number]: OverscaledTileID} = {}; const fadingTiles = {}; const ids = Object.keys(retain); for (const id of ids) { const tileID = retain[id]; - assert(tileID.key === id); + assert(tileID.key === +id); const tile = this._tiles[id]; if (!tile || tile.fadeEndTime && tile.fadeEndTime <= browser.now()) continue; // if the tile is loaded but still fading in, find parents to cross-fade with it - const parentTile = this.findLoadedParent(tileID, minCoveringZoom); + const parentTile = this.findLoadedParent(tileID, Math.max(tileID.overscaledZ - SourceCache.maxOverzooming, this._source.minzoom)); if (parentTile) { this._addTile(parentTile.tileID); parentsForFading[parentTile.tileID.key] = parentTile.tileID; @@ -533,8 +528,25 @@ class SourceCache extends Evented { fadingTiles[id] = tileID; } - // for tiles that are still fading in, also find children to cross-fade with - this._retainLoadedChildren(fadingTiles, zoom, maxCoveringZoom, retain); + // for children tiles with parent tiles still fading in, + // retain the children so the parent can fade on top + const minZoom = idealTileIDs[idealTileIDs.length - 1].overscaledZ; + for (const id in this._tiles) { + const childTile = this._tiles[id]; + if (retain[id] || !childTile.hasData()) { + continue; + } + + let parentID = childTile.tileID; + while (parentID.overscaledZ > minZoom) { + parentID = parentID.scaledTo(parentID.overscaledZ - 1); + const tile = this._tiles[parentID.key]; + if (tile && tile.hasData() && fadingTiles[parentID.key]) { + retain[id] = childTile.tileID; + break; + } + } + } for (const id in parentsForFading) { if (!retain[id]) { @@ -552,33 +564,42 @@ class SourceCache extends Evented { } // Remove the tiles we don't need anymore. - const remove = keysDifference(this._tiles, retain); + const remove = keysDifference((this._tiles: any), (retain: any)); for (const tileID of remove) { const tile = this._tiles[tileID]; if (tile.hasSymbolBuckets && !tile.holdingForFade()) { tile.setHoldDuration(this.map._fadeDuration); } else if (!tile.hasSymbolBuckets || tile.symbolFadeFinished()) { - this._removeTile(tileID); + this._removeTile(+tileID); } } // Construct a cache of loaded parents this._updateLoadedParentTileCache(); + + if (this._onlySymbols && this._source.afterUpdate) { + this._source.afterUpdate(); + } } releaseSymbolFadeTiles() { for (const id in this._tiles) { if (this._tiles[id].holdingForFade()) { - this._removeTile(id); + this._removeTile(+id); } } } - _updateRetainedTiles(idealTileIDs: Array, zoom: number): {[_: string]: OverscaledTileID} { - const retain: {[_: string]: OverscaledTileID} = {}; - const checked: {[_: string]: boolean } = {}; - const minCoveringZoom = Math.max(zoom - SourceCache.maxOverzooming, this._source.minzoom); - const maxCoveringZoom = Math.max(zoom + SourceCache.maxUnderzooming, this._source.minzoom); + _updateRetainedTiles(idealTileIDs: Array): {[_: number | string]: OverscaledTileID} { + const retain: {[_: number | string]: OverscaledTileID} = {}; + if (idealTileIDs.length === 0) { return retain; } + + const checked: {[_: number | string]: boolean } = {}; + const minZoom = idealTileIDs[idealTileIDs.length - 1].overscaledZ; + const maxZoom = idealTileIDs[0].overscaledZ; + assert(minZoom <= maxZoom); + const minCoveringZoom = Math.max(maxZoom - SourceCache.maxOverzooming, this._source.minzoom); + const maxCoveringZoom = Math.max(maxZoom + SourceCache.maxUnderzooming, this._source.minzoom); const missingTiles = {}; for (const tileID of idealTileIDs) { @@ -589,14 +610,14 @@ class SourceCache extends Evented { if (tile.hasData()) continue; - if (zoom < this._source.maxzoom) { + if (minZoom < this._source.maxzoom) { // save missing tiles that potentially have loaded children missingTiles[tileID.key] = tileID; } } // retain any loaded children of ideal tiles up to maxCoveringZoom - this._retainLoadedChildren(missingTiles, zoom, maxCoveringZoom, retain); + this._retainLoadedChildren(missingTiles, minZoom, maxCoveringZoom, retain); for (const tileID of idealTileIDs) { let tile = this._tiles[tileID.key]; @@ -606,7 +627,7 @@ class SourceCache extends Evented { // The tile we require is not yet loaded or does not exist; // Attempt to find children that fully cover it. - if (zoom + 1 > this._source.maxzoom) { + if (tileID.canonical.z >= this._source.maxzoom) { // We're looking for an overzoomed child tile. const childCoord = tileID.children(this._source.maxzoom)[0]; const childTile = this.getTile(childCoord); @@ -716,7 +737,7 @@ class SourceCache extends Evented { const cached = Boolean(tile); if (!cached) { - tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor()); + tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor(), this.transform.tileZoom); this._loadTile(tile, this._tileLoaded.bind(this, tile, tileID.key, tile.state)); } @@ -730,7 +751,7 @@ class SourceCache extends Evented { return tile; } - _setTileReloadTimer(id: string, tile: Tile) { + _setTileReloadTimer(id: number, tile: Tile) { if (id in this._timers) { clearTimeout(this._timers[id]); delete this._timers[id]; @@ -749,7 +770,7 @@ class SourceCache extends Evented { * Remove a tile, given its id, from the pyramid * @private */ - _removeTile(id: string) { + _removeTile(id: number) { const tile = this._tiles[id]; if (!tile) return; @@ -781,77 +802,41 @@ class SourceCache extends Evented { this._paused = false; for (const id in this._tiles) - this._removeTile(id); + this._removeTile(+id); this._cache.reset(); } /** - * Search through our current tiles and attempt to find the tiles that - * cover the given bounds. - * @param pointQueryGeometry coordinates of the corners of bounding rectangle - * @returns {Array} result items have {tile, minX, maxX, minY, maxY}, where min/max bounding values are the given bounds transformed in into the coordinate space of this tile. + * Search through our current tiles and attempt to find the tiles that cover the given `queryGeometry`. + * + * @param {QueryGeometry} queryGeometry + * @param {boolean} [visualizeQueryGeometry=false] + * @param {boolean} use3DQuery + * @returns * @private */ - tilesIn(pointQueryGeometry: Array, maxPitchScaleFactor: number, has3DLayer: boolean) { - + tilesIn(queryGeometry: QueryGeometry, use3DQuery: boolean, visualizeQueryGeometry: boolean): TilespaceQueryGeometry[] { const tileResults = []; const transform = this.transform; if (!transform) return tileResults; - const cameraPointQueryGeometry = has3DLayer ? - transform.getCameraQueryGeometry(pointQueryGeometry) : - pointQueryGeometry; - - const queryGeometry = pointQueryGeometry.map((p) => transform.pointCoordinate(p)); - const cameraQueryGeometry = cameraPointQueryGeometry.map((p) => transform.pointCoordinate(p)); - - const ids = this.getIds(); - - let minX = Infinity; - let minY = Infinity; - let maxX = -Infinity; - let maxY = -Infinity; - - for (const p of cameraQueryGeometry) { - minX = Math.min(minX, p.x); - minY = Math.min(minY, p.y); - maxX = Math.max(maxX, p.x); - maxY = Math.max(maxY, p.y); - } - - for (let i = 0; i < ids.length; i++) { - const tile = this._tiles[ids[i]]; + for (const tileID in this._tiles) { + const tile = this._tiles[tileID]; + if (visualizeQueryGeometry) { + tile.clearQueryDebugViz(); + } if (tile.holdingForFade()) { // Tiles held for fading are covered by tiles that are closer to ideal continue; } - const tileID = tile.tileID; - const scale = Math.pow(2, transform.zoom - tile.tileID.overscaledZ); - const queryPadding = maxPitchScaleFactor * tile.queryPadding * EXTENT / tile.tileSize / scale; - - const tileSpaceBounds = [ - tileID.getTilePoint(new MercatorCoordinate(minX, minY)), - tileID.getTilePoint(new MercatorCoordinate(maxX, maxY)) - ]; - - if (tileSpaceBounds[0].x - queryPadding < EXTENT && tileSpaceBounds[0].y - queryPadding < EXTENT && - tileSpaceBounds[1].x + queryPadding >= 0 && tileSpaceBounds[1].y + queryPadding >= 0) { - - const tileSpaceQueryGeometry: Array = queryGeometry.map((c) => tileID.getTilePoint(c)); - const tileSpaceCameraQueryGeometry = cameraQueryGeometry.map((c) => tileID.getTilePoint(c)); - - tileResults.push({ - tile, - tileID, - queryGeometry: tileSpaceQueryGeometry, - cameraQueryGeometry: tileSpaceCameraQueryGeometry, - scale - }); + + const tileResult = queryGeometry.containsTile(tile, transform, use3DQuery); + if (tileResult) { + tileResults.push(tileResult); } } - return tileResults; } @@ -912,7 +897,7 @@ class SourceCache extends Evented { * be reloaded when their dependencies change. * @private */ - setDependencies(tileKey: string, namespace: string, dependencies: Array) { + setDependencies(tileKey: number, namespace: string, dependencies: Array) { const tile = this._tiles[tileKey]; if (tile) { tile.setDependencies(namespace, dependencies); @@ -927,7 +912,7 @@ class SourceCache extends Evented { for (const id in this._tiles) { const tile = this._tiles[id]; if (tile.hasDependency(namespaces, keys)) { - this._reloadTile(id, 'reloading'); + this._reloadTile(+id, 'reloading'); } } this._cache.filter(tile => !tile.hasDependency(namespaces, keys)); diff --git a/src/source/tile.js b/src/source/tile.js index 419059b7ac2..62f768c3f00 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -9,10 +9,13 @@ import SymbolBucket from '../data/bucket/symbol_bucket'; import {CollisionBoxArray} from '../data/array_types'; import Texture from '../render/texture'; import browser from '../util/browser'; +import {Debug} from '../util/debug'; import toEvaluationFeature from '../data/evaluation_feature'; import EvaluationParameters from '../style/evaluation_parameters'; import SourceFeatureState from '../source/source_state'; import {lazyLoadRTLTextPlugin} from './rtl_text_plugin'; +import {TileSpaceDebugBuffer} from '../data/debug_viz'; +import Color from '../style-spec/util/color'; const CLOCK_SKEW_RETRY_TIMEOUT = 30000; @@ -31,6 +34,7 @@ import type Transform from '../geo/transform'; import type {LayerFeatureStates} from './source_state'; import type {Cancelable} from '../types/cancelable'; import type {FilterSpecification} from '../style-spec/types'; +import type {TilespaceQueryGeometry} from '../style/query_geometry'; export type TileState = | 'loading' // Tile data is in the process of loading. @@ -52,6 +56,7 @@ class Tile { uid: number; uses: number; tileSize: number; + tileZoom: number; buckets: {[_: string]: Bucket}; latestFeatureIndex: ?FeatureIndex; latestRawTileData: ?ArrayBuffer; @@ -70,11 +75,13 @@ class Tile { placementSource: any; actor: ?Actor; vtLayers: {[_: string]: VectorTileLayer}; + isSymbolTile: ?boolean; neighboringTiles: ?Object; dem: ?DEMData; aborted: ?boolean; needsHillshadePrepare: ?boolean; + needsDEMTextureUpload: ?boolean; request: ?Cancelable; texture: any; fbo: ?Framebuffer; @@ -89,16 +96,19 @@ class Tile { hasRTLText: boolean; dependencies: Object; + queryGeometryDebugViz: TileSpaceDebugBuffer; + queryBoundsDebugViz: TileSpaceDebugBuffer; /** * @param {OverscaledTileID} tileID * @param size * @private */ - constructor(tileID: OverscaledTileID, size: number) { + constructor(tileID: OverscaledTileID, size: number, tileZoom: number) { this.tileID = tileID; this.uid = uniqueId(); this.uses = 0; this.tileSize = size; + this.tileZoom = tileZoom; this.buckets = {}; this.expirationTime = null; this.queryPadding = 0; @@ -137,7 +147,7 @@ class Tile { * @returns {undefined} * @private */ - loadVectorData(data: WorkerTileResult, painter: any, justReloaded: ?boolean) { + loadVectorData(data: ?WorkerTileResult, painter: any, justReloaded: ?boolean) { if (this.hasData()) { this.unloadVectorData(); } @@ -229,7 +239,16 @@ class Tile { if (this.glyphAtlasTexture) { this.glyphAtlasTexture.destroy(); } - + Debug.run(() => { + if (this.queryGeometryDebugViz) { + this.queryGeometryDebugViz.unload(); + delete this.queryGeometryDebugViz; + } + if (this.queryBoundsDebugViz) { + this.queryBoundsDebugViz.unload(); + delete this.queryBoundsDebugViz; + } + }); this.latestFeatureIndex = null; this.state = 'unloaded'; } @@ -269,25 +288,33 @@ class Tile { queryRenderedFeatures(layers: {[_: string]: StyleLayer}, serializedLayers: {[string]: Object}, sourceFeatureState: SourceFeatureState, - queryGeometry: Array, - cameraQueryGeometry: Array, - scale: number, + tileResult: TilespaceQueryGeometry, params: { filter: FilterSpecification, layers: Array, availableImages: Array }, transform: Transform, - maxPitchScaleFactor: number, - pixelPosMatrix: Float32Array): {[_: string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>} { + pixelPosMatrix: Float32Array, + visualizeQueryGeometry: boolean): {[_: string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>} { + Debug.run(() => { + if (visualizeQueryGeometry) { + if (!this.queryGeometryDebugViz) { + this.queryGeometryDebugViz = new TileSpaceDebugBuffer(this.tileSize); + } + if (!this.queryBoundsDebugViz) { + this.queryBoundsDebugViz = new TileSpaceDebugBuffer(this.tileSize, Color.blue); + } + + this.queryGeometryDebugViz.addPoints(tileResult.tilespaceGeometry); + this.queryBoundsDebugViz.addPoints(tileResult.bufferedTilespaceGeometry); + } + }); + if (!this.latestFeatureIndex || !this.latestFeatureIndex.rawTileData) return {}; return this.latestFeatureIndex.query({ - queryGeometry, - cameraQueryGeometry, - scale, - tileSize: this.tileSize, + tileResult, pixelPosMatrix, transform, - params, - queryPadding: this.queryPadding * maxPitchScaleFactor + params }, layers, serializedLayers, sourceFeatureState); } @@ -453,6 +480,17 @@ class Tile { } return false; } + + clearQueryDebugViz() { + Debug.run(() => { + if (this.queryGeometryDebugViz) { + this.queryGeometryDebugViz.clearPoints(); + } + if (this.queryBoundsDebugViz) { + this.queryBoundsDebugViz.clearPoints(); + } + }); + } } export default Tile; diff --git a/src/source/tile_cache.js b/src/source/tile_cache.js index 581d8548c63..a3302bc73c0 100644 --- a/src/source/tile_cache.js +++ b/src/source/tile_cache.js @@ -12,8 +12,8 @@ import type Tile from './tile'; */ class TileCache { max: number; - data: {[key: string]: Array<{ value: Tile, timeout: ?TimeoutID}>}; - order: Array; + data: {[key: string | number]: Array<{ value: Tile, timeout: ?TimeoutID}>}; + order: Array; onRemove: (element: Tile) => void; /** * @param {number} max number of permitted values @@ -110,7 +110,7 @@ class TileCache { /* * Get and remove the value with the specified key. */ - _getAndRemoveByKey(key: string): ?Tile { + _getAndRemoveByKey(key: number): ?Tile { const data = this.data[key].shift(); if (data.timeout) clearTimeout(data.timeout); @@ -125,7 +125,7 @@ class TileCache { /* * Get the value with the specified (wrapped tile) key. */ - getByKey(key: string): ?Tile { + getByKey(key: number): ?Tile { const data = this.data[key]; return data ? data[0].value : null; } diff --git a/src/source/tile_id.js b/src/source/tile_id.js index 244209cee0b..5a90507d9f3 100644 --- a/src/source/tile_id.js +++ b/src/source/tile_id.js @@ -3,16 +3,17 @@ import {getTileBBox} from '@mapbox/whoots-js'; import EXTENT from '../data/extent'; import Point from '@mapbox/point-geometry'; -import MercatorCoordinate from '../geo/mercator_coordinate'; - +import MercatorCoordinate, {altitudeFromMercatorZ} from '../geo/mercator_coordinate'; +import {MAX_SAFE_INTEGER} from '../util/util'; import assert from 'assert'; import {register} from '../util/web_worker_transfer'; +import {vec3} from 'gl-matrix'; export class CanonicalTileID { z: number; x: number; y: number; - key: string; + key: number; constructor(z: number, x: number, y: number) { assert(z >= 0 && z <= 25); @@ -49,6 +50,13 @@ export class CanonicalTileID { (coord.y * tilesAtZoom - this.y) * EXTENT); } + getTileVec3(coord: MercatorCoordinate): vec3 { + const tilesAtZoom = Math.pow(2, this.z); + const x = (coord.x * tilesAtZoom - this.x) * EXTENT; + const y = (coord.y * tilesAtZoom - this.y) * EXTENT; + return vec3.fromValues(x, y, altitudeFromMercatorZ(coord.z, coord.y)); + } + toString() { return `${this.z}/${this.x}/${this.y}`; } @@ -57,7 +65,7 @@ export class CanonicalTileID { export class UnwrappedTileID { wrap: number; canonical: CanonicalTileID; - key: string; + key: number; constructor(wrap: number, canonical: CanonicalTileID) { this.wrap = wrap; @@ -70,7 +78,7 @@ export class OverscaledTileID { overscaledZ: number; wrap: number; canonical: CanonicalTileID; - key: string; + key: number; posMatrix: Float32Array; constructor(overscaledZ: number, wrap: number, z: number, x: number, y: number) { @@ -78,7 +86,7 @@ export class OverscaledTileID { this.overscaledZ = overscaledZ; this.wrap = wrap; this.canonical = new CanonicalTileID(z, +x, +y); - this.key = calculateKey(wrap, overscaledZ, z, x, y); + this.key = wrap === 0 && overscaledZ === z ? this.canonical.key : calculateKey(wrap, overscaledZ, z, x, y); } equals(id: OverscaledTileID) { @@ -100,12 +108,12 @@ export class OverscaledTileID { * when withWrap == true, implements the same as this.scaledTo(z).key, * when withWrap == false, implements the same as this.scaledTo(z).wrapped().key. */ - calculateScaledKey(targetZ: number, withWrap: boolean): string { - assert(targetZ <= this.overscaledZ); - const zDifference = this.canonical.z - targetZ; + calculateScaledKey(targetZ: number, withWrap: boolean = true): number { + if (this.overscaledZ === targetZ && withWrap) return this.key; if (targetZ > this.canonical.z) { return calculateKey(this.wrap * +withWrap, targetZ, this.canonical.z, this.canonical.x, this.canonical.y); } else { + const zDifference = this.canonical.z - targetZ; return calculateKey(this.wrap * +withWrap, targetZ, targetZ, this.canonical.x >> zDifference, this.canonical.y >> zDifference); } } @@ -177,13 +185,28 @@ export class OverscaledTileID { getTilePoint(coord: MercatorCoordinate) { return this.canonical.getTilePoint(new MercatorCoordinate(coord.x - this.wrap, coord.y)); } + + getTileVec3(coord: MercatorCoordinate) { + return this.canonical.getTileVec3(new MercatorCoordinate(coord.x - this.wrap, coord.y, coord.z)); + } } -function calculateKey(wrap: number, overscaledZ: number, z: number, x: number, y: number): string { - wrap *= 2; - if (wrap < 0) wrap = wrap * -1 - 1; - const dim = 1 << z; - return (dim * dim * wrap + dim * y + x).toString(36) + z.toString(36) + overscaledZ.toString(36); +function calculateKey(wrap: number, overscaledZ: number, z: number, x: number, y: number): number { + // only use 22 bits for x & y so that the key fits into MAX_SAFE_INTEGER + const dim = 1 << Math.min(z, 22); + let xy = dim * (y % dim) + (x % dim); + + // zigzag-encode wrap if we have the room for it + if (wrap && z < 22) { + const bitsAvailable = 2 * (22 - z); + xy += dim * dim * ((wrap < 0 ? -2 * wrap - 1 : 2 * wrap) % (1 << bitsAvailable)); + } + + // encode z into 5 bits (24 max) and overscaledZ into 4 bits (10 max) + const key = ((xy * 32) + z) * 16 + (overscaledZ - z); + assert(key >= 0 && key <= MAX_SAFE_INTEGER); + + return key; } function getQuadkey(z, x, y) { diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index c0ba8eea6d1..93241984752 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -4,11 +4,12 @@ import {Event, ErrorEvent, Evented} from '../util/evented'; import {extend, pick} from '../util/util'; import loadTileJSON from './load_tilejson'; -import {postTurnstileEvent, postMapLoadEvent} from '../util/mapbox'; +import {postTurnstileEvent} from '../util/mapbox'; import TileBounds from './tile_bounds'; import {ResourceType} from '../util/ajax'; import browser from '../util/browser'; import {cacheEntryPossiblyAdded} from '../util/tile_request_cache'; +import {DedupedRequest, loadVectorTile} from './vector_tile_worker_source'; import type {Source} from './source'; import type {OverscaledTileID} from './tile_id'; @@ -18,6 +19,8 @@ import type Tile from './tile'; import type {Callback} from '../types/callback'; import type {Cancelable} from '../types/cancelable'; import type {VectorSourceSpecification, PromoteIdSpecification} from '../style-spec/types'; +import type Actor from '../util/actor'; +import type {LoadVectorTileResult} from './vector_tile_worker_source'; /** * A source containing vector tiles in [Mapbox Vector Tile format](https://docs.mapbox.com/vector-tiles/reference/). @@ -66,6 +69,8 @@ class VectorTileSource extends Evented implements Source { isTileClipped: boolean; _tileJSONRequest: ?Cancelable; _loaded: boolean; + _tileWorkers: {[string]: Actor}; + _deduped: DedupedRequest; constructor(id: string, options: VectorSourceSpecification & {collectResourceTiming: boolean}, dispatcher: Dispatcher, eventedParent: Evented) { super(); @@ -91,6 +96,9 @@ class VectorTileSource extends Evented implements Source { } this.setEventedParent(eventedParent); + + this._tileWorkers = {}; + this._deduped = new DedupedRequest(); } load() { @@ -105,7 +113,6 @@ class VectorTileSource extends Evented implements Source { extend(this, tileJSON); if (tileJSON.bounds) this.tileBounds = new TileBounds(tileJSON.bounds, this.minzoom, this.maxzoom); postTurnstileEvent(tileJSON.tiles, this.map._requestManager._customAccessToken); - postMapLoadEvent(tileJSON.tiles, this.map._getMapId(), this.map._requestManager._skuToken, this.map._requestManager._customAccessToken); // `content` is included here to prevent a race condition where `Style#_updateSources` is called // before the TileJSON arrives. this makes sure the tiles needed are loaded once TileJSON arrives @@ -136,8 +143,10 @@ class VectorTileSource extends Evented implements Source { callback(); - const sourceCache = this.map.style.sourceCaches[this.id]; - sourceCache.clearTiles(); + const sourceCaches = this.map.style._getSourceCaches(this.id); + for (const sourceCache of sourceCaches) { + sourceCache.clearTiles(); + } this.load(); } @@ -183,26 +192,54 @@ class VectorTileSource extends Evented implements Source { loadTile(tile: Tile, callback: Callback) { const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme)); + const request = this.map._requestManager.transformRequest(url, ResourceType.Tile); + const params = { - request: this.map._requestManager.transformRequest(url, ResourceType.Tile), + request, + data: undefined, uid: tile.uid, tileID: tile.tileID, + tileZoom: tile.tileZoom, zoom: tile.tileID.overscaledZ, tileSize: this.tileSize * tile.tileID.overscaleFactor(), type: this.type, source: this.id, pixelRatio: browser.devicePixelRatio, showCollisionBoxes: this.map.showCollisionBoxes, - promoteId: this.promoteId + promoteId: this.promoteId, + isSymbolTile: tile.isSymbolTile }; params.request.collectResourceTiming = this._collectResourceTiming; if (!tile.actor || tile.state === 'expired') { - tile.actor = this.dispatcher.getActor(); - tile.request = tile.actor.send('loadTile', params, done.bind(this)); + tile.actor = this._tileWorkers[url] = this._tileWorkers[url] || this.dispatcher.getActor(); + + // if workers are not ready to receive messages yet, use the idle time to preemptively + // load tiles on the main thread and pass the result instead of requesting a worker to do so + if (!this.dispatcher.ready) { + const cancel = loadVectorTile.call({deduped: this._deduped}, params, (err: ?Error, data: ?LoadVectorTileResult) => { + if (err || !data) { + done.call(this, err); + } else { + // the worker will skip the network request if the data is already there + params.data = { + cacheControl: data.cacheControl, + expires: data.expires, + rawData: data.rawData.slice(0) + }; + if (tile.actor) tile.actor.send('loadTile', params, done.bind(this)); + } + }, true); + tile.request = {cancel}; + + } else { + tile.request = tile.actor.send('loadTile', params, done.bind(this)); + } + } else if (tile.state === 'loading') { // schedule tile reloading after it has been loaded tile.reloadCallback = callback; + } else { tile.request = tile.actor.send('reloadTile', params, done.bind(this)); } @@ -254,6 +291,10 @@ class VectorTileSource extends Evented implements Source { hasTransition() { return false; } + + afterUpdate() { + this._tileWorkers = {}; + } } export default VectorTileSource; diff --git a/src/source/vector_tile_worker_source.js b/src/source/vector_tile_worker_source.js index 1effce4be14..e642f598931 100644 --- a/src/source/vector_tile_worker_source.js +++ b/src/source/vector_tile_worker_source.js @@ -11,6 +11,7 @@ import {RequestPerformance} from '../util/performance'; import type { WorkerSource, WorkerTileParameters, + RequestedTileParameters, WorkerTileCallback, TileParameters } from '../source/worker_source'; @@ -18,10 +19,11 @@ import type { import type Actor from '../util/actor'; import type StyleLayerIndex from '../style/style_layer_index'; import type {Callback} from '../types/callback'; +import type Scheduler from '../util/scheduler'; export type LoadVectorTileResult = { - vectorTile: VectorTile; rawData: ArrayBuffer; + vectorTile?: VectorTile; expires?: any; cacheControl?: any; resourceTiming?: Array; @@ -36,28 +38,92 @@ export type LoadVectorTileResult = { export type LoadVectorDataCallback = Callback; export type AbortVectorData = () => void; -export type LoadVectorData = (params: WorkerTileParameters, callback: LoadVectorDataCallback) => ?AbortVectorData; +export type LoadVectorData = (params: RequestedTileParameters, callback: LoadVectorDataCallback) => ?AbortVectorData; + +export class DedupedRequest { + entries: { [string]: Object }; + scheduler: ?Scheduler; + constructor(scheduler?: Scheduler) { + this.entries = {}; + this.scheduler = scheduler; + } + + request(key: string, metadata: Object, request: any, callback: LoadVectorDataCallback) { + const entry = this.entries[key] = this.entries[key] || {callbacks: []}; + + if (entry.result) { + const [err, result] = entry.result; + if (this.scheduler) { + this.scheduler.add(() => { + callback(err, result); + }, metadata); + } else { + callback(err, result); + } + return () => {}; + } + + entry.callbacks.push(callback); + + if (!entry.cancel) { + entry.cancel = request((err, result) => { + entry.result = [err, result]; + for (const cb of entry.callbacks) { + if (this.scheduler) { + this.scheduler.add(() => { + cb(err, result); + }, metadata); + } else { + cb(err, result); + } + } + setTimeout(() => delete this.entries[key], 1000 * 3); + }); + } + + return () => { + if (entry.result) return; + entry.callbacks = entry.callbacks.filter(cb => cb !== callback); + if (!entry.callbacks.length) { + entry.cancel(); + delete this.entries[key]; + } + }; + } +} /** * @private */ -function loadVectorTile(params: WorkerTileParameters, callback: LoadVectorDataCallback) { - const request = getArrayBuffer(params.request, (err: ?Error, data: ?ArrayBuffer, cacheControl: ?string, expires: ?string) => { - if (err) { - callback(err); - } else if (data) { - callback(null, { - vectorTile: new vt.VectorTile(new Protobuf(data)), - rawData: data, - cacheControl, - expires - }); - } - }); - return () => { - request.cancel(); - callback(); +export function loadVectorTile(params: RequestedTileParameters, callback: LoadVectorDataCallback, skipParse?: boolean) { + const key = JSON.stringify(params.request); + + const makeRequest = (callback) => { + const request = getArrayBuffer(params.request, (err: ?Error, data: ?ArrayBuffer, cacheControl: ?string, expires: ?string) => { + if (err) { + callback(err); + } else if (data) { + callback(null, { + vectorTile: skipParse ? undefined : new vt.VectorTile(new Protobuf(data)), + rawData: data, + cacheControl, + expires + }); + } + }); + return () => { + request.cancel(); + callback(); + }; }; + + if (params.data) { + // if we already got the result earlier (on the main thread), return it directly + this.deduped.entries[key] = {result: [null, params.data]}; + } + + const callbackMetadata = {type: 'parseTile', isSymbolTile: params.isSymbolTile, zoom: params.tileZoom}; + return this.deduped.request(key, callbackMetadata, makeRequest, callback); } /** @@ -74,8 +140,9 @@ class VectorTileWorkerSource implements WorkerSource { layerIndex: StyleLayerIndex; availableImages: Array; loadVectorData: LoadVectorData; - loading: {[_: string]: WorkerTile }; - loaded: {[_: string]: WorkerTile }; + loading: {[_: number]: WorkerTile }; + loaded: {[_: number]: WorkerTile }; + deduped: DedupedRequest; /** * @param [loadVectorData] Optional method for custom loading of a VectorTile @@ -91,6 +158,7 @@ class VectorTileWorkerSource implements WorkerSource { this.loadVectorData = loadVectorData || loadVectorTile; this.loading = {}; this.loaded = {}; + this.deduped = new DedupedRequest(actor.scheduler); } /** @@ -102,19 +170,19 @@ class VectorTileWorkerSource implements WorkerSource { loadTile(params: WorkerTileParameters, callback: WorkerTileCallback) { const uid = params.uid; - if (!this.loading) - this.loading = {}; - const perf = (params && params.request && params.request.collectResourceTiming) ? new RequestPerformance(params.request) : false; const workerTile = this.loading[uid] = new WorkerTile(params); workerTile.abort = this.loadVectorData(params, (err, response) => { + + const aborted = !this.loading[uid]; + delete this.loading[uid]; - if (err || !response) { + if (aborted || err || !response) { workerTile.status = 'done'; - this.loaded[uid] = workerTile; + if (!aborted) this.loaded[uid] = workerTile; return callback(err); } @@ -132,8 +200,11 @@ class VectorTileWorkerSource implements WorkerSource { resourceTiming.resourceTiming = JSON.parse(JSON.stringify(resourceTimingData)); } - workerTile.vectorTile = response.vectorTile; - workerTile.parse(response.vectorTile, this.layerIndex, this.availableImages, this.actor, (err, result) => { + // response.vectorTile will be present in the GeoJSON worker case (which inherits from this class) + // because we stub the vector tile interface around JSON data instead of parsing it directly + workerTile.vectorTile = response.vectorTile || new vt.VectorTile(new Protobuf(rawTileData)); + + workerTile.parse(workerTile.vectorTile, this.layerIndex, this.availableImages, this.actor, (err, result) => { if (err || !result) return callback(err); // Transferring a copy of rawTileData because the worker needs to retain its copy. @@ -156,6 +227,7 @@ class VectorTileWorkerSource implements WorkerSource { if (loaded && loaded[uid]) { const workerTile = loaded[uid]; workerTile.showCollisionBoxes = params.showCollisionBoxes; + workerTile.enableTerrain = !!params.enableTerrain; const done = (err, data) => { const reloadCallback = workerTile.reloadCallback; @@ -187,11 +259,11 @@ class VectorTileWorkerSource implements WorkerSource { * @private */ abortTile(params: TileParameters, callback: WorkerTileCallback) { - const loading = this.loading, - uid = params.uid; - if (loading && loading[uid] && loading[uid].abort) { - loading[uid].abort(); - delete loading[uid]; + const uid = params.uid; + const tile = this.loading[uid]; + if (tile) { + if (tile.abort) tile.abort(); + delete this.loading[uid]; } callback(); } diff --git a/src/source/worker.js b/src/source/worker.js index 27a10fa3a54..afb19f031c3 100644 --- a/src/source/worker.js +++ b/src/source/worker.js @@ -9,6 +9,8 @@ import GeoJSONWorkerSource from './geojson_worker_source'; import assert from 'assert'; import {plugin as globalRTLTextPlugin} from './rtl_text_plugin'; import {enforceCacheSizeLimit} from '../util/tile_request_cache'; +import {extend} from '../util/util'; +import {PerformanceUtils} from '../util/performance'; import type { WorkerSource, @@ -36,8 +38,10 @@ export default class Worker { workerSources: {[_: string]: {[_: string]: {[_: string]: WorkerSource } } }; demWorkerSources: {[_: string]: {[_: string]: RasterDEMTileWorkerSource } }; referrer: ?string; + terrain: ?boolean; constructor(self: WorkerGlobalScopeInterface) { + PerformanceUtils.measure('workerEvaluateScript'); this.self = self; this.actor = new Actor(self, this); @@ -71,6 +75,11 @@ export default class Worker { }; } + checkIfReady(mapID: string, unused: mixed, callback: WorkerTileCallback) { + // noop, used to check if a worker is fully set up and ready to receive messages + callback(); + } + setReferrer(mapID: string, referrer: string) { this.referrer = referrer; } @@ -86,6 +95,11 @@ export default class Worker { callback(); } + enableTerrain(mapId: string, enable: boolean, callback: WorkerTileCallback) { + this.terrain = enable; + callback(); + } + setLayers(mapId: string, layers: Array, callback: WorkerTileCallback) { this.getLayerIndex(mapId).replace(layers); callback(); @@ -98,16 +112,19 @@ export default class Worker { loadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) { assert(params.type); - this.getWorkerSource(mapId, params.type, params.source).loadTile(params, callback); + const p = this.enableTerrain ? extend({enableTerrain: this.terrain}, params) : params; + this.getWorkerSource(mapId, params.type, params.source).loadTile(p, callback); } loadDEMTile(mapId: string, params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { - this.getDEMWorkerSource(mapId, params.source).loadTile(params, callback); + const p = this.enableTerrain ? extend({buildQuadTree: this.terrain}, params) : params; + this.getDEMWorkerSource(mapId, params.source).loadTile(p, callback); } reloadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) { assert(params.type); - this.getWorkerSource(mapId, params.type, params.source).reloadTile(params, callback); + const p = this.enableTerrain ? extend({enableTerrain: this.terrain}, params) : params; + this.getWorkerSource(mapId, params.type, params.source).reloadTile(p, callback); } abortTile(mapId: string, params: TileParameters & {type: string}, callback: WorkerTileCallback) { @@ -120,10 +137,6 @@ export default class Worker { this.getWorkerSource(mapId, params.type, params.source).removeTile(params, callback); } - removeDEMTile(mapId: string, params: TileParameters) { - this.getDEMWorkerSource(mapId, params.source).removeTile(params); - } - removeSource(mapId: string, params: {source: string} & {type: string}, callback: WorkerTileCallback) { assert(params.type); assert(params.source); @@ -206,9 +219,10 @@ export default class Worker { // use a wrapped actor so that we can attach a target mapId param // to any messages invoked by the WorkerSource const actor = { - send: (type, data, callback) => { - this.actor.send(type, data, callback, mapId); - } + send: (type, data, callback, mustQueue, _, metadata) => { + this.actor.send(type, data, callback, mapId, mustQueue, metadata); + }, + scheduler: this.actor.scheduler }; this.workerSources[mapId][type][source] = new (this.workerSourceTypes[type]: any)((actor: any), this.getLayerIndex(mapId), this.getAvailableImages(mapId)); } @@ -230,6 +244,10 @@ export default class Worker { enforceCacheSizeLimit(mapId: string, limit: number) { enforceCacheSizeLimit(limit); } + + getWorkerPerformanceMetrics(mapId: string, params: any, callback: (error: ?Error, result: ?Object) => void) { + callback(undefined, PerformanceUtils.getWorkerPerformanceMetrics()); + } } /* global self, WorkerGlobalScope */ diff --git a/src/source/worker_source.js b/src/source/worker_source.js index c800049e3b3..e3ebbac4dc0 100644 --- a/src/source/worker_source.js +++ b/src/source/worker_source.js @@ -17,12 +17,18 @@ const {ImageBitmap} = window; export type TileParameters = { source: string, - uid: string, + uid: number, }; -export type WorkerTileParameters = TileParameters & { +export type RequestedTileParameters = TileParameters & { tileID: OverscaledTileID, + tileZoom: number, request: RequestParameters, + data?: mixed, + isSymbolTile: ?boolean +}; + +export type WorkerTileParameters = RequestedTileParameters & { zoom: number, maxZoom: number, tileSize: number, @@ -30,13 +36,16 @@ export type WorkerTileParameters = TileParameters & { pixelRatio: number, showCollisionBoxes: boolean, collectResourceTiming?: boolean, - returnDependencies?: boolean + returnDependencies?: boolean, + enableTerrain?: boolean }; export type WorkerDEMTileParameters = TileParameters & { coord: { z: number, x: number, y: number, w: number }, rawImageData: RGBAImage | ImageBitmap, - encoding: "mapbox" | "terrarium" + encoding: "mapbox" | "terrarium", + padding: number, + buildQuadTree?: boolean }; export type WorkerTileResult = { diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index fd5e8d5d502..b73c9cf4da6 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -15,6 +15,7 @@ import ImageAtlas from '../render/image_atlas'; import GlyphAtlas from '../render/glyph_atlas'; import EvaluationParameters from '../style/evaluation_parameters'; import {OverscaledTileID} from './tile_id'; +import {PerformanceUtils} from '../util/performance'; import type {Bucket} from '../data/bucket'; import type Actor from '../util/actor'; @@ -30,8 +31,9 @@ import type {PromoteIdSpecification} from '../style-spec/types'; class WorkerTile { tileID: OverscaledTileID; - uid: string; + uid: number; zoom: number; + tileZoom: number; pixelRatio: number; tileSize: number; source: string; @@ -40,6 +42,8 @@ class WorkerTile { showCollisionBoxes: boolean; collectResourceTiming: boolean; returnDependencies: boolean; + enableTerrain: boolean; + isSymbolTile: ?boolean; status: 'parsing' | 'done'; data: VectorTile; @@ -51,6 +55,7 @@ class WorkerTile { constructor(params: WorkerTileParameters) { this.tileID = new OverscaledTileID(params.tileID.overscaledZ, params.tileID.wrap, params.tileID.canonical.z, params.tileID.canonical.x, params.tileID.canonical.y); + this.tileZoom = params.tileZoom; this.uid = params.uid; this.zoom = params.zoom; this.pixelRatio = params.pixelRatio; @@ -61,9 +66,12 @@ class WorkerTile { this.collectResourceTiming = !!params.collectResourceTiming; this.returnDependencies = !!params.returnDependencies; this.promoteId = params.promoteId; + this.enableTerrain = !!params.enableTerrain; + this.isSymbolTile = params.isSymbolTile; } parse(data: VectorTile, layerIndex: StyleLayerIndex, availableImages: Array, actor: Actor, callback: WorkerTileCallback) { + const m = PerformanceUtils.beginMeasure('parseTile1'); this.status = 'parsing'; this.data = data; @@ -90,6 +98,22 @@ class WorkerTile { continue; } + let anySymbolLayers = false; + let anyOtherLayers = false; + for (const family of layerFamilies[sourceLayerId]) { + if (family[0].type === 'symbol') { + anySymbolLayers = true; + } else { + anyOtherLayers = true; + } + } + + if (this.isSymbolTile === true && !anySymbolLayers) { + continue; + } else if (this.isSymbolTile === false && !anyOtherLayers) { + continue; + } + if (sourceLayer.version === 1) { warnOnce(`Vector tile source "${this.source}" layer "${sourceLayerId}" ` + `does not use vector tile spec v2 and therefore may have some rendering errors.`); @@ -105,6 +129,7 @@ class WorkerTile { for (const family of layerFamilies[sourceLayerId]) { const layer = family[0]; + if (this.isSymbolTile !== undefined && (layer.type === 'symbol') !== this.isSymbolTile) continue; assert(layer.source === this.source); if (layer.minzoom && this.zoom < Math.floor(layer.minzoom)) continue; @@ -121,7 +146,8 @@ class WorkerTile { overscaling: this.overscaling, collisionBoxArray: this.collisionBoxArray, sourceLayerIndex, - sourceID: this.source + sourceID: this.source, + enableTerrain: this.enableTerrain }); bucket.populate(features, options, this.tileID.canonical); @@ -133,6 +159,7 @@ class WorkerTile { let glyphMap: ?{[_: string]: {[_: number]: ?StyleGlyph}}; let iconMap: ?{[_: string]: StyleImage}; let patternMap: ?{[_: string]: StyleImage}; + const taskMetadata = {type: 'maybePrepare', isSymbolTile: this.isSymbolTile, zoom: this.zoom}; const stacks = mapObject(options.glyphDependencies, (glyphs) => Object.keys(glyphs).map(Number)); if (Object.keys(stacks).length) { @@ -142,7 +169,7 @@ class WorkerTile { glyphMap = result; maybePrepare.call(this); } - }); + }, undefined, undefined, taskMetadata); } else { glyphMap = {}; } @@ -155,7 +182,7 @@ class WorkerTile { iconMap = result; maybePrepare.call(this); } - }); + }, undefined, undefined, taskMetadata); } else { iconMap = {}; } @@ -168,17 +195,20 @@ class WorkerTile { patternMap = result; maybePrepare.call(this); } - }); + }, undefined, undefined, taskMetadata); } else { patternMap = {}; } + PerformanceUtils.endMeasure(m); + maybePrepare.call(this); function maybePrepare() { if (error) { return callback(error); } else if (glyphMap && iconMap && patternMap) { + const m = PerformanceUtils.beginMeasure('parseTile2'); const glyphAtlas = new GlyphAtlas(glyphMap); const imageAtlas = new ImageAtlas(iconMap, patternMap); @@ -186,7 +216,14 @@ class WorkerTile { const bucket = buckets[key]; if (bucket instanceof SymbolBucket) { recalculateLayers(bucket.layers, this.zoom, availableImages); - performSymbolLayout(bucket, glyphMap, glyphAtlas.positions, iconMap, imageAtlas.iconPositions, this.showCollisionBoxes, this.tileID.canonical); + performSymbolLayout(bucket, + glyphMap, + glyphAtlas.positions, + iconMap, + imageAtlas.iconPositions, + this.showCollisionBoxes, + this.tileID.canonical, + this.tileZoom); } else if (bucket.hasPattern && (bucket instanceof LineBucket || bucket instanceof FillBucket || @@ -208,6 +245,7 @@ class WorkerTile { iconMap: this.returnDependencies ? iconMap : null, glyphPositions: this.returnDependencies ? glyphAtlas.positions : null }); + PerformanceUtils.endMeasure(m); } } } diff --git a/src/style-spec/diff.js b/src/style-spec/diff.js index 9228552e4b0..18a596b7e37 100644 --- a/src/style-spec/diff.js +++ b/src/style-spec/diff.js @@ -96,7 +96,12 @@ const operations = { /* * { command: 'setLighting', args: [lightProperties] } */ - setLight: 'setLight' + setLight: 'setLight', + + /* + * { command: 'setTerrain', args: [terrainProperties] } + */ + setTerrain: 'setTerrain' }; @@ -350,6 +355,9 @@ function diffStyles(before, after) { if (!isEqual(before.light, after.light)) { commands.push({command: operations.setLight, args: [after.light]}); } + if (!isEqual(before.terrain, after.terrain)) { + commands.push({command: operations.setTerrain, args: [after.terrain]}); + } // Handle changes to `sources` // If a source is to be removed, we also--before the removeSource diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index fcc1535c8bf..6d79fdafa35 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -211,6 +211,11 @@ CompoundExpression.register(expressions, { [], (ctx) => ctx.globals.lineProgress || 0 ], + 'sky-radial-progress': [ + NumberType, + [], + (ctx) => ctx.globals.skyRadialProgress || 0 + ], 'accumulated': [ ValueType, [], diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 736ad8fdc21..4a798553176 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -42,6 +42,7 @@ export type GlobalProperties = $ReadOnly<{ zoom: number, heatmapDensity?: number, lineProgress?: number, + skyRadialProgress?: number, isSupportedScript?: (_: string) => boolean, accumulated?: Value }>; diff --git a/src/style-spec/expression/parsing_context.js b/src/style-spec/expression/parsing_context.js index 7cf8ce94e8c..fe3ddf98761 100644 --- a/src/style-spec/expression/parsing_context.js +++ b/src/style-spec/expression/parsing_context.js @@ -229,5 +229,5 @@ function isConstant(expression: Expression) { } return isFeatureConstant(expression) && - isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'line-progress', 'accumulated', 'is-supported-script']); + isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'line-progress', 'sky-radial-progress', 'accumulated', 'is-supported-script']); } diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index 0a29e67b372..a54d8d62d5f 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -57,6 +57,10 @@ "intensity": 0.4 } }, + "terrain": { + "type": "terrain", + "doc": "A global modifier that elevates layers and markers based on a DEM data source." + }, "sources": { "required": true, "type": "sources", @@ -603,6 +607,14 @@ "macos": "0.1.0" } } + }, + "sky": { + "doc": "A spherical dome around the map that is always rendered behind all other layers.", + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + } } }, "doc": "Rendering type of this layer.", @@ -654,7 +666,8 @@ "layout_symbol", "layout_raster", "layout_hillshade", - "layout_background" + "layout_background", + "layout_sky" ], "layout_background": { "visibility": { @@ -680,6 +693,27 @@ "property-type": "constant" } }, + "layout_sky": { + "visibility": { + "type": "enum", + "values": { + "visible": { + "doc": "The layer is shown." + }, + "none": { + "doc": "The layer is not shown." + } + }, + "default": "visible", + "doc": "Whether this layer is displayed.", + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + }, + "property-type": "constant" + } + }, "layout_fill": { "fill-sort-key": { "type": "number", @@ -3130,6 +3164,15 @@ } } }, + "sky-radial-progress": { + "doc": "Gets the distance of a point on the sky from the sun position. Returns 0 at sun position and 1 when the distance reaches `sky-gradient-radius`. Can only be used in the `sky-gradient` property.", + "group": "sky", + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + } + }, "accumulated": { "doc": "Gets the value of a cluster property accumulated so far. Can only be used in the `clusterProperties` option of a clustered GeoJSON source.", "group": "Feature data", @@ -3738,6 +3781,33 @@ } } }, + "terrain" : { + "source": { + "type": "string", + "doc": "Name of a source of `raster_dem` type to be used for terrain elevation.", + "required": true + }, + "exaggeration": { + "type": "number", + "property-type": "data-constant", + "default": 1.0, + "minimum": 0, + "maximum": 1000, + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "transition": true, + "doc": "Exaggerates the elevation of the terrain by multiplying the data from the DEM with this value.", + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + } + } + }, "paint": [ "paint_fill", "paint_line", @@ -3747,7 +3817,8 @@ "paint_symbol", "paint_raster", "paint_hillshade", - "paint_background" + "paint_background", + "paint_sky" ], "paint_fill": { "fill-antialias": { @@ -5863,6 +5934,219 @@ "property-type": "data-constant" } }, + "paint_sky": { + "sky-type": { + "type": "enum", + "values": { + "gradient": { + "doc": "Renders the sky with a gradient that can be configured with `sky-gradient-radius` and `sky-gradient`." + }, + "atmosphere": { + "doc": "Renders the sky with a simulated atmospheric scattering algorithm, the sun direction can be attached to the light position or explicitly set through `sky-atmosphere-sun`." + } + }, + "default": "atmosphere", + "doc": "The type of the sky", + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + }, + "expression": { + "interpolated": false, + "parameters": [ + "zoom" + ] + }, + "property-type": "data-constant" + }, + "sky-atmosphere-sun": { + "type": "array", + "value": "number", + "length": 2, + "transition": false, + "doc": "Position of the sun center [a azimuthal angle, p polar angle]. The azimuthal angle indicates the position of the sun relative to 0° north, where degrees proceed clockwise. The polar angle indicates the height of the sun, where 0° is directly above, at zenith, and 90° at the horizon. When this property is ommitted, the sun center is directly inherited from the light position.", + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + }, + "requires": [ + { + "sky-type": "atmosphere" + } + ], + "expression": { + "interpolated": false, + "parameters": [ + "zoom" + ] + }, + "property-type": "data-constant" + }, + "sky-atmosphere-sun-intensity": { + "type": "number", + "requires": [ + { + "sky-type": "atmosphere" + } + ], + "default": 10, + "minimum": 0, + "maximum": 100, + "transition": false, + "doc": "Intensity of the sun as a light source in the atmosphere (on a scale from 0 to a 100). Setting higher values will brighten up the sky.", + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + }, + "property-type": "data-constant" + }, + "sky-gradient-center": { + "type": "array", + "requires": [ + { + "sky-type": "gradient" + } + ], + "value": "number", + "default": [ + 0, + 0 + ], + "length": 2, + "transition": false, + "doc": "Position of the gradient center [a azimuthal angle, p polar angle]. The azimuthal angle indicates the position of the gradient center relative to 0° north, where degrees proceed clockwise. The polar angle indicates the height of the gradient center, where 0° is directly above, at zenith, and 90° at the horizon.", + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + }, + "expression": { + "interpolated": false, + "parameters": [ + "zoom" + ] + }, + "property-type": "data-constant" + }, + "sky-gradient-radius": { + "type": "number", + "requires": [ + { + "sky-type": "gradient" + } + ], + "default": 90, + "minimum": 0, + "maximum": 180, + "transition": false, + "doc": "The angular distance (measured in degrees) from `sky-gradient-center` up to which the gradient extends. A value of 180 causes the gradient to wrap around to the opposite direction from `sky-gradient-center`.", + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + }, + "expression": { + "interpolated": false, + "parameters": [ + "zoom" + ] + }, + "property-type": "data-constant" + }, + "sky-gradient": { + "type": "color", + "default": [ + "interpolate", + [ + "linear" + ], + [ + "sky-radial-progress" + ], + 0.8, + "#87ceeb", + 1, + "white" + ], + "doc": "Defines a radial color gradient with which to color the sky. The color values can be interpolated with an expression using `sky-radial-progress`. The range [0, 1] for the interpolant covers a radial distance (in degrees) of [0, `sky-gradient-radius`] centered at the position specified by `sky-gradient-center`.", + "transition": false, + "requires": [ + { + "sky-type": "gradient" + } + ], + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + }, + "data-driven styling": {} + }, + "expression": { + "interpolated": true, + "parameters": [ + "sky-radial-progress" + ] + }, + "property-type": "color-ramp" + }, + "sky-atmosphere-halo-color": { + "type": "color", + "default": "white", + "doc": "A color applied to the atmosphere sun halo. The alpha channel describes how strongly the sun halo is represented in an atmosphere sky layer.", + "transition": false, + "requires": [ + { + "sky-type": "atmosphere" + } + ], + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + }, + "property-type": "data-constant" + }, + "sky-atmosphere-color": { + "type": "color", + "default": "white", + "doc": "A color used to tweak the main atmospheric scattering coefficients. Using white applies the default coefficients giving the natural blue color to the atmosphere. This color affects how heavily the corresponding wavelength is represented during scattering. The alpha channel describes the density of the atmosphere, with 1 maximum density and 0 no density.", + "transition": false, + "requires": [ + { + "sky-type": "atmosphere" + } + ], + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + }, + "property-type": "data-constant" + }, + "sky-opacity": { + "type": "number", + "default": 1, + "minimum": 0, + "maximum": 1, + "doc": "The opacity of the entire sky layer.", + "transition": true, + "sdk-support": { + "basic functionality": { + "js": "2.0.0" + } + }, + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "property-type": "data-constant" + } + }, "transition": { "duration": { "type": "number", diff --git a/src/style-spec/rollup.config.js b/src/style-spec/rollup.config.js index f0829ad896f..d1dec216c09 100644 --- a/src/style-spec/rollup.config.js +++ b/src/style-spec/rollup.config.js @@ -1,6 +1,5 @@ import path from 'path'; import replace from 'rollup-plugin-replace'; -import buble from 'rollup-plugin-buble'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import unassert from 'rollup-plugin-unassert'; @@ -10,11 +9,6 @@ import {flow} from '../../build/rollup_plugins'; // Build es modules? const esm = 'esm' in process.env; -const transforms = { - dangerousForOf: true, - modules: esm ? false : undefined -}; - const ROOT_DIR = __dirname; const config = [{ @@ -53,7 +47,6 @@ const config = [{ }), flow(), json(), - buble({transforms, objectAssign: "Object.assign"}), unassert(), resolve({ browser: true, diff --git a/src/style-spec/style-spec.js b/src/style-spec/style-spec.js index 7bf17892353..43ce15d7fde 100644 --- a/src/style-spec/style-spec.js +++ b/src/style-spec/style-spec.js @@ -1,7 +1,7 @@ // @flow type ExpressionType = 'data-driven' | 'cross-faded' | 'cross-faded-data-driven' | 'color-ramp' | 'data-constant' | 'constant'; -type ExpressionParameters = Array<'zoom' | 'feature' | 'feature-state' | 'heatmap-density' | 'line-progress'>; +type ExpressionParameters = Array<'zoom' | 'feature' | 'feature-state' | 'heatmap-density' | 'line-progress' | 'sky-radial-progress'>; type ExpressionSpecification = { interpolated: boolean, diff --git a/src/style-spec/types.js b/src/style-spec/types.js index 34a380e1ba0..1186be1c05e 100644 --- a/src/style-spec/types.js +++ b/src/style-spec/types.js @@ -66,6 +66,7 @@ export type StyleSpecification = {| "bearing"?: number, "pitch"?: number, "light"?: LightSpecification, + "terrain"?: TerrainSpecification, "sources": {[_: string]: SourceSpecification}, "sprite"?: string, "glyphs"?: string, @@ -80,6 +81,11 @@ export type LightSpecification = {| "intensity"?: PropertyValueSpecification |} +export type TerrainSpecification = {| + "source": string, + "exaggeration"?: PropertyValueSpecification +|} + export type VectorSourceSpecification = { "type": "vector", "url"?: string, @@ -419,6 +425,28 @@ export type BackgroundLayerSpecification = {| |} |} +export type SkyLayerSpecification = {| + "id": string, + "type": "sky", + "metadata"?: mixed, + "minzoom"?: number, + "maxzoom"?: number, + "layout"?: {| + "visibility"?: "visible" | "none" + |}, + "paint"?: {| + "sky-type"?: PropertyValueSpecification<"gradient" | "atmosphere">, + "sky-atmosphere-sun"?: PropertyValueSpecification<[number, number]>, + "sky-atmosphere-sun-intensity"?: number, + "sky-gradient-center"?: PropertyValueSpecification<[number, number]>, + "sky-gradient-radius"?: PropertyValueSpecification, + "sky-gradient"?: ExpressionSpecification, + "sky-atmosphere-halo-color"?: ColorSpecification, + "sky-atmosphere-color"?: ColorSpecification, + "sky-opacity"?: PropertyValueSpecification + |} +|} + export type LayerSpecification = | FillLayerSpecification | LineLayerSpecification @@ -428,5 +456,6 @@ export type LayerSpecification = | FillExtrusionLayerSpecification | RasterLayerSpecification | HillshadeLayerSpecification - | BackgroundLayerSpecification; + | BackgroundLayerSpecification + | SkyLayerSpecification; diff --git a/src/style-spec/util/color.js b/src/style-spec/util/color.js index a8e59b74f05..eb100380300 100644 --- a/src/style-spec/util/color.js +++ b/src/style-spec/util/color.js @@ -30,6 +30,7 @@ class Color { static white: Color; static transparent: Color; static red: Color; + static blue: Color; /** * Parses valid CSS color strings and returns a `Color` instance. @@ -91,5 +92,6 @@ Color.black = new Color(0, 0, 0, 1); Color.white = new Color(1, 1, 1, 1); Color.transparent = new Color(0, 0, 0, 0); Color.red = new Color(1, 0, 0, 1); +Color.blue = new Color(0, 0, 1, 1); export default Color; diff --git a/src/style-spec/validate/validate.js b/src/style-spec/validate/validate.js index 2c2c8aaaa1e..b827db3ba79 100644 --- a/src/style-spec/validate/validate.js +++ b/src/style-spec/validate/validate.js @@ -17,6 +17,7 @@ import validateFilter from './validate_filter'; import validateLayer from './validate_layer'; import validateSource from './validate_source'; import validateLight from './validate_light'; +import validateTerrain from './validate_terrain'; import validateString from './validate_string'; import validateFormatted from './validate_formatted'; import validateImage from './validate_image'; @@ -37,6 +38,7 @@ const VALIDATORS = { 'object': validateObject, 'source': validateSource, 'light': validateLight, + 'terrain': validateTerrain, 'string': validateString, 'formatted': validateFormatted, 'resolvedImage': validateImage diff --git a/src/style-spec/validate/validate_layer.js b/src/style-spec/validate/validate_layer.js index 6cfc75e1f01..4c40ab098a1 100644 --- a/src/style-spec/validate/validate_layer.js +++ b/src/style-spec/validate/validate_layer.js @@ -52,7 +52,7 @@ export default function validateLayer(options) { } else { type = unbundle(parent.type); } - } else if (type !== 'background') { + } else if (!(type === 'background' || type === 'sky')) { if (!layer.source) { errors.push(new ValidationError(key, layer, 'missing required property "source"')); } else { diff --git a/src/style-spec/validate/validate_terrain.js b/src/style-spec/validate/validate_terrain.js new file mode 100644 index 00000000000..473f1488bf4 --- /dev/null +++ b/src/style-spec/validate/validate_terrain.js @@ -0,0 +1,60 @@ + +import ValidationError from '../error/validation_error'; +import validate from './validate'; +import getType from '../util/get_type'; +import {unbundle} from '../util/unbundle_jsonlint'; + +export default function validateTerrain(options) { + const terrain = options.value; + const key = options.key; + const style = options.style; + const styleSpec = options.styleSpec; + const terrainSpec = styleSpec.terrain; + let errors = []; + + const rootType = getType(terrain); + if (terrain === undefined) { + return errors; + } else if (rootType !== 'object') { + errors = errors.concat([new ValidationError('terrain', terrain, `object expected, ${rootType} found`)]); + return errors; + } + + for (const key in terrain) { + const transitionMatch = key.match(/^(.*)-transition$/); + + if (transitionMatch && terrainSpec[transitionMatch[1]] && terrainSpec[transitionMatch[1]].transition) { + errors = errors.concat(validate({ + key, + value: terrain[key], + valueSpec: styleSpec.transition, + style, + styleSpec + })); + } else if (terrainSpec[key]) { + errors = errors.concat(validate({ + key, + value: terrain[key], + valueSpec: terrainSpec[key], + style, + styleSpec + })); + } else { + errors = errors.concat([new ValidationError(key, terrain[key], `unknown property "${key}"`)]); + } + } + + if (!terrain.source) { + errors.push(new ValidationError(key, terrain, `terrain is missing required property "source"`)); + } else { + const source = style.sources && style.sources[terrain.source]; + const sourceType = source && unbundle(source.type); + if (!source) { + errors.push(new ValidationError(key, terrain.source, `source "${terrain.source}" not found`)); + } else if (sourceType !== 'raster-dem') { + errors.push(new ValidationError(key, terrain.source, `terrain cannot be used with a source of type ${sourceType}, it only be used with a "raster-dem" source type`)); + } + } + + return errors; +} diff --git a/src/style-spec/validate_style.min.js b/src/style-spec/validate_style.min.js index b3166e800ea..202a565cf83 100644 --- a/src/style-spec/validate_style.min.js +++ b/src/style-spec/validate_style.min.js @@ -6,6 +6,7 @@ import validateGlyphsURL from './validate/validate_glyphs_url'; import validateSource from './validate/validate_source'; import validateLight from './validate/validate_light'; +import validateTerrain from './validate/validate_terrain'; import validateLayer from './validate/validate_layer'; import validateFilter from './validate/validate_filter'; import validatePaintProperty from './validate/validate_paint_property'; @@ -58,6 +59,7 @@ function validateStyleMin(style, styleSpec = latestStyleSpec) { validateStyleMin.source = wrapCleanErrors(validateSource); validateStyleMin.light = wrapCleanErrors(validateLight); +validateStyleMin.terrain = wrapCleanErrors(validateTerrain); validateStyleMin.layer = wrapCleanErrors(validateLayer); validateStyleMin.filter = wrapCleanErrors(validateFilter); validateStyleMin.paintProperty = wrapCleanErrors(validatePaintProperty); diff --git a/src/style/create_style_layer.js b/src/style/create_style_layer.js index aae084e6e8d..eaac6953b63 100644 --- a/src/style/create_style_layer.js +++ b/src/style/create_style_layer.js @@ -10,6 +10,7 @@ import symbol from './style_layer/symbol_style_layer'; import background from './style_layer/background_style_layer'; import raster from './style_layer/raster_style_layer'; import CustomStyleLayer from './style_layer/custom_style_layer'; +import sky from './style_layer/sky_style_layer'; import type {CustomLayerInterface} from './style_layer/custom_style_layer'; import type {LayerSpecification} from '../style-spec/types'; @@ -23,7 +24,8 @@ const subclasses = { line, symbol, background, - raster + raster, + sky }; export default function createStyleLayer(layer: LayerSpecification | CustomLayerInterface) { diff --git a/src/style/light.js b/src/style/light.js index 99c24d0d232..0572ac014ea 100644 --- a/src/style/light.js +++ b/src/style/light.js @@ -2,7 +2,7 @@ import styleSpec from '../style-spec/reference/latest'; -import {endsWith, extend, sphericalToCartesian} from '../util/util'; +import {endsWith, extend, degToRad} from '../util/util'; import {Evented} from '../util/evented'; import { validateStyle, @@ -25,12 +25,34 @@ import type { import type {LightSpecification} from '../style-spec/types'; -type LightPosition = { +export type LightPosition = { x: number, y: number, - z: number + z: number, + azimuthal: number, + polar: number, }; +/** + * Converts spherical coordinates to cartesian LightPosition coordinates. + * + * @private + * @param spherical Spherical coordinates, in [radial, azimuthal, polar] + * @return LightPosition cartesian coordinates + */ +export function sphericalToCartesian([r, azimuthal, polar]: [number, number, number]): LightPosition { + // We abstract "north"/"up" (compass-wise) to be 0° when really this is 90° (π/2): + // correct for that here + const a = degToRad(azimuthal + 90), p = degToRad(polar); + + return { + x: r * Math.cos(a) * Math.sin(p), + y: r * Math.sin(a) * Math.sin(p), + z: r * Math.cos(p), + azimuthal, polar + }; +} + class LightPositionProperty implements Property<[number, number, number], LightPosition> { specification: StylePropertySpecification; @@ -47,6 +69,8 @@ class LightPositionProperty implements Property<[number, number, number], LightP x: interpolate(a.x, b.x, t), y: interpolate(a.y, b.y, t), z: interpolate(a.z, b.z, t), + azimuthal: interpolate(a.azimuthal, b.azimuthal, t), + polar: interpolate(a.polar, b.polar, t), }; } } diff --git a/src/style/query_geometry.js b/src/style/query_geometry.js new file mode 100644 index 00000000000..6c29162ab36 --- /dev/null +++ b/src/style/query_geometry.js @@ -0,0 +1,208 @@ +// @flow + +import Point from '@mapbox/point-geometry'; +import {getBounds, clamp, polygonizeBounds, bufferConvexPolygon} from '../util/util'; +import {polygonIntersectsBox} from '../util/intersection_tests'; +import EXTENT from '../data/extent'; +import type {PointLike} from '@mapbox/point-geometry'; +import type Transform from '../geo/transform'; +import type Tile from '../source/tile'; +import pixelsToTileUnits from '../source/pixels_to_tile_units'; +import {vec3} from 'gl-matrix'; +import {Ray} from '../util/primitives'; +import MercatorCoordinate from '../geo/mercator_coordinate'; +import type {OverscaledTileID} from '../source/tile_id'; + +/** + * A data-class that represents a screenspace query from `Map#queryRenderedFeatures`. + * All the internal geometries and data are intented to be immutable and read-only. + * Its lifetime is only for the duration of the query and fixed state of the map while the query is being processed. + * + * @class QueryGeometry + */ +export class QueryGeometry { + screenBounds: Point[]; + cameraPoint: Point; + screenGeometry: Point[]; + screenGeometryMercator: MercatorCoordinate[]; + cameraGeometry: Point[]; + + _screenRaycastCache: { [_: number]: MercatorCoordinate[]}; + _cameraRaycastCache: { [_: number]: MercatorCoordinate[]}; + + isAboveHorizon: boolean; + + constructor(screenBounds: Point[], cameraPoint: Point, aboveHorizon: boolean, transform: Transform) { + this.screenBounds = screenBounds; + this.cameraPoint = cameraPoint; + this._screenRaycastCache = {}; + this._cameraRaycastCache = {}; + this.isAboveHorizon = aboveHorizon; + + this.screenGeometry = this.bufferedScreenGeometry(0); + this.screenGeometryMercator = this.screenGeometry.map((p) => transform.pointCoordinate3D(p)); + this.cameraGeometry = this.bufferedCameraGeometry(0); + } + + /** + * Factory method to help contruct an instance while accounting for current map state. + * + * @static + * @param {(PointLike | [PointLike, PointLike])} geometry + * @param {Transform} transform + * @returns {QueryGeometry} + */ + static createFromScreenPoints(geometry: PointLike | [PointLike, PointLike], transform: Transform): QueryGeometry { + let screenGeometry; + let aboveHorizon; + if (geometry instanceof Point || typeof geometry[0] === 'number') { + const pt = Point.convert(geometry); + screenGeometry = [Point.convert(geometry)]; + aboveHorizon = transform.isPointAboveHorizon(pt); + } else { + const tl = Point.convert(geometry[0]); + const br = Point.convert(geometry[1]); + screenGeometry = [tl, br]; + aboveHorizon = polygonizeBounds(tl, br).every((p) => transform.isPointAboveHorizon(p)); + } + + return new QueryGeometry(screenGeometry, transform.getCameraPoint(), aboveHorizon, transform); + } + + /** + * Due to data-driven styling features do not uniform size(eg `circle-radius`) and can be offset differntly + * from their original location(for eg. with `*-translate`). This means we have to expand our query region for + * each tile to account for variation in these properties. + * Each tile calculates a tile level max padding value (in screenspace pixels) when its parsed, this function + * lets us calculate a buffered version of the screenspace query geometry for each tile. + * + * @param {number} buffer + * @returns {Point[]} + */ + bufferedScreenGeometry(buffer: number): Point[] { + return polygonizeBounds( + this.screenBounds[0], + this.screenBounds.length === 1 ? this.screenBounds[0] : this.screenBounds[1], + buffer + ); + } + + /** + * When the map is pitched, some of the 3D features that intersect a query will not intersect + * the query at the surface of the earth. Instead the feature may be closer and only intersect + * the query because it extrudes into the air. + * + * This returns a geometry thats a triangle, with the base of the triangle being the far points + * of the query frustum, and the top of the triangle being the point underneath the camera. + * Similar to `bufferedScreenGeometry`, buffering is added to account for variation in paint properties. + * + * @param {number} buffer + * @returns {Point[]} + */ + bufferedCameraGeometry(buffer: number): Point[] { + const cameraTriangle = [ + this.screenBounds[0], + this.screenBounds.length === 1 ? this.screenBounds[0].add(new Point(1, 0)) : this.screenBounds[1], + this.cameraPoint + ]; + + return bufferConvexPolygon(cameraTriangle, buffer); + } + + /** + * Checks if a tile is contained within this query geometry. + * + * @param {Tile} tile + * @param {Transform} transform + * @param {boolean} use3D + * @returns {?TilespaceQueryGeometry} Returns undefined if the tile does not intersect + */ + containsTile(tile: Tile, transform: Transform, use3D: boolean): ?TilespaceQueryGeometry { + // The buffer around the query geometry is applied in screen-space. + // Floating point errors when projecting into tilespace could leave a feature + // outside the query volume even if it looks like it overlaps visually, a 1px bias value overcomes that. + const bias = 1; + const padding = tile.queryPadding + bias; + + const geometryForTileCheck = use3D ? + this._bufferedCameraMercator(padding, transform).map((p) => tile.tileID.getTilePoint(p)) : + this._bufferedScreenMercator(padding, transform).map((p) => tile.tileID.getTilePoint(p)); + const tilespaceVec3s = this.screenGeometryMercator.map((p) => tile.tileID.getTileVec3(p)); + const tilespaceGeometry = tilespaceVec3s.map((v) => new Point(v[0], v[1])); + + const cameraMercator = transform.getFreeCameraOptions().position || new MercatorCoordinate(0, 0, 0); + const tilespaceCameraPosition = tile.tileID.getTileVec3(cameraMercator); + const tilespaceRays = tilespaceVec3s.map((tileVec) => { + const dir = vec3.sub(tileVec, tileVec, tilespaceCameraPosition); + vec3.normalize(dir, dir); + return new Ray(tilespaceCameraPosition, dir); + }); + const pixelToTileUnitsFactor = pixelsToTileUnits(tile, 1, transform.zoom); + + if (polygonIntersectsBox(geometryForTileCheck, 0, 0, EXTENT, EXTENT)) { + return { + queryGeometry: this, + tilespaceGeometry, + tilespaceRays, + bufferedTilespaceGeometry: geometryForTileCheck, + bufferedTilespaceBounds: clampBoundsToTileExtents(getBounds(geometryForTileCheck)), + tile, + tileID: tile.tileID, + pixelToTileUnitsFactor + }; + } + } + + /** + * These methods add caching on top of the terrain raycasting provided by `Transform#pointCoordinate3d`. + * Tiles come with different values of padding, however its very likely that multiple tiles share the same value of padding + * based on the style. In that case we want to reuse the result from a previously computed terrain raycast. + */ + + _bufferedScreenMercator(padding: number, transform: Transform): MercatorCoordinate[] { + const key = cacheKey(padding); + if (this._screenRaycastCache[key]) { + return this._screenRaycastCache[key]; + } else { + const poly = this.bufferedScreenGeometry(padding).map((p) => transform.pointCoordinate3D(p)); + this._screenRaycastCache[key] = poly; + return poly; + } + } + + _bufferedCameraMercator(padding: number, transform: Transform): MercatorCoordinate[] { + const key = cacheKey(padding); + if (this._cameraRaycastCache[key]) { + return this._cameraRaycastCache[key]; + } else { + const poly = this.bufferedCameraGeometry(padding).map((p) => transform.pointCoordinate3D(p)); + this._cameraRaycastCache[key] = poly; + return poly; + } + } +} + +//Padding is in screen pixels and is only used as a coarse check, so 2 decimal places of precision should be good enough for a cache. +function cacheKey(padding: number): number { + return (padding * 100) | 0; +} + +export type TilespaceQueryGeometry = { + queryGeometry: QueryGeometry, + tilespaceGeometry: Point[], + tilespaceRays: Ray[], + bufferedTilespaceGeometry: Point[], + bufferedTilespaceBounds: { min: Point, max: Point}, + tile: Tile, + tileID: OverscaledTileID, + pixelToTileUnitsFactor: number +}; + +function clampBoundsToTileExtents(bounds: {min: Point, max: Point}): {min: Point, max: Point} { + bounds.min.x = clamp(bounds.min.x, 0, EXTENT); + bounds.min.y = clamp(bounds.min.y, 0, EXTENT); + + bounds.max.x = clamp(bounds.max.x, 0, EXTENT); + bounds.max.y = clamp(bounds.max.y, 0, EXTENT); + return bounds; +} diff --git a/src/style/query_utils.js b/src/style/query_utils.js index dc0f6754a86..48396116949 100644 --- a/src/style/query_utils.js +++ b/src/style/query_utils.js @@ -41,3 +41,16 @@ export function translate(queryGeometry: Array, } return translated; } + +export function tilespaceTranslate(translate: [number, number], + translateAnchor: 'viewport' | 'map', + bearing: number, + pixelsToTileUnits: number): Point { + const pt = Point.convert(translate)._mult(pixelsToTileUnits); + + if (translateAnchor === "viewport") { + pt._rotate(-bearing); + } + + return pt; +} diff --git a/src/style/style.js b/src/style/style.js index 70523941871..e48eb9be494 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -9,14 +9,17 @@ import loadSprite from './load_sprite'; import ImageManager from '../render/image_manager'; import GlyphManager from '../render/glyph_manager'; import Light from './light'; +import Terrain from './terrain'; import LineAtlas from '../render/line_atlas'; -import {pick, clone, extend, deepEqual, filterObject, mapObject} from '../util/util'; +import {pick, clone, extend, deepEqual, filterObject} from '../util/util'; import {getJSON, getReferrer, makeRequest, ResourceType} from '../util/ajax'; import {isMapboxURL} from '../util/mapbox'; import browser from '../util/browser'; import Dispatcher from '../util/dispatcher'; import {validateStyle, emitValidationErrors as _emitValidationErrors} from './validate_style'; +import {QueryGeometry} from '../style/query_geometry'; import { + create as createSource, getType as getSourceType, setType as setSourceType, type SourceClass @@ -60,11 +63,13 @@ import type { FilterSpecification, StyleSpecification, LightSpecification, - SourceSpecification + SourceSpecification, + TerrainSpecification } from '../style-spec/types'; import type {CustomLayerInterface} from './style_layer/custom_style_layer'; import type {Validator} from './validate_style'; import type {OverscaledTileID} from '../source/tile_id'; +import type {PointLike} from '@mapbox/point-geometry'; const supportedDiffOperations = pick(diffOperations, [ 'addLayer', @@ -77,7 +82,8 @@ const supportedDiffOperations = pick(diffOperations, [ 'setLayerZoomRange', 'setLight', 'setTransition', - 'setGeoJSONSourceData' + 'setGeoJSONSourceData', + 'setTerrain' // 'setGlyphs', // 'setSprite', ]); @@ -110,13 +116,17 @@ class Style extends Evented { glyphManager: GlyphManager; lineAtlas: LineAtlas; light: Light; + terrain: ?Terrain; _request: ?Cancelable; _spriteRequest: ?Cancelable; _layers: {[_: string]: StyleLayer}; + _num3DLayers: number; _serializedLayers: {[_: string]: Object}; _order: Array; - sourceCaches: {[_: string]: SourceCache}; + _sourceCaches: {[_: string]: SourceCache}; + _otherSourceCaches: {[_: string]: SourceCache}; + _symbolSourceCaches: {[_: string]: SourceCache}; zoomHistory: ZoomHistory; _loaded: boolean; _rtlTextPluginCallback: Function; @@ -151,9 +161,12 @@ class Style extends Evented { this.crossTileSymbolIndex = new CrossTileSymbolIndex(); this._layers = {}; + this._num3DLayers = 0; this._serializedLayers = {}; this._order = []; - this.sourceCaches = {}; + this._sourceCaches = {}; + this._otherSourceCaches = {}; + this._symbolSourceCaches = {}; this.zoomHistory = new ZoomHistory(); this._loaded = false; this._availableImages = []; @@ -173,8 +186,12 @@ class Style extends Evented { if (results) { const allComplete = results.every((elem) => elem); if (allComplete) { - for (const id in self.sourceCaches) { - self.sourceCaches[id].reload(); // Should be a no-op if the plugin loads before any tiles load + for (const id in self._sourceCaches) { + const sourceCache = self._sourceCaches[id]; + const sourceCacheType = sourceCache.getSource().type; + if (sourceCacheType === 'vector' || sourceCacheType === 'geojson') { + sourceCache.reload(); // Should be a no-op if the plugin loads before any tiles load + } } } } @@ -187,12 +204,7 @@ class Style extends Evented { return; } - const sourceCache = this.sourceCaches[event.sourceId]; - if (!sourceCache) { - return; - } - - const source = sourceCache.getSource(); + const source = this.getSource(event.sourceId); if (!source || !source.vectorLayerIds) { return; } @@ -252,7 +264,7 @@ class Style extends Evented { for (const id in json.sources) { this.addSource(id, json.sources[id], {validate: false}); } - + this._changed = false; // avoid triggering redundant style update after adding initial sources if (json.sprite) { this._loadSprite(json.sprite); } else { @@ -272,10 +284,16 @@ class Style extends Evented { layer.setEventedParent(this, {layer: {id: layer.id}}); this._layers[layer.id] = layer; this._serializedLayers[layer.id] = layer.serialize(); + if (layer.is3D()) { + this._num3DLayers++; + } } this.dispatcher.broadcast('setLayers', this._serializeLayers(this._order)); this.light = new Light(this.stylesheet.light); + if (this.stylesheet.terrain) { + this._createTerrain(this.stylesheet.terrain); + } this.fire(new Event('data', {dataType: 'style'})); this.fire(new Event('style.load')); @@ -300,8 +318,8 @@ class Style extends Evented { } _validateLayer(layer: StyleLayer) { - const sourceCache = this.sourceCaches[layer.source]; - if (!sourceCache) { + const source = this.getSource(layer.source); + if (!source) { return; } @@ -310,7 +328,6 @@ class Style extends Evented { return; } - const source = sourceCache.getSource(); if (source.type === 'geojson' || (source.vectorLayerIds && source.vectorLayerIds.indexOf(sourceLayer) === -1)) { this.fire(new ErrorEvent(new Error( `Source layer "${sourceLayer}" ` + @@ -327,8 +344,8 @@ class Style extends Evented { if (Object.keys(this._updatedSources).length) return false; - for (const id in this.sourceCaches) - if (!this.sourceCaches[id].loaded()) + for (const id in this._sourceCaches) + if (!this._sourceCaches[id].loaded()) return false; if (!this.imageManager.isLoaded()) @@ -353,8 +370,8 @@ class Style extends Evented { return true; } - for (const id in this.sourceCaches) { - if (this.sourceCaches[id].hasTransition()) { + for (const id in this._sourceCaches) { + if (this._sourceCaches[id].hasTransition()) { return true; } } @@ -414,8 +431,8 @@ class Style extends Evented { const sourcesUsedBefore = {}; - for (const sourceId in this.sourceCaches) { - const sourceCache = this.sourceCaches[sourceId]; + for (const sourceId in this._sourceCaches) { + const sourceCache = this._sourceCaches[sourceId]; sourcesUsedBefore[sourceId] = sourceCache.used; sourceCache.used = false; } @@ -424,19 +441,35 @@ class Style extends Evented { const layer = this._layers[layerId]; layer.recalculate(parameters, this._availableImages); - if (!layer.isHidden(parameters.zoom) && layer.source) { - this.sourceCaches[layer.source].used = true; + if (!layer.isHidden(parameters.zoom)) { + const sourceCache = this._getLayerSourceCache(layer); + if (sourceCache) sourceCache.used = true; + } + + const painter = this.map.painter; + if (painter) { + const programIds = layer.getProgramIds(); + if (!programIds) continue; + + const programConfiguration = layer.getProgramConfiguration(parameters.zoom); + + for (const programId of programIds) { + painter.useProgram(programId, programConfiguration); + } } } for (const sourceId in sourcesUsedBefore) { - const sourceCache = this.sourceCaches[sourceId]; + const sourceCache = this._sourceCaches[sourceId]; if (sourcesUsedBefore[sourceId] !== sourceCache.used) { - sourceCache.fire(new Event('data', {sourceDataType: 'visibility', dataType:'source', sourceId})); + sourceCache.getSource().fire(new Event('data', {sourceDataType: 'visibility', dataType:'source', sourceId: sourceCache.getSource().id})); } } this.light.recalculate(parameters); + if (this.terrain) { + this.terrain.recalculate(parameters); + } this.z = parameters.zoom; if (changed) { @@ -451,8 +484,8 @@ class Style extends Evented { _updateTilesForChangedImages() { const changedImages = Object.keys(this._changedImages); if (changedImages.length) { - for (const name in this.sourceCaches) { - this.sourceCaches[name].reloadTilesForDependencies(['icons', 'patterns'], changedImages); + for (const name in this._sourceCaches) { + this._sourceCaches[name].reloadTilesForDependencies(['icons', 'patterns'], changedImages); } this._changedImages = {}; } @@ -562,7 +595,7 @@ class Style extends Evented { addSource(id: string, source: SourceSpecification, options: StyleSetterOptions = {}) { this._checkLoaded(); - if (this.sourceCaches[id] !== undefined) { + if (this.getSource(id) !== undefined) { throw new Error('There is already a source with this ID'); } @@ -575,15 +608,31 @@ class Style extends Evented { if (shouldValidate && this._validate(validateStyle.source, `sources.${id}`, source, null, options)) return; if (this.map && this.map._collectResourceTiming) (source: any).collectResourceTiming = true; - const sourceCache = this.sourceCaches[id] = new SourceCache(id, source, this.dispatcher); - sourceCache.style = this; - sourceCache.setEventedParent(this, () => ({ + + const sourceInstance = createSource(id, source, this.dispatcher, this); + + sourceInstance.setEventedParent(this, () => ({ isSourceLoaded: this.loaded(), - source: sourceCache.serialize(), + source: sourceInstance.serialize(), sourceId: id })); - sourceCache.onAdd(this.map); + const addSourceCache = (onlySymbols) => { + const sourceCacheId = (onlySymbols ? 'symbol:' : 'other:') + id; + const sourceCache = this._sourceCaches[sourceCacheId] = new SourceCache(sourceCacheId, sourceInstance, onlySymbols); + (onlySymbols ? this._symbolSourceCaches : this._otherSourceCaches)[id] = sourceCache; + sourceCache.style = this; + + sourceCache.onAdd(this.map); + }; + + addSourceCache(false); + if (source.type === 'vector' || source.type === 'geojson') { + addSourceCache(true); + } + + if (sourceInstance.onAdd) sourceInstance.onAdd(this.map); + this._changed = true; } @@ -596,7 +645,8 @@ class Style extends Evented { removeSource(id: string) { this._checkLoaded(); - if (this.sourceCaches[id] === undefined) { + const source = this.getSource(id); + if (source === undefined) { throw new Error('There is no source with this ID'); } for (const layerId in this._layers) { @@ -604,15 +654,25 @@ class Style extends Evented { return this.fire(new ErrorEvent(new Error(`Source "${id}" cannot be removed while layer "${layerId}" is using it.`))); } } + if (this.terrain && this.terrain.get().source === id) { + return this.fire(new ErrorEvent(new Error(`Source "${id}" cannot be removed while terrain is using it.`))); + } - const sourceCache = this.sourceCaches[id]; - delete this.sourceCaches[id]; - delete this._updatedSources[id]; - sourceCache.fire(new Event('data', {sourceDataType: 'metadata', dataType:'source', sourceId: id})); - sourceCache.setEventedParent(null); - sourceCache.clearTiles(); + const sourceCaches = this._getSourceCaches(id); + for (const sourceCache of sourceCaches) { + delete this._sourceCaches[sourceCache.id]; + delete this._updatedSources[sourceCache.id]; + sourceCache.fire(new Event('data', {sourceDataType: 'metadata', dataType:'source', sourceId: sourceCache.getSource().id})); + sourceCache.setEventedParent(null); + sourceCache.clearTiles(); + } + delete this._otherSourceCaches[id]; + delete this._symbolSourceCaches[id]; - if (sourceCache.onRemove) sourceCache.onRemove(this.map); + source.setEventedParent(null); + if (source.onRemove) { + source.onRemove(this.map); + } this._changed = true; } @@ -624,8 +684,8 @@ class Style extends Evented { setGeoJSONSourceData(id: string, data: GeoJSON | string) { this._checkLoaded(); - assert(this.sourceCaches[id] !== undefined, 'There is no source with this ID'); - const geojsonSource: GeoJSONSource = (this.sourceCaches[id].getSource(): any); + assert(this.getSource(id) !== undefined, 'There is no source with this ID'); + const geojsonSource: GeoJSONSource = (this.getSource(id): any); assert(geojsonSource.type === 'geojson'); geojsonSource.setData(data); @@ -638,7 +698,8 @@ class Style extends Evented { * @returns {Object} source */ getSource(id: string): Object { - return this.sourceCaches[id] && this.sourceCaches[id].getSource(); + const sourceCache = this._getSourceCache(id); + return sourceCache && sourceCache.getSource(); } /** @@ -682,6 +743,9 @@ class Style extends Evented { layer.setEventedParent(this, {layer: {id}}); this._serializedLayers[layer.id] = layer.serialize(); + if (layer.is3D()) { + this._num3DLayers++; + } } const index = before ? this._order.indexOf(before) : this._order.length; @@ -695,7 +759,8 @@ class Style extends Evented { this._layers[id] = layer; - if (this._removedLayers[id] && layer.source && layer.type !== 'custom') { + const sourceCache = this._getLayerSourceCache(layer); + if (this._removedLayers[id] && layer.source && sourceCache && layer.type !== 'custom') { // If, in the current batch, we have already removed this layer // and we are now re-adding it with a different `type`, then we // need to clear (rather than just reload) the underyling source's @@ -709,7 +774,7 @@ class Style extends Evented { this._updatedSources[layer.source] = 'clear'; } else { this._updatedSources[layer.source] = 'reload'; - this.sourceCaches[layer.source].pause(); + sourceCache.pause(); } } this._updateLayer(layer); @@ -771,6 +836,10 @@ class Style extends Evented { layer.setEventedParent(null); + if (layer.is3D()) { + this._num3DLayers--; + } + const index = this._order.indexOf(id); this._order.splice(index, 1); @@ -922,13 +991,13 @@ class Style extends Evented { this._checkLoaded(); const sourceId = target.source; const sourceLayer = target.sourceLayer; - const sourceCache = this.sourceCaches[sourceId]; + const source = this.getSource(sourceId); - if (sourceCache === undefined) { + if (source === undefined) { this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); return; } - const sourceType = sourceCache.getSource().type; + const sourceType = source.type; if (sourceType === 'geojson' && sourceLayer) { this.fire(new ErrorEvent(new Error(`GeoJSON sources cannot have a sourceLayer parameter.`))); return; @@ -941,20 +1010,23 @@ class Style extends Evented { this.fire(new ErrorEvent(new Error(`The feature id parameter must be provided.`))); } - sourceCache.setFeatureState(sourceLayer, target.id, state); + const sourceCaches = this._getSourceCaches(sourceId); + for (const sourceCache of sourceCaches) { + sourceCache.setFeatureState(sourceLayer, target.id, state); + } } removeFeatureState(target: { source: string; sourceLayer?: string; id?: string | number; }, key?: string) { this._checkLoaded(); const sourceId = target.source; - const sourceCache = this.sourceCaches[sourceId]; + const source = this.getSource(sourceId); - if (sourceCache === undefined) { + if (source === undefined) { this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); return; } - const sourceType = sourceCache.getSource().type; + const sourceType = source.type; const sourceLayer = sourceType === 'vector' ? target.sourceLayer : undefined; if (sourceType === 'vector' && !sourceLayer) { @@ -967,20 +1039,23 @@ class Style extends Evented { return; } - sourceCache.removeFeatureState(sourceLayer, target.id, key); + const sourceCaches = this._getSourceCaches(sourceId); + for (const sourceCache of sourceCaches) { + sourceCache.removeFeatureState(sourceLayer, target.id, key); + } } getFeatureState(target: { source: string; sourceLayer?: string; id: string | number; }) { this._checkLoaded(); const sourceId = target.source; const sourceLayer = target.sourceLayer; - const sourceCache = this.sourceCaches[sourceId]; + const source = this.getSource(sourceId); - if (sourceCache === undefined) { + if (source === undefined) { this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); return; } - const sourceType = sourceCache.getSource().type; + const sourceType = source.type; if (sourceType === 'vector' && !sourceLayer) { this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`))); return; @@ -989,7 +1064,8 @@ class Style extends Evented { this.fire(new ErrorEvent(new Error(`The feature id parameter must be provided.`))); } - return sourceCache.getFeatureState(sourceLayer, target.id); + const sourceCaches = this._getSourceCaches(sourceId); + return sourceCaches[0].getFeatureState(sourceLayer, target.id); } getTransition() { @@ -997,11 +1073,19 @@ class Style extends Evented { } serialize() { + const sources = {}; + for (const cacheId in this._sourceCaches) { + const source = this._sourceCaches[cacheId].getSource(); + if (!sources[source.id]) { + sources[source.id] = source.serialize(); + } + } return filterObject({ version: this.stylesheet.version, name: this.stylesheet.name, metadata: this.stylesheet.metadata, light: this.stylesheet.light, + terrain: this.stylesheet.terrain, center: this.stylesheet.center, zoom: this.stylesheet.zoom, bearing: this.stylesheet.bearing, @@ -1009,18 +1093,20 @@ class Style extends Evented { sprite: this.stylesheet.sprite, glyphs: this.stylesheet.glyphs, transition: this.stylesheet.transition, - sources: mapObject(this.sourceCaches, (source) => source.serialize()), + sources, layers: this._serializeLayers(this._order) }, (value) => { return value !== undefined; }); } _updateLayer(layer: StyleLayer) { this._updatedLayers[layer.id] = true; + const sourceCache = this._getLayerSourceCache(layer); if (layer.source && !this._updatedSources[layer.source] && //Skip for raster layers (https://github.com/mapbox/mapbox-gl-js/issues/7865) - this.sourceCaches[layer.source].getSource().type !== 'raster') { + sourceCache && + sourceCache.getSource().type !== 'raster') { this._updatedSources[layer.source] = 'reload'; - this.sourceCaches[layer.source].pause(); + sourceCache.pause(); } this._changed = true; } @@ -1093,7 +1179,7 @@ class Style extends Evented { return features; } - queryRenderedFeatures(queryGeometry: any, params: any, transform: Transform) { + queryRenderedFeatures(queryGeometry: PointLike | [PointLike, PointLike], params: any, transform: Transform) { if (params && params.filter) { this._validate(validateStyle.filter, 'queryRenderedFeatures.filter', params.filter, null, params); } @@ -1119,16 +1205,27 @@ class Style extends Evented { params.availableImages = this._availableImages; - for (const id in this.sourceCaches) { - if (params.layers && !includedSources[id]) continue; + const has3DLayer = (params && params.layers) ? + params.layers.some((layerId) => { + const layer = this.getLayer(layerId); + return layer && layer.is3D(); + }) : + this.has3DLayers(); + const queryGeometryStruct = QueryGeometry.createFromScreenPoints(queryGeometry, transform); + + for (const id in this._sourceCaches) { + const sourceId = this._sourceCaches[id].getSource().id; + if (params.layers && !includedSources[sourceId]) continue; sourceResults.push( queryRenderedFeatures( - this.sourceCaches[id], + this._sourceCaches[id], this._layers, this._serializedLayers, - queryGeometry, + queryGeometryStruct, params, - transform) + transform, + has3DLayer, + !!this.map._showQueryGeometry) ); } @@ -1139,8 +1236,8 @@ class Style extends Evented { queryRenderedSymbols( this._layers, this._serializedLayers, - this.sourceCaches, - queryGeometry, + this._getLayerSourceCache.bind(this), + queryGeometryStruct.screenGeometry, params, this.placement.collisionIndex, this.placement.retainedQueryData) @@ -1154,8 +1251,12 @@ class Style extends Evented { if (params && params.filter) { this._validate(validateStyle.filter, 'querySourceFeatures.filter', params.filter, null, params); } - const sourceCache = this.sourceCaches[sourceID]; - return sourceCache ? querySourceFeatures(sourceCache, params) : []; + const sourceCaches = this._getSourceCaches(sourceID); + let results = []; + for (const sourceCache of sourceCaches) { + results = results.concat(querySourceFeatures(sourceCache, params)); + } + return results; } addSourceType(name: string, SourceType: SourceClass, callback: Callback) { @@ -1204,6 +1305,78 @@ class Style extends Evented { this.light.updateTransitions(parameters); } + // eslint-disable-next-line no-warning-comments + // TODO: generic approach for root level property: light, terrain, skybox. + // It is not done here to prevent rebasing issues. + setTerrain(terrainOptions: TerrainSpecification) { + this._checkLoaded(); + + //Disabling + if (!terrainOptions) { + delete this.terrain; + delete this.stylesheet.terrain; + this.dispatcher.broadcast('enableTerrain', false); + this._force3DLayerUpdate(); + return; + } + + // Input validation and source object unrolling + if (typeof terrainOptions.source === 'object') { + const id = 'terrain-dem-src'; + this.addSource(id, ((terrainOptions.source): any)); + terrainOptions = clone(terrainOptions); + terrainOptions = (extend(terrainOptions, {source: id}): any); + } + if (this._validate(validateStyle.terrain, 'terrain', terrainOptions)) return; + + // Enabling + if (!this.terrain) { + this._createTerrain(terrainOptions); + } else { // Updating + const terrain = this.terrain; + const currSpec = terrain.get(); + for (const key in terrainOptions) { + if (!deepEqual(terrainOptions[key], currSpec[key])) { + terrain.set(terrainOptions); + this.stylesheet.terrain = terrainOptions; + const parameters = { + now: browser.now(), + transition: extend({ + duration: 0 + }, this.stylesheet.transition) + }; + + terrain.updateTransitions(parameters); + break; + } + } + } + } + + _createTerrain(terrainOptions: TerrainSpecification) { + const terrain = this.terrain = new Terrain(terrainOptions); + this.stylesheet.terrain = terrainOptions; + this.dispatcher.broadcast('enableTerrain', true); + this._force3DLayerUpdate(); + const parameters = { + now: browser.now(), + transition: extend({ + duration: 0 + }, this.stylesheet.transition) + }; + + terrain.updateTransitions(parameters); + } + + _force3DLayerUpdate() { + for (const layerId in this._layers) { + const layer = this._layers[layerId]; + if (layer.type === 'fill-extrusion') { + this._updateLayer(layer); + } + } + } + _validate(validate: Validator, key: string, value: any, props: any, options: { validate?: boolean } = {}) { if (options && options.validate === false) { return false; @@ -1230,9 +1403,9 @@ class Style extends Evented { const layer: StyleLayer = this._layers[layerId]; layer.setEventedParent(null); } - for (const id in this.sourceCaches) { - this.sourceCaches[id].clearTiles(); - this.sourceCaches[id].setEventedParent(null); + for (const id in this._sourceCaches) { + this._sourceCaches[id].clearTiles(); + this._sourceCaches[id].setEventedParent(null); } this.imageManager.setEventedParent(null); this.setEventedParent(null); @@ -1240,23 +1413,31 @@ class Style extends Evented { } _clearSource(id: string) { - this.sourceCaches[id].clearTiles(); + const sourceCaches = this._getSourceCaches(id); + for (const sourceCache of sourceCaches) { + sourceCache.clearTiles(); + } } _reloadSource(id: string) { - this.sourceCaches[id].resume(); - this.sourceCaches[id].reload(); + const sourceCaches = this._getSourceCaches(id); + for (const sourceCache of sourceCaches) { + sourceCache.resume(); + sourceCache.reload(); + } } _updateSources(transform: Transform) { - for (const id in this.sourceCaches) { - this.sourceCaches[id].update(transform); + for (const id in this._sourceCaches) { + this._sourceCaches[id].update(transform); } } _generateCollisionBoxes() { - for (const id in this.sourceCaches) { - this._reloadSource(id); + for (const id in this._sourceCaches) { + const sourceCache = this._sourceCaches[id]; + sourceCache.resume(); + sourceCache.reload(); } } @@ -1271,7 +1452,8 @@ class Style extends Evented { if (styleLayer.type !== 'symbol') continue; if (!layerTiles[styleLayer.source]) { - const sourceCache = this.sourceCaches[styleLayer.source]; + const sourceCache = this._getLayerSourceCache(styleLayer); + if (!sourceCache) continue; layerTiles[styleLayer.source] = sourceCache.getRenderableIds(true) .map((id) => sourceCache.getTileByID(id)) .sort((a, b) => (b.tileID.overscaledZ - a.tileID.overscaledZ) || (a.tileID.isLessThan(b.tileID) ? -1 : 1)); @@ -1331,8 +1513,8 @@ class Style extends Evented { } _releaseSymbolFadeTiles() { - for (const id in this.sourceCaches) { - this.sourceCaches[id].releaseSymbolFadeTiles(); + for (const id in this._sourceCaches) { + this._sourceCaches[id].releaseSymbolFadeTiles(); } } @@ -1352,10 +1534,13 @@ class Style extends Evented { // - the next frame triggers a reload of this tile even though it already has the latest version this._updateTilesForChangedImages(); - const sourceCache = this.sourceCaches[params.source]; - if (sourceCache) { - sourceCache.setDependencies(params.tileID.key, params.type, params.icons); - } + const setDependencies = (sourceCache: SourceCache) => { + if (sourceCache) { + sourceCache.setDependencies(params.tileID.key, params.type, params.icons); + } + }; + setDependencies(this._otherSourceCaches[params.source]); + setDependencies(this._symbolSourceCaches[params.source]); } getGlyphs(mapId: string, params: {stacks: {[_: string]: Array}}, callback: Callback<{[_: string]: {[_: number]: ?StyleGlyph}}>) { @@ -1365,6 +1550,31 @@ class Style extends Evented { getResource(mapId: string, params: RequestParameters, callback: ResponseCallback): Cancelable { return makeRequest(params, callback); } + + _getSourceCache(source: string): SourceCache | void { + return this._otherSourceCaches[source]; + } + + _getLayerSourceCache(layer: StyleLayer): SourceCache | void { + return layer.type === 'symbol' ? + this._symbolSourceCaches[layer.source] : + this._otherSourceCaches[layer.source]; + } + + _getSourceCaches(source: string): Array { + const sourceCaches = []; + if (this._otherSourceCaches[source]) { + sourceCaches.push(this._otherSourceCaches[source]); + } + if (this._symbolSourceCaches[source]) { + sourceCaches.push(this._symbolSourceCaches[source]); + } + return sourceCaches; + } + + has3DLayers(): boolean { + return this._num3DLayers > 0; + } } Style.getSourceType = getSourceType; diff --git a/src/style/style_layer.js b/src/style/style_layer.js index c2a7d6cb0cb..b12f8c3aa65 100644 --- a/src/style/style_layer.js +++ b/src/style/style_layer.js @@ -12,6 +12,7 @@ import { import {Evented} from '../util/evented'; import {Layout, Transitionable, Transitioning, Properties, PossiblyEvaluated, PossiblyEvaluatedPropertyValue} from './properties'; import {supportsPropertyExpression} from '../style-spec/util/properties'; +import ProgramConfiguration from '../data/program_configuration'; import type {FeatureState} from '../style-spec/expression'; import type {Bucket} from '../data/bucket'; @@ -27,6 +28,8 @@ import type { import type {CustomLayerInterface} from './style_layer/custom_style_layer'; import type Map from '../ui/map'; import type {StyleSetterOptions} from './style'; +import type {TilespaceQueryGeometry} from './query_geometry'; +import type {DEMSampler} from '../terrain/elevation'; const TRANSITION_SUFFIX = '-transition'; @@ -52,14 +55,14 @@ class StyleLayer extends Evented { _featureFilter: FeatureFilter; +queryRadius: (bucket: Bucket) => number; - +queryIntersectsFeature: (queryGeometry: Array, + +queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, feature: VectorTileFeature, featureState: FeatureState, geometry: Array>, zoom: number, transform: Transform, - pixelsToTileUnits: number, - pixelPosMatrix: Float32Array) => boolean | number; + pixelPosMatrix: Float32Array, + elevationHelper: ?DEMSampler) => boolean | number; +onAdd: ?(map: Map) => void; +onRemove: ?(map: Map) => void; @@ -79,7 +82,7 @@ class StyleLayer extends Evented { this.minzoom = layer.minzoom; this.maxzoom = layer.maxzoom; - if (layer.type !== 'background') { + if (layer.type !== 'background' && layer.type !== 'sky') { this.source = layer.source; this.sourceLayer = layer['source-layer']; this.filter = layer.filter; @@ -175,6 +178,16 @@ class StyleLayer extends Evented { // No-op; can be overridden by derived classes. } + getProgramIds(): string[] | null { + // No-op; can be overridden by derived classes. + return null; + } + + getProgramConfiguration(_: number): ProgramConfiguration | null { + // No-op; can be overridden by derived classes. + return null; + } + // eslint-disable-next-line no-unused-vars _handleOverridablePaintPropertyUpdate(name: string, oldValue: PropertyValue, newValue: PropertyValue): boolean { // No-op; can be overridden by derived classes. @@ -252,6 +265,10 @@ class StyleLayer extends Evented { return false; } + isSky() { + return false; + } + isTileClipped() { return false; } diff --git a/src/style/style_layer/background_style_layer.js b/src/style/style_layer/background_style_layer.js index 33a2ead347e..cce94b305aa 100644 --- a/src/style/style_layer/background_style_layer.js +++ b/src/style/style_layer/background_style_layer.js @@ -16,6 +16,11 @@ class BackgroundStyleLayer extends StyleLayer { constructor(layer: LayerSpecification) { super(layer, properties); } + + getProgramIds() { + const image = this.paint.get('background-pattern'); + return [image ? 'backgroundPattern' : 'background']; + } } export default BackgroundStyleLayer; diff --git a/src/style/style_layer/circle_style_layer.js b/src/style/style_layer/circle_style_layer.js index e5168f56a29..941fe755502 100644 --- a/src/style/style_layer/circle_style_layer.js +++ b/src/style/style_layer/circle_style_layer.js @@ -4,17 +4,22 @@ import StyleLayer from '../style_layer'; import CircleBucket from '../../data/bucket/circle_bucket'; import {polygonIntersectsBufferedPoint} from '../../util/intersection_tests'; -import {getMaximumPaintValue, translateDistance, translate} from '../query_utils'; +import {getMaximumPaintValue, translateDistance, tilespaceTranslate} from '../query_utils'; import properties from './circle_style_layer_properties'; import {Transitionable, Transitioning, Layout, PossiblyEvaluated} from '../properties'; -import {vec4} from 'gl-matrix'; +import {vec4, vec3} from 'gl-matrix'; import Point from '@mapbox/point-geometry'; +import ProgramConfiguration from '../../data/program_configuration'; +import {Ray} from '../../util/primitives'; +import assert from 'assert'; import type {FeatureState} from '../../style-spec/expression'; import type Transform from '../../geo/transform'; import type {Bucket, BucketParameters} from '../../data/bucket'; import type {LayoutProps, PaintProps} from './circle_style_layer_properties'; import type {LayerSpecification} from '../../style-spec/types'; +import type {TilespaceQueryGeometry} from '../query_geometry'; +import type {DEMSampler} from '../../terrain/elevation'; class CircleStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; @@ -39,37 +44,44 @@ class CircleStyleLayer extends StyleLayer { translateDistance(this.paint.get('circle-translate')); } - queryIntersectsFeature(queryGeometry: Array, + queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, feature: VectorTileFeature, featureState: FeatureState, geometry: Array>, zoom: number, transform: Transform, - pixelsToTileUnits: number, - pixelPosMatrix: Float32Array): boolean { - const translatedPolygon = translate(queryGeometry, - this.paint.get('circle-translate'), + pixelPosMatrix: Float32Array, + elevationHelper: ?DEMSampler): boolean { + const alignWithMap = this.paint.get('circle-pitch-alignment') === 'map'; + if (alignWithMap && queryGeometry.queryGeometry.isAboveHorizon) return false; + + const translation = tilespaceTranslate(this.paint.get('circle-translate'), this.paint.get('circle-translate-anchor'), - transform.angle, pixelsToTileUnits); + transform.angle, queryGeometry.pixelToTileUnitsFactor); const radius = this.paint.get('circle-radius').evaluate(feature, featureState); const stroke = this.paint.get('circle-stroke-width').evaluate(feature, featureState); - const size = radius + stroke; + const size = radius + stroke; // For pitch-alignment: map, compare feature geometry to query geometry in the plane of the tile // // Otherwise, compare geometry in the plane of the viewport // // A circle with fixed scaling relative to the viewport gets larger in tile space as it moves into the distance // // A circle with fixed scaling relative to the map gets smaller in viewport space as it moves into the distance - const alignWithMap = this.paint.get('circle-pitch-alignment') === 'map'; - const transformedPolygon = alignWithMap ? translatedPolygon : projectQueryGeometry(translatedPolygon, pixelPosMatrix); - const transformedSize = alignWithMap ? size * pixelsToTileUnits : size; + const transformedSize = alignWithMap ? size * queryGeometry.pixelToTileUnitsFactor : size; for (const ring of geometry) { for (const point of ring) { + const translatedPoint = point.add(translation); + const z = (elevationHelper && transform.elevation) ? + transform.elevation.exaggeration() * elevationHelper.getElevationAt(translatedPoint.x, translatedPoint.y, true) : + 0; - const transformedPoint = alignWithMap ? point : projectPoint(point, pixelPosMatrix); + const transformedPoint = alignWithMap ? translatedPoint : projectPoint(translatedPoint, z, pixelPosMatrix); + const transformedPolygon = alignWithMap ? + queryGeometry.tilespaceRays.map((r) => intersectAtHeight(r, z)) : + queryGeometry.queryGeometry.screenGeometry; let adjustedSize = transformedSize; - const projectedCenter = vec4.transformMat4([], [point.x, point.y, 0, 1], pixelPosMatrix); + const projectedCenter = vec4.transformMat4([], [point.x, point.y, z, 1], pixelPosMatrix); if (this.paint.get('circle-pitch-scale') === 'viewport' && this.paint.get('circle-pitch-alignment') === 'map') { adjustedSize *= projectedCenter[3] / transform.cameraToCenterDistance; } else if (this.paint.get('circle-pitch-scale') === 'map' && this.paint.get('circle-pitch-alignment') === 'viewport') { @@ -82,17 +94,31 @@ class CircleStyleLayer extends StyleLayer { return false; } + + getProgramIds() { + return ['circle']; + } + + getProgramConfiguration(zoom: number): ProgramConfiguration { + return new ProgramConfiguration(this, zoom); + } } -function projectPoint(p: Point, pixelPosMatrix: Float32Array) { - const point = vec4.transformMat4([], [p.x, p.y, 0, 1], pixelPosMatrix); +function projectPoint(p: Point, z: number, pixelPosMatrix: Float32Array) { + const point = vec4.transformMat4([], [p.x, p.y, z, 1], pixelPosMatrix); return new Point(point[0] / point[3], point[1] / point[3]); } -function projectQueryGeometry(queryGeometry: Array, pixelPosMatrix: Float32Array) { - return queryGeometry.map((p) => { - return projectPoint(p, pixelPosMatrix); - }); +const origin = vec3.fromValues(0, 0, 0); +const up = vec3.fromValues(0, 0, 1); + +function intersectAtHeight(r: Ray, z: number): Point { + const intersectionPt = vec3.create(); + origin[2] = z; + const intersects = r.intersectsPlane(origin, up, intersectionPt); + assert(intersects, 'tilespacePoint should always be below horizon, and since camera cannot have pitch >90, ray should always intersect'); + + return new Point(intersectionPt[0], intersectionPt[1]); } export default CircleStyleLayer; diff --git a/src/style/style_layer/fill_extrusion_style_layer.js b/src/style/style_layer/fill_extrusion_style_layer.js index 900879b8118..a3ec38ce33f 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.js +++ b/src/style/style_layer/fill_extrusion_style_layer.js @@ -4,17 +4,18 @@ import StyleLayer from '../style_layer'; import FillExtrusionBucket from '../../data/bucket/fill_extrusion_bucket'; import {polygonIntersectsPolygon, polygonIntersectsMultiPolygon} from '../../util/intersection_tests'; -import {translateDistance, translate} from '../query_utils'; +import {translateDistance, tilespaceTranslate} from '../query_utils'; import properties from './fill_extrusion_style_layer_properties'; import {Transitionable, Transitioning, PossiblyEvaluated} from '../properties'; -import {vec4} from 'gl-matrix'; import Point from '@mapbox/point-geometry'; +import ProgramConfiguration from '../../data/program_configuration'; import type {FeatureState} from '../../style-spec/expression'; import type {BucketParameters} from '../../data/bucket'; import type {PaintProps} from './fill_extrusion_style_layer_properties'; import type Transform from '../../geo/transform'; import type {LayerSpecification} from '../../style-spec/types'; +import type {TilespaceQueryGeometry} from '../query_geometry'; class FillExtrusionStyleLayer extends StyleLayer { _transitionablePaint: Transitionable; @@ -37,26 +38,34 @@ class FillExtrusionStyleLayer extends StyleLayer { return true; } - queryIntersectsFeature(queryGeometry: Array, + getProgramIds(): string[] { + const patternProperty = this.paint.get('fill-extrusion-pattern'); + const image = patternProperty.constantOr((1: any)); + return [image ? 'fillExtrusionPattern' : 'fillExtrusion']; + } + + getProgramConfiguration(zoom: number): ProgramConfiguration { + return new ProgramConfiguration(this, zoom); + } + + queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, feature: VectorTileFeature, featureState: FeatureState, geometry: Array>, zoom: number, transform: Transform, - pixelsToTileUnits: number, pixelPosMatrix: Float32Array): boolean | number { - const translatedPolygon = translate(queryGeometry, - this.paint.get('fill-extrusion-translate'), - this.paint.get('fill-extrusion-translate-anchor'), - transform.angle, pixelsToTileUnits); - + const translation = tilespaceTranslate(this.paint.get('fill-extrusion-translate'), + this.paint.get('fill-extrusion-translate-anchor'), + transform.angle, + queryGeometry.pixelToTileUnitsFactor); const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState); const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState); - const projectedQueryGeometry = projectQueryGeometry(translatedPolygon, pixelPosMatrix, transform, 0); + const projectedQueryGeometry = queryGeometry.queryGeometry.screenGeometry; - const projected = projectExtrusion(geometry, base, height, pixelPosMatrix); + const projected = projectExtrusion(geometry, base, height, translation, pixelPosMatrix); const projectedBase = projected[0]; const projectedTop = projected[1]; return checkIntersection(projectedBase, projectedTop, projectedQueryGeometry); @@ -162,7 +171,7 @@ function checkIntersection(projectedBase: Array, projectedTop: Array>, zBase: number, zTop: number, m: Float32Array) { +function projectExtrusion(geometry: Array>, zBase: number, zTop: number, translation: Point, m: Float32Array) { const projectedBase = []; const projectedTop = []; @@ -179,8 +188,8 @@ function projectExtrusion(geometry: Array>, zBase: number, zTop: nu const ringBase = []; const ringTop = []; for (const p of r) { - const x = p.x; - const y = p.y; + const x = p.x + translation.x; + const y = p.y + translation.y; const sX = m[0] * x + m[4] * y + m[12]; const sY = m[1] * x + m[5] * y + m[13]; @@ -211,14 +220,4 @@ function projectExtrusion(geometry: Array>, zBase: number, zTop: nu return [projectedBase, projectedTop]; } -function projectQueryGeometry(queryGeometry: Array, pixelPosMatrix: Float32Array, transform: Transform, z: number) { - const projectedQueryGeometry = []; - for (const p of queryGeometry) { - const v = [p.x, p.y, z, 1]; - vec4.transformMat4(v, v, pixelPosMatrix); - projectedQueryGeometry.push(new Point(v[0] / v[3], v[1] / v[3])); - } - return projectedQueryGeometry; -} - export default FillExtrusionStyleLayer; diff --git a/src/style/style_layer/fill_style_layer.js b/src/style/style_layer/fill_style_layer.js index 7afb6bca33d..7858af8c8cb 100644 --- a/src/style/style_layer/fill_style_layer.js +++ b/src/style/style_layer/fill_style_layer.js @@ -7,6 +7,7 @@ import {polygonIntersectsMultiPolygon} from '../../util/intersection_tests'; import {translateDistance, translate} from '../query_utils'; import properties from './fill_style_layer_properties'; import {Transitionable, Transitioning, Layout, PossiblyEvaluated} from '../properties'; +import ProgramConfiguration from '../../data/program_configuration'; import type {FeatureState} from '../../style-spec/expression'; import type {BucketParameters} from '../../data/bucket'; @@ -15,6 +16,7 @@ import type {LayoutProps, PaintProps} from './fill_style_layer_properties'; import type EvaluationParameters from '../evaluation_parameters'; import type Transform from '../../geo/transform'; import type {LayerSpecification} from '../../style-spec/types'; +import type {TilespaceQueryGeometry} from '../query_geometry'; class FillStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; @@ -28,6 +30,23 @@ class FillStyleLayer extends StyleLayer { super(layer, properties); } + getProgramIds(): string[] { + const pattern = this.paint.get('fill-pattern'); + const image = pattern && pattern.constantOr((1: any)); + + const ids = [image ? 'fillPattern' : 'fill']; + + if (this.paint.get('fill-antialias')) { + ids.push(image && !this.getPaintProperty('fill-outline-color') ? 'fillOutlinePattern' : 'fillOutline'); + } + + return ids; + } + + getProgramConfiguration(zoom: number): ProgramConfiguration { + return new ProgramConfiguration(this, zoom); + } + recalculate(parameters: EvaluationParameters, availableImages: Array) { super.recalculate(parameters, availableImages); @@ -45,17 +64,18 @@ class FillStyleLayer extends StyleLayer { return translateDistance(this.paint.get('fill-translate')); } - queryIntersectsFeature(queryGeometry: Array, + queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, feature: VectorTileFeature, featureState: FeatureState, geometry: Array>, zoom: number, - transform: Transform, - pixelsToTileUnits: number): boolean { - const translatedPolygon = translate(queryGeometry, + transform: Transform): boolean { + if (queryGeometry.queryGeometry.isAboveHorizon) return false; + + const translatedPolygon = translate(queryGeometry.tilespaceGeometry, this.paint.get('fill-translate'), this.paint.get('fill-translate-anchor'), - transform.angle, pixelsToTileUnits); + transform.angle, queryGeometry.pixelToTileUnitsFactor); return polygonIntersectsMultiPolygon(translatedPolygon, geometry); } diff --git a/src/style/style_layer/heatmap_style_layer.js b/src/style/style_layer/heatmap_style_layer.js index a8f115c798e..8efd0b220bc 100644 --- a/src/style/style_layer/heatmap_style_layer.js +++ b/src/style/style_layer/heatmap_style_layer.js @@ -12,6 +12,7 @@ import type Texture from '../../render/texture'; import type Framebuffer from '../../gl/framebuffer'; import type {PaintProps} from './heatmap_style_layer_properties'; import type {LayerSpecification} from '../../style-spec/types'; +import ProgramConfiguration from '../../data/program_configuration'; class HeatmapStyleLayer extends StyleLayer { @@ -68,6 +69,14 @@ class HeatmapStyleLayer extends StyleLayer { hasOffscreenPass() { return this.paint.get('heatmap-opacity') !== 0 && this.visibility !== 'none'; } + + getProgramIds() { + return ['heatmap', 'heatmapTexture']; + } + + getProgramConfiguration(zoom: number): ProgramConfiguration { + return new ProgramConfiguration(this, zoom); + } } export default HeatmapStyleLayer; diff --git a/src/style/style_layer/hillshade_style_layer.js b/src/style/style_layer/hillshade_style_layer.js index 782ebe3d564..908c15f04c0 100644 --- a/src/style/style_layer/hillshade_style_layer.js +++ b/src/style/style_layer/hillshade_style_layer.js @@ -7,6 +7,7 @@ import {Transitionable, Transitioning, PossiblyEvaluated} from '../properties'; import type {PaintProps} from './hillshade_style_layer_properties'; import type {LayerSpecification} from '../../style-spec/types'; +import ProgramConfiguration from '../../data/program_configuration'; class HillshadeStyleLayer extends StyleLayer { _transitionablePaint: Transitionable; @@ -20,6 +21,14 @@ class HillshadeStyleLayer extends StyleLayer { hasOffscreenPass() { return this.paint.get('hillshade-exaggeration') !== 0 && this.visibility !== 'none'; } + + getProgramIds() { + return ['hillshade', 'hillshadePrepare']; + } + + getProgramConfiguration(zoom: number): ProgramConfiguration { + return new ProgramConfiguration(this, zoom); + } } export default HillshadeStyleLayer; diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index 480975c7428..5be939cb9ca 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -10,6 +10,7 @@ import properties from './line_style_layer_properties'; import {extend, MAX_SAFE_INTEGER} from '../../util/util'; import EvaluationParameters from '../evaluation_parameters'; import {Transitionable, Transitioning, Layout, PossiblyEvaluated, DataDrivenProperty} from '../properties'; +import ProgramConfiguration from '../../data/program_configuration'; import Step from '../../style-spec/expression/definitions/step'; import type {FeatureState, ZoomConstantExpression} from '../../style-spec/expression'; @@ -17,6 +18,7 @@ import type {Bucket, BucketParameters} from '../../data/bucket'; import type {LayoutProps, PaintProps} from './line_style_layer_properties'; import type Transform from '../../geo/transform'; import type {LayerSpecification} from '../../style-spec/types'; +import type {TilespaceQueryGeometry} from '../query_geometry'; class LineFloorwidthProperty extends DataDrivenProperty { useIntegerZoom: true; @@ -79,6 +81,22 @@ class LineStyleLayer extends StyleLayer { return new LineBucket(parameters); } + getProgramIds(): string[] { + const dasharray = this.paint.get('line-dasharray'); + const patternProperty = this.paint.get('line-pattern'); + const image = patternProperty.constantOr((1: any)); + const gradient = this.paint.get('line-gradient'); + const programId = + image ? 'linePattern' : + dasharray ? 'lineSDF' : + gradient ? 'lineGradient' : 'line'; + return [programId]; + } + + getProgramConfiguration(zoom: number): ProgramConfiguration { + return new ProgramConfiguration(this, zoom); + } + queryRadius(bucket: Bucket): number { const lineBucket: LineBucket = (bucket: any); const width = getLineWidth( @@ -88,23 +106,24 @@ class LineStyleLayer extends StyleLayer { return width / 2 + Math.abs(offset) + translateDistance(this.paint.get('line-translate')); } - queryIntersectsFeature(queryGeometry: Array, + queryIntersectsFeature(queryGeometry: TilespaceQueryGeometry, feature: VectorTileFeature, featureState: FeatureState, geometry: Array>, zoom: number, - transform: Transform, - pixelsToTileUnits: number): boolean { - const translatedPolygon = translate(queryGeometry, + transform: Transform): boolean { + if (queryGeometry.queryGeometry.isAboveHorizon) return false; + + const translatedPolygon = translate(queryGeometry.tilespaceGeometry, this.paint.get('line-translate'), this.paint.get('line-translate-anchor'), - transform.angle, pixelsToTileUnits); - const halfWidth = pixelsToTileUnits / 2 * getLineWidth( + transform.angle, queryGeometry.pixelToTileUnitsFactor); + const halfWidth = queryGeometry.pixelToTileUnitsFactor / 2 * getLineWidth( this.paint.get('line-width').evaluate(feature, featureState), this.paint.get('line-gap-width').evaluate(feature, featureState)); const lineOffset = this.paint.get('line-offset').evaluate(feature, featureState); if (lineOffset) { - geometry = offsetLine(geometry, lineOffset * pixelsToTileUnits); + geometry = offsetLine(geometry, lineOffset * queryGeometry.pixelToTileUnitsFactor); } return polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth); diff --git a/src/style/style_layer/raster_style_layer.js b/src/style/style_layer/raster_style_layer.js index 33033909cb6..5f22edb3592 100644 --- a/src/style/style_layer/raster_style_layer.js +++ b/src/style/style_layer/raster_style_layer.js @@ -16,6 +16,10 @@ class RasterStyleLayer extends StyleLayer { constructor(layer: LayerSpecification) { super(layer, properties); } + + getProgramIds() { + return ['raster']; + } } export default RasterStyleLayer; diff --git a/src/style/style_layer/sky_style_layer.js b/src/style/style_layer/sky_style_layer.js new file mode 100644 index 00000000000..e218d4ed4d8 --- /dev/null +++ b/src/style/style_layer/sky_style_layer.js @@ -0,0 +1,132 @@ +// @flow + +import StyleLayer from '../style_layer'; +import properties from './sky_style_layer_properties'; +import {Transitionable, Transitioning, PossiblyEvaluated} from '../properties'; +import {renderColorRamp} from '../../util/color_ramp'; +import type {PaintProps} from './sky_style_layer_properties'; +import type Texture from '../../render/texture'; +import type Painter from '../../render/painter'; +import type {LayerSpecification} from '../../style-spec/types'; +import type Framebuffer from '../../gl/framebuffer'; +import type {RGBAImage} from '../../util/image'; +import type SkyboxGeometry from '../../render/skybox_geometry'; +import type {LightPosition} from '../light'; +import {warnOnce, degToRad} from '../../util/util'; +import {vec3, quat} from 'gl-matrix'; + +function getCelestialDirection(azimuth: number, altitude: number, leftHanded: boolean): vec3 { + const up = vec3.fromValues(0, 0, 1); + const rotation = quat.identity(quat.create()); + + quat.rotateY(rotation, rotation, leftHanded ? -degToRad(azimuth) + Math.PI : degToRad(azimuth)); + quat.rotateX(rotation, rotation, -degToRad(altitude)); + vec3.transformQuat(up, up, rotation); + + return vec3.normalize(up, up); +} + +class SkyLayer extends StyleLayer { + _transitionablePaint: Transitionable; + _transitioningPaint: Transitioning; + paint: PossiblyEvaluated; + _lightPosition: LightPosition; + + skyboxFbo: ?Framebuffer; + skyboxTexture: ?WebGLTexture; + _skyboxInvalidated: ?boolean; + + colorRamp: RGBAImage; + colorRampTexture: ?Texture; + + skyboxGeometry: SkyboxGeometry; + + constructor(layer: LayerSpecification) { + super(layer, properties); + this._updateColorRamp(); + } + + _handleSpecialPaintPropertyUpdate(name: string) { + if (name === 'sky-gradient') { + this._updateColorRamp(); + } else if (name === 'sky-atmosphere-sun' || + name === 'sky-atmosphere-halo-color' || + name === 'sky-atmosphere-color' || + name === 'sky-atmosphere-sun-intensity') { + this._skyboxInvalidated = true; + } + } + + _updateColorRamp() { + const expression = this._transitionablePaint._values['sky-gradient'].value.expression; + this.colorRamp = renderColorRamp({ + expression, + evaluationKey: 'skyRadialProgress' + }); + if (this.colorRampTexture) { + this.colorRampTexture.destroy(); + this.colorRampTexture = null; + } + } + + needsSkyboxCapture(painter: Painter) { + if (!!this._skyboxInvalidated || !this.skyboxTexture || !this.skyboxGeometry) { + return true; + } + if (!this.paint.get('sky-atmosphere-sun')) { + const lightPosition = painter.style.light.properties.get('position'); + return this._lightPosition.azimuthal !== lightPosition.azimuthal || + this._lightPosition.polar !== lightPosition.polar; + } + } + + getCenter(painter: Painter, leftHanded: boolean) { + const type = this.paint.get('sky-type'); + if (type === 'atmosphere') { + const sunPosition = this.paint.get('sky-atmosphere-sun'); + const useLightPosition = !sunPosition; + const light = painter.style.light; + const lightPosition = light.properties.get('position'); + + if (useLightPosition && light.properties.get('anchor') === 'viewport') { + warnOnce('The sun direction is attached to a light with viewport anchor, lighting may behave unexpectedly.'); + } + + return useLightPosition ? + getCelestialDirection(lightPosition.azimuthal, -lightPosition.polar + 90, leftHanded) : + getCelestialDirection(sunPosition[0], -sunPosition[1] + 90, leftHanded); + } else if (type === 'gradient') { + const direction = this.paint.get('sky-gradient-center'); + return getCelestialDirection(direction[0], -direction[1] + 90, leftHanded); + } + } + + is3D() { + return false; + } + + isSky() { + return true; + } + + markSkyboxValid(painter: Painter) { + this._skyboxInvalidated = false; + this._lightPosition = painter.style.light.properties.get('position'); + } + + hasOffscreenPass() { + return true; + } + + getProgramIds(): string[] | null { + const type = this.paint.get('sky-type'); + if (type === 'atmosphere') { + return ['skyboxCapture', 'skybox']; + } else if (type === 'gradient') { + return ['skyboxGradient']; + } + return null; + } +} + +export default SkyLayer; diff --git a/src/style/style_layer/sky_style_layer_properties.js b/src/style/style_layer/sky_style_layer_properties.js new file mode 100644 index 00000000000..190fec6d482 --- /dev/null +++ b/src/style/style_layer/sky_style_layer_properties.js @@ -0,0 +1,52 @@ +// This file is generated. Edit build/generate-style-code.js, then run `yarn run codegen`. +// @flow +/* eslint-disable */ + +import styleSpec from '../../style-spec/reference/latest'; + +import { + Properties, + DataConstantProperty, + DataDrivenProperty, + CrossFadedDataDrivenProperty, + CrossFadedProperty, + ColorRampProperty +} from '../properties'; + +import type Color from '../../style-spec/util/color'; + +import type Formatted from '../../style-spec/expression/types/formatted'; + +import type ResolvedImage from '../../style-spec/expression/types/resolved_image'; + + +export type PaintProps = {| + "sky-type": DataConstantProperty<"gradient" | "atmosphere">, + "sky-atmosphere-sun": DataConstantProperty<[number, number]>, + "sky-atmosphere-sun-intensity": DataConstantProperty, + "sky-gradient-center": DataConstantProperty<[number, number]>, + "sky-gradient-radius": DataConstantProperty, + "sky-gradient": ColorRampProperty, + "sky-atmosphere-halo-color": DataConstantProperty, + "sky-atmosphere-color": DataConstantProperty, + "sky-opacity": DataConstantProperty, +|}; + +const paint: Properties = new Properties({ + "sky-type": new DataConstantProperty(styleSpec["paint_sky"]["sky-type"]), + "sky-atmosphere-sun": new DataConstantProperty(styleSpec["paint_sky"]["sky-atmosphere-sun"]), + "sky-atmosphere-sun-intensity": new DataConstantProperty(styleSpec["paint_sky"]["sky-atmosphere-sun-intensity"]), + "sky-gradient-center": new DataConstantProperty(styleSpec["paint_sky"]["sky-gradient-center"]), + "sky-gradient-radius": new DataConstantProperty(styleSpec["paint_sky"]["sky-gradient-radius"]), + "sky-gradient": new ColorRampProperty(styleSpec["paint_sky"]["sky-gradient"]), + "sky-atmosphere-halo-color": new DataConstantProperty(styleSpec["paint_sky"]["sky-atmosphere-halo-color"]), + "sky-atmosphere-color": new DataConstantProperty(styleSpec["paint_sky"]["sky-atmosphere-color"]), + "sky-opacity": new DataConstantProperty(styleSpec["paint_sky"]["sky-opacity"]), +}); + +// Note: without adding the explicit type annotation, Flow infers weaker types +// for these objects from their use in the constructor to StyleLayer, as +// {layout?: Properties<...>, paint: Properties<...>} +export default ({ paint }: $Exact<{ + paint: Properties +}>); diff --git a/src/style/style_layer/symbol_style_layer.js b/src/style/style_layer/symbol_style_layer.js index 37254264509..b77d1b294dc 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -36,6 +36,7 @@ import Formatted from '../../style-spec/expression/types/formatted'; import FormatSectionOverride from '../format_section_override'; import FormatExpression from '../../style-spec/expression/definitions/format'; import Literal from '../../style-spec/expression/definitions/literal'; +import ProgramConfiguration from '../../data/program_configuration'; class SymbolStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; @@ -185,6 +186,10 @@ class SymbolStyleLayer extends StyleLayer { return hasOverrides; } + + getProgramConfiguration(zoom: number): ProgramConfiguration { + return new ProgramConfiguration(this, zoom); + } } export default SymbolStyleLayer; diff --git a/src/style/terrain.js b/src/style/terrain.js new file mode 100644 index 00000000000..2b24eff7d3d --- /dev/null +++ b/src/style/terrain.js @@ -0,0 +1,64 @@ +// @flow + +import styleSpec from '../style-spec/reference/latest'; +import {endsWith} from '../util/util'; +import {Evented} from '../util/evented'; +import {Properties, Transitionable, Transitioning, PossiblyEvaluated, DataConstantProperty} from './properties'; + +import type EvaluationParameters from './evaluation_parameters'; +import type {TransitionParameters} from './properties'; +import type {TerrainSpecification} from '../style-spec/types'; + +type Props = {| + "source": DataConstantProperty, + "exaggeration": DataConstantProperty, +|}; + +const properties: Properties = new Properties({ + "source": new DataConstantProperty(styleSpec.terrain.source), + "exaggeration": new DataConstantProperty(styleSpec.terrain.exaggeration), +}); + +const TRANSITION_SUFFIX = '-transition'; + +class Terrain extends Evented { + _transitionable: Transitionable; + _transitioning: Transitioning; + properties: PossiblyEvaluated; + + constructor(terrainOptions: TerrainSpecification) { + super(); + this._transitionable = new Transitionable(properties); + this.set(terrainOptions); + this._transitioning = this._transitionable.untransitioned(); + } + + get() { + return this._transitionable.serialize(); + } + + set(terrain: TerrainSpecification) { + for (const name in terrain) { + const value = terrain[name]; + if (endsWith(name, TRANSITION_SUFFIX)) { + this._transitionable.setTransition(name.slice(0, -TRANSITION_SUFFIX.length), value); + } else { + this._transitionable.setValue(name, value); + } + } + } + + updateTransitions(parameters: TransitionParameters) { + this._transitioning = this._transitionable.transitioned(parameters, this._transitioning); + } + + hasTransition() { + return this._transitioning.hasTransition(); + } + + recalculate(parameters: EvaluationParameters) { + this.properties = this._transitioning.possiblyEvaluate(parameters); + } +} + +export default Terrain; diff --git a/src/style/validate_style.js b/src/style/validate_style.js index a84a38de698..1260502ce0d 100644 --- a/src/style/validate_style.js +++ b/src/style/validate_style.js @@ -17,6 +17,7 @@ type ValidateStyle = { source: Validator, layer: Validator, light: Validator, + terrain: Validator, filter: Validator, paintProperty: Validator, layoutProperty: Validator @@ -26,6 +27,7 @@ export const validateStyle = (validateStyleMin: ValidateStyle); export const validateSource = validateStyle.source; export const validateLight = validateStyle.light; +export const validateTerrain = validateStyle.terrain; export const validateFilter = validateStyle.filter; export const validatePaintProperty = validateStyle.paintProperty; export const validateLayoutProperty = validateStyle.layoutProperty; diff --git a/src/symbol/collision_feature.js b/src/symbol/collision_feature.js deleted file mode 100644 index faf3eb48a59..00000000000 --- a/src/symbol/collision_feature.js +++ /dev/null @@ -1,109 +0,0 @@ -// @flow - -import type {CollisionBoxArray} from '../data/array_types'; -import Point from '@mapbox/point-geometry'; -import type Anchor from './anchor'; - -/** - * A CollisionFeature represents the area of the tile covered by a single label. - * It is used with CollisionIndex to check if the label overlaps with any - * previous labels. A CollisionFeature is mostly just a set of CollisionBox - * objects. - * - * @private - */ -class CollisionFeature { - boxStartIndex: number; - boxEndIndex: number; - circleDiameter: ?number; - - /** - * Create a CollisionFeature, adding its collision box data to the given collisionBoxArray in the process. - * For line aligned labels a collision circle diameter is computed instead. - * - * @param anchor The point along the line around which the label is anchored. - * @param shaped The text or icon shaping results. - * @param boxScale A magic number used to convert from glyph metrics units to geometry units. - * @param padding The amount of padding to add around the label edges. - * @param alignLine Whether the label is aligned with the line or the viewport. - * @private - */ - constructor(collisionBoxArray: CollisionBoxArray, - anchor: Anchor, - featureIndex: number, - sourceLayerIndex: number, - bucketIndex: number, - shaped: Object, - boxScale: number, - padding: number, - alignLine: boolean, - rotate: number) { - - this.boxStartIndex = collisionBoxArray.length; - - if (alignLine) { - // Compute height of the shape in glyph metrics and apply collision padding. - // Note that the pixel based 'text-padding' is applied at runtime - let top = shaped.top; - let bottom = shaped.bottom; - const collisionPadding = shaped.collisionPadding; - - if (collisionPadding) { - top -= collisionPadding[1]; - bottom += collisionPadding[3]; - } - - let height = bottom - top; - - if (height > 0) { - // set minimum box height to avoid very many small labels - height = Math.max(10, height); - this.circleDiameter = height; - } - } else { - let y1 = shaped.top * boxScale - padding; - let y2 = shaped.bottom * boxScale + padding; - let x1 = shaped.left * boxScale - padding; - let x2 = shaped.right * boxScale + padding; - - const collisionPadding = shaped.collisionPadding; - if (collisionPadding) { - x1 -= collisionPadding[0] * boxScale; - y1 -= collisionPadding[1] * boxScale; - x2 += collisionPadding[2] * boxScale; - y2 += collisionPadding[3] * boxScale; - } - - if (rotate) { - // Account for *-rotate in point collision boxes - // See https://github.com/mapbox/mapbox-gl-js/issues/6075 - // Doesn't account for icon-text-fit - - const tl = new Point(x1, y1); - const tr = new Point(x2, y1); - const bl = new Point(x1, y2); - const br = new Point(x2, y2); - - const rotateRadians = rotate * Math.PI / 180; - - tl._rotate(rotateRadians); - tr._rotate(rotateRadians); - bl._rotate(rotateRadians); - br._rotate(rotateRadians); - - // Collision features require an "on-axis" geometry, - // so take the envelope of the rotated geometry - // (may be quite large for wide labels rotated 45 degrees) - x1 = Math.min(tl.x, tr.x, bl.x, br.x); - x2 = Math.max(tl.x, tr.x, bl.x, br.x); - y1 = Math.min(tl.y, tr.y, bl.y, br.y); - y2 = Math.max(tl.y, tr.y, bl.y, br.y); - } - collisionBoxArray.emplaceBack(anchor.x, anchor.y, x1, y1, x2, y2, featureIndex, sourceLayerIndex, bucketIndex); - } - - this.boxEndIndex = collisionBoxArray.length; - } -} - -export default CollisionFeature; diff --git a/src/symbol/collision_index.js b/src/symbol/collision_index.js index f553e610761..ff5fd23349a 100644 --- a/src/symbol/collision_index.js +++ b/src/symbol/collision_index.js @@ -6,18 +6,18 @@ import PathInterpolator from './path_interpolator'; import * as intersectionTests from '../util/intersection_tests'; import Grid from './grid_index'; -import {mat4} from 'gl-matrix'; +import {mat4, vec4} from 'gl-matrix'; import ONE_EM from '../symbol/one_em'; import assert from 'assert'; import * as projection from '../symbol/projection'; - import type Transform from '../geo/transform'; import type {SingleCollisionBox} from '../data/bucket/symbol_bucket'; import type { GlyphOffsetArray, SymbolLineVertexArray } from '../data/array_types'; +import {OverscaledTileID} from '../source/tile_id'; // When a symbol crosses the edge that causes it to be included in // collision detection, it will cause changes in the symbols around @@ -66,16 +66,24 @@ class CollisionIndex { this.gridBottomBoundary = transform.height + 2 * viewportPadding; } - placeCollisionBox(collisionBox: SingleCollisionBox, allowOverlap: boolean, textPixelRatio: number, posMatrix: mat4, collisionGroupPredicate?: any): { box: Array, offscreen: boolean } { - const projectedPoint = this.projectAndGetPerspectiveRatio(posMatrix, collisionBox.anchorPointX, collisionBox.anchorPointY); + placeCollisionBox(scale: number, collisionBox: SingleCollisionBox, shift: Point, allowOverlap: boolean, textPixelRatio: number, posMatrix: mat4, collisionGroupPredicate?: any): { box: Array, offscreen: boolean } { + assert(!this.transform.elevation || collisionBox.elevation !== undefined); + const projectedPoint = this.projectAndGetPerspectiveRatio(posMatrix, collisionBox.anchorPointX, collisionBox.anchorPointY, collisionBox.elevation); const tileToViewport = textPixelRatio * projectedPoint.perspectiveRatio; - const tlX = collisionBox.x1 * tileToViewport + projectedPoint.point.x; - const tlY = collisionBox.y1 * tileToViewport + projectedPoint.point.y; - const brX = collisionBox.x2 * tileToViewport + projectedPoint.point.x; - const brY = collisionBox.y2 * tileToViewport + projectedPoint.point.y; + const tlX = (collisionBox.x1 * scale + shift.x - collisionBox.padding) * tileToViewport + projectedPoint.point.x; + const tlY = (collisionBox.y1 * scale + shift.y - collisionBox.padding) * tileToViewport + projectedPoint.point.y; + const brX = (collisionBox.x2 * scale + shift.x + collisionBox.padding) * tileToViewport + projectedPoint.point.x; + const brY = (collisionBox.y2 * scale + shift.y + collisionBox.padding) * tileToViewport + projectedPoint.point.y; + // Clip at 10 times the distance of the map center or, said otherwise, when the label + // would be drawn at 10% the size of the features around it without scaling. Refer: + // https://github.com/mapbox/mapbox-gl-native/wiki/Text-Rendering#perspective-scaling + // 0.55 === projection.getPerspectiveRatio(camera_to_center, camera_to_center * 10) + const minPerspectiveRatio = 0.55; + const isClipped = projectedPoint.perspectiveRatio <= minPerspectiveRatio; if (!this.isInsideGrid(tlX, tlY, brX, brY) || - (!allowOverlap && this.grid.hitTest(tlX, tlY, brX, brY, collisionGroupPredicate))) { + (!allowOverlap && this.grid.hitTest(tlX, tlY, brX, brY, collisionGroupPredicate)) || + isClipped) { return { box: [], offscreen: false @@ -100,22 +108,26 @@ class CollisionIndex { pitchWithMap: boolean, collisionGroupPredicate?: any, circlePixelDiameter: number, - textPixelPadding: number): { circles: Array, offscreen: boolean, collisionDetected: boolean } { + textPixelPadding: number, + tileID: OverscaledTileID): { circles: Array, offscreen: boolean, collisionDetected: boolean } { const placedCollisionCircles = []; + const elevation = this.transform.elevation; + const getElevation = elevation ? (p => elevation.getAtTileOffset(tileID, p.x, p.y)) : (_ => 0); const tileUnitAnchorPoint = new Point(symbol.anchorX, symbol.anchorY); - const screenAnchorPoint = projection.project(tileUnitAnchorPoint, posMatrix); + const anchorElevation = getElevation(tileUnitAnchorPoint); + const screenAnchorPoint = projection.project(tileUnitAnchorPoint, posMatrix, anchorElevation); const perspectiveRatio = projection.getPerspectiveRatio(this.transform.cameraToCenterDistance, screenAnchorPoint.signedDistanceFromCamera); const labelPlaneFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio; const labelPlaneFontScale = labelPlaneFontSize / ONE_EM; - const labelPlaneAnchorPoint = projection.project(tileUnitAnchorPoint, labelPlaneMatrix).point; + const labelPlaneAnchorPoint = projection.project(tileUnitAnchorPoint, labelPlaneMatrix, anchorElevation).point; const projectionCache = {}; const lineOffsetX = symbol.lineOffsetX * labelPlaneFontScale; const lineOffsetY = symbol.lineOffsetY * labelPlaneFontScale; - const firstAndLastGlyph = projection.placeFirstAndLastGlyph( + const firstAndLastGlyph = screenAnchorPoint.signedDistanceFromCamera > 0 ? projection.placeFirstAndLastGlyph( labelPlaneFontScale, glyphOffsetArray, lineOffsetX, @@ -126,7 +138,10 @@ class CollisionIndex { symbol, lineVertexArray, labelPlaneMatrix, - projectionCache); + projectionCache, + elevation && !pitchWithMap ? getElevation : null, // pitchWithMap: no need to sample elevation as it has no effect when projecting using scale/rotate to tile space labelPlaneMatrix. + pitchWithMap && !!elevation + ) : null; let collisionDetected = false; let inGrid = false; @@ -156,7 +171,13 @@ class CollisionIndex { // The path might need to be converted into screen space if a pitched map is used as the label space if (labelToScreenMatrix) { - const screenSpacePath = projectedPath.map(p => projection.project(p, labelToScreenMatrix)); + assert(pitchWithMap); + const screenSpacePath = elevation ? + projectedPath.map((p, index) => { + const z = getElevation(index < first.path.length - 1 ? first.tilePath[first.path.length - 1 - index] : last.tilePath[index - first.path.length + 2]); + return projection.project(p, labelToScreenMatrix, z); + }) : + projectedPath.map(p => projection.project(p, labelToScreenMatrix)); // Do not try to place collision circles if even of the points is behind the camera. // This is a plausible scenario with big camera pitch angles @@ -334,9 +355,14 @@ class CollisionIndex { } } - projectAndGetPerspectiveRatio(posMatrix: mat4, x: number, y: number) { + projectAndGetPerspectiveRatio(posMatrix: mat4, x: number, y: number, elevation?: number) { const p = [x, y, 0, 1]; - projection.xyTransformMat4(p, p, posMatrix); + if (elevation) { + p[2] = elevation; + vec4.transformMat4(p, p, posMatrix); + } else { + projection.xyTransformMat4(p, p, posMatrix); + } const a = new Point( (((p[0] / p[3] + 1) / 2) * this.transform.width) + viewportPadding, (((-p[1] / p[3] + 1) / 2) * this.transform.height) + viewportPadding @@ -346,7 +372,7 @@ class CollisionIndex { // See perspective ratio comment in symbol_sdf.vertex // We're doing collision detection in viewport space so we need // to scale down boxes in the distance - perspectiveRatio: 0.5 + 0.5 * (this.transform.cameraToCenterDistance / p[3]) + perspectiveRatio: Math.min(0.5 + 0.5 * (this.transform.cameraToCenterDistance / p[3]), 1.5) }; } diff --git a/src/symbol/placement.js b/src/symbol/placement.js index b7e06fb865a..3d7ec4ee5aa 100644 --- a/src/symbol/placement.js +++ b/src/symbol/placement.js @@ -12,7 +12,6 @@ import pixelsToTileUnits from '../source/pixels_to_tile_units'; import Point from '@mapbox/point-geometry'; import type Transform from '../geo/transform'; import type StyleLayer from '../style/style_layer'; - import type Tile from '../source/tile'; import type SymbolBucket, {CollisionArrays, SingleCollisionBox} from '../data/bucket/symbol_bucket'; import type {CollisionBoxArray, CollisionVertexArray, SymbolInstance} from '../data/array_types'; @@ -141,24 +140,12 @@ function calculateVariableLayoutShift(anchor: TextAnchor, width: number, height: ); } -function shiftVariableCollisionBox(collisionBox: SingleCollisionBox, - shiftX: number, shiftY: number, - rotateWithMap: boolean, pitchWithMap: boolean, - angle: number) { - const {x1, x2, y1, y2, anchorPointX, anchorPointY} = collisionBox; - const rotatedOffset = new Point(shiftX, shiftY); +function offsetShift(shiftX: number, shiftY: number, rotateWithMap: boolean, pitchWithMap: boolean, angle: number): Point { + const shift = new Point(shiftX, shiftY); if (rotateWithMap) { - rotatedOffset._rotate(pitchWithMap ? angle : -angle); + shift._rotate(pitchWithMap ? angle : -angle); } - return { - x1: x1 + rotatedOffset.x, - y1: y1 + rotatedOffset.y, - x2: x2 + rotatedOffset.x, - y2: y2 + rotatedOffset.y, - // symbol anchor point stays the same regardless of text-anchor - anchorPointX, - anchorPointY - }; + return shift; } export type VariableOffset = { @@ -291,6 +278,7 @@ export class Placement { holdingForFade: tile.holdingForFade(), collisionBoxArray, partiallyEvaluatedTextSize: symbolSize.evaluateSizeForZoom(symbolBucket.textSizeData, this.transform.zoom), + partiallyEvaluatedIconSize: symbolSize.evaluateSizeForZoom(symbolBucket.iconSizeData, this.transform.zoom), collisionGroup: this.collisionGroups.get(symbolBucket.sourceID) }; @@ -309,24 +297,23 @@ export class Placement { } attemptAnchorPlacement(anchor: TextAnchor, textBox: SingleCollisionBox, width: number, height: number, - textBoxScale: number, rotateWithMap: boolean, - pitchWithMap: boolean, textPixelRatio: number, posMatrix: mat4, collisionGroup: CollisionGroup, - textAllowOverlap: boolean, symbolInstance: SymbolInstance, bucket: SymbolBucket, orientation: number, iconBox: ?SingleCollisionBox): ?{ shift: Point, placedGlyphBoxes: { box: Array, offscreen: boolean } } { + textBoxScale: number, rotateWithMap: boolean, pitchWithMap: boolean, textPixelRatio: number, + posMatrix: mat4, collisionGroup: CollisionGroup, textAllowOverlap: boolean, + symbolInstance: SymbolInstance, symbolIndex: number, bucket: SymbolBucket, + orientation: number, iconBox: ?SingleCollisionBox, textSize: any, iconSize: any): ?{ shift: Point, placedGlyphBoxes: { box: Array, offscreen: boolean } } { const textOffset = [symbolInstance.textOffset0, symbolInstance.textOffset1]; + const textScale = bucket.getSymbolInstanceTextSize(textSize, symbolInstance, this.transform.zoom, symbolIndex); const shift = calculateVariableLayoutShift(anchor, width, height, textOffset, textBoxScale); const placedGlyphBoxes = this.collisionIndex.placeCollisionBox( - shiftVariableCollisionBox( - textBox, shift.x, shift.y, - rotateWithMap, pitchWithMap, this.transform.angle), + textScale, textBox, offsetShift(shift.x, shift.y, rotateWithMap, pitchWithMap, this.transform.angle), textAllowOverlap, textPixelRatio, posMatrix, collisionGroup.predicate); if (iconBox) { const placedIconBoxes = this.collisionIndex.placeCollisionBox( - shiftVariableCollisionBox( - iconBox, shift.x, shift.y, - rotateWithMap, pitchWithMap, this.transform.angle), + bucket.getSymbolInstanceIconSize(iconSize, this.transform.zoom, symbolIndex), + iconBox, offsetShift(shift.x, shift.y, rotateWithMap, pitchWithMap, this.transform.angle), textAllowOverlap, textPixelRatio, posMatrix, collisionGroup.predicate); if (placedIconBoxes.box.length === 0) return; } @@ -373,6 +360,7 @@ export class Placement { holdingForFade, collisionBoxArray, partiallyEvaluatedTextSize, + partiallyEvaluatedIconSize, collisionGroup } = bucketPart.parameters; @@ -406,7 +394,11 @@ export class Placement { bucket.deserializeCollisionBoxes(collisionBoxArray); } - const placeSymbol = (symbolInstance: SymbolInstance, collisionArrays: CollisionArrays) => { + if (showCollisionBoxes) { + bucket.updateCollisionDebugBuffers(this.transform.zoom, collisionBoxArray); + } + + const placeSymbol = (symbolInstance: SymbolInstance, symbolIndex: number, collisionArrays: CollisionArrays) => { if (seenCrossTileIDs[symbolInstance.crossTileID]) return; if (holdingForFade) { // Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't @@ -439,9 +431,16 @@ export class Placement { verticalTextFeatureIndex = collisionArrays.verticalTextFeatureIndex; } + const updateElevation = (box: SingleCollisionBox) => { + if (!this.transform.elevation && !box.elevation) return; + box.elevation = this.transform.elevation ? this.transform.elevation.getAtTileOffset( + this.retainedQueryData[bucket.bucketInstanceId].tileID, + box.anchorPointX, box.anchorPointY) : 0; + }; + const textBox = collisionArrays.textBox; if (textBox) { - + updateElevation(textBox); const updatePreviousOrientationIfNotPlaced = (isPlaced) => { let previousOrientation = WritingMode.horizontal; if (bucket.allowVerticalPlacement && !isPlaced && this.prevPlacement) { @@ -473,8 +472,9 @@ export class Placement { if (!layout.get('text-variable-anchor')) { const placeBox = (collisionTextBox, orientation) => { - const placedFeature = this.collisionIndex.placeCollisionBox(collisionTextBox, textAllowOverlap, - textPixelRatio, posMatrix, collisionGroup.predicate); + const textScale = bucket.getSymbolInstanceTextSize(partiallyEvaluatedTextSize, symbolInstance, this.transform.zoom, symbolIndex); + const placedFeature = this.collisionIndex.placeCollisionBox(textScale, collisionTextBox, + new Point(0, 0), textAllowOverlap, textPixelRatio, posMatrix, collisionGroup.predicate); if (placedFeature && placedFeature.box && placedFeature.box.length) { this.markUsedOrientation(bucket, orientation, symbolInstance); this.placedOrientations[symbolInstance.crossTileID] = orientation; @@ -489,6 +489,7 @@ export class Placement { const placeVertical = () => { const verticalTextBox = collisionArrays.verticalTextBox; if (bucket.allowVerticalPlacement && symbolInstance.numVerticalGlyphVertices > 0 && verticalTextBox) { + updateElevation(verticalTextBox); return placeBox(verticalTextBox, WritingMode.vertical); } return {box: null, offscreen: null}; @@ -512,11 +513,12 @@ export class Placement { } const placeBoxForVariableAnchors = (collisionTextBox, collisionIconBox, orientation) => { - const width = collisionTextBox.x2 - collisionTextBox.x1; - const height = collisionTextBox.y2 - collisionTextBox.y1; const textBoxScale = symbolInstance.textBoxScale; + const width = (collisionTextBox.x2 - collisionTextBox.x1) * textBoxScale + 2.0 * collisionTextBox.padding; + const height = (collisionTextBox.y2 - collisionTextBox.y1) * textBoxScale + 2.0 * collisionTextBox.padding; const variableIconBox = hasIconTextFit && !iconAllowOverlap ? collisionIconBox : null; + if (variableIconBox) updateElevation(variableIconBox); let placedBox: ?{ box: Array, offscreen: boolean } = {box: [], offscreen: false}; const placementAttempts = textAllowOverlap ? anchors.length * 2 : anchors.length; @@ -524,9 +526,10 @@ export class Placement { const anchor = anchors[i % anchors.length]; const allowOverlap = (i >= anchors.length); const result = this.attemptAnchorPlacement( - anchor, collisionTextBox, width, height, - textBoxScale, rotateWithMap, pitchWithMap, textPixelRatio, posMatrix, - collisionGroup, allowOverlap, symbolInstance, bucket, orientation, variableIconBox); + anchor, collisionTextBox, width, height, textBoxScale, rotateWithMap, + pitchWithMap, textPixelRatio, posMatrix, collisionGroup, allowOverlap, + symbolInstance, symbolIndex, bucket, orientation, variableIconBox, + partiallyEvaluatedTextSize, partiallyEvaluatedIconSize); if (result) { placedBox = result.placedGlyphBoxes; @@ -547,6 +550,7 @@ export class Placement { const placeVertical = () => { const verticalTextBox = collisionArrays.verticalTextBox; + if (verticalTextBox) updateElevation(verticalTextBox); const wasPlaced = placed && placed.box && placed.box.length; if (bucket.allowVerticalPlacement && !wasPlaced && symbolInstance.numVerticalGlyphVertices > 0 && verticalTextBox) { return placeBoxForVariableAnchors(verticalTextBox, collisionArrays.verticalIconBox, WritingMode.vertical); @@ -600,7 +604,8 @@ export class Placement { pitchWithMap, collisionGroup.predicate, circlePixelDiameter, - textPixelPadding); + textPixelPadding, + this.retainedQueryData[bucket.bucketInstanceId].tileID); assert(!placedGlyphCircles.circles.length || (!placedGlyphCircles.collisionDetected || showCollisionBoxes)); // If text-allow-overlap is set, force "placedCircles" to true @@ -618,12 +623,13 @@ export class Placement { if (collisionArrays.iconBox) { const placeIconFeature = iconBox => { - const shiftedIconBox = hasIconTextFit && shift ? - shiftVariableCollisionBox( - iconBox, shift.x, shift.y, - rotateWithMap, pitchWithMap, this.transform.angle) : - iconBox; - return this.collisionIndex.placeCollisionBox(shiftedIconBox, + updateElevation(iconBox); + const shiftPoint: Point = hasIconTextFit && shift ? + offsetShift(shift.x, shift.y, rotateWithMap, pitchWithMap, this.transform.angle) : + new Point(0, 0); + + const iconScale = bucket.getSymbolInstanceIconSize(partiallyEvaluatedIconSize, this.transform.zoom, symbolIndex); + return this.collisionIndex.placeCollisionBox(iconScale, iconBox, shiftPoint, iconAllowOverlap, textPixelRatio, posMatrix, collisionGroup.predicate); }; @@ -700,11 +706,11 @@ export class Placement { const symbolIndexes = bucket.getSortedSymbolIndexes(this.transform.angle); for (let i = symbolIndexes.length - 1; i >= 0; --i) { const symbolIndex = symbolIndexes[i]; - placeSymbol(bucket.symbolInstances.get(symbolIndex), bucket.collisionArrays[symbolIndex]); + placeSymbol(bucket.symbolInstances.get(symbolIndex), symbolIndex, bucket.collisionArrays[symbolIndex]); } } else { for (let i = bucketPart.symbolInstanceStart; i < bucketPart.symbolInstanceEnd; i++) { - placeSymbol(bucket.symbolInstances.get(i), bucket.collisionArrays[i]); + placeSymbol(bucket.symbolInstances.get(i), i, bucket.collisionArrays[i]); } } diff --git a/src/symbol/projection.js b/src/symbol/projection.js index e0c16142989..73afd87b73c 100644 --- a/src/symbol/projection.js +++ b/src/symbol/projection.js @@ -103,9 +103,13 @@ function getGlCoordMatrix(posMatrix: mat4, } } -function project(point: Point, matrix: mat4) { - const pos = [point.x, point.y, 0, 1]; - xyTransformMat4(pos, pos, matrix); +function project(point: Point, matrix: mat4, elevation: number = 0) { + const pos = [point.x, point.y, elevation, 1]; + if (elevation) { + vec4.transformMat4(pos, pos, matrix); + } else { + xyTransformMat4(pos, pos, matrix); + } const w = pos[3]; return { point: new Point(pos[0] / w, pos[1] / w), @@ -114,7 +118,7 @@ function project(point: Point, matrix: mat4) { } function getPerspectiveRatio(cameraToCenterDistance: number, signedDistanceFromCamera: number): number { - return 0.5 + 0.5 * (cameraToCenterDistance / signedDistanceFromCamera); + return Math.min(0.5 + 0.5 * (cameraToCenterDistance / signedDistanceFromCamera), 1.5); } function isVisible(anchorPos: [number, number, number, number], @@ -140,7 +144,8 @@ function updateLineLabels(bucket: SymbolBucket, labelPlaneMatrix: mat4, glCoordMatrix: mat4, pitchWithMap: boolean, - keepUpright: boolean) { + keepUpright: boolean, + getElevation: ?((p: Point) => number)) { const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData; const partiallyEvaluatedSize = symbolSize.evaluateSizeForZoom(sizeData, painter.transform.zoom); @@ -172,7 +177,8 @@ function updateLineLabels(bucket: SymbolBucket, // Awkward... but we're counting on the paired "vertical" symbol coming immediately after its horizontal counterpart useVertical = false; - const anchorPos = [symbol.anchorX, symbol.anchorY, 0, 1]; + const elevation = getElevation ? getElevation({x: symbol.anchorX, y: symbol.anchorY}) : 0; + const anchorPos = [symbol.anchorX, symbol.anchorY, elevation, 1]; vec4.transformMat4(anchorPos, anchorPos, posMatrix); // Don't bother calculating the correct point for invisible labels. @@ -188,18 +194,28 @@ function updateLineLabels(bucket: SymbolBucket, const pitchScaledFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio; const tileAnchorPoint = new Point(symbol.anchorX, symbol.anchorY); - const anchorPoint = project(tileAnchorPoint, labelPlaneMatrix).point; - const projectionCache = {}; + const transformedTileAnchor = project(tileAnchorPoint, labelPlaneMatrix, elevation); + + // Skip labels behind the camera + if (transformedTileAnchor.signedDistanceFromCamera <= 0.0) { + hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray); + continue; + } + const anchorPoint = transformedTileAnchor.point; + let projectionCache = {}; + + const getElevationForPlacement = pitchWithMap ? null : getElevation; // When pitchWithMap, we're projecting to scaled tile coordinate space: there is no need to get elevation as it doesn't affect projection. const placeUnflipped: any = placeGlyphsAlongLine(symbol, pitchScaledFontSize, false /*unflipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, - bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio); + bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, getElevationForPlacement); useVertical = placeUnflipped.useVertical; + if (getElevationForPlacement && placeUnflipped.needsFlipping) projectionCache = {}; // Truncated points should be recalculated. if (placeUnflipped.notEnoughRoom || useVertical || (placeUnflipped.needsFlipping && placeGlyphsAlongLine(symbol, pitchScaledFontSize, true /*flipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, - bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio).notEnoughRoom)) { + bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, getElevationForPlacement).notEnoughRoom)) { hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray); } } @@ -211,7 +227,7 @@ function updateLineLabels(bucket: SymbolBucket, } } -function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffsetArray, lineOffsetX: number, lineOffsetY: number, flip: boolean, anchorPoint: Point, tileAnchorPoint: Point, symbol: any, lineVertexArray: SymbolLineVertexArray, labelPlaneMatrix: mat4, projectionCache: any) { +function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffsetArray, lineOffsetX: number, lineOffsetY: number, flip: boolean, anchorPoint: Point, tileAnchorPoint: Point, symbol: any, lineVertexArray: SymbolLineVertexArray, labelPlaneMatrix: mat4, projectionCache: any, getElevation: ?((p: Point) => number), returnPathInTileCoords: ?boolean) { const glyphEndIndex = symbol.glyphStartIndex + symbol.numGlyphs; const lineStartIndex = symbol.lineStartIndex; const lineEndIndex = symbol.lineStartIndex + symbol.lineLength; @@ -220,12 +236,12 @@ function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffset const lastGlyphOffset = glyphOffsetArray.getoffsetX(glyphEndIndex - 1); const firstPlacedGlyph = placeGlyphAlongLine(fontScale * firstGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, - lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache); + lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, getElevation, returnPathInTileCoords, true); if (!firstPlacedGlyph) return null; const lastPlacedGlyph = placeGlyphAlongLine(fontScale * lastGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, - lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache); + lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, getElevation, returnPathInTileCoords, true); if (!lastPlacedGlyph) return null; @@ -253,7 +269,7 @@ function requiresOrientationChange(writingMode, firstPoint, lastPoint, aspectRat return null; } -function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio) { +function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, getElevation) { const fontScale = fontSize / 24; const lineOffsetX = symbol.lineOffsetX * fontScale; const lineOffsetY = symbol.lineOffsetY * fontScale; @@ -266,7 +282,7 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la // Place the first and the last glyph in the label first, so we can figure out // the overall orientation of the label and determine whether it needs to be flipped in keepUpright mode - const firstAndLastGlyph = placeFirstAndLastGlyph(fontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol, lineVertexArray, labelPlaneMatrix, projectionCache); + const firstAndLastGlyph = placeFirstAndLastGlyph(fontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol, lineVertexArray, labelPlaneMatrix, projectionCache, getElevation); if (!firstAndLastGlyph) { return {notEnoughRoom: true}; } @@ -285,7 +301,7 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la // Since first and last glyph fit on the line, we're sure that the rest of the glyphs can be placed // $FlowFixMe placedGlyphs.push(placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(glyphIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, - lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache)); + lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, getElevation)); } placedGlyphs.push(firstAndLastGlyph.last); } else { @@ -311,7 +327,7 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la } // $FlowFixMe const singleGlyph = placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(symbol.glyphStartIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, - symbol.lineStartIndex, symbol.lineStartIndex + symbol.lineLength, lineVertexArray, labelPlaneMatrix, projectionCache); + symbol.lineStartIndex, symbol.lineStartIndex + symbol.lineLength, lineVertexArray, labelPlaneMatrix, projectionCache, getElevation); if (!singleGlyph) return {notEnoughRoom: true}; @@ -324,17 +340,23 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la return {}; } -function projectTruncatedLineSegment(previousTilePoint: Point, currentTilePoint: Point, previousProjectedPoint: Point, minimumLength: number, projectionMatrix: mat4) { +function projectTruncatedLineSegment(previousTilePoint: Point, currentTilePoint: Point, previousProjectedPoint: Point, minimumLength: number, projectionMatrix: mat4, getElevation: ?((p: Point) => number)) { // We are assuming "previousTilePoint" won't project to a point within one unit of the camera plane // If it did, that would mean our label extended all the way out from within the viewport to a (very distant) // point near the plane of the camera. We wouldn't be able to render the label anyway once it crossed the // plane of the camera. - const projectedUnitVertex = project(previousTilePoint.add(previousTilePoint.sub(currentTilePoint)._unit()), projectionMatrix).point; + const unitVertex = previousTilePoint.add(previousTilePoint.sub(currentTilePoint)._unit()); + const projectedUnitVertex = project(unitVertex, projectionMatrix, getElevation ? getElevation(unitVertex) : 0).point; const projectedUnitSegment = previousProjectedPoint.sub(projectedUnitVertex); return previousProjectedPoint.add(projectedUnitSegment._mult(minimumLength / projectedUnitSegment.mag())); } +function interpolate(p1, p2, a) { + const b = 1 - a; + return new Point(p1.x * b + p2.x * a, p1.y * b + p2.y * a); +} + function placeGlyphAlongLine(offsetX: number, lineOffsetX: number, lineOffsetY: number, @@ -346,7 +368,10 @@ function placeGlyphAlongLine(offsetX: number, lineEndIndex: number, lineVertexArray: SymbolLineVertexArray, labelPlaneMatrix: mat4, - projectionCache: {[_: number]: Point}) { + projectionCache: {[_: number]: Point}, + getElevation: ?((p: Point) => number), + returnPathInTileCoords: ?boolean, + endGlyph: ?boolean) { const combinedOffsetX = flip ? offsetX - lineOffsetX : @@ -374,6 +399,19 @@ function placeGlyphAlongLine(offsetX: number, let currentSegmentDistance = 0; const absOffsetX = Math.abs(combinedOffsetX); const pathVertices = []; + const tilePath = []; + let currentVertex = tileAnchorPoint; + + const previousTilePoint = () => { + const previousLineVertexIndex = currentIndex - dir; + return distanceToPrev === 0 ? + tileAnchorPoint : + new Point(lineVertexArray.getx(previousLineVertexIndex), lineVertexArray.gety(previousLineVertexIndex)); + }; + + const getTruncatedLineSegment = () => { + return projectTruncatedLineSegment(previousTilePoint(), currentVertex, prev, absOffsetX - distanceToPrev + 1, labelPlaneMatrix, getElevation); + }; while (distanceToPrev + currentSegmentDistance <= absOffsetX) { currentIndex += dir; @@ -384,45 +422,59 @@ function placeGlyphAlongLine(offsetX: number, prev = current; pathVertices.push(current); + if (returnPathInTileCoords) tilePath.push(currentVertex || previousTilePoint()); current = projectionCache[currentIndex]; if (current === undefined) { - const currentVertex = new Point(lineVertexArray.getx(currentIndex), lineVertexArray.gety(currentIndex)); - const projection = project(currentVertex, labelPlaneMatrix); + currentVertex = new Point(lineVertexArray.getx(currentIndex), lineVertexArray.gety(currentIndex)); + const projection = project(currentVertex, labelPlaneMatrix, getElevation ? getElevation(currentVertex) : 0); if (projection.signedDistanceFromCamera > 0) { current = projectionCache[currentIndex] = projection.point; } else { // The vertex is behind the plane of the camera, so we can't project it // Instead, we'll create a vertex along the line that's far enough to include the glyph - const previousLineVertexIndex = currentIndex - dir; - const previousTilePoint = distanceToPrev === 0 ? - tileAnchorPoint : - new Point(lineVertexArray.getx(previousLineVertexIndex), lineVertexArray.gety(previousLineVertexIndex)); // Don't cache because the new vertex might not be far enough out for future glyphs on the same segment - current = projectTruncatedLineSegment(previousTilePoint, currentVertex, prev, absOffsetX - distanceToPrev + 1, labelPlaneMatrix); + current = getTruncatedLineSegment(); } + } else { + currentVertex = null; // null stale data } distanceToPrev += currentSegmentDistance; currentSegmentDistance = prev.dist(current); } + if (endGlyph && getElevation) { + // For terrain, always truncate end points in order to handle terrain curvature. + // If previously truncated, on signedDistanceFromCamera < 0, don't do it. + // Cache as end point. The cache is cleared if there is need for flipping in updateLineLabels. + currentVertex = currentVertex || new Point(lineVertexArray.getx(currentIndex), lineVertexArray.gety(currentIndex)); + projectionCache[currentIndex] = current = (projectionCache[currentIndex] === undefined) ? current : getTruncatedLineSegment(); + currentSegmentDistance = prev.dist(current); + } + // The point is on the current segment. Interpolate to find it. const segmentInterpolationT = (absOffsetX - distanceToPrev) / currentSegmentDistance; const prevToCurrent = current.sub(prev); const p = prevToCurrent.mult(segmentInterpolationT)._add(prev); // offset the point from the line to text-offset and icon-offset - p._add(prevToCurrent._unit()._perp()._mult(lineOffsetY * dir)); + if (lineOffsetY) p._add(prevToCurrent._unit()._perp()._mult(lineOffsetY * dir)); const segmentAngle = angle + Math.atan2(current.y - prev.y, current.x - prev.x); pathVertices.push(p); + if (returnPathInTileCoords) { + currentVertex = currentVertex || new Point(lineVertexArray.getx(currentIndex), lineVertexArray.gety(currentIndex)); + const prevVertex = tilePath.length > 0 ? tilePath[tilePath.length - 1] : currentVertex; + tilePath.push(interpolate(prevVertex, currentVertex, segmentInterpolationT)); + } return { point: p, angle: segmentAngle, - path: pathVertices + path: pathVertices, + tilePath }; } diff --git a/src/symbol/symbol_layout.js b/src/symbol/symbol_layout.js index 990c8417282..f5f126fd106 100644 --- a/src/symbol/symbol_layout.js +++ b/src/symbol/symbol_layout.js @@ -6,8 +6,7 @@ import {getAnchors, getCenterAnchor} from './get_anchors'; import clipLine from './clip_line'; import {shapeText, shapeIcon, WritingMode, fitIconToText} from './shaping'; import {getGlyphQuads, getIconQuads} from './quads'; -import CollisionFeature from './collision_feature'; -import {warnOnce} from '../util/util'; +import {warnOnce, degToRad} from '../util/util'; import { allowsVerticalWritingMode, allowsLetterSpacing @@ -153,7 +152,8 @@ export function performSymbolLayout(bucket: SymbolBucket, imageMap: {[_: string]: StyleImage}, imagePositions: {[_: string]: ImagePosition}, showCollisionBoxes: boolean, - canonical: CanonicalTileID) { + canonical: CanonicalTileID, + tileZoom: number) { bucket.createArrays(); const tileSize = 512 * bucket.overscaling; @@ -182,9 +182,9 @@ export function performSymbolLayout(bucket: SymbolBucket, ]; } - sizes.layoutTextSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(bucket.zoom + 1), canonical); - sizes.layoutIconSize = unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(bucket.zoom + 1), canonical); - sizes.textMaxSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(18)); + sizes.layoutTextSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(tileZoom + 1), canonical); + sizes.layoutIconSize = unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(tileZoom + 1), canonical); + sizes.textMaxSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(18), canonical); const lineHeight = layout.get('text-line-height') * ONE_EM; const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point'; @@ -322,7 +322,7 @@ export function performSymbolLayout(bucket: SymbolBucket, } if (showCollisionBoxes) { - bucket.generateCollisionDebugBuffers(); + bucket.generateCollisionDebugBuffers(tileZoom, bucket.collisionBoxArray); } } @@ -362,14 +362,14 @@ function addFeature(bucket: SymbolBucket, // to use a text-size value that is the same for all zoom levels. // bucket calculates text-size at a high zoom level so that all tiles can // use the same value when calculating anchor positions. - let textMaxSize = sizes.textMaxSize.evaluate(feature, {}); + let textMaxSize = sizes.textMaxSize.evaluate(feature, {}, canonical); if (textMaxSize === undefined) { textMaxSize = layoutTextSize; } const layout = bucket.layers[0].layout; const iconOffset = layout.get('icon-offset').evaluate(feature, {}, canonical); const defaultHorizontalShaping = getDefaultHorizontalShaping(shapedTextOrientations.horizontal); - const glyphSize = 24, + const glyphSize = ONE_EM, fontScale = layoutTextSize / glyphSize, textBoxScale = bucket.tilePixelRatio * fontScale, textMaxBoxScale = bucket.tilePixelRatio * textMaxSize / glyphSize, @@ -377,7 +377,7 @@ function addFeature(bucket: SymbolBucket, symbolMinDistance = bucket.tilePixelRatio * layout.get('symbol-spacing'), textPadding = layout.get('text-padding') * bucket.tilePixelRatio, iconPadding = layout.get('icon-padding') * bucket.tilePixelRatio, - textMaxAngle = layout.get('text-max-angle') / 180 * Math.PI, + textMaxAngle = degToRad(layout.get('text-max-angle')), textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point', iconAlongLine = layout.get('icon-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point', symbolPlacement = layout.get('symbol-placement'), @@ -496,7 +496,7 @@ function addTextVertices(bucket: SymbolBucket, if (sizeData.kind === 'source') { textSizeData = [ - SIZE_PACK_FACTOR * layer.layout.get('text-size').evaluate(feature, {}) + SIZE_PACK_FACTOR * layer.layout.get('text-size').evaluate(feature, {}, canonical) ]; if (textSizeData[0] > MAX_PACKED_SIZE) { warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`); @@ -543,6 +543,72 @@ function getDefaultHorizontalShaping(horizontalShaping: {[_: TextJustify]: Shapi return null; } +export function evaluateBoxCollisionFeature(collisionBoxArray: CollisionBoxArray, + anchor: Anchor, + featureIndex: number, + sourceLayerIndex: number, + bucketIndex: number, + shaped: Object, + boxScale: number, + padding: number, + rotate: number): number { + let y1 = shaped.top; + let y2 = shaped.bottom; + let x1 = shaped.left; + let x2 = shaped.right; + + const collisionPadding = shaped.collisionPadding; + if (collisionPadding) { + x1 -= collisionPadding[0]; + y1 -= collisionPadding[1]; + x2 += collisionPadding[2]; + y2 += collisionPadding[3]; + } + + if (rotate) { + // Account for *-rotate in point collision boxes + // See https://github.com/mapbox/mapbox-gl-js/issues/6075 + // Doesn't account for icon-text-fit + + const tl = new Point(x1, y1); + const tr = new Point(x2, y1); + const bl = new Point(x1, y2); + const br = new Point(x2, y2); + + const rotateRadians = degToRad(rotate); + + tl._rotate(rotateRadians); + tr._rotate(rotateRadians); + bl._rotate(rotateRadians); + br._rotate(rotateRadians); + + // Collision features require an "on-axis" geometry, + // so take the envelope of the rotated geometry + // (may be quite large for wide labels rotated 45 degrees) + x1 = Math.min(tl.x, tr.x, bl.x, br.x); + x2 = Math.max(tl.x, tr.x, bl.x, br.x); + y1 = Math.min(tl.y, tr.y, bl.y, br.y); + y2 = Math.max(tl.y, tr.y, bl.y, br.y); + } + + collisionBoxArray.emplaceBack(anchor.x, anchor.y, x1, y1, x2, y2, padding, featureIndex, sourceLayerIndex, bucketIndex); + + return collisionBoxArray.length - 1; +} + +export function evaluateCircleCollisionFeature(shaped: Object): number | null { + if (shaped.collisionPadding) { + // Compute height of the shape in glyph metrics and apply padding. + // Note that the pixel based 'text-padding' is applied at runtime + shaped.top -= shaped.collisionPadding[1]; + shaped.bottom += shaped.collisionPadding[3]; + } + + // Set minimum box height to avoid very many small labels + const height = shaped.bottom - shaped.top; + return height > 0 ? Math.max(10, height) : null; +} + /** * Add a single label & icon placement. * @@ -575,7 +641,8 @@ function addSymbol(bucket: SymbolBucket, layoutTextSize: number) { const lineArray = bucket.addToLineVertexArray(anchor, line); - let textCollisionFeature, iconCollisionFeature, verticalTextCollisionFeature, verticalIconCollisionFeature; + let textBoxIndex, iconBoxIndex, verticalTextBoxIndex, verticalIconBoxIndex; + let textCircle, verticalTextCircle, verticalIconCircle; let numIconVertices = 0; let numVerticalIconVertices = 0; @@ -596,13 +663,19 @@ function addSymbol(bucket: SymbolBucket, } if (bucket.allowVerticalPlacement && shapedTextOrientations.vertical) { - const textRotation = layer.layout.get('text-rotate').evaluate(feature, {}, canonical); - const verticalTextRotation = textRotation + 90.0; const verticalShaping = shapedTextOrientations.vertical; - verticalTextCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, verticalShaping, textBoxScale, textPadding, textAlongLine, verticalTextRotation); - - if (verticallyShapedIcon) { - verticalIconCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, verticallyShapedIcon, iconBoxScale, iconPadding, textAlongLine, verticalTextRotation); + if (textAlongLine) { + verticalTextCircle = evaluateCircleCollisionFeature(verticalShaping); + if (verticallyShapedIcon) { + verticalIconCircle = evaluateCircleCollisionFeature(verticallyShapedIcon); + } + } else { + const textRotation = layer.layout.get('text-rotate').evaluate(feature, {}, canonical); + const verticalTextRotation = textRotation + 90.0; + verticalTextBoxIndex = evaluateBoxCollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, verticalShaping, textBoxScale, textPadding, verticalTextRotation); + if (verticallyShapedIcon) { + verticalIconBoxIndex = evaluateBoxCollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, verticallyShapedIcon, iconBoxScale, iconPadding, verticalTextRotation); + } } } @@ -611,12 +684,11 @@ function addSymbol(bucket: SymbolBucket, //If the style specifies an `icon-text-fit` then the icon would have to shift along with it. // For more info check `updateVariableAnchors` in `draw_symbol.js` . if (shapedIcon) { - const iconRotate = layer.layout.get('icon-rotate').evaluate(feature, {}); + const iconRotate = layer.layout.get('icon-rotate').evaluate(feature, {}, canonical); const hasIconTextFit = layer.layout.get('icon-text-fit') !== 'none'; const iconQuads = getIconQuads(shapedIcon, iconRotate, isSDFIcon, hasIconTextFit); const verticalIconQuads = verticallyShapedIcon ? getIconQuads(verticallyShapedIcon, iconRotate, isSDFIcon, hasIconTextFit) : undefined; - iconCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconBoxScale, iconPadding, /*align boxes to line*/false, iconRotate); - + iconBoxIndex = evaluateBoxCollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconBoxScale, iconPadding, iconRotate); numIconVertices = iconQuads.length * 4; const sizeData = bucket.iconSizeData; @@ -624,7 +696,7 @@ function addSymbol(bucket: SymbolBucket, if (sizeData.kind === 'source') { iconSizeData = [ - SIZE_PACK_FACTOR * layer.layout.get('icon-size').evaluate(feature, {}) + SIZE_PACK_FACTOR * layer.layout.get('icon-size').evaluate(feature, {}, canonical) ]; if (iconSizeData[0] > MAX_PACKED_SIZE) { warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "icon-size".`); @@ -679,12 +751,16 @@ function addSymbol(bucket: SymbolBucket, for (const justification: any in shapedTextOrientations.horizontal) { const shaping = shapedTextOrientations.horizontal[justification]; - if (!textCollisionFeature) { + if (!textBoxIndex) { key = murmur3(shaping.text); - const textRotate = layer.layout.get('text-rotate').evaluate(feature, {}, canonical); // As a collision approximation, we can use either the vertical or any of the horizontal versions of the feature // We're counting on all versions having similar dimensions - textCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, shaping, textBoxScale, textPadding, textAlongLine, textRotate); + if (textAlongLine) { + textCircle = evaluateCircleCollisionFeature(shaping); + } else { + const textRotate = layer.layout.get('text-rotate').evaluate(feature, {}, canonical); + textBoxIndex = evaluateBoxCollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, shaping, textBoxScale, textPadding, textRotate); + } } const singleLine = shaping.positionedLines.length === 1; @@ -705,33 +781,18 @@ function addSymbol(bucket: SymbolBucket, textOffset, lineArray, WritingMode.vertical, ['vertical'], placedTextSymbolIndices, verticalPlacedIconSymbolIndex, sizes, canonical); } - const textBoxStartIndex = textCollisionFeature ? textCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; - const textBoxEndIndex = textCollisionFeature ? textCollisionFeature.boxEndIndex : bucket.collisionBoxArray.length; - - const verticalTextBoxStartIndex = verticalTextCollisionFeature ? verticalTextCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; - const verticalTextBoxEndIndex = verticalTextCollisionFeature ? verticalTextCollisionFeature.boxEndIndex : bucket.collisionBoxArray.length; - - const iconBoxStartIndex = iconCollisionFeature ? iconCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; - const iconBoxEndIndex = iconCollisionFeature ? iconCollisionFeature.boxEndIndex : bucket.collisionBoxArray.length; - - const verticalIconBoxStartIndex = verticalIconCollisionFeature ? verticalIconCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; - const verticalIconBoxEndIndex = verticalIconCollisionFeature ? verticalIconCollisionFeature.boxEndIndex : bucket.collisionBoxArray.length; - // Check if runtime collision circles should be used for any of the collision features. // It is enough to choose the tallest feature shape as circles are always placed on a line. // All measurements are in glyph metrics and later converted into pixels using proper font size "layoutTextSize" let collisionCircleDiameter = -1; - const getCollisionCircleHeight = (feature: ?CollisionFeature, prevHeight: number): number => { - if (feature && feature.circleDiameter) - return Math.max(feature.circleDiameter, prevHeight); - return prevHeight; + const getCollisionCircleHeight = (diameter: ?number, prevHeight: number): number => { + return diameter ? Math.max(diameter, prevHeight) : prevHeight; }; - collisionCircleDiameter = getCollisionCircleHeight(textCollisionFeature, collisionCircleDiameter); - collisionCircleDiameter = getCollisionCircleHeight(verticalTextCollisionFeature, collisionCircleDiameter); - collisionCircleDiameter = getCollisionCircleHeight(iconCollisionFeature, collisionCircleDiameter); - collisionCircleDiameter = getCollisionCircleHeight(verticalIconCollisionFeature, collisionCircleDiameter); + collisionCircleDiameter = getCollisionCircleHeight(textCircle, collisionCircleDiameter); + collisionCircleDiameter = getCollisionCircleHeight(verticalTextCircle, collisionCircleDiameter); + collisionCircleDiameter = getCollisionCircleHeight(verticalIconCircle, collisionCircleDiameter); const useRuntimeCollisionCircles = (collisionCircleDiameter > -1) ? 1 : 0; // Convert circle collision height into pixels @@ -756,14 +817,14 @@ function addSymbol(bucket: SymbolBucket, placedIconSymbolIndex, verticalPlacedIconSymbolIndex, key, - textBoxStartIndex, - textBoxEndIndex, - verticalTextBoxStartIndex, - verticalTextBoxEndIndex, - iconBoxStartIndex, - iconBoxEndIndex, - verticalIconBoxStartIndex, - verticalIconBoxEndIndex, + textBoxIndex !== undefined ? textBoxIndex : bucket.collisionBoxArray.length, + textBoxIndex !== undefined ? textBoxIndex + 1 : bucket.collisionBoxArray.length, + verticalTextBoxIndex !== undefined ? verticalTextBoxIndex : bucket.collisionBoxArray.length, + verticalTextBoxIndex !== undefined ? verticalTextBoxIndex + 1 : bucket.collisionBoxArray.length, + iconBoxIndex !== undefined ? iconBoxIndex : bucket.collisionBoxArray.length, + iconBoxIndex !== undefined ? iconBoxIndex + 1 : bucket.collisionBoxArray.length, + verticalIconBoxIndex ? verticalIconBoxIndex : bucket.collisionBoxArray.length, + verticalIconBoxIndex ? verticalIconBoxIndex + 1 : bucket.collisionBoxArray.length, featureIndex, numHorizontalGlyphVertices, numVerticalGlyphVertices, diff --git a/src/terrain/draw_terrain_raster.js b/src/terrain/draw_terrain_raster.js new file mode 100644 index 00000000000..668f53f85d5 --- /dev/null +++ b/src/terrain/draw_terrain_raster.js @@ -0,0 +1,205 @@ +// @flow + +import DepthMode from '../gl/depth_mode'; +import CullFaceMode from '../gl/cull_face_mode'; +import {terrainRasterUniformValues} from './terrain_raster_program'; +import {Terrain} from './terrain'; +import Tile from '../source/tile'; +import assert from 'assert'; +import {easeCubicInOut} from '../util/util'; + +import type Painter from '../render/painter'; +import type SourceCache from '../source/source_cache'; +import type {OverscaledTileID, CanonicalTileID} from '../source/tile_id'; +import StencilMode from '../gl/stencil_mode'; +import ColorMode from '../gl/color_mode'; + +export { + drawTerrainRaster, + drawTerrainDepth +}; + +type DEMChain = { + startTime: number, + phase: number, + duration: number, // Interpolation duration in milliseconds + from: Tile, + to: Tile, + queued: ?Tile +}; + +class VertexMorphing { + operations: {[string | number]: DEMChain }; + + constructor() { + this.operations = {}; + } + + newMorphing(key: number, from: Tile, to: Tile, now: number, duration: number) { + assert(from.demTexture && to.demTexture); + assert(from.tileID.key !== to.tileID.key); + + if (key in this.operations) { + const op = this.operations[key]; + assert(op.from && op.to); + // Queue the target tile unless it's being morphed to already + if (op.to.tileID.key !== to.tileID.key) + op.queued = to; + } else { + this.operations[key] = { + startTime: now, + phase: 0.0, + duration, + from, + to, + queued: null + }; + } + } + + getMorphValuesForProxy(key: number): ?{from: Tile, to: Tile, phase: number} { + if (!(key in this.operations)) + return null; + + const op = this.operations[key]; + const from = op.from; + const to = op.to; + assert(from && to); + + return {from, to, phase: op.phase}; + } + + update(now: number) { + for (const key in this.operations) { + const op = this.operations[key]; + assert(op.from && op.to); + + op.phase = (now - op.startTime) / op.duration; + + // Start the queued operation if the current one is finished or the data has expired + while (op.phase >= 1.0 || !this._validOp(op)) { + if (!this._nextOp(op, now)) { + delete this.operations[key]; + break; + } + } + } + } + + _nextOp(op: DEMChain, now: number): boolean { + if (!op.queued) + return false; + op.from = op.to; + op.to = op.queued; + op.queued = null; + op.phase = 0.0; + op.startTime = now; + return true; + } + + _validOp(op: DEMChain): boolean { + return op.from.hasData() && op.to.hasData(); + } +} + +function demTileChanged(prev: ?Tile, next: ?Tile): boolean { + if (prev == null || next == null) + return false; + if (!prev.hasData() || !next.hasData()) + return false; + if (prev.demTexture == null || next.demTexture == null) + return false; + return prev.tileID.key !== next.tileID.key; +} + +const vertexMorphing = new VertexMorphing(); +const SHADER_DEFAULT = 0; +const SHADER_MORPHING = 1; +const defaultDuration = 250; + +const shaderDefines = { + "0": null, + "1": 'TERRAIN_VERTEX_MORPHING' +}; + +function drawTerrainRaster(painter: Painter, terrain: Terrain, sourceCache: SourceCache, tileIDs: Array, now: number) { + const context = painter.context; + const gl = context.gl; + + let program = painter.useProgram('terrainRaster'); + let programMode = SHADER_DEFAULT; + + const setShaderMode = (mode) => { + if (programMode === mode) + return; + program = painter.useProgram('terrainRaster', null, shaderDefines[mode]); + programMode = mode; + }; + + const colorMode = painter.colorModeForRenderPass(); + const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D); + vertexMorphing.update(now); + const tr = painter.transform; + const skirt = skirtHeight(tr.zoom) * terrain.exaggeration(); + + for (const coord of tileIDs) { + const tile = sourceCache.getTile(coord); + const stencilMode = StencilMode.disabled; + + const prevDemTile = terrain.prevTerrainTileForTile[coord.key]; + const nextDemTile = terrain.terrainTileForTile[coord.key]; + + if (demTileChanged(prevDemTile, nextDemTile)) { + vertexMorphing.newMorphing(coord.key, prevDemTile, nextDemTile, now, defaultDuration); + } + + // Bind the main draped texture + context.activeTexture.set(gl.TEXTURE0); + tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); + + const morph = vertexMorphing.getMorphValuesForProxy(coord.key); + const shaderMode = morph ? SHADER_MORPHING : SHADER_DEFAULT; + let elevationOptions; + + if (morph) { + elevationOptions = {morphing: {srcDemTile: morph.from, dstDemTile: morph.to, phase: easeCubicInOut(morph.phase)}}; + } + const uniformValues = terrainRasterUniformValues(coord.posMatrix, isEdgeTile(coord.canonical, tr.renderWorldCopies) ? skirt / 10 : skirt); + + setShaderMode(shaderMode); + terrain.setupElevationDraw(tile, program, elevationOptions); + program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW, + uniformValues, "terrain_raster", terrain.gridBuffer, terrain.gridIndexBuffer, terrain.gridSegments); + } +} + +function drawTerrainDepth(painter: Painter, terrain: Terrain, sourceCache: SourceCache, tileIDs: Array) { + const context = painter.context; + const gl = context.gl; + context.clear({depth: 1}); + const program = painter.useProgram('terrainDepth'); + const depthMode = new DepthMode(gl.LESS, DepthMode.ReadWrite, painter.depthRangeFor3D); + + for (const coord of tileIDs) { + const tile = sourceCache.getTile(coord); + const uniformValues = terrainRasterUniformValues(coord.posMatrix, 0); + terrain.setupElevationDraw(tile, program); + program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, ColorMode.unblended, CullFaceMode.backCCW, + uniformValues, "terrain_depth", terrain.gridBuffer, terrain.gridIndexBuffer, terrain.gridNoSkirtSegments); + } +} + +function skirtHeight(zoom) { + // Skirt height calculation is heuristic: provided value hides + // seams between tiles and it is not too large: 9 at zoom 22, ~20000m at zoom 0. + return 6 * Math.pow(1.5, 22 - zoom); +} + +function isEdgeTile(cid: CanonicalTileID, renderWorldCopies: boolean): boolean { + const numTiles = 1 << cid.z; + return (!renderWorldCopies && (cid.x === 0 || cid.x === numTiles - 1)) || cid.y === 0 || cid.y === numTiles - 1; +} + +export { + VertexMorphing +}; diff --git a/src/terrain/elevation.js b/src/terrain/elevation.js new file mode 100644 index 00000000000..6cb6ad32c96 --- /dev/null +++ b/src/terrain/elevation.js @@ -0,0 +1,203 @@ +// @flow + +import MercatorCoordinate from '../geo/mercator_coordinate'; +import DEMData from '../data/dem_data'; +import SourceCache from '../source/source_cache'; +import {number as interpolate} from '../style-spec/util/interpolate'; +import EXTENT from '../data/extent'; +import {vec3} from 'gl-matrix'; + +import {OverscaledTileID} from '../source/tile_id'; + +import type Tile from '../source/tile'; + +/** + * Provides access to elevation data from raster-dem source cache. + */ +export class Elevation { + + /** + * Altitude above sea level in meters at specified point. + * @param {MercatorCoordinate} point Mercator coordinate of the point. + * @param {number} defaultIfNotLoaded Value that is returned if the dem tile of the provided point is not loaded + * @returns {number} Altitude in meters. + * If there is no loaded tile that carries information for the requested + * point elevation, returns `defaultIfNotLoaded`. + * Doesn't invoke network request to fetch the data. + */ + getAtPoint(point: MercatorCoordinate, defaultIfNotLoaded: number = 0): number { + const src = this._source(); + if (!src) return defaultIfNotLoaded; + if (point.y < 0.0 || point.y > 1.0) { + return defaultIfNotLoaded; + } + const cache: SourceCache = src; + const z = cache.getSource().maxzoom; + const tiles = 1 << z; + const wrap = Math.floor(point.x); + const px = point.x - wrap; + const tileID = new OverscaledTileID(z, wrap, z, Math.floor(px * tiles), Math.floor(point.y * tiles)); + const demTile = this.findDEMTileFor(tileID); + if (!(demTile && demTile.dem)) { return defaultIfNotLoaded; } + const dem: DEMData = demTile.dem; + const tilesAtTileZoom = 1 << demTile.tileID.canonical.z; + const x = (px * tilesAtTileZoom - demTile.tileID.canonical.x) * dem.dim; + const y = (point.y * tilesAtTileZoom - demTile.tileID.canonical.y) * dem.dim; + const i = Math.floor(x); + const j = Math.floor(y); + + return this.exaggeration() * interpolate( + interpolate(dem.get(i, j), dem.get(i, j + 1), y - j), + interpolate(dem.get(i + 1, j), dem.get(i + 1, j + 1), y - j), + x - i); + } + + /* + * x and y are offset within tile, in 0 .. EXTENT coordinate space. + */ + getAtTileOffset(tileID: OverscaledTileID, x: number, y: number): number { + const tilesAtTileZoom = 1 << tileID.canonical.z; + return this.getAtPoint(new MercatorCoordinate( + tileID.wrap + (tileID.canonical.x + x / EXTENT) / tilesAtTileZoom, + (tileID.canonical.y + y / EXTENT) / tilesAtTileZoom)); + } + + /* + * Batch fetch for multiple tile points: points holds input and return value: + * vec3's items on index 0 and 1 define x and y offset within tile, in [0 .. EXTENT] + * range, respectively. vec3 item at index 2 is output value, in meters. + * If a DEM tile that covers tileID is loaded, true is returned, otherwise false. + * Nearest filter sampling on dem data is done (no interpolation). + */ + getForTilePoints(tileID: OverscaledTileID, points: Array, interpolated: ?boolean, useDemTile: ?Tile): boolean { + const helper = DEMSampler.create(this, tileID, useDemTile); + if (!helper) { return false; } + + points.forEach(p => { + p[2] = this.exaggeration() * helper.getElevationAt(p[0], p[1], interpolated); + }); + return true; + } + + /** + * Get elevation minimum and maximum for tile identified by `tileID`. + * @param {*} tileID is a sub tile (or covers the same space) of the DEM tile we read the information from. + */ + getMinMaxForTile(tileID: OverscaledTileID): ?{min: number, max: number} { + const demTile = this.findDEMTileFor(tileID); + if (!(demTile && demTile.dem)) { return null; } + const dem: DEMData = demTile.dem; + const tree = dem.tree; + const demTileID = demTile.tileID; + const scale = 1 << tileID.canonical.z - demTileID.canonical.z; + let xOffset = tileID.canonical.x / scale - demTileID.canonical.x; + let yOffset = tileID.canonical.y / scale - demTileID.canonical.y; + let index = 0; // Start from DEM tree root. + for (let i = 0; i < tileID.canonical.z - demTileID.canonical.z; i++) { + if (tree.leaves[index]) break; + xOffset *= 2; + yOffset *= 2; + const childOffset = 2 * Math.floor(yOffset) + Math.floor(xOffset); + index = tree.childOffsets[index] + childOffset; + xOffset = xOffset % 1; + yOffset = yOffset % 1; + } + return {min: this.exaggeration() * tree.minimums[index], max: this.exaggeration() * tree.maximums[index]}; + } + + /** + * Performs raycast against visible DEM tiles on the screen and returns the distance travelled along the ray. + * x & y components of the position are expected to be in normalized mercator coordinates [0, 1] and z in meters. + */ + raycast(position: vec3, dir: vec3, exaggeration: number): ?number { + throw new Error('Pure virtual method called.'); + } + + /** + * Given a point on screen, returns 3D MercatorCoordinate on terrain. + * Reconstructs a picked world position by casting a ray from screen coordinates + * and sampling depth from the custom depth buffer. This function (currently) introduces + * a potential stall (few frames) due to it reading pixel information from the gpu. + * Depth buffer will also be generated if it doesn't already exist. + * @param {Point} screenPoint Screen point in pixels in top-left origin coordinate system. + * @returns {vec3} If there is intersection with terrain, returns 3D MercatorCoordinate's of + * intersection, as vec3(x, y, z), otherwise null. + */ /* eslint no-unused-vars: ["error", { "args": "none" }] */ + pointCoordinate(screenPoint: Point): ?vec3 { + throw new Error('Pure virtual method called.'); + } + + /* + * Implementation provides SourceCache of raster-dem source type cache, in + * order to access already loaded cached tiles. + */ + _source(): ?SourceCache { + throw new Error('Pure virtual method called.'); + } + + /* + * A multiplier defined by style as terrain exaggeration. Elevation provided + * by getXXXX methods is multiplied by this. + */ + exaggeration(): number { + throw new Error('Pure virtual method called.'); + } + + /** + * Lookup DEM tile that corresponds to (covers) tileID. + * @private + */ + findDEMTileFor(_: OverscaledTileID): ?Tile { + throw new Error('Pure virtual method called.'); + } + + /** + * Get list of DEM tiles used to render current frame. + * @private + */ + get visibleDemTiles(): Array { + throw new Error('Getter must be implemented in subclass.'); + } +} + +/** + * Helper class computes and caches data required to lookup elevation offsets at the tile level. + */ +export class DEMSampler { + _dem: DEMData; + _scale: number; + _offset: [number, number]; + + constructor(dem: DEMData, scale: number, offset: [number, number]) { + this._dem = dem; + this._scale = scale; + this._offset = offset; + } + + static create(elevation: Elevation, tileID: OverscaledTileID, useDemTile: ?Tile): ?DEMSampler { + const demTile = useDemTile || elevation.findDEMTileFor(tileID); + if (!(demTile && demTile.dem)) { return; } + const dem: DEMData = demTile.dem; + const demTileID = demTile.tileID; + const scale = 1 << tileID.canonical.z - demTileID.canonical.z; + const xOffset = (tileID.canonical.x / scale - demTileID.canonical.x) * dem.dim; + const yOffset = (tileID.canonical.y / scale - demTileID.canonical.y) * dem.dim; + const k = demTile.tileSize / EXTENT / scale; + + return new DEMSampler(dem, k, [xOffset, yOffset]); + } + + getElevationAt(x: number, y: number, interpolated: ?boolean): number { + const px = x * this._scale + this._offset[0]; + const py = y * this._scale + this._offset[1]; + const i = Math.floor(px); + const j = Math.floor(py); + const dem = this._dem; + + return interpolated ? interpolate( + interpolate(dem.get(i, j), dem.get(i, j + 1), py - j), + interpolate(dem.get(i + 1, j), dem.get(i + 1, j + 1), py - j), + px - i) : + dem.get(i, j); + } +} diff --git a/src/terrain/terrain.js b/src/terrain/terrain.js new file mode 100644 index 00000000000..69d6033670c --- /dev/null +++ b/src/terrain/terrain.js @@ -0,0 +1,1344 @@ +// @flow + +import Point from '@mapbox/point-geometry'; +import SourceCache from '../source/source_cache'; +import {OverscaledTileID} from '../source/tile_id'; +import Tile from '../source/tile'; +import rasterBoundsAttributes from '../data/raster_bounds_attributes'; +import {RasterBoundsArray, TriangleIndexArray} from '../data/array_types'; +import SegmentVector from '../data/segment'; +import Texture from '../render/texture'; +import Program from '../render/program'; +import {Uniform1i, Uniform1f, Uniform2f, Uniform4f, UniformMatrix4f} from '../render/uniform_binding'; +import {prepareDEMTexture} from '../render/draw_hillshade'; +import EXTENT from '../data/extent'; +import {clamp, warnOnce} from '../util/util'; +import assert from 'assert'; +import {vec3, mat4, vec4} from 'gl-matrix'; +import getWorkerPool from '../util/global_worker_pool'; +import Dispatcher from '../util/dispatcher'; +import GeoJSONSource from '../source/geojson_source'; +import ImageSource from '../source/image_source'; +import RasterDEMTileSource from '../source/raster_dem_tile_source'; +import RasterTileSource from '../source/raster_tile_source'; +import Color from '../style-spec/util/color'; +import StencilMode from '../gl/stencil_mode'; +import {DepthStencilAttachment} from '../gl/value'; +import {drawTerrainRaster, drawTerrainDepth} from './draw_terrain_raster'; +import type RasterStyleLayer from '../style/style_layer/raster_style_layer'; +import {Elevation} from './elevation'; +import Framebuffer from '../gl/framebuffer'; +import ColorMode from '../gl/color_mode'; +import DepthMode from '../gl/depth_mode'; +import CullFaceMode from '../gl/cull_face_mode'; +import {clippingMaskUniformValues} from '../render/program/clipping_mask_program'; +import MercatorCoordinate, {mercatorZfromAltitude} from '../geo/mercator_coordinate'; +import browser from '../util/browser'; +import DEMData from '../data/dem_data'; +import rasterFade from '../render/raster_fade'; +import {create as createSource} from '../source/source'; + +import type Map from '../ui/map'; +import type Painter from '../render/painter'; +import type Style from '../style/style'; +import type StyleLayer from '../style/style_layer'; +import type VertexBuffer from '../gl/vertex_buffer'; +import type IndexBuffer from '../gl/index_buffer'; +import type Context from '../gl/context'; +import type {UniformLocations, UniformValues} from '../render/uniform_binding'; +import type Transform from '../geo/transform'; +import type {DEMEncoding} from '../data/dem_data'; + +export const GRID_DIM = 128; + +const FBO_POOL_SIZE = 5; +const RENDER_CACHE_MAX_SIZE = 50; + +// Symbols are draped only for specific cases: see _isLayerDrapedOverTerrain +const drapedLayers = {'fill': true, 'line': true, 'background': true, "hillshade": true, "raster": true}; + +/** + * Proxy source cache gets ideal screen tile cover coordinates. All the other + * source caches's coordinates get mapped to subrects of proxy coordinates (or + * vice versa, subrects of larger tiles from all source caches get mapped to + * full proxy tile). This happens on every draw call in Terrain.updateTileBinding. + * Approach is used here for terrain : all the visible source tiles of all the + * source caches get rendered to proxy source cache textures and then draped over + * terrain. It is in future reusable for handling overscalling as buckets could be + * constructed only for proxy tile content, not for full overscalled vector tile. + */ +class ProxySourceCache extends SourceCache { + renderCache: Array; + renderCachePool: Array; + proxyCachedFBO: {[string | number]: number}; + + constructor(map: Map) { + + const source = createSource('proxy', { + type: 'geojson', + maxzoom: map.transform.maxZoom + }, new Dispatcher(getWorkerPool(), null), map.style); + + super('proxy', source, false); + + source.setEventedParent(this); + + // This source is not to be added as a map source: we use it's tile management. + // For that, initialize internal structures used for tile cover update. + this.map = ((this.getSource(): any): GeoJSONSource).map = map; + this.used = this._sourceLoaded = true; + this.renderCache = []; + this.renderCachePool = []; + this.proxyCachedFBO = {}; + } + + // Override for transient nature of cover here: don't cache and retain. + /* eslint-disable no-unused-vars */ + update(transform: Transform, tileSize?: number, updateForTerrain?: boolean) { + if (transform.freezeTileCoverage) { return; } + this.transform = transform; + const idealTileIDs = transform.coveringTiles({ + tileSize: this._source.tileSize, + minzoom: this._source.minzoom, + maxzoom: this._source.maxzoom, + roundZoom: this._source.roundZoom, + reparseOverscaled: this._source.reparseOverscaled, + useElevationData: true + }); + + const incoming: {[string]: string} = idealTileIDs.reduce((acc, tileID) => { + acc[tileID.key] = ''; + if (!this._tiles[tileID.key]) { + const tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor(), transform.tileZoom); + tile.state = 'loaded'; + this._tiles[tileID.key] = tile; + } + return acc; + }, {}); + + for (const id in this._tiles) { + if (!(id in incoming)) { + this.freeFBO(id); + this._tiles[id].state = 'unloaded'; + delete this._tiles[id]; + } + } + } + + freeFBO(id: string) { + const fboIndex = this.proxyCachedFBO[id]; + if (fboIndex !== undefined) { + this.renderCachePool.push(fboIndex); + delete this.proxyCachedFBO[id]; + } + } + + deallocRenderCache() { + this.renderCache.forEach(fbo => fbo.fb.destroy()); + this.renderCache = []; + this.renderCachePool = []; + this.proxyCachedFBO = {}; + } +} + +/** + * Canonical, wrap and overscaledZ contain information of original source cache tile. + * This tile gets ortho-rendered to proxy tile (defined by proxyTileKey). + * posMatrix holds orthographic, scaling and translation information that is used + * for rendering original tile content to a proxy tile. Proxy tile covers whole + * or sub-rectangle of the original tile. + */ +class ProxiedTileID extends OverscaledTileID { + proxyTileKey: number; + + constructor(tileID: OverscaledTileID, proxyTileKey: number, posMatrix: Float32Array) { + super(tileID.overscaledZ, tileID.wrap, tileID.canonical.z, tileID.canonical.x, tileID.canonical.y); + this.proxyTileKey = proxyTileKey; + this.posMatrix = posMatrix; + } +} + +type OverlapStencilType = false | 'Clip' | 'Mask'; +type FBO = {fb: Framebuffer, tex: Texture, dirty: boolean, ref: number}; + +export class Terrain extends Elevation { + terrainTileForTile: {[number | string]: Tile}; + prevTerrainTileForTile: {[number | string]: Tile}; + painter: Painter; + sourceCache: SourceCache; + gridBuffer: VertexBuffer; + gridIndexBuffer: IndexBuffer; + gridSegments: SegmentVector; + gridNoSkirtSegments: SegmentVector; + proxiedCoords: {[string]: Array}; + proxyCoords: Array; + proxyToSource: {[number]: {[string]: Array}}; + proxySourceCache: ProxySourceCache; + renderingToTexture: boolean; + style: Style; + orthoMatrix: mat4; + enabled: boolean; + + drapeFirst: boolean; + drapeFirstPending: boolean; + forceDrapeFirst: boolean; // debugging purpose. + + _visibleDemTiles: Array; + _sourceTilesOverlap: {[string]: boolean}; + _overlapStencilMode: StencilMode; + _overlapStencilType: OverlapStencilType; + + _exaggeration: number; + _depthFBO: Framebuffer; + _depthTexture: Texture; + _depthDone: boolean; + _previousZoom: number; + _updateTimestamp: number; + pool: Array; + currentFBO: FBO; + renderedToTile: boolean; + + _findCoveringTileCache: {[string]: {[number]: ?number}}; + + _tilesDirty: {[string]: {[number]: boolean}}; + _invalidateRenderCache: boolean; + + _emptyDEMTexture: ?Texture; + _initializing: ?boolean; + _emptyDEMTextureDirty: ?boolean; + + constructor(painter: Painter, style: Style) { + super(); + this.painter = painter; + this.terrainTileForTile = {}; + this.prevTerrainTileForTile = {}; + + // Terrain rendering grid is 129x129 cell grid, made by 130x130 points. + // 130 vertices map to 128 DEM data + 1px padding on both sides. + // DEM texture is padded (1, 1, 1, 1) and padding pixels are backfilled + // by neighboring tile edges. This way we achieve tile stitching as + // edge vertices from neighboring tiles evaluate to the same 3D point. + const [triangleGridArray, triangleGridIndices, skirtIndicesOffset] = createGrid(GRID_DIM + 1); + const context = painter.context; + this.gridBuffer = context.createVertexBuffer(triangleGridArray, rasterBoundsAttributes.members); + this.gridIndexBuffer = context.createIndexBuffer(triangleGridIndices); + this.gridSegments = SegmentVector.simpleSegment(0, 0, triangleGridArray.length, triangleGridIndices.length); + this.gridNoSkirtSegments = SegmentVector.simpleSegment(0, 0, triangleGridArray.length, skirtIndicesOffset); + this.proxyCoords = []; + this.proxiedCoords = {}; + this._visibleDemTiles = []; + this._sourceTilesOverlap = {}; + this.proxySourceCache = new ProxySourceCache(style.map); + this.orthoMatrix = mat4.create(); + mat4.ortho(this.orthoMatrix, 0, EXTENT, 0, EXTENT, 0, 1); + const gl = context.gl; + this._overlapStencilMode = new StencilMode({func: gl.GEQUAL, mask: 0xFF}, 0, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE); + this._previousZoom = painter.transform.zoom; + this.pool = []; + this._findCoveringTileCache = {}; + this._tilesDirty = {}; + this.style = style; + style.on('data', this._onStyleDataEvent.bind(this)); + } + + /* + * Validate terrain and update source cache used for elevation. + * Explicitly pass transform to update elevation (Transform.updateElevation) + * before using transform for source cache update. + * cameraChanging is true when camera is zooming, panning or orbiting. + */ + update(style: Style, transform: Transform, cameraChanging: boolean) { + if (style && style.terrain) { + if (this.style !== style) { + style.on('data', this._onStyleDataEvent.bind(this)); + this.style = style; + } + this.enabled = true; + const terrainProps = style.terrain.properties; + this.sourceCache = ((style._getSourceCache(terrainProps.get('source')): any): SourceCache); + this._exaggeration = terrainProps.get('exaggeration'); + + const updateSourceCache = () => { + if (this.sourceCache.used) { + warnOnce(`Raster DEM source '${this.sourceCache.id}' is used both for terrain and as layer source.\n` + + 'This leads to lower resolution of hillshade. For full hillshade resolution but higher memory consumption, define another raster DEM source.'); + } + // Lower tile zoom is sufficient for terrain, given the size of terrain grid. + const demScale = this.sourceCache.getSource().tileSize / GRID_DIM; + const proxyTileSize = this.proxySourceCache.getSource().tileSize; + // Dem tile needs to be parent or at least of the same zoom level as proxy tile. + // Tile cover roundZoom behavior is set to the same as for proxy (false) in SourceCache.update(). + this.sourceCache.update(transform, demScale * proxyTileSize, true); + // As a result of update, we get new set of tiles: reset lookup cache. + this._findCoveringTileCache[this.sourceCache.id] = {}; + }; + + if (!this.sourceCache.usedForTerrain) { + // Init cache entry. + this._findCoveringTileCache[this.sourceCache.id] = {}; + // When toggling terrain on/off load available terrain tiles from cache + // before reading elevation at center. + this.sourceCache.usedForTerrain = true; + updateSourceCache(); + this._initializing = true; + } + + updateSourceCache(); + // Camera, when changing, gets constrained over terrain. Issue constrainCameraOverTerrain = true + // here to cover potential under terrain situation on data or style change. + transform.updateElevation(!cameraChanging); + + // Reset tile lookup cache and update draped tiles coordinates. + this._findCoveringTileCache[this.proxySourceCache.id] = {}; + this.proxySourceCache.update(transform); + + this._depthDone = false; + this._emptyDEMTextureDirty = true; + } else { + this._disable(); + } + } + + _onStyleDataEvent(event: any) { + if (event.coord && event.dataType === 'source') { + this._clearRenderCacheForTile(event.sourceCacheId, event.coord); + } else if (event.dataType === 'style') { + this._invalidateRenderCache = true; + } + } + + // Terrain + _disable() { + if (!this.enabled) return; + this.enabled = false; + this.proxySourceCache.deallocRenderCache(); + if (this.painter.style) { + for (const id in this.painter.style._sourceCaches) { + this.painter.style._sourceCaches[id].usedForTerrain = false; + } + } + } + + destroy() { + this._disable(); + if (this._emptyDEMTexture) this._emptyDEMTexture.destroy(); + this.pool.forEach(fbo => fbo.fb.destroy()); + this.pool = []; + if (this._depthFBO) { + this._depthFBO.destroy(); + delete this._depthFBO; + delete this._depthTexture; + } + } + + // Implements Elevation::_source. + _source(): ?SourceCache { + return this.enabled ? this.sourceCache : null; + } + + // Implements Elevation::exaggeration. + exaggeration(): number { + return this._exaggeration; + } + + get visibleDemTiles(): Array { + return this._visibleDemTiles; + } + + get drapeBufferSize(): [number, number] { + const extent = this.proxySourceCache.getSource().tileSize * 2; // *2 is to avoid upscaling bitmap on zoom. + return [extent, extent]; + } + + // For every renderable coordinate in every source cache, assign one proxy + // tile (see _setupProxiedCoordsForOrtho). Mapping of source tile to proxy + // tile is modeled by ProxiedTileID. In general case, source and proxy tile + // are of different zoom: ProxiedTileID.posMatrix models ortho, scale and + // translate from source to proxy. This matrix is used when rendering source + // tile to proxy tile's texture. + // One proxy tile can have multiple source tiles, or pieces of source tiles, + // that get rendered to it. + // For each proxy tile we assign one terrain tile (_assignTerrainTiles). The + // terrain tile provides elevation data when rendering (draping) proxy tile + // texture over terrain grid. + updateTileBinding(sourcesCoords: {[string]: Array}) { + if (!this.enabled) return; + this.prevTerrainTileForTile = this.terrainTileForTile; + + const psc = this.proxySourceCache; + const tr = this.painter.transform; + if (this._initializing) { + // Don't activate terrain until center tile gets loaded. + this._initializing = tr._centerAltitude === 0 && this.getAtPoint(MercatorCoordinate.fromLngLat(tr.center), -1) === -1; + this._emptyDEMTextureDirty = !this._initializing; + } + + const options = this.painter.options; + this.drapeFirst = (options.zooming || options.moving || options.rotating || !!this.forceDrapeFirst) && !this._invalidateRenderCache; + this._invalidateRenderCache = false; + const coords = this.proxyCoords = psc.getIds().map((id) => { + const tileID = psc.getTileByID(id).tileID; + tileID.posMatrix = tr.calculatePosMatrix(tileID.toUnwrapped()); + return tileID; + }); + sortByDistanceToCamera(coords, this.painter); + this._previousZoom = tr.zoom; + + const previousProxyToSource = this.proxyToSource || {}; + this.proxyToSource = {}; + coords.forEach((tileID) => { + this.proxyToSource[tileID.key] = {}; + }); + + this.terrainTileForTile = {}; + const sourceCaches = this.painter.style._sourceCaches; + for (const id in sourceCaches) { + const sourceCache = sourceCaches[id]; + if (!sourceCache.used) continue; + if (sourceCache !== this.sourceCache) this._findCoveringTileCache[sourceCache.id] = {}; + this._setupProxiedCoordsForOrtho(sourceCache, sourcesCoords[id], previousProxyToSource); + if (sourceCache.usedForTerrain) continue; + const coordinates = sourcesCoords[id]; + if (sourceCache.getSource().reparseOverscaled) { + // Do this for layers that are not rasterized to proxy tile. + this._assignTerrainTiles(coordinates); + } + } + + // Background has no source. Using proxy coords with 1-1 ortho (this.proxiedCoords[psc.id]) + // when rendering background to proxy tiles. + this.proxiedCoords[psc.id] = coords.map(tileID => new ProxiedTileID(tileID, tileID.key, this.orthoMatrix)); + this._assignTerrainTiles(coords); + this._prepareDEMTextures(); + + this._setupRenderCache(previousProxyToSource); + + this.drapeFirstPending = this.drapeFirst; + this.renderingToTexture = false; + this._initFBOPool(); + this._updateTimestamp = browser.now(); + + // Gather all dem tiles that are assigned to proxy tiles + const visibleKeys = {}; + this._visibleDemTiles = []; + + for (const id of this.proxyCoords) { + const demTile = this.terrainTileForTile[id.key]; + if (!demTile) + continue; + const key = demTile.tileID.key; + if (key in visibleKeys) + continue; + this._visibleDemTiles.push(demTile); + visibleKeys[key] = key; + } + + } + + _assignTerrainTiles(coords: Array) { + if (this._initializing) return; + coords.forEach((tileID) => { + if (this.terrainTileForTile[tileID.key]) return; + const demTile = this._findTileCoveringTileID(tileID, this.sourceCache); + if (demTile) this.terrainTileForTile[tileID.key] = demTile; + }); + } + + _prepareDEMTextures() { + const context = this.painter.context; + const gl = context.gl; + for (const key in this.terrainTileForTile) { + const tile = this.terrainTileForTile[key]; + const dem = tile.dem; + if (dem && (!tile.demTexture || tile.needsDEMTextureUpload)) { + context.activeTexture.set(gl.TEXTURE1); + prepareDEMTexture(this.painter, tile, dem); + } + } + } + + _prepareDemTileUniforms(proxyTile: Tile, demTile: ?Tile, uniforms: UniformValues, uniformSuffix: ?string): boolean { + if (!demTile || demTile.demTexture == null) + return false; + + assert(demTile.dem); + const proxyId = proxyTile.tileID.canonical; + const demId = demTile.tileID.canonical; + const demScaleBy = Math.pow(2, demId.z - proxyId.z); + const suffix = uniformSuffix || ""; + uniforms[`u_dem_tl${suffix}`] = [proxyId.x * demScaleBy % 1, proxyId.y * demScaleBy % 1]; + uniforms[`u_dem_scale${suffix}`] = demScaleBy; + return true; + } + + get emptyDEMTexture(): Texture { + return !this._emptyDEMTextureDirty && this._emptyDEMTexture ? + this._emptyDEMTexture : this._updateEmptyDEMTexture(); + } + + _getLoadedAreaMinimum(): number { + let nonzero = 0; + const min = this._visibleDemTiles.reduce((acc, tile) => { + if (!tile.dem) return acc; + const m = tile.dem.tree.minimums[0]; + acc += m; + if (m > 0) nonzero++; + return acc; + }, 0); + return nonzero ? min / nonzero : 0; + } + + _updateEmptyDEMTexture(): Texture { + const context = this.painter.context; + const gl = context.gl; + context.activeTexture.set(gl.TEXTURE2); + + const min = this._getLoadedAreaMinimum(); + const image = { + width: 1, height: 1, + data: new Uint8Array(DEMData.pack(min, ((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding)) + }; + + this._emptyDEMTextureDirty = false; + let texture = this._emptyDEMTexture; + if (!texture) { + texture = this._emptyDEMTexture = new Texture(context, image, gl.RGBA, {premultiply: false}); + } else { + texture.update(image, {premultiply: false}); + } + return texture; + } + + // useDepthForOcclusion: Pre-rendered depth to texture (this._depthTexture) is + // used to hide (actually moves all object's vertices out of viewport). + // useMeterToDem: u_meter_to_dem uniform is not used for all terrain programs, + // optimization to avoid unnecessary computation and upload. + setupElevationDraw(tile: Tile, program: Program<*>, + options?: { + useDepthForOcclusion?: boolean, + useMeterToDem?: boolean, + labelPlaneMatrixInv?: ?Float32Array, + morphing?: { srcDemTile: Tile, dstDemTile: Tile, phase: number } + }) { + const context = this.painter.context; + const gl = context.gl; + const uniforms = defaultTerrainUniforms(((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding); + uniforms['u_dem_size'] = this.sourceCache.getSource().tileSize; + uniforms['u_exaggeration'] = this.exaggeration(); + + let demTile = null; + let prevDemTile = null; + let morphingPhase = 1.0; + + if (options && options.morphing) { + const srcTile = options.morphing.srcDemTile; + const dstTile = options.morphing.dstDemTile; + morphingPhase = options.morphing.phase; + + if (srcTile && dstTile) { + if (this._prepareDemTileUniforms(tile, srcTile, uniforms, "_prev")) + prevDemTile = srcTile; + if (this._prepareDemTileUniforms(tile, dstTile, uniforms)) + demTile = dstTile; + } + } + + if (prevDemTile && demTile) { + // Both DEM textures are expected to be correctly set if geomorphing is enabled + context.activeTexture.set(gl.TEXTURE2); + (demTile.demTexture: any).bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST); + context.activeTexture.set(gl.TEXTURE4); + (prevDemTile.demTexture: any).bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST); + uniforms["u_dem_lerp"] = morphingPhase; + } else { + demTile = this.terrainTileForTile[tile.tileID.key]; + context.activeTexture.set(gl.TEXTURE2); + const demTexture = this._prepareDemTileUniforms(tile, demTile, uniforms) ? + (demTile.demTexture: any) : this.emptyDEMTexture; + demTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST); + } + + if (options && options.useDepthForOcclusion) { + context.activeTexture.set(gl.TEXTURE3); + this._depthTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST); + uniforms['u_depth_size_inv'] = [1 / this._depthFBO.width, 1 / this._depthFBO.height]; + } + + if (options && options.useMeterToDem && demTile) { + const meterToDEM = (1 << demTile.tileID.canonical.z) * mercatorZfromAltitude(1, this.painter.transform.center.lat) * this.sourceCache.getSource().tileSize; + uniforms['u_meter_to_dem'] = meterToDEM; + } + if (options && options.labelPlaneMatrixInv) { + uniforms['u_label_plane_matrix_inv'] = options.labelPlaneMatrixInv; + } + program.setTerrainUniformValues(context, uniforms); + } + + // If terrain handles layer rendering (rasterize it), return true. + renderLayer(layer: StyleLayer, _?: SourceCache): boolean { + const painter = this.painter; + if (painter.renderPass !== 'translucent') { + // Depth texture is used only for POI symbols and circles, to skip render of symbols occluded by e.g. hill. + if (!this._depthDone && (layer.type === 'symbol' || layer.type === 'circle')) this.drawDepth(); + return true; // Early leave: all rendering is done in translucent pass. + } + if (this.drapeFirst && this.drapeFirstPending) { + this.render(); + this.drapeFirstPending = false; + return true; + } else if (this._isLayerDrapedOverTerrain(layer)) { + if (this.drapeFirst && !this.renderingToTexture) { + // It's done. nothing to do for this layer but to advance. + return true; + } + this.render(); + return true; + } + return false; + } + + // For each proxy tile, render all layers until the non-draped layer (and + // render the tile to the screen) before advancing to the next proxy tile. + // Apart to layer-by-layer rendering used in 2D, here we have proxy-tile-by-proxy-tile + // rendering. + render() { + this.renderingToTexture = true; + const painter = this.painter; + const context = this.painter.context; + const psc = this.proxySourceCache; + const proxies = this.proxiedCoords[psc.id]; + const setupRenderToScreen = () => { + context.bindFramebuffer.set(null); + context.viewport.set([0, 0, painter.width, painter.height]); + this.renderingToTexture = false; + }; + + const start = painter.currentLayer; + let end = start; // end is computed as the first next non draped layer. It is not used in drapeFirst mode. + + let drawAsRasterCoords = []; + const layerIds = painter.style._order; + + let poolIndex = 0; + for (let i = 0; i < proxies.length; i++) { + const proxy = proxies[i]; + + // bind framebuffer and assign texture to the tile (texture used in drawTerrainRaster). + const tile = psc.getTileByID(proxy.proxyTileKey); + const renderCacheIndex = psc.proxyCachedFBO[proxy.key]; + const fbo = this.currentFBO = renderCacheIndex !== undefined ? psc.renderCache[renderCacheIndex] : this.pool[poolIndex++]; + tile.texture = fbo.tex; + if (renderCacheIndex !== undefined && !fbo.dirty) { + drawAsRasterCoords.push(tile.tileID); // use cached render from previous pass, no need to render again. + continue; + } + context.bindFramebuffer.set(fbo.fb.framebuffer); + this.renderedToTile = false; // reset flag. + if (fbo.dirty) { + // Clear on start. + context.clear({color: Color.transparent}); + fbo.dirty = false; + } + + let currentStencilSource; // There is no need to setup stencil for the same source for consecutive layers. + for (painter.currentLayer = start; painter.currentLayer < layerIds.length; painter.currentLayer++) { + const layer = painter.style._layers[layerIds[painter.currentLayer]]; + const hidden = layer.isHidden(painter.transform.zoom); + const draped = this._isLayerDrapedOverTerrain(layer); + + if (this.drapeFirst && !draped) continue; + if (painter.currentLayer > end) { + if (!hidden && !draped) { + break; + } + end++; + } + if (hidden) continue; + + const sourceCache = this.painter.style._getLayerSourceCache(layer); + const proxiedCoords = sourceCache ? this.proxyToSource[proxy.key][sourceCache.id] : [proxy]; + if (!proxiedCoords) continue; // when tile is not loaded yet for the source cache. + const coords = ((proxiedCoords: any): Array); + context.viewport.set([0, 0, fbo.fb.width, fbo.fb.height]); + if (currentStencilSource !== (sourceCache ? sourceCache.id : null)) { + this._setupStencil(proxiedCoords, layer, sourceCache); + currentStencilSource = sourceCache ? sourceCache.id : null; + } + painter.renderLayer(painter, sourceCache, layer, coords); + } + fbo.dirty = this.renderedToTile; + if (this.renderedToTile) drawAsRasterCoords.push(tile.tileID); + + if (poolIndex === FBO_POOL_SIZE) { + poolIndex = 0; + if (drawAsRasterCoords.length > 0) { + setupRenderToScreen(); + drawTerrainRaster(painter, this, psc, drawAsRasterCoords, this._updateTimestamp); + this.renderingToTexture = true; + drawAsRasterCoords = []; + } + } + } + setupRenderToScreen(); + if (drawAsRasterCoords.length > 0) drawTerrainRaster(painter, this, psc, drawAsRasterCoords, this._updateTimestamp); + painter.currentLayer = this.drapeFirst ? -1 : end; + assert(!this.drapeFirst || (start === 0 && painter.currentLayer === -1)); + } + + // Performs raycast against visible DEM tiles on the screen and returns the distance travelled along the ray. + // x & y components of the position are expected to be in normalized mercator coordinates [0, 1] and z in meters. + raycast(pos: vec3, dir: vec3, exaggeration: number): ?number { + if (!this._visibleDemTiles) + return null; + + // Perform initial raycasts against root nodes of the available dem tiles + // and use this information to sort them from closest to furthest. + const preparedTiles = this._visibleDemTiles.filter(tile => tile.dem).map(tile => { + const id = tile.tileID; + const tiles = Math.pow(2.0, id.overscaledZ); + const {x, y} = id.canonical; + + // Compute tile boundaries in mercator coordinates + const minx = x / tiles; + const maxx = (x + 1) / tiles; + const miny = y / tiles; + const maxy = (y + 1) / tiles; + const tree = (tile.dem: any).tree; + + return { + minx, miny, maxx, maxy, + t: tree.raycastRoot(minx, miny, maxx, maxy, pos, dir, exaggeration), + tile + }; + }); + + preparedTiles.sort((a, b) => { + const at = a.t !== null ? a.t : Number.MAX_VALUE; + const bt = b.t !== null ? b.t : Number.MAX_VALUE; + return at - bt; + }); + + for (const obj of preparedTiles) { + if (obj.t == null) + return null; + + // Perform more accurate raycast against the dem tree. First intersection is the closest on + // as all tiles are sorted from closest to furthest + const tree = (obj.tile.dem: any).tree; + const t = tree.raycast(obj.minx, obj.miny, obj.maxx, obj.maxy, pos, dir, exaggeration); + + if (t != null) + return t; + } + + return null; + } + + _createFBO(): FBO { + const painter = this.painter; + const context = painter.context; + const gl = context.gl; + const bufferSize = this.drapeBufferSize; + context.activeTexture.set(gl.TEXTURE0); + const tex = new Texture(context, {width: bufferSize[0], height: bufferSize[1], data: null}, gl.RGBA); + tex.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + const fb = context.createFramebuffer(bufferSize[0], bufferSize[1], false); + fb.colorAttachment.set(tex.texture); + + if (context.extTextureFilterAnisotropic && !context.extTextureFilterAnisotropicForceOff) { + gl.texParameterf(gl.TEXTURE_2D, + context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, + context.extTextureFilterAnisotropicMax); + } + + return {fb, tex, dirty: false, ref: 1}; + } + + _initFBOPool() { + while (this.pool.length < Math.min(FBO_POOL_SIZE, this.proxyCoords.length)) { + this.pool.push(this._createFBO()); + } + } + + _shouldDisableRenderCache(): boolean { + // Disable render caches on dynamic events due to fading. + const isCrossFading = id => { + const layer = this.painter.style._layers[id]; + const isHidden = !layer.isHidden(this.painter.transform.zoom); + const crossFade = layer.getCrossfadeParameters(); + const isFading = !!crossFade && crossFade.t !== 1; + return layer.type !== 'custom' && !isHidden && isFading; + }; + return !this.drapeFirst || this.painter.style._order.some(isCrossFading); + } + + _clearRasterFadeFromRenderCache() { + for (const id in this.painter.style._sourceCaches) { + if (!(this.painter.style._sourceCaches[id]._source instanceof RasterTileSource)) { + return; + } + } + + // Check if any raster tile is in a fading state + for (let i = 0; i < this.painter.style._order.length; ++i) { + const layer = this.painter.style._layers[this.painter.style._order[i]]; + const isHidden = layer.isHidden(this.painter.transform.zoom); + const sourceCache = this.painter.style._getLayerSourceCache(layer); + if (layer.type !== 'raster' || isHidden || !sourceCache) { continue; } + + const rasterLayer = ((layer: any): RasterStyleLayer); + const fadeDuration = rasterLayer.paint.get('raster-fade-duration'); + for (const proxy of this.proxyCoords) { + const proxiedCoords = this.proxyToSource[proxy.key][sourceCache.id]; + const coords = ((proxiedCoords: any): Array); + if (!coords) { continue; } + + for (const coord of coords) { + const tile = sourceCache.getTile(coord); + const parent = sourceCache.findLoadedParent(coord, 0); + const fade = rasterFade(tile, parent, sourceCache, this.painter.transform, fadeDuration); + const isFading = fade.opacity !== 1 || fade.mix !== 0; + if (isFading) { + this._clearRenderCacheForTile(sourceCache.id, coord); + } + } + } + } + } + + _setupRenderCache(previousProxyToSource: {[number]: {[string]: Array}}) { + const psc = this.proxySourceCache; + if (this._shouldDisableRenderCache()) { + if (psc.renderCache.length > psc.renderCachePool.length) { + const used = ((Object.values(psc.proxyCachedFBO): any): Array); + psc.proxyCachedFBO = {}; + assert(psc.renderCache.length === psc.renderCachePool.length + used.length); + psc.renderCachePool = psc.renderCachePool.concat(used); + } + return; + } + + this._clearRasterFadeFromRenderCache(); + + const coords = this.proxyCoords; + const dirty = this._tilesDirty; + for (let i = coords.length - 1; i >= 0; i--) { + const proxy = coords[i]; + const tile = psc.getTileByID(proxy.key); + + if (psc.proxyCachedFBO[proxy.key] !== undefined) { + assert(tile.texture); + const prev = previousProxyToSource[proxy.key]; + assert(prev); + // Reuse previous render from cache if there was no change of + // content that was used to render proxy tile. + const current = this.proxyToSource[proxy.key]; + let equal = 0; + for (const source in current) { + const tiles = current[source]; + const prevTiles = prev[source]; + if (!prevTiles || prevTiles.length !== tiles.length || + tiles.some((t, index) => (t !== prevTiles[index] || (dirty[source] && dirty[source].hasOwnProperty(t.key))))) { + equal = -1; + break; + } + ++equal; + } + // dirty === false: doesn't need to be rendered to, just use cached render. + psc.renderCache[psc.proxyCachedFBO[proxy.key]].dirty = equal < 0 || equal !== Object.values(prev).length; + } else { + // Assign renderCache FBO if there are available FBOs in pool. + let index = psc.renderCachePool.pop(); + if (index === undefined && psc.renderCache.length < RENDER_CACHE_MAX_SIZE) { + index = psc.renderCache.length; + psc.renderCache.push(this._createFBO()); + assert(psc.renderCache.length <= coords.length); + } + if (index !== undefined) { + psc.proxyCachedFBO[proxy.key] = index; + psc.renderCache[index].dirty = true; // needs to be rendered to. + } + } + } + this._tilesDirty = {}; + } + + _setupStencil(proxiedCoords: Array, layer: StyleLayer, sourceCache?: SourceCache) { + if (!sourceCache || !this._sourceTilesOverlap[sourceCache.id]) { + if (this._overlapStencilType) this._overlapStencilType = false; + return; + } + const context = this.painter.context; + const gl = context.gl; + + // If needed, setup stencilling. Don't bother to remove when there is no + // more need: in such case, if there is no overlap, stencilling is disabled. + if (proxiedCoords.length <= 1) { this._overlapStencilType = false; return; } + + const fbo = this.currentFBO; + const fb = fbo.fb; + let stencilRange; + if (layer.isTileClipped()) { + stencilRange = proxiedCoords.length; + this._overlapStencilMode.test = {func: gl.EQUAL, mask: 0xFF}; + this._overlapStencilType = 'Clip'; + } else if (proxiedCoords[0].overscaledZ > proxiedCoords[proxiedCoords.length - 1].overscaledZ) { + stencilRange = 1; + this._overlapStencilMode.test = {func: gl.GREATER, mask: 0xFF}; + this._overlapStencilType = 'Mask'; + } else { + this._overlapStencilType = false; + return; + } + if (!fb.depthAttachment) { + const renderbuffer = context.createRenderbuffer(context.gl.DEPTH_STENCIL, fb.width, fb.height); + fb.depthAttachment = new DepthStencilAttachment(context, fb.framebuffer); + fb.depthAttachment.set(renderbuffer); + context.clear({stencil: 0}); + } + if (fbo.ref + stencilRange > 255) { + context.clear({stencil: 0}); + fbo.ref = 0; + } + fbo.ref += stencilRange; + this._overlapStencilMode.ref = fbo.ref; + if (layer.isTileClipped()) { + this._renderTileClippingMasks(proxiedCoords, this._overlapStencilMode.ref); + } + } + + stencilModeForRTTOverlap(id: OverscaledTileID) { + if (!this.renderingToTexture || !this._overlapStencilType) { + return StencilMode.disabled; + } + // All source tiles contributing to the same proxy are processed in sequence, in zoom descending order. + // For raster / hillshade overlap masking, ref is based on zoom dif. + // For vector layer clipping, every tile gets dedicated stencil ref. + if (this._overlapStencilType === 'Clip') { + // In immediate 2D mode, we render rects to mark clipping area and handle behavior on tile borders. + // Here, there is no need for now for this: + // 1. overlap is handled by proxy render to texture tiles (there is no overlap there) + // 2. here we handle only brief zoom out semi-transparent color intensity flickering + // and that is avoided fine by stenciling primitives as part of drawing (instead of additional tile quad step). + this._overlapStencilMode.ref = this.painter._tileClippingMaskIDs[id.key]; + } // else this._overlapStencilMode.ref is set to a single value used per proxy tile, in _setupStencil. + return this._overlapStencilMode; + } + + _renderTileClippingMasks(proxiedCoords: Array, ref: number) { + const painter = this.painter; + const context = this.painter.context; + const gl = context.gl; + painter._tileClippingMaskIDs = {}; + context.setColorMode(ColorMode.disabled); + context.setDepthMode(DepthMode.disabled); + + const program = painter.useProgram('clippingMask'); + + for (const tileID of proxiedCoords) { + const id = painter._tileClippingMaskIDs[tileID.key] = --ref; + program.draw(context, gl.TRIANGLES, DepthMode.disabled, + // Tests will always pass, and ref value will be written to stencil buffer. + new StencilMode({func: gl.ALWAYS, mask: 0}, id, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE), + ColorMode.disabled, CullFaceMode.disabled, clippingMaskUniformValues(tileID.posMatrix), + '$clipping', painter.tileExtentBuffer, + painter.quadTriangleIndexBuffer, painter.tileExtentSegments); + } + } + + // Casts a ray from a point on screen and returns the intersection point with the terrain. + // The returned point contains the mercator coordinates in its first 3 components, and elevation + // in meter in its 4th coordinate. + pointCoordinate(screenPoint: Point): ?vec4 { + const transform = this.painter.transform; + if (screenPoint.x < 0 || screenPoint.x > transform.width || + screenPoint.y < 0 || screenPoint.y > transform.height) { + return null; + } + + const far = [screenPoint.x, screenPoint.y, 1, 1]; + vec4.transformMat4(far, far, transform.pixelMatrixInverse); + vec4.scale(far, far, 1.0 / far[3]); + // x & y in pixel coordinates, z is altitude in meters + far[0] /= transform.worldSize; + far[1] /= transform.worldSize; + const camera = transform._camera.position; + const mercatorZScale = mercatorZfromAltitude(1, transform.center.lat); + const p = [camera[0], camera[1], camera[2] / mercatorZScale, 0.0]; + const dir = vec3.subtract([], far.slice(0, 3), p); + vec3.normalize(dir, dir); + const distanceAlongRay = this.raycast(p, dir, this._exaggeration); + + if (distanceAlongRay === null || !distanceAlongRay) return null; + vec3.scaleAndAdd(p, p, dir, distanceAlongRay); + p[3] = p[2]; + p[2] *= mercatorZScale; + return p; + } + + drawDepth() { + const painter = this.painter; + const context = painter.context; + const psc = this.proxySourceCache; + + const width = Math.ceil(painter.width), height = Math.ceil(painter.height); + if (this._depthFBO && (this._depthFBO.width !== width || this._depthFBO.height !== height)) { + this._depthFBO.destroy(); + delete this._depthFBO; + delete this._depthTexture; + } + if (!this._depthFBO) { + const gl = context.gl; + const fbo = context.createFramebuffer(width, height, true); + context.activeTexture.set(gl.TEXTURE0); + const texture = new Texture(context, {width, height, data: null}, gl.RGBA); + texture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE); + fbo.colorAttachment.set(texture.texture); + const renderbuffer = context.createRenderbuffer(context.gl.DEPTH_COMPONENT16, width, height); + fbo.depthAttachment.set(renderbuffer); + this._depthFBO = fbo; + this._depthTexture = texture; + } + context.bindFramebuffer.set(this._depthFBO.framebuffer); + context.viewport.set([0, 0, width, height]); + + drawTerrainDepth(painter, this, psc, this.proxyCoords); + context.bindFramebuffer.set(null); + context.viewport.set([0, 0, painter.width, painter.height]); + + this._depthDone = true; + } + + _isLayerDrapedOverTerrain(styleLayer: StyleLayer): boolean { + if (!this.enabled) return false; + return drapedLayers.hasOwnProperty(styleLayer.type); + } + + _setupProxiedCoordsForOrtho(sourceCache: SourceCache, sourceCoords: Array, previousProxyToSource: {[number]: {[string]: Array}}) { + if (sourceCache.getSource() instanceof ImageSource) { + return this._setupProxiedCoordsForImageSource(sourceCache, sourceCoords, previousProxyToSource); + } + this._findCoveringTileCache[sourceCache.id] = this._findCoveringTileCache[sourceCache.id] || {}; + const coords = this.proxiedCoords[sourceCache.id] = []; + const proxys = this.proxyCoords; + for (let i = 0; i < proxys.length; i++) { + const proxyTileID = proxys[i]; + const proxied = this._findTileCoveringTileID(proxyTileID, sourceCache); + if (proxied) { + assert(proxied.hasData()); + const id = this._createProxiedId(proxyTileID, proxied, previousProxyToSource[proxyTileID.key] && previousProxyToSource[proxyTileID.key][sourceCache.id]); + coords.push(id); + this.proxyToSource[proxyTileID.key][sourceCache.id] = [id]; + } + } + let hasOverlap = false; + for (let i = 0; i < sourceCoords.length; i++) { + const tile = sourceCache.getTile(sourceCoords[i]); + if (!tile || !tile.hasData()) continue; + const proxy = this._findTileCoveringTileID(tile.tileID, this.proxySourceCache); + // Don't add the tile if already added in loop above. + if (proxy && proxy.tileID.canonical.z !== tile.tileID.canonical.z) { + const array = this.proxyToSource[proxy.tileID.key][sourceCache.id]; + const id = this._createProxiedId(proxy.tileID, tile, previousProxyToSource[proxy.tileID.key] && previousProxyToSource[proxy.tileID.key][sourceCache.id]); + if (!array) { + this.proxyToSource[proxy.tileID.key][sourceCache.id] = [id]; + } else { + // The last element is parent added in loop above. This way we get + // a list in Z descending order which is needed for stencil masking. + array.splice(array.length - 1, 0, id); + } + coords.push(id); + hasOverlap = true; + } + } + this._sourceTilesOverlap[sourceCache.id] = hasOverlap; + } + + _setupProxiedCoordsForImageSource(sourceCache: SourceCache, sourceCoords: Array, previousProxyToSource: {[number]: {[string]: Array}}) { + if (!sourceCache.getSource().loaded()) return; + + const coords = this.proxiedCoords[sourceCache.id] = []; + const proxys = this.proxyCoords; + const imageSource: ImageSource = ((sourceCache.getSource(): any): ImageSource); + + const anchor = new Point(imageSource.tileID.x, imageSource.tileID.y)._div(1 << imageSource.tileID.z); + const aabb = imageSource.coordinates.map(MercatorCoordinate.fromLngLat).reduce((acc, coord) => { + acc.min.x = Math.min(acc.min.x, coord.x - anchor.x); + acc.min.y = Math.min(acc.min.y, coord.y - anchor.y); + acc.max.x = Math.max(acc.max.x, coord.x - anchor.x); + acc.max.y = Math.max(acc.max.y, coord.y - anchor.y); + return acc; + }, {min: new Point(Number.MAX_VALUE, Number.MAX_VALUE), max: new Point(-Number.MAX_VALUE, -Number.MAX_VALUE)}); + + // Fast conservative check using aabb: content outside proxy tile gets clipped out by on render, anyway. + const tileOutsideImage = (tileID, imageTileID) => { + const x = tileID.wrap + tileID.canonical.x / (1 << tileID.canonical.z); + const y = tileID.canonical.y / (1 << tileID.canonical.z); + const d = EXTENT / (1 << tileID.canonical.z); + + const ix = imageTileID.wrap + imageTileID.canonical.x / (1 << imageTileID.canonical.z); + const iy = imageTileID.canonical.y / (1 << imageTileID.canonical.z); + + return x + d < ix + aabb.min.x || x > ix + aabb.max.x || y + d < iy + aabb.min.y || y > iy + aabb.max.y; + }; + + for (let i = 0; i < proxys.length; i++) { + const proxyTileID = proxys[i]; + for (let j = 0; j < sourceCoords.length; j++) { + const tile = sourceCache.getTile(sourceCoords[j]); + if (!tile || !tile.hasData()) continue; + + // Setup proxied -> proxy mapping only if image on given tile wrap intersects the proxy tile. + if (tileOutsideImage(proxyTileID, tile.tileID)) continue; + + const id = this._createProxiedId(proxyTileID, tile, previousProxyToSource[proxyTileID.key] && previousProxyToSource[proxyTileID.key][sourceCache.id]); + const array = this.proxyToSource[proxyTileID.key][sourceCache.id]; + if (!array) { + this.proxyToSource[proxyTileID.key][sourceCache.id] = [id]; + } else { + array.push(id); + } + coords.push(id); + } + } + } + + // recycle is previous pass content that likely contains proxied ID combining proxy and source tile. + _createProxiedId(proxyTileID: OverscaledTileID, tile: Tile, recycle: Array): ProxiedTileID { + let matrix = this.orthoMatrix; + if (recycle) { + const recycled = recycle.find(proxied => (proxied.key === tile.tileID.key)); + if (recycled) return recycled; + } + if (tile.tileID.key !== proxyTileID.key) { + const scale = proxyTileID.canonical.z - tile.tileID.canonical.z; + matrix = mat4.create(); + let size, xOffset, yOffset; + const wrap = (tile.tileID.wrap - proxyTileID.wrap) << proxyTileID.overscaledZ; + if (scale > 0) { + size = EXTENT >> scale; + xOffset = size * ((tile.tileID.canonical.x << scale) - proxyTileID.canonical.x + wrap); + yOffset = size * ((tile.tileID.canonical.y << scale) - proxyTileID.canonical.y); + } else { + size = EXTENT << -scale; + xOffset = EXTENT * (tile.tileID.canonical.x - ((proxyTileID.canonical.x + wrap) << -scale)); + yOffset = EXTENT * (tile.tileID.canonical.y - (proxyTileID.canonical.y << -scale)); + } + mat4.ortho(matrix, 0, size, 0, size, 0, 1); + mat4.translate(matrix, matrix, [xOffset, yOffset, 0]); + } + return new ProxiedTileID(tile.tileID, proxyTileID.key, matrix); + } + + // A variant of SourceCache.findLoadedParent that considers only visible + // tiles (and doesn't check SourceCache._cache). Another difference is in + // caching "not found" results along the lookup, to leave the lookup early. + // Not found is cached by this._findCoveringTileCache[key] = null; + _findTileCoveringTileID(tileID: OverscaledTileID, sourceCache: SourceCache): ?Tile { + let tile = sourceCache.getTile(tileID); + if (tile && tile.hasData()) return tile; + + const lookup = this._findCoveringTileCache[sourceCache.id]; + const key = lookup[tileID.key]; + tile = key ? sourceCache.getTileByID(key) : null; + if ((tile && tile.hasData()) || key === null) return tile; + + assert(!key || tile); + let sourceTileID = tile ? tile.tileID : tileID; + let z = sourceTileID.overscaledZ; + const minzoom = sourceCache.getSource().minzoom; + const path = []; + if (!key) { + const maxzoom = sourceCache.getSource().maxzoom; + if (tileID.canonical.z >= maxzoom) { + const downscale = tileID.canonical.z - maxzoom; + if (sourceCache.getSource().reparseOverscaled) { + z = Math.max(tileID.canonical.z + 2, sourceCache.transform.tileZoom); + sourceTileID = new OverscaledTileID(z, tileID.wrap, maxzoom, + tileID.canonical.x >> downscale, tileID.canonical.y >> downscale); + } else if (downscale !== 0) { + z = maxzoom; + sourceTileID = new OverscaledTileID(z, tileID.wrap, maxzoom, + tileID.canonical.x >> downscale, tileID.canonical.y >> downscale); + } + } + if (sourceTileID.key !== tileID.key) { + path.push(sourceTileID.key); + tile = sourceCache.getTile(sourceTileID); + } + } + + const pathToLookup = (key) => { + path.forEach(id => { lookup[id] = key; }); + path.length = 0; + }; + + for (z = z - 1; z >= minzoom && !(tile && tile.hasData()); z--) { + if (tile) { + pathToLookup(tile.tileID.key); // Store lookup to parents not loaded (yet). + } + const id = sourceTileID.calculateScaledKey(z); + tile = sourceCache.getTileByID(id); + if (tile && tile.hasData()) break; + const key = lookup[id]; + if (key === null) { + break; // There's no tile loaded and no point searching further. + } else if (key !== undefined) { + tile = sourceCache.getTileByID(key); + assert(tile); + continue; + } + path.push(id); + } + + pathToLookup(tile ? tile.tileID.key : null); + return tile && tile.hasData() ? tile : null; + } + + findDEMTileFor(tileID: OverscaledTileID): ?Tile { + return this.enabled ? this._findTileCoveringTileID(tileID, this.sourceCache) : null; + } + + /* + * Bookkeeping if something gets rendered to the tile. + */ + prepareDrawTile(coord: OverscaledTileID) { + if (!this.renderedToTile) { + this.renderedToTile = true; + } + } + + _clearRenderCacheForTile(source: string, coord: OverscaledTileID) { + let sourceTiles = this._tilesDirty[source]; + if (!sourceTiles) sourceTiles = this._tilesDirty[source] = {}; + sourceTiles[coord.key] = true; + } +} + +function sortByDistanceToCamera(tileIDs, painter) { + const cameraCoordinate = painter.transform.pointCoordinate(painter.transform.getCameraPoint()); + const cameraPoint = new Point(cameraCoordinate.x, cameraCoordinate.y); + tileIDs.sort((a, b) => { + if (b.overscaledZ - a.overscaledZ) return b.overscaledZ - a.overscaledZ; + const aPoint = new Point(a.canonical.x + (1 << a.canonical.z) * a.wrap, a.canonical.y); + const bPoint = new Point(b.canonical.x + (1 << b.canonical.z) * b.wrap, b.canonical.y); + const cameraScaled = cameraPoint.mult(1 << a.canonical.z); + cameraScaled.x -= 0.5; + cameraScaled.y -= 0.5; + return cameraScaled.distSqr(aPoint) - cameraScaled.distSqr(bPoint); + }); +} + +/** + * Creates uniform grid of triangles, covering EXTENT x EXTENT square, with two + * adjustent traigles forming a quad, so that there are |count| columns and rows + * of these quads in EXTENT x EXTENT square. + * e.g. for count of 2: + * ------------- + * | /| /| + * | / | / | + * |/ |/ | + * ------------- + * | /| /| + * | / | / | + * |/ |/ | + * ------------- + * @param {number} count Count of rows and columns + * @private + */ +function createGrid(count: number): [RasterBoundsArray, TriangleIndexArray, number] { + const boundsArray = new RasterBoundsArray(); + // Around the grid, add one more row/column padding for "skirt". + const indexArray = new TriangleIndexArray(); + const size = count + 2; + boundsArray.reserve(size * size); + indexArray.reserve((size - 1) * (size - 1) * 2); + const step = EXTENT / (count - 1); + const gridBound = EXTENT + step / 2; + const bound = gridBound + step; + + // Skirt offset of 0x5FFF is chosen randomly to encode boolean value (skirt + // on/off) with x position (max value EXTENT = 4096) to 16-bit signed integer. + const skirtOffset = 24575; // 0x5FFF + for (let y = -step; y < bound; y += step) { + for (let x = -step; x < bound; x += step) { + const offset = (x < 0 || x > gridBound || y < 0 || y > gridBound) ? skirtOffset : 0; + const xi = clamp(Math.round(x), 0, EXTENT); + const yi = clamp(Math.round(y), 0, EXTENT); + boundsArray.emplaceBack(xi + offset, yi, xi, yi); + } + } + + // For cases when there's no need to render "skirt", the "inner" grid indices + // are followed by skirt indices. + const skirtIndicesOffset = (size - 3) * (size - 3) * 2; + const quad = (i, j) => { + const index = j * size + i; + indexArray.emplaceBack(index + 1, index, index + size); + indexArray.emplaceBack(index + size, index + size + 1, index + 1); + }; + for (let j = 1; j < size - 2; j++) { + for (let i = 1; i < size - 2; i++) { + quad(i, j); + } + } + // Padding (skirt) indices: + [0, size - 2].forEach(j => { + for (let i = 0; i < size - 1; i++) { + quad(i, j); + quad(j, i); + } + }); + return [boundsArray, indexArray, skirtIndicesOffset]; +} + +export type TerrainUniformsType = {| + 'u_dem': Uniform1i, + 'u_dem_prev': Uniform1i, + 'u_dem_unpack': Uniform4f, + 'u_dem_tl': Uniform2f, + 'u_dem_scale': Uniform1f, + 'u_dem_tl_prev': Uniform2f, + 'u_dem_scale_prev': Uniform1f, + 'u_dem_size': Uniform1f, + 'u_dem_lerp': Uniform1f, + "u_exaggeration": Uniform1f, + 'u_depth': Uniform1i, + 'u_depth_size_inv': Uniform2f, + 'u_meter_to_dem'?: Uniform1f, + 'u_label_plane_matrix_inv'?: UniformMatrix4f +|}; + +export const terrainUniforms = (context: Context, locations: UniformLocations): TerrainUniformsType => ({ + 'u_dem': new Uniform1i(context, locations.u_dem), + 'u_dem_prev': new Uniform1i(context, locations.u_dem_prev), + 'u_dem_unpack': new Uniform4f(context, locations.u_dem_unpack), + 'u_dem_tl': new Uniform2f(context, locations.u_dem_tl), + 'u_dem_scale': new Uniform1f(context, locations.u_dem_scale), + 'u_dem_tl_prev': new Uniform2f(context, locations.u_dem_tl_prev), + 'u_dem_scale_prev': new Uniform1f(context, locations.u_dem_scale_prev), + 'u_dem_size': new Uniform1f(context, locations.u_dem_size), + 'u_dem_lerp': new Uniform1f(context, locations.u_dem_lerp), + 'u_exaggeration': new Uniform1f(context, locations.u_exaggeration), + 'u_depth': new Uniform1i(context, locations.u_depth), + 'u_depth_size_inv': new Uniform2f(context, locations.u_depth_size_inv), + 'u_meter_to_dem': new Uniform1f(context, locations.u_meter_to_dem), + 'u_label_plane_matrix_inv': new UniformMatrix4f(context, locations.u_label_plane_matrix_inv) +}); + +function defaultTerrainUniforms(encoding: DEMEncoding): UniformValues { + return { + 'u_dem': 2, + 'u_dem_prev': 4, + 'u_dem_unpack': DEMData.getUnpackVector(encoding), + 'u_dem_tl': [0, 0], + 'u_dem_tl_prev': [0, 0], + 'u_dem_scale': 0, + 'u_dem_scale_prev': 0, + 'u_dem_size': 0, + 'u_dem_lerp': 1.0, + 'u_depth': 3, + 'u_depth_size_inv': [0, 0], + 'u_exaggeration': 0 + }; +} diff --git a/src/terrain/terrain_raster_program.js b/src/terrain/terrain_raster_program.js new file mode 100644 index 00000000000..33a1d2fee38 --- /dev/null +++ b/src/terrain/terrain_raster_program.js @@ -0,0 +1,33 @@ +// @flow + +import { + Uniform1i, + Uniform1f, + UniformMatrix4f +} from '../render/uniform_binding'; + +import type Context from '../gl/context'; +import type {UniformValues, UniformLocations} from '../render/uniform_binding'; + +export type TerrainRasterUniformsType = {| + 'u_matrix': UniformMatrix4f, + 'u_image0': Uniform1i, + 'u_skirt_height': Uniform1f +|}; + +const terrainRasterUniforms = (context: Context, locations: UniformLocations): TerrainRasterUniformsType => ({ + 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), + 'u_image0': new Uniform1i(context, locations.u_image0), + 'u_skirt_height': new Uniform1f(context, locations.u_skirt_height) +}); + +const terrainRasterUniformValues = ( + matrix: Float32Array, + skirtHeight: number +): UniformValues => ({ + 'u_matrix': matrix, + 'u_image0': 0, + 'u_skirt_height': skirtHeight +}); + +export {terrainRasterUniforms, terrainRasterUniformValues}; diff --git a/src/ui/camera.js b/src/ui/camera.js index f0a922acb8a..7f6ce70a9fd 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -7,7 +7,8 @@ import { clamp, wrap, ease as defaultEasing, - pick + pick, + degToRad } from '../util/util'; import {number as interpolate} from '../style-spec/util/interpolate'; import browser from '../util/browser'; @@ -17,12 +18,15 @@ import Point from '@mapbox/point-geometry'; import {Event, Evented} from '../util/evented'; import assert from 'assert'; import {Debug} from '../util/debug'; - +import MercatorCoordinate, {mercatorZfromAltitude} from '../geo/mercator_coordinate'; +import {vec3} from 'gl-matrix'; +import type {FreeCameraOptions} from './free_camera'; import type Transform from '../geo/transform'; import type {LngLatLike} from '../geo/lng_lat'; import type {LngLatBoundsLike} from '../geo/lng_lat_bounds'; import type {TaskID} from '../util/task_queue'; import type {PointLike} from '@mapbox/point-geometry'; +import {Aabb, Frustum} from '../util/primitives.js'; import type {PaddingOptions} from '../geo/edge_insets'; /** @@ -87,6 +91,13 @@ export type AnimationOptions = { essential?: boolean }; +export type ElevationBoxRaycast = { + minLngLat: LngLat, + maxLngLat: LngLat, + minAltitude: number, + maxAltitude: number +}; + /** * Options for setting padding on calls to methods such as {@link Map#fitBounds}, {@link Map#fitScreenCoordinates}, and {@link Map#setPadding}. Adjust these options to set the amount of padding in pixels added to the edges of the canvas. Set a uniform padding on all edges or individual values for each edge. All properties of this object must be * non-negative integers. @@ -496,6 +507,32 @@ class Camera extends Evented { return this._cameraForBoxAndBearing(bounds.getNorthWest(), bounds.getSouthEast(), bearing, options); } + _extendCameraOptions(options?: CameraOptions) { + const defaultPadding = { + top: 0, + bottom: 0, + right: 0, + left: 0 + }; + options = extend({ + padding: defaultPadding, + offset: [0, 0], + maxZoom: this.transform.maxZoom + }, options); + + if (typeof options.padding === 'number') { + const p = options.padding; + options.padding = { + top: p, + bottom: p, + right: p, + left: p + }; + } + options.padding = extend(defaultPadding, options.padding); + return options; + } + /** * Calculate the center of these two points in the viewport and use * the highest zoom level up to and including `Map#getMaxZoom()` that fits @@ -520,29 +557,7 @@ class Camera extends Evented { * }); */ _cameraForBoxAndBearing(p0: LngLatLike, p1: LngLatLike, bearing: number, options?: CameraOptions): void | CameraOptions & AnimationOptions { - const defaultPadding = { - top: 0, - bottom: 0, - right: 0, - left: 0 - }; - options = extend({ - padding: defaultPadding, - offset: [0, 0], - maxZoom: this.transform.maxZoom - }, options); - - if (typeof options.padding === 'number') { - const p = options.padding; - options.padding = { - top: p, - bottom: p, - right: p, - left: p - }; - } - - options.padding = extend(defaultPadding, options.padding); + const eOptions = this._extendCameraOptions(options); const tr = this.transform; const edgePadding = tr.padding; @@ -550,16 +565,16 @@ class Camera extends Evented { // in a coordinate system rotate to match the destination bearing. const p0world = tr.project(LngLat.convert(p0)); const p1world = tr.project(LngLat.convert(p1)); - const p0rotated = p0world.rotate(-bearing * Math.PI / 180); - const p1rotated = p1world.rotate(-bearing * Math.PI / 180); + const p0rotated = p0world.rotate(-degToRad(bearing)); + const p1rotated = p1world.rotate(-degToRad(bearing)); const upperRight = new Point(Math.max(p0rotated.x, p1rotated.x), Math.max(p0rotated.y, p1rotated.y)); const lowerLeft = new Point(Math.min(p0rotated.x, p1rotated.x), Math.min(p0rotated.y, p1rotated.y)); // Calculate zoom: consider the original bbox and padding. const size = upperRight.sub(lowerLeft); - const scaleX = (tr.width - (edgePadding.left + edgePadding.right + options.padding.left + options.padding.right)) / size.x; - const scaleY = (tr.height - (edgePadding.top + edgePadding.bottom + options.padding.top + options.padding.bottom)) / size.y; + const scaleX = (tr.width - (edgePadding.left + edgePadding.right + eOptions.padding.left + eOptions.padding.right)) / size.x; + const scaleY = (tr.height - (edgePadding.top + edgePadding.bottom + eOptions.padding.top + eOptions.padding.bottom)) / size.y; if (scaleY < 0 || scaleX < 0) { warnOnce( @@ -567,13 +582,12 @@ class Camera extends Evented { ); return; } - - const zoom = Math.min(tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), options.maxZoom); + const zoom = Math.min(tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), eOptions.maxZoom); // Calculate center: apply the zoom, the configured offset, as well as offset that exists as a result of padding. - const offset = (typeof options.offset.x === 'number') ? new Point(options.offset.x, options.offset.y) : Point.convert(options.offset); - const paddingOffsetX = (options.padding.left - options.padding.right) / 2; - const paddingOffsetY = (options.padding.top - options.padding.bottom) / 2; + const offset = (typeof eOptions.offset.x === 'number') ? new Point(eOptions.offset.x, eOptions.offset.y) : Point.convert(eOptions.offset); + const paddingOffsetX = (eOptions.padding.left - eOptions.padding.right) / 2; + const paddingOffsetY = (eOptions.padding.top - eOptions.padding.bottom) / 2; const paddingOffset = new Point(paddingOffsetX, paddingOffsetY); const rotatedPaddingOffset = paddingOffset.rotate(bearing * Math.PI / 180); const offsetAtInitialZoom = offset.add(rotatedPaddingOffset); @@ -588,6 +602,104 @@ class Camera extends Evented { }; } + /** + * Finds the best camera fit for two given viewport point coordinates. + * The method will iteratively ray march towards the target and stops + * when any of the given input points collides with the view frustum. + * @memberof Map# + * @param {LngLatLike} p0 First point + * @param {LngLatLike} p1 Second point + * @param {number} minAltitude Optional min altitude in meters + * @param {number} maxAltitude Optional max altitude in meters + * @param options + * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds. + * @returns {CameraOptions | void} If map is able to fit to provided bounds, returns `CameraOptions` with + * `center`, `zoom`, `bearing` and `pitch`. If map is unable to fit, method will warn and return undefined. + * @private + */ + _cameraForBox(p0: LngLatLike, p1: LngLatLike, minAltitude?: number, maxAltitude?: number, options?: CameraOptions): void | CameraOptions & AnimationOptions { + const eOptions = this._extendCameraOptions(options); + + minAltitude = minAltitude || 0; + maxAltitude = maxAltitude || 0; + + p0 = LngLat.convert(p0); + p1 = LngLat.convert(p1); + + const tr = this.transform.clone(); + tr.padding = eOptions.padding; + + const camera = this.getFreeCameraOptions(); + const focus = new LngLat((p0.lng + p1.lng) * 0.5, (p0.lat + p1.lat) * 0.5); + const focusAltitude = (minAltitude + maxAltitude) * 0.5; + + if (tr._camera.position[2] < mercatorZfromAltitude(focusAltitude, focus.lat)) { + warnOnce('Map cannot fit within canvas with the given bounds, padding, and/or offset.'); + return; + } + + camera.lookAtPoint(focus); + + tr.setFreeCameraOptions(camera); + + const coord0 = MercatorCoordinate.fromLngLat(p0); + const coord1 = MercatorCoordinate.fromLngLat(p1); + + const toVec3 = (p: MercatorCoordinate): vec3 => [p.x, p.y, p.z]; + + const centerIntersectionPoint = tr.pointRayIntersection(tr.centerPoint, focusAltitude); + const centerIntersectionCoord = toVec3(tr.rayIntersectionCoordinate(centerIntersectionPoint)); + const centerMercatorRay = tr.screenPointToMercatorRay(tr.centerPoint); + + const maxMarchingSteps = 10; + + let steps = 0; + let halfDistanceToGround; + do { + const z = Math.floor(tr.zoom); + const z2 = 1 << z; + + const minX = Math.min(z2 * coord0.x, z2 * coord1.x); + const minY = Math.min(z2 * coord0.y, z2 * coord1.y); + const maxX = Math.max(z2 * coord0.x, z2 * coord1.x); + const maxY = Math.max(z2 * coord0.y, z2 * coord1.y); + + const aabb = new Aabb([minX, minY, minAltitude], [maxX, maxY, maxAltitude]); + + const frustum = Frustum.fromInvProjectionMatrix(tr.invProjMatrix, tr.worldSize, z); + + // Stop marching when frustum intersection + // reports any aabb point not fully inside + if (aabb.intersects(frustum) !== 2) { + // Went too far, step one iteration back + if (halfDistanceToGround) { + tr._camera.position = vec3.scaleAndAdd([], tr._camera.position, centerMercatorRay.dir, -halfDistanceToGround); + tr._updateStateFromCamera(); + } + break; + } + + const cameraPositionToGround = vec3.sub([], tr._camera.position, centerIntersectionCoord); + halfDistanceToGround = 0.5 * vec3.length(cameraPositionToGround); + + // March the camera position forward by half the distance to the ground + tr._camera.position = vec3.scaleAndAdd([], tr._camera.position, centerMercatorRay.dir, halfDistanceToGround); + try { + tr._updateStateFromCamera(); + } catch (e) { + warnOnce('Map cannot fit within canvas with the given bounds, padding, and/or offset.'); + return; + } + } while (++steps < maxMarchingSteps); + + return { + center: tr.center, + zoom: tr.zoom, + bearing: tr.bearing, + pitch: tr.pitch + }; + } + /** * Pans and zooms the map to contain its visible area within the specified geographical bounds. * This function will also reset the map's bearing to 0 if bearing is nonzero. @@ -621,6 +733,43 @@ class Camera extends Evented { eventData); } + _raycastElevationBox(point0: Point, point1: Point): ?ElevationBoxRaycast { + const elevation = this.transform.elevation; + + if (!elevation) return; + + const point2 = new Point(point0.x, point1.y); + const point3 = new Point(point1.x, point0.y); + + const r0 = elevation.pointCoordinate(point0); + if (!r0) return; + const r1 = elevation.pointCoordinate(point1); + if (!r1) return; + const r2 = elevation.pointCoordinate(point2); + if (!r2) return; + const r3 = elevation.pointCoordinate(point3); + if (!r3) return; + + const m0 = new MercatorCoordinate(r0[0], r0[1]).toLngLat(); + const m1 = new MercatorCoordinate(r1[0], r1[1]).toLngLat(); + const m2 = new MercatorCoordinate(r2[0], r2[1]).toLngLat(); + const m3 = new MercatorCoordinate(r3[0], r3[1]).toLngLat(); + + const minLng = Math.min(m0.lng, Math.min(m1.lng, Math.min(m2.lng, m3.lng))); + const minLat = Math.min(m0.lat, Math.min(m1.lat, Math.min(m2.lat, m3.lat))); + + const maxLng = Math.max(m0.lng, Math.max(m1.lng, Math.max(m2.lng, m3.lng))); + const maxLat = Math.max(m0.lat, Math.max(m1.lat, Math.max(m2.lat, m3.lat))); + + const minAltitude = Math.min(r0[3], Math.min(r1[3], Math.min(r2[3], r3[3]))); + const maxAltitude = Math.max(r0[3], Math.max(r1[3], Math.max(r2[3], r3[3]))); + + const minLngLat = new LngLat(minLng, minLat); + const maxLngLat = new LngLat(maxLng, maxLat); + + return {minLngLat, maxLngLat, minAltitude, maxAltitude}; + } + /** * Pans, rotates and zooms the map to to fit the box made by points p0 and p1 * once the map is rotated to the specified bearing. To zoom without rotating, @@ -629,7 +778,7 @@ class Camera extends Evented { * @memberof Map# * @param p0 First point on screen, in pixel coordinates * @param p1 Second point on screen, in pixel coordinates - * @param bearing Desired map bearing at end of animation, in degrees + * @param bearing Desired map bearing at end of animation, in degrees. This value is ignored if the map has non-zero pitch. * @param options Options object * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds. * @param {boolean} [options.linear=false] If `true`, the map transitions using @@ -651,14 +800,45 @@ class Camera extends Evented { * @see Used by {@link BoxZoomHandler} */ fitScreenCoordinates(p0: PointLike, p1: PointLike, bearing: number, options?: AnimationOptions & CameraOptions, eventData?: Object) { + let lngLat0, lngLat1, minAltitude, maxAltitude; + const point0 = Point.convert(p0); + const point1 = Point.convert(p1); + + const raycast = this._raycastElevationBox(point0, point1); + + if (!raycast) { + if (this.transform.isHorizonVisibleForPoints(point0, point1)) { + return this; + } + + lngLat0 = this.transform.pointLocation(point0); + lngLat1 = this.transform.pointLocation(point1); + } else { + lngLat0 = raycast.minLngLat; + lngLat1 = raycast.maxLngLat; + minAltitude = raycast.minAltitude; + maxAltitude = raycast.maxAltitude; + } + + if (this.transform.pitch === 0) { + return this._fitInternal( + this._cameraForBoxAndBearing( + this.transform.pointLocation(Point.convert(p0)), + this.transform.pointLocation(Point.convert(p1)), + bearing, + options), + options, + eventData); + } + return this._fitInternal( - this._cameraForBoxAndBearing( - this.transform.pointLocation(Point.convert(p0)), - this.transform.pointLocation(Point.convert(p1)), - bearing, + this._cameraForBox( + lngLat0, + lngLat1, + minAltitude, + maxAltitude, options), - options, - eventData); + options, eventData); } _fitInternal(calculatedOptions?: CameraOptions & AnimationOptions, options?: AnimationOptions & CameraOptions, eventData?: Object) { @@ -761,6 +941,78 @@ class Camera extends Evented { return this.fire(new Event('moveend', eventData)); } + /** + * Returns position and orientation of the camera entity. + * + * @memberof Map# + * @returns {FreeCameraOptions} The camera state + */ + getFreeCameraOptions(): FreeCameraOptions { + return this.transform.getFreeCameraOptions(); + } + + /** + * FreeCameraOptions provides more direct access to the underlying camera entity. + * For backwards compatibility the state set using this API must be representable with + * `CameraOptions` as well. Parameters are clamped into a valid range or discarded as invalid + * if the conversion to the pitch and bearing presentation is ambiguous. For example orientation + * can be invalid if it leads to the camera being upside down, the quaternion has zero length, + * or the pitch is over the maximum pitch limit. + * + * @memberof Map# + * @param {FreeCameraOptions} options FreeCameraOptions object + * @param eventData Additional properties to be added to event objects of events triggered by this method. + * @fires movestart + * @fires zoomstart + * @fires pitchstart + * @fires rotate + * @fires move + * @fires zoom + * @fires pitch + * @fires moveend + * @fires zoomend + * @fires pitchend + * @returns {Map} `this` + */ + setFreeCameraOptions(options: FreeCameraOptions, eventData?: Object) { + this.stop(); + + const tr = this.transform; + const prevZoom = tr.zoom; + const prevPitch = tr.pitch; + const prevBearing = tr.bearing; + + tr.setFreeCameraOptions(options); + + const zoomChanged = prevZoom !== tr.zoom; + const pitchChanged = prevPitch !== tr.pitch; + const bearingChanged = prevBearing !== tr.bearing; + + this.fire(new Event('movestart', eventData)) + .fire(new Event('move', eventData)); + + if (zoomChanged) { + this.fire(new Event('zoomstart', eventData)) + .fire(new Event('zoom', eventData)) + .fire(new Event('zoomend', eventData)); + } + + if (bearingChanged) { + this.fire(new Event('rotatestart', eventData)) + .fire(new Event('rotate', eventData)) + .fire(new Event('rotateend', eventData)); + } + + if (pitchChanged) { + this.fire(new Event('pitchstart', eventData)) + .fire(new Event('pitch', eventData)) + .fire(new Event('pitchend', eventData)); + } + + this.fire(new Event('moveend', eventData)); + return this; + } + /** * Changes any combination of `center`, `zoom`, `bearing`, `pitch`, and `padding` with an animated transition * between old and new values. The map will retain its current values for any @@ -873,6 +1125,7 @@ class Camera extends Evented { this._fireMoveEvents(eventData); }, (interruptingEaseId?: string) => { + tr.recenterOnTerrain(); this._afterEase(eventData, interruptingEaseId); }, options); @@ -881,6 +1134,7 @@ class Camera extends Evented { _prepareEase(eventData?: Object, noMoveStart: boolean, currently: Object = {}) { this._moving = true; + this.transform.cameraElevationReference = "sea"; if (!noMoveStart && !currently.moving) { this.fire(new Event('movestart', eventData)); @@ -916,6 +1170,7 @@ class Camera extends Evented { return; } delete this._easeId; + this.transform.cameraElevationReference = "ground"; const wasZooming = this._zooming; const wasRotating = this._rotating; @@ -1147,6 +1402,7 @@ class Camera extends Evented { const newCenter = k === 1 ? center : tr.unproject(from.add(delta.mult(u(s))).mult(scale)); tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset); + tr._updateCenterElevation(); this._fireMoveEvents(eventData); diff --git a/src/ui/control/attribution_control.js b/src/ui/control/attribution_control.js index 21b6a289a71..638fa86dd1d 100644 --- a/src/ui/control/attribution_control.js +++ b/src/ui/control/attribution_control.js @@ -162,7 +162,7 @@ class AttributionControl { this.styleId = stylesheet.id; } - const sourceCaches = this._map.style.sourceCaches; + const sourceCaches = this._map.style._sourceCaches; for (const id in sourceCaches) { const sourceCache = sourceCaches[id]; if (sourceCache.used) { diff --git a/src/ui/control/logo_control.js b/src/ui/control/logo_control.js index a56fdb912a1..ad8dd98db20 100644 --- a/src/ui/control/logo_control.js +++ b/src/ui/control/logo_control.js @@ -63,16 +63,16 @@ class LogoControl { _logoRequired() { if (!this._map.style) return; - - const sourceCaches = this._map.style.sourceCaches; + const sourceCaches = this._map.style._sourceCaches; + if (Object.entries(sourceCaches).length === 0) return true; for (const id in sourceCaches) { const source = sourceCaches[id].getSource(); - if (source.mapbox_logo) { - return true; + if (source.hasOwnProperty('mapbox_logo') && !source.mapbox_logo) { + return false; } } - return false; + return true; } _updateCompact() { diff --git a/src/ui/events.js b/src/ui/events.js index 275225d61e5..fe9623ca20e 100644 --- a/src/ui/events.js +++ b/src/ui/events.js @@ -1298,4 +1298,22 @@ export type MapEvent = * @instance * @private */ - | 'style.load'; + | 'style.load' + + /** + * Fired after speed index calculation is completed if speedIndexTiming option has set to true + * + * @event speedindexcompleted + * @memberof Map + * @instance + * @example + * // Initialize the map + * var map = new mapboxgl.Map({ // map options }); + * map.speedIndexTiming = true; + * // Set an event listener that fires + * map.on('speedindexcompleted', function() { + * console.log(`speed index is ${map.speedIndexNumber}`); + * }); + */ + | 'speedindexcompleted' +; diff --git a/src/ui/free_camera.js b/src/ui/free_camera.js new file mode 100644 index 00000000000..975d996bdd6 --- /dev/null +++ b/src/ui/free_camera.js @@ -0,0 +1,297 @@ +// @flow + +import MercatorCoordinate from '../geo/mercator_coordinate'; +import {degToRad, wrap} from '../util/util'; +import {vec3, vec4, quat, mat4} from 'gl-matrix'; +import type {Elevation} from '../terrain/elevation'; + +import type {LngLatLike} from '../geo/lng_lat'; + +function getColumn(matrix: mat4, col: number): vec4 { + return [matrix[col * 4], matrix[col * 4 + 1], matrix[col * 4 + 2], matrix[col * 4 + 3]]; +} + +function setColumn(matrix: mat4, col: number, values: vec4) { + matrix[col * 4 + 0] = values[0]; + matrix[col * 4 + 1] = values[1]; + matrix[col * 4 + 2] = values[2]; + matrix[col * 4 + 3] = values[3]; +} + +function updateTransformOrientation(matrix: mat4, orientation: quat) { + // Take temporary copy of position to prevent it from being overwritten + const position: vec4 = getColumn(matrix, 3); + + // Convert quaternion to rotation matrix + mat4.fromQuat(matrix, orientation); + setColumn(matrix, 3, position); +} + +function updateTransformPosition(matrix: mat4, position: vec3) { + setColumn(matrix, 3, [position[0], position[1], position[2], 1.0]); +} + +function wrapCameraPosition(position: vec3 | MercatorCoordinate) { + if (!position) return; + const mercatorCoordinate = Array.isArray(position) ? new MercatorCoordinate(position[0], position[1], position[2]) : position; + mercatorCoordinate.x = wrap(mercatorCoordinate.x, 0, 1); + return mercatorCoordinate; +} + +function orientationFromPitchBearing(pitch: number, bearing: number): quat { + // Both angles are considered to define CW rotation around their respective axes. + // Values have to be negated to achieve the proper quaternion in left handed coordinate space + const orientation = quat.identity([]); + quat.rotateZ(orientation, orientation, -bearing); + quat.rotateX(orientation, orientation, -pitch); + return orientation; +} + +export function orientationFromFrame(forward: vec3, up: vec3): ?quat { + // Find right-vector of the resulting coordinate frame. Up-vector has to be + // sanitized first in order to remove the roll component from the orientation + const xyForward = [forward[0], forward[1], 0]; + const xyUp = [up[0], up[1], 0]; + + const epsilon = 1e-15; + + if (vec3.length(xyForward) >= epsilon) { + // Roll rotation can be seen as the right vector not being on the xy-plane, ie. right[2] != 0.0. + // It can be negated by projecting the up vector on top of the forward vector. + const xyDir = vec3.normalize([], xyForward); + vec3.scale(xyUp, xyDir, vec3.dot(xyUp, xyDir)); + + up[0] = xyUp[0]; + up[1] = xyUp[1]; + } + + const right = vec3.cross([], up, forward); + if (vec3.len(right) < epsilon) { + return null; + } + + const bearing = Math.atan2(-right[1], right[0]); + const pitch = Math.atan2(Math.sqrt(forward[0] * forward[0] + forward[1] * forward[1]), -forward[2]); + + return orientationFromPitchBearing(pitch, bearing); +} + +/** + * Various options for accessing physical properties of the underlying camera entity. + * A direct access to these properties allows more flexible and precise controlling of the camera + * while also being fully compatible and interchangeable with CameraOptions. All fields are optional. + * See {@Link Camera#setFreeCameraOptions} and {@Link Camera#getFreeCameraOptions} + * + * @param {MercatorCoordinate} position Position of the camera in slightly modified web mercator coordinates + - The size of 1 unit is the width of the projected world instead of the "mercator meter". + Coordinate [0, 0, 0] is the north-west corner and [1, 1, 0] is the south-east corner. + - Z coordinate is conformal and must respect minimum and maximum zoom values. + - Zoom is automatically computed from the altitude (z) + * @param {quat} orientation Orientation of the camera represented as a unit quaternion [x, y, z, w] + in a left-handed coordinate space. Direction of the rotation is clockwise around the respective axis. + The default pose of the camera is such that the forward vector is looking up the -Z axis and + the up vector is aligned with north orientation of the map: + forward: [0, 0, -1] + up: [0, -1, 0] + right [1, 0, 0] + Orientation can be set freely but certain constraints still apply + - Orientation must be representable with only pitch and bearing. + - Pitch has an upper limit + */ +class FreeCameraOptions { + orientation: ?quat; + _position: ?MercatorCoordinate; + _elevation: ?Elevation; + _renderWorldCopies: boolean; + + constructor(position: ?MercatorCoordinate, orientation: ?quat) { + this.position = position; + this.orientation = orientation; + } + + get position(): ?MercatorCoordinate { + return this._position; + } + + set position(position: ?MercatorCoordinate) { + this._position = this._renderWorldCopies ? wrapCameraPosition(position) : position; + } + + /** + * Helper function for setting orientation of the camera by defining a focus point + * on the map. + * + * @param {LngLatLike} location Location of the focus point on the map + * @param {vec3} up Up vector of the camera is required in certain scenarios where bearing can't be deduced + * from the viewing direction. + */ + lookAtPoint(location: LngLatLike, up: ?vec3) { + this.orientation = null; + if (!this.position) { + return; + } + + const altitude = this._elevation ? this._elevation.getAtPoint(MercatorCoordinate.fromLngLat(location)) : 0; + const pos: MercatorCoordinate = this.position; + const target = MercatorCoordinate.fromLngLat(location, altitude); + const forward = [target.x - pos.x, target.y - pos.y, target.z - pos.z]; + if (!up) + up = [0, 0, 1]; + + // flip z-component if the up vector is pointing downwards + up[2] = Math.abs(up[2]); + + this.orientation = orientationFromFrame(forward, up); + } + + /** + * Helper function for setting the orientation of the camera as a pitch and a bearing. + * + * @param {number} pitch Pitch angle in degrees + * @param {number} bearing Bearing angle in degrees + */ + setPitchBearing(pitch: number, bearing: number) { + this.orientation = orientationFromPitchBearing(degToRad(pitch), degToRad(-bearing)); + } +} + +/** + * While using the free camera API the outcome value of isZooming, isMoving and isRotating + * is not a result of the free camera API. + * If the user sets the map.interactive to true, there will be conflicting behaviors while + * interacting with map via zooming or moving using mouse or/and keyboard which will result + * in isZooming, isMoving and isRotating to return true while using free camera API. In order + * to prevent the confilicting behavior please set map.interactive to false which will result + * in muting the following events: zoom, zoomend, zoomstart, rotate, rotateend, rotatestart, + * move, moveend, movestart, pitch, pitchend, pitchstart. + */ + +class FreeCamera { + _transform: mat4; + _orientation: quat; + + constructor(position: ?vec3, orientation: ?quat) { + this._transform = mat4.identity([]); + this._orientation = quat.identity([]); + + if (orientation) { + this._orientation = orientation; + updateTransformOrientation(this._transform, this._orientation); + } + + if (position) { + updateTransformPosition(this._transform, position); + } + } + + get mercatorPosition(): MercatorCoordinate { + const pos = this.position; + return new MercatorCoordinate(pos[0], pos[1], pos[2]); + } + + get position(): vec3 { + const col: vec4 = getColumn(this._transform, 3); + return [col[0], col[1], col[2]]; + } + + set position(value: vec3) { + updateTransformPosition(this._transform, value); + } + + get orientation(): quat { + return this._orientation; + } + + set orientation(value: quat) { + this._orientation = value; + updateTransformOrientation(this._transform, this._orientation); + } + + getPitchBearing(): {pitch: number, bearing: number} { + const f = this.forward(); + const r = this.right(); + + return { + bearing: Math.atan2(-r[1], r[0]), + pitch: Math.atan2(Math.sqrt(f[0] * f[0] + f[1] * f[1]), -f[2]) + }; + } + + setPitchBearing(pitch: number, bearing: number) { + this._orientation = orientationFromPitchBearing(pitch, bearing); + updateTransformOrientation(this._transform, this._orientation); + } + + forward(): vec3 { + const col: vec4 = getColumn(this._transform, 2); + // Forward direction is towards the negative Z-axis + return [-col[0], -col[1], -col[2]]; + } + + up(): vec3 { + const col: vec4 = getColumn(this._transform, 1); + // Up direction has to be flipped to point towards north + return [-col[0], -col[1], -col[2]]; + } + + right(): vec3 { + const col: vec4 = getColumn(this._transform, 0); + return [col[0], col[1], col[2]]; + } + + getCameraToWorld(worldSize: number, pixelsPerMeter: number): Float64Array { + const cameraToWorld = new Float64Array(16); + mat4.invert(cameraToWorld, this.getWorldToCamera(worldSize, pixelsPerMeter)); + return cameraToWorld; + } + + getWorldToCamera(worldSize: number, pixelsPerMeter: number): Float64Array { + // transformation chain from world space to camera space: + // 1. Height value (z) of renderables is in meters. Scale z coordinate by pixelsPerMeter + // 2. Transform from pixel coordinates to camera space with cameraMatrix^-1 + // 3. flip Y if required + + // worldToCamera: flip * cam^-1 * zScale + // cameraToWorld: (flip * cam^-1 * zScale)^-1 => (zScale^-1 * cam * flip^-1) + const matrix = new Float64Array(16); + + // Compute inverse of camera matrix and post-multiply negated translation + const invOrientation = new Float64Array(4); + const invPosition = this.position; + + quat.conjugate(invOrientation, this._orientation); + vec3.scale(invPosition, invPosition, -worldSize); + + mat4.fromQuat(matrix, invOrientation); + mat4.translate(matrix, matrix, invPosition); + + // Pre-multiply y (2nd row) + matrix[1] *= -1.0; + matrix[5] *= -1.0; + matrix[9] *= -1.0; + matrix[13] *= -1.0; + + // Post-multiply z (3rd column) + matrix[8] *= pixelsPerMeter; + matrix[9] *= pixelsPerMeter; + matrix[10] *= pixelsPerMeter; + matrix[11] *= pixelsPerMeter; + + return matrix; + } + + getCameraToClipPerspective(fovy: number, aspectRatio: number, nearZ: number, farZ: number): Float64Array { + const matrix = new Float64Array(16); + mat4.perspective(matrix, fovy, aspectRatio, nearZ, farZ); + return matrix; + } + + clone(): FreeCamera { + return new FreeCamera([...this.position], [...this.orientation]); + } +} + +export { + FreeCamera, + FreeCameraOptions +}; diff --git a/src/ui/handler/box_zoom.js b/src/ui/handler/box_zoom.js index be8415ab7e3..c770d674855 100644 --- a/src/ui/handler/box_zoom.js +++ b/src/ui/handler/box_zoom.js @@ -128,7 +128,7 @@ class BoxZoomHandler { } else { this._map.fire(new Event('boxzoomend', {originalEvent: e})); return { - cameraAnimation: map => map.fitScreenCoordinates(p0, p1, this._map.getBearing(), {linear: true}) + cameraAnimation: map => map.fitScreenCoordinates(p0, p1, this._map.getBearing(), {linear: false}) }; } } diff --git a/src/ui/handler/scroll_zoom.js b/src/ui/handler/scroll_zoom.js index 1ae4553487f..71599f4bf7c 100644 --- a/src/ui/handler/scroll_zoom.js +++ b/src/ui/handler/scroll_zoom.js @@ -7,11 +7,11 @@ import {ease as _ease, bindAll, bezier} from '../../util/util'; import browser from '../../util/browser'; import window from '../../util/window'; import {number as interpolate} from '../../style-spec/util/interpolate'; -import LngLat from '../../geo/lng_lat'; +import Point from '@mapbox/point-geometry'; import type Map from '../map'; import type HandlerManager from '../handler_manager'; -import type Point from '@mapbox/point-geometry'; +import MercatorCoordinate from '../../geo/mercator_coordinate'; // deltaY value for mouse scroll wheel identification const wheelZoomDelta = 4.000244140625; @@ -35,8 +35,8 @@ class ScrollZoomHandler { _active: boolean; _zooming: boolean; _aroundCenter: boolean; - _around: Point; _aroundPoint: Point; + _aroundCoord: MercatorCoordinate; _type: 'wheel' | 'trackpad' | null; _lastValue: number; _timeout: ?TimeoutID; // used for delayed-handling of a single wheel movement @@ -226,9 +226,10 @@ class ScrollZoomHandler { } const pos = DOM.mousePos(this._el, e); + this._aroundPoint = this._aroundCenter ? this._map.transform.centerPoint : pos; + this._aroundCoord = this._map.transform.pointCoordinate3D(this._aroundPoint); + this._targetZoom = undefined; - this._around = LngLat.convert(this._aroundCenter ? this._map.getCenter() : this._map.unproject(pos)); - this._aroundPoint = this._map.transform.locationPoint(this._around); if (!this._frameId) { this._frameId = true; this._handler._triggerRenderFrame(); @@ -242,6 +243,10 @@ class ScrollZoomHandler { if (!this.isActive()) return; const tr = this._map.transform; + const startingZoom = () => { + return tr._terrainEnabled() ? tr.computeZoomRelativeTo(this._aroundCoord) : tr.zoom; + }; + // if we've had scroll events since the last render frame, consume the // accumulated delta, and update the target zoom level accordingly if (this._delta !== 0) { @@ -254,22 +259,24 @@ class ScrollZoomHandler { scale = 1 / scale; } - const fromScale = typeof this._targetZoom === 'number' ? tr.zoomScale(this._targetZoom) : tr.scale; + const startZoom = startingZoom(); + const startScale = Math.pow(2.0, startZoom); + + const fromScale = typeof this._targetZoom === 'number' ? tr.zoomScale(this._targetZoom) : startScale; this._targetZoom = Math.min(tr.maxZoom, Math.max(tr.minZoom, tr.scaleZoom(fromScale * scale))); // if this is a mouse wheel, refresh the starting zoom and easing // function we're using to smooth out the zooming between wheel // events if (this._type === 'wheel') { - this._startZoom = tr.zoom; + this._startZoom = startingZoom(); this._easing = this._smoothOutEasing(200); } this._delta = 0; } - const targetZoom = typeof this._targetZoom === 'number' ? - this._targetZoom : tr.zoom; + this._targetZoom : startingZoom(); const startZoom = this._startZoom; const easing = this._easing; @@ -308,8 +315,9 @@ class ScrollZoomHandler { return { noInertia: true, needsRenderFrame: !finished, - zoomDelta: zoom - tr.zoom, + zoomDelta: zoom - startingZoom(), around: this._aroundPoint, + aroundCoord: this._aroundCoord, originalEvent: this._lastWheelEvent }; } diff --git a/src/ui/handler_manager.js b/src/ui/handler_manager.js index 56d538443c4..ed72e8e699c 100644 --- a/src/ui/handler_manager.js +++ b/src/ui/handler_manager.js @@ -22,6 +22,8 @@ import {bindAll, extend} from '../util/util'; import window from '../util/window'; import Point from '@mapbox/point-geometry'; import assert from 'assert'; +import {vec3} from 'gl-matrix'; +import MercatorCoordinate, {altitudeFromMercatorZ} from '../geo/mercator_coordinate'; export type InputEvent = MouseEvent | TouchEvent | KeyboardEvent | WheelEvent; @@ -32,6 +34,51 @@ class RenderFrameEvent extends Event { timeStamp: number; } +class TrackingEllipsoid { + constants: Array; + radius: number; + + constructor() { + // a, b, c in the equation x²/a² + y²/b² + z²/c² = 1 + this.constants = [1, 1, 0.01]; + this.radius = 0; + } + + setup(center: vec3, pointOnSurface: vec3) { + const centerToSurface = vec3.sub([], pointOnSurface, center); + if (centerToSurface[2] < 0) { + this.radius = vec3.length(vec3.div([], centerToSurface, this.constants)); + } else { + // The point on surface is above the center. This can happen for example when the camera is + // below the clicked point (like a mountain) Use slightly shorter radius for less aggressive movement + this.radius = vec3.length([centerToSurface[0], centerToSurface[1], 0]); + } + } + + // Cast a ray from the center of the ellipsoid and the intersection point. + projectRay(dir: vec3): vec3 { + // Perform the intersection test against a unit sphere + vec3.div(dir, dir, this.constants); + vec3.normalize(dir, dir); + vec3.mul(dir, dir, this.constants); + + const intersection = vec3.scale([], dir, this.radius); + + if (intersection[2] > 0) { + // The intersection point is above horizon so special handling is required. + // Otherwise direction of the movement would be inverted due to the ellipsoid shape + const h = vec3.scale([], [0, 0, 1], vec3.dot(intersection, [0, 0, 1])); + const r = vec3.scale([], vec3.normalize([], [intersection[0], intersection[1], 0]), this.radius); + const p = vec3.add([], intersection, vec3.scale([], vec3.sub([], vec3.add([], r, h), intersection), 2)); + + intersection[0] = p[0]; + intersection[1] = p[1]; + } + + return intersection; + } +} + // Handlers interpret dom events and return camera changes that should be // applied to the map (`HandlerResult`s). The camera changes are all deltas. // The handler itself should have no knowledge of the map's current state. @@ -76,6 +123,8 @@ export type HandlerResult = {| around?: Point | null, // same as above, except for pinch actions, which are given higher priority pinchAround?: Point | null, + // the point to not move when changing the camera in mercator coordinates + aroundCoord?: MercatorCoordinate | null, // A method that can fire a one-off easing by directly changing the map's camera. cameraAnimation?: (map: Map) => any; @@ -105,6 +154,8 @@ class HandlerManager { _changes: Array<[HandlerResult, Object, any]>; _previousActiveHandlers: { [string]: Handler }; _listeners: Array<[HTMLElement, string, void | {passive?: boolean, capture?: boolean}]>; + _trackingEllipsoid: TrackingEllipsoid; + _dragOrigin: ?vec3; constructor(map: Map, options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number, bearingSnap: number}) { this._map = map; @@ -116,6 +167,8 @@ class HandlerManager { this._inertia = new HandlerInertia(map); this._bearingSnap = options.bearingSnap; this._previousActiveHandlers = {}; + this._trackingEllipsoid = new TrackingEllipsoid(); + this._dragOrigin = null; // Track whether map is currently moving, to compute start/move/end events this._eventsInProgress = {}; @@ -383,7 +436,6 @@ class HandlerManager { if (handlerResult.bearingDelta !== undefined) { eventsInProgress.rotate = eventData; } - } _applyChanges() { @@ -398,6 +450,7 @@ class HandlerManager { if (change.bearingDelta) combined.bearingDelta = (combined.bearingDelta || 0) + change.bearingDelta; if (change.pitchDelta) combined.pitchDelta = (combined.pitchDelta || 0) + change.pitchDelta; if (change.around !== undefined) combined.around = change.around; + if (change.aroundCoord !== undefined) combined.aroundCoord = change.aroundCoord; if (change.pinchAround !== undefined) combined.pinchAround = change.pinchAround; if (change.noInertia) combined.noInertia = change.noInertia; @@ -414,25 +467,104 @@ class HandlerManager { const map = this._map; const tr = map.transform; + const eventStarted = (type) => { + const newEvent = combinedEventsInProgress[type]; + return newEvent && !this._eventsInProgress[type]; + }; + + const eventEnded = (type) => { + const event = this._eventsInProgress[type]; + return event && !this._handlersById[event.handlerName].isActive(); + }; + + const toVec3 = (p: MercatorCoordinate): vec3 => [p.x, p.y, p.z]; + + if (eventEnded("drag") && !hasChange(combinedResult)) { + const preZoom = tr.zoom; + tr.cameraElevationReference = "sea"; + tr.recenterOnTerrain(); + tr.cameraElevationReference = "ground"; + // Map zoom might change during the pan operation due to terrain elevation. + if (preZoom !== tr.zoom) this._map._update(true); + } + if (!hasChange(combinedResult)) { return this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true); } - - let {panDelta, zoomDelta, bearingDelta, pitchDelta, around, pinchAround} = combinedResult; + let {panDelta, zoomDelta, bearingDelta, pitchDelta, around, aroundCoord, pinchAround} = combinedResult; if (pinchAround !== undefined) { around = pinchAround; } + if (eventStarted("drag") && around) { + this._dragOrigin = toVec3(tr.pointCoordinate3D(around)); + // Construct the tracking ellipsoid every time user changes the drag origin. + // Direction of the ray will define size of the shape and hence defining the available range of movement + this._trackingEllipsoid.setup(tr._camera.position, this._dragOrigin); + } + + // All movement of the camera is done relative to the sea level + tr.cameraElevationReference = "sea"; + // stop any ongoing camera animations (easeTo, flyTo) map._stop(true); around = around || map.transform.centerPoint; - const loc = tr.pointLocation(panDelta ? around.sub(panDelta) : around); if (bearingDelta) tr.bearing += bearingDelta; if (pitchDelta) tr.pitch += pitchDelta; - if (zoomDelta) tr.zoom += zoomDelta; - tr.setLocationAtPoint(loc, around); + tr._updateCameraState(); + + // Compute Mercator 3D camera offset based on screenspace panDelta + const panVec = [0, 0, 0]; + if (panDelta) { + assert(this._dragOrigin, '_dragOrigin should have been setup with a previous dragstart'); + const startRay = tr.screenPointToMercatorRay(around); + const endRay = tr.screenPointToMercatorRay(around.sub(panDelta)); + + const startPoint = this._trackingEllipsoid.projectRay(startRay.dir); + const endPoint = this._trackingEllipsoid.projectRay(endRay.dir); + panVec[0] = endPoint[0] - startPoint[0]; + panVec[1] = endPoint[1] - startPoint[1]; + } + + const originalZoom = tr.zoom; + // Compute Mercator 3D camera offset based on screenspace requested ZoomDelta + const zoomVec = [0, 0, 0]; + if (zoomDelta) { + // Zoom value has to be computed relative to a secondary map plane that is created from the terrain position below the cursor. + // This way the zoom interpolation can be kept linear and independent of the (possible) terrain elevation + const pickedPosition: vec3 = aroundCoord ? toVec3(aroundCoord) : toVec3(tr.pointCoordinate3D(around)); + + const aroundRay = {dir: vec3.normalize([], vec3.sub([], pickedPosition, tr._camera.position))}; + const centerRay = tr.screenPointToMercatorRay(tr.centerPoint); + + if (aroundRay.dir[2] < 0) { + // Compute center point on the elevated map plane by casting a ray from the center of the screen. + // ZoomDelta is then subtracted from the relative zoom value and converted to a movement vector + const pickedAltitude = altitudeFromMercatorZ(pickedPosition[2], pickedPosition[1]); + const centerOnTargetPlane = tr.rayIntersectionCoordinate(tr.pointRayIntersection(tr.centerPoint, pickedAltitude)); + const movement = tr.zoomDeltaToMovement(toVec3(centerOnTargetPlane), zoomDelta) * (centerRay.dir[2] / aroundRay.dir[2]); + + vec3.scale(zoomVec, aroundRay.dir, movement); + } else if (tr._terrainEnabled()) { + // Special handling is required if the ray created from the cursor is heading up. + // This scenario is possible if user is trying to zoom towards e.g. a hill or a mountain. + // Convert zoomDelta to a movement vector as if the camera would be orbiting around the picked point + const movement = tr.zoomDeltaToMovement(pickedPosition, zoomDelta); + vec3.scale(zoomVec, aroundRay.dir, movement); + } + } + + // Mutate camera state via CameraAPI + const translation = vec3.add(panVec, panVec, zoomVec); + tr._translateCameraConstrained(translation); + + if (zoomDelta && Math.abs(tr.zoom - originalZoom) > 0.0001) { + tr.recenterOnTerrain(); + } + + tr.cameraElevationReference = "ground"; this._map._update(); if (!combinedResult.noInertia) this._inertia.record(combinedResult); @@ -530,7 +662,6 @@ class HandlerManager { this._frameId = this._requestFrame(); } } - } export default HandlerManager; diff --git a/src/ui/map.js b/src/ui/map.js index 851236d15bb..5527a662935 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -7,7 +7,7 @@ import window from '../util/window'; const {HTMLImageElement, HTMLElement, ImageBitmap} = window; import DOM from '../util/dom'; import {getImage, getJSON, ResourceType} from '../util/ajax'; -import {RequestManager} from '../util/mapbox'; +import {RequestManager, getMapSessionAPI, postMapLoadEvent, AUTH_ERR_MSG} from '../util/mapbox'; import Style from '../style/style'; import EvaluationParameters from '../style/evaluation_parameters'; import Painter from '../render/painter'; @@ -55,6 +55,7 @@ import type { FilterSpecification, StyleSpecification, LightSpecification, + TerrainSpecification, SourceSpecification } from '../style-spec/types'; @@ -110,7 +111,7 @@ const defaultMaxZoom = 22; // the default values, but also the valid range const defaultMinPitch = 0; -const defaultMaxPitch = 60; +const defaultMaxPitch = 85; const defaultOptions = { center: [0, 0], @@ -168,8 +169,8 @@ const defaultOptions = { * @param {HTMLElement|string} options.container The HTML element in which Mapbox GL JS will render the map, or the element's string `id`. The specified element must have no children. * @param {number} [options.minZoom=0] The minimum zoom level of the map (0-24). * @param {number} [options.maxZoom=22] The maximum zoom level of the map (0-24). - * @param {number} [options.minPitch=0] The minimum pitch of the map (0-60). - * @param {number} [options.maxPitch=60] The maximum pitch of the map (0-60). + * @param {number} [options.minPitch=0] The minimum pitch of the map (0-85). + * @param {number} [options.maxPitch=85] The maximum pitch of the map (0-85). * @param {Object|string} [options.style] The map's Mapbox style. This must be an a JSON object conforming to * the schema described in the [Mapbox Style Specification](https://mapbox.com/mapbox-gl-style-spec/), or a URL to * such JSON. @@ -224,7 +225,7 @@ const defaultOptions = { * @param {LngLatLike} [options.center=[0, 0]] The inital geographical centerpoint of the map. If `center` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `[0, 0]` Note: Mapbox GL uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match GeoJSON. * @param {number} [options.zoom=0] The initial zoom level of the map. If `zoom` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`. * @param {number} [options.bearing=0] The initial bearing (rotation) of the map, measured in degrees counter-clockwise from north. If `bearing` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`. - * @param {number} [options.pitch=0] The initial pitch (tilt) of the map, measured in degrees away from the plane of the screen (0-60). If `pitch` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`. + * @param {number} [options.pitch=0] The initial pitch (tilt) of the map, measured in degrees away from the plane of the screen (0-85). If `pitch` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`. * @param {LngLatBoundsLike} [options.bounds] The initial bounds of the map. If `bounds` is specified, it overrides `center` and `zoom` constructor options. * @param {Object} [options.fitBoundsOptions] A {@link Map#fitBounds} options object to use _only_ when fitting the initial `bounds` provided above. * @param {boolean} [options.renderWorldCopies=true] If `true`, multiple copies of the world will be rendered side by side beyond -180 and 180 degrees longitude. If set to `false`: @@ -276,6 +277,7 @@ class Map extends Camera { _controlPositions: {[_: string]: HTMLElement}; _interactive: ?boolean; _showTileBoundaries: ?boolean; + _showQueryGeometry: ?boolean; _showCollisionBoxes: ?boolean; _showPadding: ?boolean; _showOverdrawInspector: boolean; @@ -284,6 +286,7 @@ class Map extends Camera { _canvas: HTMLCanvasElement; _maxTileCacheSize: number; _frame: ?Cancelable; + _renderNextFrame: ?boolean; _styleDirty: ?boolean; _sourcesDirty: ?boolean; _placementDirty: ?boolean; @@ -297,17 +300,21 @@ class Map extends Camera { _refreshExpiredTiles: boolean; _hash: Hash; _delegatedListeners: any; + _isInitialLoad: boolean; + _shouldCheckAccess: boolean; _fadeDuration: number; _crossSourceCollisions: boolean; _crossFadingFactor: number; _collectResourceTiming: boolean; _renderTaskQueue: TaskQueue; _controls: Array; + _logoControl: IControl; _mapId: number; _localIdeographFontFamily: string; _requestManager: RequestManager; _locale: Object; _removed: boolean; + _speedIndexTiming: boolean; _clickTolerance: number; /** @@ -392,6 +399,7 @@ class Map extends Camera { this._bearingSnap = options.bearingSnap; this._refreshExpiredTiles = options.refreshExpiredTiles; this._fadeDuration = options.fadeDuration; + this._isInitialLoad = true; this._crossSourceCollisions = options.crossSourceCollisions; this._crossFadingFactor = 1; this._collectResourceTiming = options.collectResourceTiming; @@ -469,7 +477,8 @@ class Map extends Camera { if (options.attributionControl) this.addControl(new AttributionControl({customAttribution: options.customAttribution})); - this.addControl(new LogoControl(), options.logoPosition); + this._logoControl = new LogoControl(); + this.addControl(this._logoControl, options.logoPosition); this.on('style.load', () => { if (this.transform.unmodified) { @@ -556,7 +565,7 @@ class Map extends Camera { } /** - * Checks if a control exists on the map. + * Checks if a control is on the map. * * @param {IControl} control The {@link IControl} to check. * @returns {boolean} True if map contains control. @@ -566,7 +575,8 @@ class Map extends Camera { * // Add zoom and rotation controls to the map. * map.addControl(navigation); * // Check that the navigation control exists on the map. - * map.hasControl(navigation); + * const added = map.hasControl(navigation); + * // added === true */ hasControl(control: IControl) { return this._controls.indexOf(control) > -1; @@ -740,7 +750,7 @@ class Map extends Camera { * If the map's current pitch is lower than the new minimum, * the map will pitch to the new minimum. * - * @param {number | null | undefined} minPitch The minimum pitch to set (0-60). + * @param {number | null | undefined} minPitch The minimum pitch to set (0-85). * If `null` or `undefined` is provided, the function removes the current minimum pitch (i.e. sets it to 0). * @returns {Map} `this` */ @@ -776,7 +786,7 @@ class Map extends Camera { * the map will pitch to the new maximum. * * @param {number | null | undefined} maxPitch The maximum pitch to set. - * If `null` or `undefined` is provided, the function removes the current maximum pitch (sets it to 60). + * If `null` or `undefined` is provided, the function removes the current maximum pitch (sets it to 85). * @returns {Map} `this` */ setMaxPitch(maxPitch?: ?number) { @@ -842,6 +852,10 @@ class Map extends Camera { * Returns a {@link Point} representing pixel coordinates, relative to the map's `container`, * that correspond to the specified geographical location. * + * When the map is pitched and `lnglat` is completely behind the camera, there are no pixel + * coordinates corresponding to that location. In that case, + * the `x` and `y` components of the returned {@link Point} are set to Number.MAX_VALUE. + * * @param {LngLatLike} lnglat The geographical location to project. * @returns {Point} The {@link Point} corresponding to `lnglat`, relative to the map's `container`. * @example @@ -849,12 +863,14 @@ class Map extends Camera { * var point = map.project(coordinate); */ project(lnglat: LngLatLike) { - return this.transform.locationPoint(LngLat.convert(lnglat)); + return this.transform.locationPoint3D(LngLat.convert(lnglat)); } /** * Returns a {@link LngLat} representing geographical coordinates that correspond - * to the specified pixel coordinates. + * to the specified pixel coordinates. If horizon is visible, and specified pixel is + * above horizon, returns a {@link LngLat} corresponding to point on horizon, nearest + * to the point. * * @param {PointLike} point The pixel coordinates to unproject. * @returns {LngLat} The {@link LngLat} corresponding to `point`. @@ -865,7 +881,7 @@ class Map extends Camera { * }); */ unproject(point: PointLike) { - return this.transform.pointLocation(Point.convert(point)); + return this.transform.pointLocation3D(Point.convert(point)); } /** @@ -875,7 +891,7 @@ class Map extends Camera { * var isMoving = map.isMoving(); */ isMoving(): boolean { - return this._moving || this.handlers.isMoving(); + return this._moving || this.handlers && this.handlers.isMoving(); } /** @@ -885,7 +901,7 @@ class Map extends Camera { * var isZooming = map.isZooming(); */ isZooming(): boolean { - return this._zooming || this.handlers.isZooming(); + return this._zooming || this.handlers && this.handlers.isZooming(); } /** @@ -895,7 +911,7 @@ class Map extends Camera { * map.isRotating(); */ isRotating(): boolean { - return this._rotating || this.handlers.isRotating(); + return this._rotating || this.handlers && this.handlers.isRotating(); } _createDelegatedListener(type: MapEvent, layerId: any, listener: any) { @@ -1255,16 +1271,7 @@ class Map extends Camera { options = options || {}; geometry = geometry || [[0, 0], [this.transform.width, this.transform.height]]; - let queryGeometry; - if (geometry instanceof Point || typeof geometry[0] === 'number') { - queryGeometry = [Point.convert(geometry)]; - } else { - const tl = Point.convert(geometry[0]); - const br = Point.convert(geometry[1]); - queryGeometry = [tl, new Point(br.x, tl.y), br, new Point(tl.x, br.y), tl]; - } - - return this.style.queryRenderedFeatures(queryGeometry, options, this.transform); + return this.style.queryRenderedFeatures(geometry, options, this.transform); } /** @@ -1360,23 +1367,20 @@ class Map extends Camera { if (this.style) { this.style.setEventedParent(null); this.style._remove(); - } - - if (!style) { delete this.style; - return this; - } else { - this.style = new Style(this, options || {}); } - this.style.setEventedParent(this, {style: this.style}); + if (style) { + this.style = new Style(this, options || {}); + this.style.setEventedParent(this, {style: this.style}); - if (typeof style === 'string') { - this.style.loadURL(style); - } else { - this.style.loadJSON(style); + if (typeof style === 'string') { + this.style.loadURL(style); + } else { + this.style.loadJSON(style); + } } - + this._updateTerrain(); return this; } @@ -1408,6 +1412,7 @@ class Map extends Camera { try { if (this.style.setState(style)) { this._update(true); + this._updateTerrain(); } } catch (e) { warnOnce( @@ -1494,12 +1499,13 @@ class Map extends Camera { * var sourceLoaded = map.isSourceLoaded('bathymetry-data'); */ isSourceLoaded(id: string) { - const source = this.style && this.style.sourceCaches[id]; - if (source === undefined) { + const sourceCaches = this.style && this.style._getSourceCaches(id); + if (sourceCaches.length === 0) { this.fire(new ErrorEvent(new Error(`There is no source with ID '${id}'`))); return; } - return source.loaded(); + + return sourceCaches.every(sc => sc.loaded()); } /** @@ -1512,7 +1518,7 @@ class Map extends Camera { */ areTilesLoaded() { - const sources = this.style && this.style.sourceCaches; + const sources = this.style && this.style._sourceCaches; for (const id in sources) { const source = sources[id]; const tiles = source._tiles; @@ -1547,6 +1553,7 @@ class Map extends Camera { */ removeSource(id: string) { this.style.removeSource(id); + this._updateTerrain(); return this._update(true); } @@ -2101,6 +2108,28 @@ class Map extends Camera { } // eslint-disable-next-line jsdoc/require-returns + /** + * Sets the terrain property of the style. + * + * @param terrain Terrain properties to set. Must conform to the [Mapbox Style Specification](https://www.mapbox.com/mapbox-gl-style-spec/#terrain). + * If `null` or `undefined` is provided, function removes terrain. + * @returns {Map} `this` + * @example + * map.addSource('mapbox-dem', { + * 'type': 'raster-dem', + * 'url': 'mapbox://mapbox.mapbox-terrain-dem-v1', + * 'tileSize': 512, + * 'maxzoom': 14 + * }); + * // add the DEM source as a terrain layer with exaggerated height + * map.setTerrain({ 'source': 'mapbox-dem', 'exaggeration': 1.5 }); + */ + setTerrain(terrain: TerrainSpecification) { + this._lazyInitEmptyStyle(); + this.style.setTerrain(terrain); + return this._update(true); + } + /** * Sets the `state` of a feature. * A feature's `state` is a set of user-defined key-value pairs that are assigned to a feature at runtime. @@ -2349,6 +2378,11 @@ class Map extends Camera { } this.painter = new Painter(gl, this.transform); + this.on('data', (event: MapDataEvent) => { + if (event.dataType === 'source') { + this.painter.setTileLoadedFlag(true); + } + }); webpSupported.testSupport(gl); } @@ -2445,6 +2479,8 @@ class Map extends Camera { frameStartTime = browser.now(); } + const m = PerformanceUtils.beginMeasure('render'); + // A custom layer may have used the context asynchronously. Mark the state as dirty. this.painter.context.setDirty(); this.painter.setBaseState(); @@ -2454,6 +2490,7 @@ class Map extends Camera { if (this._removed) return; let crossFading = false; + const fadeDuration = this._isInitialLoad ? 0 : this._fadeDuration; // If the style has changed, the map is being zoomed, or a transition or fade is in progress: // - Apply style changes (in a batch) @@ -2467,7 +2504,7 @@ class Map extends Camera { const parameters = new EvaluationParameters(zoom, { now, - fadeDuration: this._fadeDuration, + fadeDuration, zoomHistory: this.style.zoomHistory, transition: this.style.getTransition() }); @@ -2486,21 +2523,25 @@ class Map extends Camera { // need for the current transform if (this.style && this._sourcesDirty) { this._sourcesDirty = false; + this._updateTerrain(); // Terrain DEM source updates here and skips update in style._updateSources. this.style._updateSources(this.transform); } - this._placementDirty = this.style && this.style._updatePlacement(this.painter.transform, this.showCollisionBoxes, this._fadeDuration, this._crossSourceCollisions); + this._placementDirty = this.style && this.style._updatePlacement(this.painter.transform, this.showCollisionBoxes, fadeDuration, this._crossSourceCollisions); // Actually draw this.painter.render(this.style, { showTileBoundaries: this.showTileBoundaries, showOverdrawInspector: this._showOverdrawInspector, + showQueryGeometry: !!this._showQueryGeometry, rotating: this.isRotating(), zooming: this.isZooming(), moving: this.isMoving(), - fadeDuration: this._fadeDuration, + fadeDuration, + isInitialLoad: this._isInitialLoad, showPadding: this.showPadding, gpuTiming: !!this.listens('gpu-timing-layer'), + speedIndexTiming: this.speedIndexTiming, }); this.fire(new Event('render')); @@ -2535,6 +2576,8 @@ class Map extends Camera { }, 50); // Wait 50ms to give time for all GPU calls to finish before querying } + PerformanceUtils.endMeasure(m); + if (this.listens('gpu-timing-layer')) { // Resetting the Painter's per-layer timing queries here allows us to isolate // the queries to individual frames. @@ -2557,8 +2600,21 @@ class Map extends Camera { const somethingDirty = this._sourcesDirty || this._styleDirty || this._placementDirty; if (somethingDirty || this._repaint) { this.triggerRepaint(); - } else if (!this.isMoving() && this.loaded()) { - this.fire(new Event('idle')); + } else { + this._triggerFrame(false); + if (!this.isMoving() && this.loaded()) { + this.fire(new Event('idle')); + if (this._isInitialLoad) { + this._authenticate(); + } + this._isInitialLoad = false; + // check the options to see if need to calculate the speed index + if (this.speedIndexTiming) { + const speedIndexNumber = this._calculateSpeedIndex(); + this.fire(new Event('speedindexcompleted', {speedIndex: speedIndexNumber})); + this.speedIndexTiming = false; + } + } } if (this._loaded && !this._fullyLoaded && !somethingDirty) { @@ -2569,6 +2625,88 @@ class Map extends Camera { return this; } + /***** START WARNING - REMOVAL OR MODIFICATION OF THE + * FOLLOWING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ****** + * The following code is used to access Mapbox's APIs. Removal or modification + * of this code can result in higher fees and/or + * termination of your account with Mapbox. + * + * Under the Mapbox Terms of Service, you may not use this code to access Mapbox + * Mapping APIs other than through Mapbox SDKs. + * + * The Mapping APIs documentation is available at https://docs.mapbox.com/api/maps/#maps + * and the Mapbox Terms of Service are available at https://www.mapbox.com/tos/ + ******************************************************************************/ + + _authenticate() { + getMapSessionAPI(this._getMapId(), this._requestManager._skuToken, this._requestManager._customAccessToken, (err) => { + if (err) { + // throwing an error here will cause the callback to be called again unnecessarily + if (err.message === AUTH_ERR_MSG || err.status === 401) { + console.error('Error: A valid Mapbox access token is required to use Mapbox GL JS. To create an account or a new access token, visit https://account.mapbox.com/'); + browser.setErrorState(); + const gl = this.painter.context.gl; + if (this._logoControl instanceof LogoControl) { + this._logoControl._updateLogo(); + } + if (gl) gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); + } + } + }); + postMapLoadEvent(this._getMapId(), this._requestManager._skuToken, this._requestManager._customAccessToken, () => {}); + } + + /***** END WARNING - REMOVAL OR MODIFICATION OF THE + PRECEDING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ******/ + + _updateTerrain() { + // Recalculate if enabled/disabled and calculate elevation cover. As camera is using elevation tiles before + // render (and deferred update after zoom recalculation), this needs to be called when removing terrain source. + this.painter.updateTerrain(this.style, this.isMoving() || this.isRotating() || this.isZooming()); + } + + _calculateSpeedIndex(): number { + const finalFrame = this.painter.canvasCopy(); + const canvasCopyInstances = this.painter.getCanvasCopiesAndTimestamps(); + canvasCopyInstances.timeStamps.push(performance.now()); + + const gl = this.painter.context.gl; + const framebuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); + + function read(texture) { + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + const pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4); + gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + return pixels; + } + + return this._canvasPixelComparison(read(finalFrame), canvasCopyInstances.canvasCopies.map(read), canvasCopyInstances.timeStamps); + } + + _canvasPixelComparison(finalFrame: Uint8Array, allFrames: Uint8Array[], timeStamps: number[]): number { + let finalScore = timeStamps[1] - timeStamps[0]; + const numPixels = finalFrame.length / 4; + + for (let i = 0; i < allFrames.length; i++) { + const frame = allFrames[i]; + let cnt = 0; + for (let j = 0; j < frame.length; j += 4) { + if (frame[j] === finalFrame[j] && + frame[j + 1] === finalFrame[j + 1] && + frame[j + 2] === finalFrame[j + 2] && + frame[j + 3] === finalFrame[j + 3]) { + cnt = cnt + 1; + } + } + //calculate the % visual completeness + const interval = timeStamps[i + 2] - timeStamps[i + 1]; + const visualCompletness = cnt / numPixels; + finalScore += interval * (1 - visualCompletness); + } + return finalScore; + } + /** * Clean up and release all internal resources associated with this map. * @@ -2622,11 +2760,20 @@ class Map extends Camera { * @see [Add an animated icon to the map](https://docs.mapbox.com/mapbox-gl-js/example/add-image-animated/) */ triggerRepaint() { + this._triggerFrame(true); + } + + _triggerFrame(render: boolean) { + this._renderNextFrame = this._renderNextFrame || render; if (this.style && !this._frame) { this._frame = browser.frame((paintStartTimeStamp: number) => { - PerformanceUtils.frame(paintStartTimeStamp); + const isRenderFrame = !!this._renderNextFrame; + PerformanceUtils.frame(paintStartTimeStamp, isRenderFrame); this._frame = null; - this._render(paintStartTimeStamp); + this._renderNextFrame = null; + if (isRenderFrame) { + this._render(paintStartTimeStamp); + } }); } } @@ -2663,6 +2810,24 @@ class Map extends Camera { this._update(); } + /** + * Gets and sets a Boolean indicating whether the speedindex metric calculation is on or off + * + * @name speedIndexTiming + * @type {boolean} + * @instance + * @memberof Map + * @example + * map.speedIndexTiming = true; + * @private + */ + get speedIndexTiming(): boolean { return !!this._speedIndexTiming; } + set speedIndexTiming(value: boolean) { + if (this._speedIndexTiming === value) return; + this._speedIndexTiming = value; + this._update(); + } + /** * Gets and sets a Boolean indicating whether the map will visualize * the padding offsets. diff --git a/src/ui/marker.js b/src/ui/marker.js index 75d2de3d787..67b2e80bc68 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -76,6 +76,7 @@ export default class Marker extends Evented { _pitchAlignment: string; _rotationAlignment: string; _originalTabIndex: ?string; // original tabindex of _element + _occlusionTimer: ?TimeoutID; constructor(options?: Options, legacyOptions?: Options) { super(); @@ -439,6 +440,30 @@ export default class Marker extends Evented { return this; } + _updateOcclusion() { + if (!this._occlusionTimer) { + this._occlusionTimer = setTimeout(this._onOcclusionTimer.bind(this), 60); + } + } + + _onOcclusionTimer() { + const tr = this._map.transform; + const pos = this._pos ? this._pos.sub(this._transformedOffset()) : null; + if (pos && pos.x >= 0 && pos.x < tr.width && pos.y >= 0 && pos.y < tr.height) { + // calculate if occluded. + const raycastLoc = this._map.unproject(pos); + const camera = this._map.getFreeCameraOptions(); + if (camera.position) { + const cameraPos = camera.position.toLngLat(); + const raycastDistance = cameraPos.distanceTo(raycastLoc); + const posDistance = cameraPos.distanceTo(this._lngLat); + const occluded = raycastDistance < posDistance * 0.9; + this._element.classList.toggle('mapboxgl-marker-occluded', occluded); + } + } + this._occlusionTimer = null; + } + _update(e?: {type: 'move' | 'moveend'}) { if (!this._map) return; @@ -446,7 +471,9 @@ export default class Marker extends Evented { this._lngLat = smartWrap(this._lngLat, this._pos, this._map.transform); } - this._pos = this._map.project(this._lngLat)._add(this._offset); + this._pos = this._map.project(this._lngLat)._add(this._transformedOffset()); + + if (this._map.transform.elevation) this._updateOcclusion(); let rotation = ""; if (this._rotationAlignment === "viewport" || this._rotationAlignment === "auto") { @@ -472,6 +499,20 @@ export default class Marker extends Evented { DOM.setTransform(this._element, `${anchorTranslate[this._anchor]} translate(${this._pos.x}px, ${this._pos.y}px) ${pitch} ${rotation}`); } + /** + * This is initially added to fix the behavior of default symbols only, in order + * to prevent any regression for custom symbols in client code. + * @private + */ + _transformedOffset() { + if (!this._defaultMarker) return this._offset; + const tr = this._map.transform; + const offset = this._offset.mult(this._scale); + if (this._rotationAlignment === "map") offset._rotate(tr.angle); + if (this._pitchAlignment === "map") offset.y *= Math.cos(tr._pitch); + return offset; + } + /** * Get the marker's offset. * @returns {Point} The marker's screen coordinates in pixels. @@ -570,7 +611,7 @@ export default class Marker extends Evented { // to calculate the new marker position. // If we don't do this, the marker 'jumps' to the click position // creating a jarring UX effect. - this._positionDelta = e.point.sub(this._pos).add(this._offset); + this._positionDelta = e.point.sub(this._pos).add(this._transformedOffset()); this._pointerdownPos = e.point; diff --git a/src/util/actor.js b/src/util/actor.js index ac441726c41..f4692514f8a 100644 --- a/src/util/actor.js +++ b/src/util/actor.js @@ -3,7 +3,7 @@ import {bindAll, isWorker, isSafari} from './util'; import window from './window'; import {serialize, deserialize} from './web_worker_transfer'; -import ThrottledInvoker from './throttled_invoker'; +import Scheduler from './scheduler'; import type {Transferable} from '../types/transferable'; import type {Cancelable} from '../types/cancelable'; @@ -25,24 +25,20 @@ class Actor { mapId: ?number; callbacks: { number: any }; name: string; - tasks: { number: any }; - taskQueue: Array; cancelCallbacks: { number: Cancelable }; - invoker: ThrottledInvoker; globalScope: any; + scheduler: Scheduler; constructor(target: any, parent: any, mapId: ?number) { this.target = target; this.parent = parent; this.mapId = mapId; this.callbacks = {}; - this.tasks = {}; - this.taskQueue = []; this.cancelCallbacks = {}; - bindAll(['receive', 'process'], this); - this.invoker = new ThrottledInvoker(this.process); + bindAll(['receive'], this); this.target.addEventListener('message', this.receive, false); this.globalScope = isWorker() ? target : window; + this.scheduler = new Scheduler(); } /** @@ -53,13 +49,14 @@ class Actor { * @param targetMapId A particular mapId to which to send this message. * @private */ - send(type: string, data: mixed, callback: ?Function, targetMapId: ?string, mustQueue: boolean = false): ?Cancelable { + send(type: string, data: mixed, callback: ?Function, targetMapId: ?string, mustQueue: boolean = false, callbackMetadata?: Object): ?Cancelable { // We're using a string ID instead of numbers because they are being used as object keys // anyway, and thus stringified implicitly. We use random IDs because an actor may receive // message from multiple other actors which could run in different execution context. A // linearly increasing ID could produce collisions. const id = Math.round((Math.random() * 1e18)).toString(36).substring(0, 10); if (callback) { + callback.metadata = callbackMetadata; this.callbacks[id] = callback; } const buffers: ?Array = isSafari(this.globalScope) ? undefined : []; @@ -104,11 +101,10 @@ class Actor { // Remove the original request from the queue. This is only possible if it // hasn't been kicked off yet. The id will remain in the queue, but because // there is no associated task, it will be dropped once it's time to execute it. - delete this.tasks[id]; const cancel = this.cancelCallbacks[id]; delete this.cancelCallbacks[id]; if (cancel) { - cancel(); + cancel.cancel(); } } else { if (isWorker() || data.mustQueue) { @@ -118,9 +114,9 @@ class Actor { // executing the next task in our queue, postMessage preempts this and // messages can be processed. We're using a MessageChannel object to get throttle the // process() flow to one at a time. - this.tasks[id] = data; - this.taskQueue.push(id); - this.invoker.trigger(); + const callback = this.callbacks[id]; + const metadata = (callback && callback.metadata) || {type: "message"}; + this.cancelCallbacks[id] = this.scheduler.add(() => this.processTask(id, data), metadata); } else { // In the main thread, process messages immediately so that other work does not slip in // between getting partial data back from workers. @@ -129,27 +125,6 @@ class Actor { } } - process() { - if (!this.taskQueue.length) { - return; - } - const id = this.taskQueue.shift(); - const task = this.tasks[id]; - delete this.tasks[id]; - // Schedule another process call if we know there's more to process _before_ invoking the - // current task. This is necessary so that processing continues even if the current task - // doesn't execute successfully. - if (this.taskQueue.length) { - this.invoker.trigger(); - } - if (!task) { - // If the task ID doesn't have associated task data anymore, it was canceled. - return; - } - - this.processTask(id, task); - } - processTask(id: number, task: any) { if (task.type === '') { // The done() function in the counterpart has been called, and we are now @@ -165,10 +140,8 @@ class Actor { } } } else { - let completed = false; const buffers: ?Array = isSafari(this.globalScope) ? undefined : []; const done = task.hasCallback ? (err, data) => { - completed = true; delete this.cancelCallbacks[id]; this.target.postMessage({ id, @@ -178,33 +151,26 @@ class Actor { data: serialize(data, buffers) }, buffers); } : (_) => { - completed = true; }; - let callback = null; const params = (deserialize(task.data): any); if (this.parent[task.type]) { // task.type == 'loadTile', 'removeTile', etc. - callback = this.parent[task.type](task.sourceMapId, params, done); + this.parent[task.type](task.sourceMapId, params, done); } else if (this.parent.getWorkerSource) { // task.type == sourcetype.method const keys = task.type.split('.'); const scope = (this.parent: any).getWorkerSource(task.sourceMapId, keys[0], params.source); - callback = scope[keys[1]](params, done); + scope[keys[1]](params, done); } else { // No function was found. done(new Error(`Could not find function ${task.type}`)); } - - if (!completed && callback && callback.cancel) { - // Allows canceling the task as long as it hasn't been completed yet. - this.cancelCallbacks[id] = callback.cancel; - } } } remove() { - this.invoker.remove(); + this.scheduler.remove(); this.target.removeEventListener('message', this.receive, false); } } diff --git a/src/util/ajax.js b/src/util/ajax.js index 86f58e6e37f..58874727d9f 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -79,10 +79,6 @@ class AJAXError extends Error { super(message); this.status = status; this.url = url; - - // work around for https://github.com/Rich-Harris/buble/issues/40 - this.name = this.constructor.name; - this.message = message; } toString() { @@ -236,10 +232,9 @@ function makeXMLHttpRequest(requestParameters: RequestParameters, callback: Resp export const makeRequest = function(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { // We're trying to use the Fetch API if possible. However, in some situations we can't use it: - // - IE11 doesn't support it at all. In this case, we dispatch the request to the main thread so - // that we can get an accruate referrer header. // - Safari exposes window.AbortController, but it doesn't work actually abort any requests in - // some versions (see https://bugs.webkit.org/show_bug.cgi?id=174980#c2) + // older versions (see https://bugs.webkit.org/show_bug.cgi?id=174980#c2). In this case, + // we dispatch the request to the main thread so that we can get an accruate referrer header. // - Requests for resources with the file:// URI scheme don't work with the Fetch API either. In // this case we unconditionally use XHR on the current thread since referrers don't matter. if (!isFileURL(requestParameters.url)) { @@ -266,6 +261,10 @@ export const postData = function(requestParameters: RequestParameters, callback: return makeRequest(extend(requestParameters, {method: 'POST'}), callback); }; +export const getData = function(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { + return makeRequest(extend(requestParameters, {method: 'GET'}), callback); +}; + function sameOrigin(url) { const a: HTMLAnchorElement = window.document.createElement('a'); a.href = url; diff --git a/src/util/browser.js b/src/util/browser.js index 808a6fcc5c4..ed7f559cde5 100755 --- a/src/util/browser.js +++ b/src/util/browser.js @@ -21,6 +21,8 @@ let linkEl; let reducedMotionQuery: MediaQueryList; +let errorState = false; + /** * @private */ @@ -31,7 +33,20 @@ const exported = { */ now, + setErrorState() { + errorState = true; + }, + + setNow(time: number) { + exported.now = () => time; + }, + + restoreNow() { + exported.now = now; + }, + frame(fn: (paintStartTimestamp: number) => void): Cancelable { + if (errorState) return {cancel: () => { }}; const frame = raf(fn); return {cancel: () => cancel(frame)}; }, @@ -54,8 +69,6 @@ const exported = { return linkEl.href; }, - hardwareConcurrency: window.navigator && window.navigator.hardwareConcurrency || 4, - get devicePixelRatio() { return window.devicePixelRatio; }, get prefersReducedMotion(): boolean { if (!window.matchMedia) return false; diff --git a/src/util/config.js b/src/util/config.js index 8c9872d1d20..20b616f110c 100644 --- a/src/util/config.js +++ b/src/util/config.js @@ -3,8 +3,11 @@ type Config = {| API_URL: string, EVENTS_URL: ?string, + SESSION_PATH: string, FEEDBACK_URL: string, REQUIRE_ACCESS_TOKEN: boolean, + TILE_URL_VERSION: string, + RASTER_URL_PREFIX: string, ACCESS_TOKEN: ?string, MAX_PARALLEL_IMAGE_REQUESTS: number |}; @@ -21,7 +24,10 @@ const config: Config = { return null; } }, + SESSION_PATH: '/map-sessions/v1', FEEDBACK_URL: 'https://apps.mapbox.com/feedback', + TILE_URL_VERSION: 'v4', + RASTER_URL_PREFIX: 'raster/v1', REQUIRE_ACCESS_TOKEN: true, ACCESS_TOKEN: null, MAX_PARALLEL_IMAGE_REQUESTS: 16 diff --git a/src/util/dispatcher.js b/src/util/dispatcher.js index 7a184f8489e..490ca82858c 100644 --- a/src/util/dispatcher.js +++ b/src/util/dispatcher.js @@ -17,6 +17,7 @@ class Dispatcher { actors: Array; currentActor: number; id: number; + ready: boolean; // exposed to allow stubbing in unit tests static Actor: Class; @@ -34,6 +35,11 @@ class Dispatcher { this.actors.push(actor); } assert(this.actors.length); + + // track whether all workers are instantiated and ready to receive messages; + // used for optimizations on initial map load + this.ready = false; + this.broadcast('checkIfReady', null, () => { this.ready = true; }); } /** diff --git a/src/util/mapbox.js b/src/util/mapbox.js index aaced940736..ec02068a51e 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -1,9 +1,9 @@ // @flow -/***** START WARNING - IF YOU USE THIS CODE WITH MAPBOX MAPPING APIS, REMOVAL OR -* MODIFICATION OF THE FOLLOWING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ****** -* The following code is used to access Mapbox's Mapping APIs. Removal or modification -* of this code when used with Mapbox's Mapping APIs can result in higher fees and/or +/***** START WARNING REMOVAL OR MODIFICATION OF THE +* FOLLOWING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ****** +* The following code is used to access Mapbox's APIs. Removal or modification +* of this code can result in higher fees and/or * termination of your account with Mapbox. * * Under the Mapbox Terms of Service, you may not use this code to access Mapbox @@ -14,14 +14,12 @@ ******************************************************************************/ import config from './config'; - -import browser from './browser'; import window from './window'; import webpSupported from './webp_supported'; import {createSkuToken, SKU_ID} from './sku_token'; import {version as sdkVersion} from '../../package.json'; import {uuid, validateUuid, storageAvailable, b64DecodeUnicode, b64EncodeUnicode, warnOnce, extend} from './util'; -import {postData, ResourceType} from './ajax'; +import {postData, ResourceType, getData} from './ajax'; import type {RequestParameters} from './ajax'; import type {Cancelable} from '../types/cancelable'; @@ -37,6 +35,8 @@ type UrlObject = {| params: Array |}; +export const AUTH_ERR_MSG: string = 'NO_ACCESS_TOKEN'; + export class RequestManager { _skuToken: string; _skuTokenExpiresAt: number; @@ -101,7 +101,7 @@ export class RequestManager { return this._makeAPIURL(urlObject, this._customAccessToken || accessToken); } - normalizeTileURL(tileURL: string, tileSize?: ?number): string { + normalizeTileURL(tileURL: string, use2x?: boolean, rasterTileSize?: number): string { if (this._isSkuTokenExpired()) { this._createSkuToken(); } @@ -110,16 +110,21 @@ export class RequestManager { const urlObject = parseUrl(tileURL); const imageExtensionRe = /(\.(png|jpg)\d*)(?=$)/; - const tileURLAPIPrefixRe = /^.+\/v4\//; - - // The v4 mapbox tile API supports 512x512 image tiles only when @2x - // is appended to the tile URL. If `tileSize: 512` is specified for - // a Mapbox raster source force the @2x suffix even if a non hidpi device. - const suffix = browser.devicePixelRatio >= 2 || tileSize === 512 ? '@2x' : ''; const extension = webpSupported.supported ? '.webp' : '$1'; + + // The v4 mapbox tile API supports 512x512 image tiles but they must be requested as '@2x' tiles. + const use2xAs512 = rasterTileSize && urlObject.authority !== 'raster' && rasterTileSize === 512; + + const suffix = use2x || use2xAs512 ? '@2x' : ''; urlObject.path = urlObject.path.replace(imageExtensionRe, `${suffix}${extension}`); - urlObject.path = urlObject.path.replace(tileURLAPIPrefixRe, '/'); - urlObject.path = `/v4${urlObject.path}`; + + if (urlObject.authority === 'raster') { + urlObject.path = `/${config.RASTER_URL_PREFIX}${urlObject.path}`; + } else { + const tileURLAPIPrefixRe = /^.+\/v4\//; + urlObject.path = urlObject.path.replace(tileURLAPIPrefixRe, '/'); + urlObject.path = `/${config.TILE_URL_VERSION}${urlObject.path}`; + } const accessToken = this._customAccessToken || getAccessToken(urlObject.params) || config.ACCESS_TOKEN; if (config.REQUIRE_ACCESS_TOKEN && accessToken && this._skuToken) { @@ -130,20 +135,26 @@ export class RequestManager { } canonicalizeTileURL(url: string, removeAccessToken: boolean) { - const version = "/v4/"; // matches any file extension specified by a dot and one or more alphanumeric characters const extensionRe = /\.[\w]+$/; const urlObject = parseUrl(url); // Make sure that we are dealing with a valid Mapbox tile URL. - // Has to begin with /v4/, with a valid filename + extension - if (!urlObject.path.match(/(^\/v4\/)/) || !urlObject.path.match(extensionRe)) { + // Has to begin with /v4/ or /raster/v1, with a valid filename + extension + if (!urlObject.path.match(/^(\/v4\/|\/raster\/v1\/)/) || !urlObject.path.match(extensionRe)) { // Not a proper Mapbox tile URL. return url; } // Reassemble the canonical URL from the parts we've parsed before. - let result = "mapbox://tiles/"; - result += urlObject.path.replace(version, ''); + let result = "mapbox://"; + if (urlObject.path.match(/^\/raster\/v1\//)) { + // If the tile url has /raster/v1/, make the final URL mapbox://raster/.... + const rasterPrefix = `/${config.RASTER_URL_PREFIX}/`; + result += `raster/${urlObject.path.replace(rasterPrefix, '')}`; + } else { + const tilesPrefix = `/${config.TILE_URL_VERSION}/`; + result += `tiles/${urlObject.path.replace(tilesPrefix, '')}`; + } // Append the query string, minus the access token parameter. let params = urlObject.params; @@ -261,7 +272,7 @@ function parseAccessToken(accessToken: ?string) { } } -type TelemetryEventType = 'appUserTurnstile' | 'map.load'; +type TelemetryEventType = 'appUserTurnstile' | 'map.load' | 'map.auth'; class TelemetryEvent { eventData: any; @@ -377,6 +388,7 @@ class TelemetryEvent { export class MapLoadEvent extends TelemetryEvent { +success: {[_: number]: boolean}; skuToken: string; + errorCb: (err: ?Error) => void; constructor() { super('map.load'); @@ -384,16 +396,16 @@ export class MapLoadEvent extends TelemetryEvent { this.skuToken = ''; } - postMapLoadEvent(tileUrls: Array, mapId: number, skuToken: string, customAccessToken: string) { - //Enabled only when Mapbox Access Token is set and a source uses - // mapbox tiles. + postMapLoadEvent(mapId: number, skuToken: string, customAccessToken: string, callback: (err: ?Error) => void) { this.skuToken = skuToken; + this.errorCb = callback; - if (config.EVENTS_URL && - customAccessToken || config.ACCESS_TOKEN && - Array.isArray(tileUrls) && - tileUrls.some(url => isMapboxURL(url) || isMapboxHTTPURL(url))) { - this.queueRequest({id: mapId, timestamp: Date.now()}, customAccessToken); + if (config.EVENTS_URL) { + if (customAccessToken || config.ACCESS_TOKEN) { + this.queueRequest({id: mapId, timestamp: Date.now()}, customAccessToken); + } else { + this.errorCb(new Error('A valid Mapbox access token is required to use Mapbox GL JS. To create an account or a new access token, visit https://account.mapbox.com/')); + } } } @@ -413,7 +425,72 @@ export class MapLoadEvent extends TelemetryEvent { } this.postEvent(timestamp, {skuToken: this.skuToken}, (err) => { - if (!err) { + if (err) { + this.errorCb(err); + } else { + if (id) this.success[id] = true; + } + + }, customAccessToken); + } +} + +export class MapSessionAPI extends TelemetryEvent { + +success: {[_: number]: boolean}; + skuToken: string; + errorCb: (err: ?Error) => void; + + constructor() { + super('map.auth'); + this.success = {}; + this.skuToken = ''; + } + + getSession(timestamp: number, token: string, callback: (err: ?Error) => void, customAccessToken?: ?string) { + if (!config.API_URL || !config.SESSION_PATH) return; + const authUrlObject: UrlObject = parseUrl(config.API_URL + config.SESSION_PATH); + authUrlObject.params.push(`sku=${token || ''}`); + authUrlObject.params.push(`access_token=${customAccessToken || config.ACCESS_TOKEN || ''}`); + + const request: RequestParameters = { + url: formatUrl(authUrlObject), + headers: { + 'Content-Type': 'text/plain', //Skip the pre-flight OPTIONS request + } + }; + + this.pendingRequest = getData(request, (error) => { + this.pendingRequest = null; + callback(error); + this.saveEventData(); + this.processRequests(customAccessToken); + }); + } + + getSessionAPI(mapId: number, skuToken: string, customAccessToken: string, callback: (err: ?Error) => void) { + this.skuToken = skuToken; + this.errorCb = callback; + + if (config.SESSION_PATH && config.API_URL) { + if (customAccessToken || config.ACCESS_TOKEN) { + this.queueRequest({id: mapId, timestamp: Date.now()}, customAccessToken); + } else { + this.errorCb(new Error(AUTH_ERR_MSG)); + } + } + } + + processRequests(customAccessToken?: ?string) { + if (this.pendingRequest || this.queue.length === 0) return; + const {id, timestamp} = this.queue.shift(); + + // Only one load event should fire per map + if (id && this.success[id]) return; + + this.getSession(timestamp, this.skuToken, (err) => { + if (err) { + this.errorCb(err); + } else { if (id) this.success[id] = true; } }, customAccessToken); @@ -487,5 +564,8 @@ export const postTurnstileEvent = turnstileEvent_.postTurnstileEvent.bind(turnst const mapLoadEvent_ = new MapLoadEvent(); export const postMapLoadEvent = mapLoadEvent_.postMapLoadEvent.bind(mapLoadEvent_); +const mapSessionAPI_ = new MapSessionAPI(); +export const getMapSessionAPI = mapSessionAPI_.getSessionAPI.bind(mapSessionAPI_); + /***** END WARNING - REMOVAL OR MODIFICATION OF THE PRECEDING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ******/ diff --git a/src/util/performance.js b/src/util/performance.js index 0b6a26c377b..d03a8e94170 100644 --- a/src/util/performance.js +++ b/src/util/performance.js @@ -9,8 +9,16 @@ export type PerformanceMetrics = { loadTime: number, fullLoadTime: number, fps: number, - percentDroppedFrames: number -} + percentDroppedFrames: number, + parseTile: number, + parseTile1: number, + parseTile2: number, + workerTask: number, + workerInitialization: number, + workerEvaluateScript: number, + workerIdle: number, + workerIdlePercent: number +}; export const PerformanceMarkers = { create: 'create', @@ -20,21 +28,52 @@ export const PerformanceMarkers = { let lastFrameTime = null; let frameTimes = []; +const frameSequences = [frameTimes]; +let i = 0; + +// The max milliseconds we should spend to render a single frame. +// This value may need to be tweaked. I chose 14 by increasing frame +// times with busy work and measuring the number of dropped frames. +// On a page with only a map, more frames started being dropped after +// going above 14ms. We might want to lower this to leave more room +// for other work. +const CPU_FRAME_BUDGET = 14; -const minFramerateTarget = 30; -const frameTimeTarget = 1000 / minFramerateTarget; +const framerateTarget = 60; +const frameTimeTarget = 1000 / framerateTarget; export const PerformanceUtils = { mark(marker: $Keys) { performance.mark(marker); }, - frame(timestamp: number) { + measure(name: string, begin?: string, end?: string) { + performance.measure(name, begin, end); + }, + beginMeasure(name: string) { + const mark = name + i++; + performance.mark(mark); + return { + mark, + name + }; + }, + endMeasure(m: { name: string, mark: string }) { + performance.measure(m.name, m.mark); + }, + frame(timestamp: number, isRenderFrame: boolean) { const currTimestamp = timestamp; if (lastFrameTime != null) { const frameTime = currTimestamp - lastFrameTime; frameTimes.push(frameTime); } - lastFrameTime = currTimestamp; + + if (isRenderFrame) { + lastFrameTime = currTimestamp; + } else { + lastFrameTime = null; + frameTimes = []; + frameSequences.push(frameTimes); + } }, clearMetrics() { lastFrameTime = null; @@ -46,28 +85,78 @@ export const PerformanceUtils = { performance.clearMarks(PerformanceMarkers[marker]); } }, + getPerformanceMetrics(): PerformanceMetrics { - const loadTime = performance.measure('loadTime', PerformanceMarkers.create, PerformanceMarkers.load).duration; - const fullLoadTime = performance.measure('fullLoadTime', PerformanceMarkers.create, PerformanceMarkers.fullLoad).duration; - const totalFrames = frameTimes.length; + const metrics = {}; - const avgFrameTime = frameTimes.reduce((prev, curr) => prev + curr, 0) / totalFrames / 1000; - const fps = 1 / avgFrameTime; + performance.measure('loadTime', PerformanceMarkers.create, PerformanceMarkers.load); + performance.measure('fullLoadTime', PerformanceMarkers.create, PerformanceMarkers.fullLoad); - // count frames that missed our framerate target - const droppedFrames = frameTimes - .filter((frameTime) => frameTime > frameTimeTarget) - .reduce((acc, curr) => { - return acc + (curr - frameTimeTarget) / frameTimeTarget; - }, 0); - const percentDroppedFrames = (droppedFrames / (totalFrames + droppedFrames)) * 100; + const measures = performance.getEntriesByType('measure'); + for (const measure of measures) { + metrics[measure.name] = (metrics[measure.name] || 0) + measure.duration; + } - return { - loadTime, - fullLoadTime, - fps, - percentDroppedFrames - }; + // We don't have a perfect way of measuring the actual number of dropped frames. + // The best way of determining when frames happen is the timestamp passed to + // requestAnimationFrame. In Chrome and Firefox the timestamps are generally + // multiples of 1000/60ms (+-2ms). + // + // The differences between the timestamps vary a lot more in Safari. + // It's not uncommon to see a 24ms difference followedd by a 8ms difference. + // I'm not sure, but I think these might not be dropped frames (due to multiple + // buffering?). + // + // For Safari, I think comparing the number of expected frames with the number of actual + // frames is a more accurate way of measuring dropped frames than comparing + // individual frame time differences to a target time. In Firefox and Chrome + // both approaches produce the same result most of the time. + let droppedFrames = 0; + let totalFrameTimeSum = 0; + let totalFrames = 0; + metrics.jank = 0; + + for (const frameTimes of frameSequences) { + if (!frameTimes.length) continue; + const frameTimeSum = frameTimes.reduce((prev, curr) => prev + curr, 0); + const expectedFrames = Math.max(1, Math.round(frameTimeSum / frameTimeTarget)); + droppedFrames += expectedFrames - frameTimes.length; + totalFrameTimeSum += frameTimeSum; + totalFrames += frameTimes.length; + + // Jank is a change in the frame rate. + // Count the number of times a frame has a worse rate than the previous frame. + // A consistent rate does not increase jank even if it is continuosly dropping frames. + // A one-off frame does not increase jank even if it is really long. + // + // This is not that accurate in Safari because the differences between animation frame + // times is not as close to a multiple of 1000/60ms. + const roundedTimes = frameTimes.map(frameTime => Math.max(1, Math.round(frameTime / frameTimeTarget))); + for (let n = 0; n < roundedTimes.length - 1; n++) { + if (roundedTimes[n + 1] > roundedTimes[n]) { + metrics.jank++; + } + } + } + const avgFrameTime = totalFrameTimeSum / totalFrames / 1000; + metrics.fps = 1 / avgFrameTime; + metrics.droppedFrames = droppedFrames; + metrics.percentDroppedFrames = (droppedFrames / (totalFrames + droppedFrames)) * 100; + + metrics.cpuFrameBudgetExceeded = 0; + const renderFrames = performance.getEntriesByName('render'); + for (const renderFrame of renderFrames) { + metrics.cpuFrameBudgetExceeded += Math.max(0, renderFrame.duration - CPU_FRAME_BUDGET); + } + + return metrics; + }, + + getWorkerPerformanceMetrics() { + return JSON.parse(JSON.stringify({ + timeOrigin: performance.timeOrigin, + measures: performance.getEntriesByType("measure") + })); } }; diff --git a/src/util/primitives.js b/src/util/primitives.js index b1cd69e6fe3..0d0cd037221 100644 --- a/src/util/primitives.js +++ b/src/util/primitives.js @@ -1,7 +1,28 @@ // @flow import {vec3, vec4} from 'gl-matrix'; -import assert from 'assert'; + +class Ray { + pos: vec3; + dir: vec3; + + constructor(pos_: vec3, dir_: vec3) { + this.pos = pos_; + this.dir = dir_; + } + + intersectsPlane(pt: vec3, normal: vec3, out: vec3): boolean { + const D = vec3.dot(normal, this.dir); + + // ray is parallel to plane, so it misses + if (Math.abs(D) < 1e-6) { return false; } + + const t = vec3.dot(vec3.sub(vec3.create(), pt, this.pos), normal) / D; + const intersection = vec3.scaleAndAdd(vec3.create(), this.pos, this.dir, t); + vec3.copy(out, intersection); + return true; + } +} class Frustum { points: Array>; @@ -28,8 +49,12 @@ class Frustum { // Transform frustum corner points from clip space to tile space const frustumCoords = clipSpaceCorners - .map(v => vec4.transformMat4([], v, invProj)) - .map(v => vec4.scale([], v, 1.0 / v[3] / worldSize * scale)); + .map(v => { + const s = vec4.transformMat4([], v, invProj); + const k = 1.0 / s[3] / worldSize * scale; + // Z scale in meters. + return vec4.mul(s, s, [k, k, 1.0 / s[3], k]); + }); const frustumPlanePointIndices = [ [0, 1, 2], // near @@ -71,7 +96,7 @@ class Aabb { qMin[axis] = split[axis] ? this.min[axis] : this.center[axis]; qMax[axis] = split[axis] ? this.center[axis] : this.max[axis]; } - // Elevation is always constant, hence quadrant.max.z = this.max.z + // Temporarily, elevation is constant, hence quadrant.max.z = this.max.z qMax[2] = this.max[2]; return new Aabb(qMin, qMax); } @@ -86,19 +111,26 @@ class Aabb { return pointOnAabb - point[1]; } + distanceZ(point: Array): number { + const pointOnAabb = Math.max(Math.min(this.max[2], point[2]), this.min[2]); + return pointOnAabb - point[2]; + } + // Performs a frustum-aabb intersection test. Returns 0 if there's no intersection, // 1 if shapes are intersecting and 2 if the aabb if fully inside the frustum. intersects(frustum: Frustum): number { // Execute separating axis test between two convex objects to find intersections // Each frustum plane together with 3 major axes define the separating axes - // Note: test only 4 points as both min and max points have equal elevation - assert(this.min[2] === 0 && this.max[2] === 0); const aabbPoints = [ - [this.min[0], this.min[1], 0.0, 1], - [this.max[0], this.min[1], 0.0, 1], - [this.max[0], this.max[1], 0.0, 1], - [this.min[0], this.max[1], 0.0, 1] + [this.min[0], this.min[1], this.min[2], 1], + [this.max[0], this.min[1], this.min[2], 1], + [this.max[0], this.max[1], this.min[2], 1], + [this.min[0], this.max[1], this.min[2], 1], + [this.min[0], this.min[1], this.max[2], 1], + [this.max[0], this.min[1], this.max[2], 1], + [this.max[0], this.max[1], this.max[2], 1], + [this.min[0], this.max[1], this.max[2], 1], ]; let fullyInside = true; @@ -141,5 +173,6 @@ class Aabb { } export { Aabb, - Frustum + Frustum, + Ray }; diff --git a/src/util/scheduler.js b/src/util/scheduler.js new file mode 100644 index 00000000000..7f851a82bd4 --- /dev/null +++ b/src/util/scheduler.js @@ -0,0 +1,97 @@ +// @flow + +import ThrottledInvoker from './throttled_invoker'; +import {bindAll, isWorker} from './util'; +import {PerformanceUtils} from './performance'; + +class Scheduler { + + tasks: { [number]: any }; + taskQueue: Array; + invoker: ThrottledInvoker; + nextId: number; + + constructor() { + this.tasks = {}; + this.taskQueue = []; + bindAll(['process'], this); + this.invoker = new ThrottledInvoker(this.process); + + this.nextId = 0; + } + + add(fn: () => void, metadata: Object) { + const id = this.nextId++; + this.tasks[id] = {fn, metadata, priority: getPriority(metadata), id}; + this.taskQueue.push(id); + this.invoker.trigger(); + return { + cancel: () => { + delete this.tasks[id]; + } + }; + } + + process() { + const m = isWorker() ? PerformanceUtils.beginMeasure('workerTask') : undefined; + try { + this.taskQueue = this.taskQueue.filter(id => !!this.tasks[id]); + + if (!this.taskQueue.length) { + return; + } + const id = this.pick(); + if (id === null) return; + + const task = this.tasks[id]; + delete this.tasks[id]; + // Schedule another process call if we know there's more to process _before_ invoking the + // current task. This is necessary so that processing continues even if the current task + // doesn't execute successfully. + if (this.taskQueue.length) { + this.invoker.trigger(); + } + if (!task) { + // If the task ID doesn't have associated task data anymore, it was canceled. + return; + } + + task.fn(); + } finally { + if (m) PerformanceUtils.endMeasure(m); + } + } + + pick() { + let minIndex = null; + let minPriority = Infinity; + for (let i = 0; i < this.taskQueue.length; i++) { + const id = this.taskQueue[i]; + const task = this.tasks[id]; + if (task.priority < minPriority) { + minPriority = task.priority; + minIndex = i; + } + } + if (minIndex === null) return null; + const id = this.taskQueue[minIndex]; + this.taskQueue.splice(minIndex, 1); + return id; + } + + remove() { + this.invoker.remove(); + } +} + +function getPriority({type, isSymbolTile, zoom}: Object) { + zoom = zoom || 0; + if (type === 'message') return 0; + if (type === 'maybePrepare' && !isSymbolTile) return 100 - zoom; + if (type === 'parseTile' && !isSymbolTile) return 200 - zoom; + if (type === 'parseTile' && isSymbolTile) return 300 - zoom; + if (type === 'maybePrepare' && isSymbolTile) return 400 - zoom; + return 500; +} + +export default Scheduler; diff --git a/src/util/sku_token.js b/src/util/sku_token.js index 990a1c60c1e..d784253af1e 100644 --- a/src/util/sku_token.js +++ b/src/util/sku_token.js @@ -1,9 +1,9 @@ // @flow -/***** START WARNING - IF YOU USE THIS CODE WITH MAPBOX MAPPING APIS, REMOVAL OR -* MODIFICATION OF THE FOLLOWING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ****** -* The following code is used to access Mapbox's Mapping APIs. Removal or modification -* of this code when used with Mapbox's Mapping APIs can result in higher fees and/or +/***** START WARNING REMOVAL OR MODIFICATION OF THE +* FOLLOWING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ****** +* The following code is used to access Mapbox's APIs. Removal or modification +* of this code can result in higher fees and/or * termination of your account with Mapbox. * * Under the Mapbox Terms of Service, you may not use this code to access Mapbox diff --git a/src/util/smart_wrap.js b/src/util/smart_wrap.js index a713d1e2b9d..b1635366cdc 100644 --- a/src/util/smart_wrap.js +++ b/src/util/smart_wrap.js @@ -24,15 +24,20 @@ export default function(lngLat: LngLat, priorPos: ?Point, transform: Transform): lngLat = new LngLat(lngLat.lng, lngLat.lat); // First, try shifting one world in either direction, and see if either is closer to the - // prior position. This preserves object constancy when the map center is auto-wrapped - // during animations. + // prior position. Don't shift away if it new position is further from center. + // This preserves object constancy when the map center is auto-wrapped during animations, + // but don't allow it to run away on horizon (points towards horizon get closer and closer). if (priorPos) { const left = new LngLat(lngLat.lng - 360, lngLat.lat); const right = new LngLat(lngLat.lng + 360, lngLat.lat); + // Unless offscreen, keep the marker within same wrap distance to center. This is to prevent + // running it to infinity `lng` near horizon when bearing is ~90°. + const withinWrap = Math.ceil(Math.abs(lngLat.lng - transform.center.lng) / 360) * 360; const delta = transform.locationPoint(lngLat).distSqr(priorPos); - if (transform.locationPoint(left).distSqr(priorPos) < delta) { + const offscreen = priorPos.x < 0 || priorPos.y < 0 || priorPos.x > transform.width || priorPos.y > transform.height; + if (transform.locationPoint(left).distSqr(priorPos) < delta && (offscreen || Math.abs(left.lng - transform.center.lng) < withinWrap)) { lngLat = left; - } else if (transform.locationPoint(right).distSqr(priorPos) < delta) { + } else if (transform.locationPoint(right).distSqr(priorPos) < delta && (offscreen || Math.abs(right.lng - transform.center.lng) < withinWrap)) { lngLat = right; } } diff --git a/src/util/util.js b/src/util/util.js index f5e3d55c58a..493e05f26e6 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -4,12 +4,44 @@ import UnitBezier from '@mapbox/unitbezier'; import Point from '@mapbox/point-geometry'; import window from './window'; +import assert from 'assert'; import type {Callback} from '../types/callback'; // Number.MAX_SAFE_INTEGER not available in IE export const MAX_SAFE_INTEGER = Math.pow(2, 53) - 1; +const DEG_TO_RAD = Math.PI / 180; +const RAD_TO_DEG = 180 / Math.PI; + +/** + * Converts an angle in degrees to radians + * copy all properties from the source objects into the destination. + * The last source object given overrides properties from previous + * source objects. + * + * @param a angle to convert + * @returns the angle in radians + * @private + */ +export function degToRad(a: number): number { + return a * DEG_TO_RAD; +} + +/** + * Converts an angle in radians to degrees + * copy all properties from the source objects into the destination. + * The last source object given overrides properties from previous + * source objects. + * + * @param a angle to convert + * @returns the angle in degrees + * @private + */ +export function radToDeg(a: number): number { + return a * RAD_TO_DEG; +} + /** * @module util * @private @@ -30,6 +62,74 @@ export function easeCubicInOut(t: number): number { return 4 * (t < 0.5 ? t3 : 3 * (t - t2) + t3 - 0.75); } +/** + * Computes an AABB for a set of points. + * + * @param {Point[]} points + * @returns {{ min: Point, max: Point}} + * @private + */ +export function getBounds(points: Point[]): { min: Point, max: Point} { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + for (const p of points) { + minX = Math.min(minX, p.x); + minY = Math.min(minY, p.y); + maxX = Math.max(maxX, p.x); + maxY = Math.max(maxY, p.y); + } + + return { + min: new Point(minX, minY), + max: new Point(maxX, maxY), + }; +} + +/** + * Converts a AABB into a closed polygon with clockwise winding order. + * + * @param {Point} min + * @param {Point} max + * @param {number} [buffer=0] + * @returns {Point[]} + */ +export function polygonizeBounds(min: Point, max: Point, buffer: number = 0): Point[] { + const offset = new Point(buffer, buffer); + const minBuf = min.sub(offset); + const maxBuf = max.add(offset); + return [minBuf, new Point(maxBuf.x, minBuf.y), maxBuf, new Point(minBuf.x, maxBuf.y), minBuf]; +} + +/** + * Takes a convex ring and applies and expands it outward by applying a buffer around it. + * This function assumes that the ring is in clockwise winding order. + * + * @param {Point[]} ring + * @param {number} buffer + * @returns {Point[]} + */ +export function bufferConvexPolygon(ring: Point[], buffer: number): Point[] { + assert(ring.length > 2, 'bufferConvexPolygon requires the ring to have atleast 3 points'); + const output = []; + for (let currIdx = 0; currIdx < ring.length; currIdx++) { + const prevIdx = wrap(currIdx - 1, -1, ring.length - 1); + const nextIdx = wrap(currIdx + 1, -1, ring.length - 1); + const prev = ring[prevIdx]; + const curr = ring[currIdx]; + const next = ring[nextIdx]; + const p1 = prev.sub(curr).unit(); + const p2 = next.sub(curr).unit(); + const interiorAngle = p2.angleWithSep(p1.x, p1.y); + // Calcuate a vector that points in the direction of the angle bisector between two sides. + // Scale it based on a right angled triangle constructed at that corner. + const offset = p1.add(p2).unit().mult(-1 * buffer / Math.sin(interiorAngle / 2)); + output.push(curr.add(offset)); + } + return output; +} + /** * Given given (x, y), (x1, y1) control points for a bezier curve, * return a function that interpolates along that curve. @@ -229,6 +329,15 @@ export function nextPowerOfTwo(value: number): number { return Math.pow(2, Math.ceil(Math.log(value) / Math.LN2)); } +/** + * Return the previous power of two, or the input value if already a power of two + * @private + */ +export function prevPowerOfTwo(value: number): number { + if (value <= 1) return 1; + return Math.pow(2, Math.floor(Math.log(value) / Math.LN2)); +} + /** * Validate a string to match UUID(v4) of the * form: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx @@ -405,30 +514,6 @@ export function isClosedPolygon(points: Array): boolean { return Math.abs(calculateSignedArea(points)) > 0.01; } -/** - * Converts spherical coordinates to cartesian coordinates. - * - * @private - * @param spherical Spherical coordinates, in [radial, azimuthal, polar] - * @return cartesian coordinates in [x, y, z] - */ - -export function sphericalToCartesian([r, azimuthal, polar]: [number, number, number]): {x: number, y: number, z: number} { - // We abstract "north"/"up" (compass-wise) to be 0° when really this is 90° (π/2): - // correct for that here - azimuthal += 90; - - // Convert azimuthal and polar angles to radians - azimuthal *= Math.PI / 180; - polar *= Math.PI / 180; - - return { - x: r * Math.cos(azimuthal) * Math.sin(polar), - y: r * Math.sin(azimuthal) * Math.sin(polar), - z: r * Math.cos(polar) - }; -} - /* global self, WorkerGlobalScope */ /** * Retuns true if the when run in the web-worker context. diff --git a/src/util/worker_performance_utils.js b/src/util/worker_performance_utils.js new file mode 100644 index 00000000000..c2b36e9ec4d --- /dev/null +++ b/src/util/worker_performance_utils.js @@ -0,0 +1,47 @@ +// @flow + +import window from '../util/window'; + +import Dispatcher from './dispatcher'; +import getWorkerPool from './global_worker_pool'; +import {PerformanceUtils} from './performance'; + +const performance = window.performance; + +// separate from PerformanceUtils to avoid circular dependency + +export const WorkerPerformanceUtils = { + + getPerformanceMetricsAsync(callback: (error: ?Error, result: ?Object) => void) { + const metrics = PerformanceUtils.getPerformanceMetrics(); + const dispatcher = new Dispatcher(getWorkerPool(), this); + + const createTime = performance.getEntriesByName('create', 'mark')[0].startTime; + + dispatcher.broadcast('getWorkerPerformanceMetrics', {}, (err, results) => { + dispatcher.remove(); + if (err) return callback(err); + + const sums = {}; + + for (const result of results) { + for (const measure of result.measures) { + sums[measure.name] = (sums[measure.name] || 0) + measure.duration; + } + + sums.workerInitialization = result.timeOrigin - performance.timeOrigin - createTime; + } + + for (const name in sums) { + metrics[name] = sums[name] / results.length; + } + + metrics.workerIdle = metrics.loadTime - metrics.workerInitialization - metrics.workerEvaluateScript - metrics.workerTask; + metrics.workerIdlePercent = metrics.workerIdle / metrics.loadTime; + + metrics.parseTile = metrics.parseTile1 + metrics.parseTile2; + + return callback(undefined, metrics); + }); + } +}; diff --git a/src/util/worker_pool.js b/src/util/worker_pool.js index f9a899dfcca..249fa103510 100644 --- a/src/util/worker_pool.js +++ b/src/util/worker_pool.js @@ -2,7 +2,6 @@ import WebWorker from './web_worker'; import type {WorkerInterface} from './web_worker'; -import browser from './browser'; export const PRELOAD_POOL_ID = 'mapboxgl_preloaded_worker_pool'; @@ -53,5 +52,6 @@ export default class WorkerPool { } } -const availableLogicalProcessors = Math.floor(browser.hardwareConcurrency / 2); -WorkerPool.workerCount = Math.max(Math.min(availableLogicalProcessors, 6), 1); +// extensive benchmarking showed 2 to be the best default for both desktop and mobile devices; +// we can't rely on hardwareConcurrency because of wild inconsistency of reported numbers between browsers +WorkerPool.workerCount = 2; diff --git a/test/ajax_stubs.js b/test/ajax_stubs.js deleted file mode 100644 index ab9c4fca1a8..00000000000 --- a/test/ajax_stubs.js +++ /dev/null @@ -1,128 +0,0 @@ - -import {PNG} from 'pngjs'; -import request from 'request'; -// we're using a require hook to load this file instead of src/util/ajax.js, -// so we import browser module as if it were in an adjacent file -import browser from './browser'; // eslint-disable-line import/no-unresolved -const cache = {}; - -/** - * The type of a resource. - * @private - * @readonly - * @enum {string} - */ -const ResourceType = { - Unknown: 'Unknown', - Style: 'Style', - Source: 'Source', - Tile: 'Tile', - Glyphs: 'Glyphs', - SpriteImage: 'SpriteImage', - SpriteJSON: 'SpriteJSON', - Image: 'Image' -}; -export {ResourceType}; - -if (typeof Object.freeze == 'function') { - Object.freeze(ResourceType); -} - -function cached(data, callback) { - setImmediate(() => { - callback(null, data); - }); -} - -export const getReferrer = () => undefined; - -export const getJSON = function({url}, callback) { - if (cache[url]) return cached(cache[url], callback); - return request(url, (error, response, body) => { - if (!error && response.statusCode >= 200 && response.statusCode < 300) { - let data; - try { - data = JSON.parse(body); - } catch (err) { - return callback(err); - } - cache[url] = data; - callback(null, data); - } else { - callback(error || new Error(response.statusCode)); - } - }); -}; - -export const getArrayBuffer = function({url}, callback) { - if (cache[url]) return cached(cache[url], callback); - return request({url, encoding: null}, (error, response, body) => { - if (!error && response.statusCode >= 200 && response.statusCode < 300) { - cache[url] = body; - callback(null, body); - } else { - if (!error) error = {status: +response.statusCode}; - callback(error); - } - }); -}; - -export const makeRequest = getArrayBuffer; - -export const postData = function({url, body}, callback) { - return request.post(url, body, (error, response, body) => { - if (!error && response.statusCode >= 200 && response.statusCode < 300) { - callback(null, body); - } else { - callback(error || new Error(response.statusCode)); - } - }); -}; - -export const getImage = function({url}, callback) { - if (cache[url]) return cached(cache[url], callback); - return request({url, encoding: null}, (error, response, body) => { - if (!error && response.statusCode >= 200 && response.statusCode < 300) { - new PNG().parse(body, (err, png) => { - if (err) return callback(err); - cache[url] = png; - callback(null, png); - }); - } else { - callback(error || {status: response.statusCode}); - } - }); -}; - -browser.getImageData = function({width, height, data}, padding = 0) { - const source = new Uint8Array(data); - const dest = new Uint8Array((2 * padding + width) * (2 * padding + height) * 4); - - const offset = (2 * padding + width) * padding + padding; - for (let i = 0; i < height; i++) { - dest.set(source.slice(i * width * 4, (i + 1) * width * 4), 4 * (offset + (width + 2 * padding) * i)); - } - return {width: width + 2 * padding, height: height + 2 * padding, data: dest}; -}; - -// Hack: since node doesn't have any good video codec modules, just grab a png with -// the first frame and fake the video API. -export const getVideo = function(urls, callback) { - return request({url: urls[0], encoding: null}, (error, response, body) => { - if (!error && response.statusCode >= 200 && response.statusCode < 300) { - new PNG().parse(body, (err, png) => { - if (err) return callback(err); - callback(null, { - readyState: 4, // HAVE_ENOUGH_DATA - addEventListener() {}, - play() {}, - width: png.width, - height: png.height, - data: png.data - }); - }); - } else { - callback(error || new Error(response.statusCode)); - } - }); -}; diff --git a/test/browser/drag.test.js b/test/browser/drag.test.js index ffcb08b59fe..165889441c5 100644 --- a/test/browser/drag.test.js +++ b/test/browser/drag.test.js @@ -23,7 +23,7 @@ test("dragging", async t => { /* eslint-disable no-undef */ return map.getCenter(); }); - equalWithPrecision(t, center.lng, -35.15625, 0.001); - equalWithPrecision(t, center.lat, 0, 0.0000001); + equalWithPrecision(t, center.lng, -35.15625, 0.01); + equalWithPrecision(t, center.lat, 0, 0.01); }); }); diff --git a/test/build/min.test.js b/test/build/min.test.js index 649a893274c..d87363ea50f 100644 --- a/test/build/min.test.js +++ b/test/build/min.test.js @@ -2,7 +2,6 @@ import {test} from '../util/test'; import fs from 'fs'; import path from 'path'; import reference from '../../src/style-spec/reference/latest'; -import {Linter} from 'eslint'; import {scripts} from '../../package.json'; const minBundle = fs.readFileSync('dist/mapbox-gl.js', 'utf8'); @@ -40,19 +39,3 @@ test('evaluates without errors', (t) => { t.doesNotThrow(() => require(path.join(__dirname, '../../dist/mapbox-gl.js'))); t.end(); }); - -test('distributed in plain ES5 code', (t) => { - const linter = new Linter(); - const messages = linter.verify(minBundle, { - parserOptions: { - ecmaVersion: 5 - }, - rules: {}, - env: { - node: true - } - }); - t.deepEqual(messages.map(message => `${message.line}:${message.column}: ${message.message}`), []); - t.end(); -}); - diff --git a/test/build/style-spec.test.js b/test/build/style-spec.test.js index 4d98121e42d..71fb72f466d 100644 --- a/test/build/style-spec.test.js +++ b/test/build/style-spec.test.js @@ -1,11 +1,9 @@ /* eslint-disable import/no-commonjs */ -const fs = require('fs'); const path = require('path'); const isBuiltin = require('is-builtin-module'); -const Linter = require('eslint').Linter; const rollup = require('rollup'); import {test} from '../util/test'; @@ -14,22 +12,9 @@ import rollupConfig from '../../src/style-spec/rollup.config'; // some paths const styleSpecDirectory = path.join(__dirname, '../../src/style-spec'); const styleSpecPackage = require('../../src/style-spec/package.json'); -const styleSpecDistBundle = fs.readFileSync(path.join(__dirname, '../../dist/style-spec/index.js'), 'utf-8'); test('@mapbox/mapbox-gl-style-spec npm package', (t) => { - t.test('build plain ES5 bundle in prepublish', (t) => { - const linter = new Linter(); - const messages = linter.verify(styleSpecDistBundle, { - parserOptions: { - ecmaVersion: 5 - }, - rules: {}, - env: { - node: true - } - }).map(message => `${message.line}:${message.column}: ${message.message}`); - t.deepEqual(messages, [], 'distributed bundle is plain ES5 code'); - + t.test('builds self-contained bundle without undeclared dependencies', (t) => { t.stub(console, 'warn'); rollup.rollup({ input: `${styleSpecDirectory}/style-spec.js`, diff --git a/test/ignores.json b/test/ignores.json index 0501676a329..843a2805cec 100644 --- a/test/ignores.json +++ b/test/ignores.json @@ -1,31 +1,49 @@ { "query-tests/regressions/mapbox-gl-js#4494": "https://github.com/mapbox/mapbox-gl-js/issues/2716", - "render-tests/geojson/inline-linestring-fill": "current behavior is arbitrary", - "render-tests/line-dasharray/case/square": "https://github.com/mapbox/mapbox-gl-js/issues/9531", + "render-tests/debug/tile": "skip - inconsistent text rendering with canvas on different platforms", + "render-tests/debug/tile-overscaled": "skip - inconsistent text rendering with canvas on different platforms", + "render-tests/fill-extrusion-pattern/1.5x-on-1x-add-image": "skip - non-deterministic on AMD graphics cards", + "render-tests/fill-extrusion-pattern/multiple-layers-flat": "skip - mapbox-gl-js-internal#223", + "render-tests/fill-extrusion-pattern/opacity-terrain-flat-on-border": "skip - mapbox-gl-js-internal#223", + "render-tests/fill-extrusion-pattern/tile-buffer": "skip - not rendering correctly on CI", + "render-tests/fill-pattern/update-feature-state": "https://github.com/mapbox/mapbox-gl-js/issues/7207", + "render-tests/geojson/inline-linestring-fill": "skip - current behavior is arbitrary", + "render-tests/icon-image/icon-sdf-non-sdf-one-layer": "skip - render sdf icon and normal icon in one layer", + "render-tests/icon-text-fit/text-variable-anchor-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", + "render-tests/line-dasharray/case/square": "skip - https://github.com/mapbox/mapbox-gl-js/issues/9531", "render-tests/map-mode/static": "https://github.com/mapbox/mapbox-gl-js/issues/5649", "render-tests/map-mode/tile": "skip - mapbox-gl-js does not support tile-mode", "render-tests/map-mode/tile-avoid-edges": "skip - mapbox-gl-js does not support tile-mode", + "render-tests/mixed-zoom/z10-z11": "current behavior conflicts with https://github.com/mapbox/mapbox-gl-js/pull/6803. can be fixed when https://github.com/mapbox/api-maps/issues/1480 is done", "render-tests/projection/axonometric": "axonometric rendering in gl-js tbd", "render-tests/projection/axonometric-multiple": "axonometric rendering in gl-js tbd", "render-tests/projection/skew": "axonometric rendering in gl-js tbd", - "render-tests/regressions/mapbox-gl-js#3682": "skip - true", - "render-tests/runtime-styling/image-update-icon": "skip - https://github.com/mapbox/mapbox-gl-js/issues/4804", - "render-tests/runtime-styling/image-update-pattern": "skip - https://github.com/mapbox/mapbox-gl-js/issues/4804", - "render-tests/mixed-zoom/z10-z11": "current behavior conflicts with https://github.com/mapbox/mapbox-gl-js/pull/6803. can be fixed when https://github.com/mapbox/api-maps/issues/1480 is done", - "render-tests/fill-extrusion-pattern/tile-buffer": "https://github.com/mapbox/mapbox-gl-js/issues/4403", - "render-tests/symbol-placement/line-center-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", "render-tests/symbol-placement/line-center-buffer-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", + "render-tests/symbol-placement/line-center-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", "render-tests/symbol-sort-key/text-ignore-placement": "skip - text drawn over icons", - "render-tests/text-variable-anchor/remember-last-placement": "skip - not sure this is correct behavior", - "render-tests/icon-image/icon-sdf-non-sdf-one-layer": "skip - render sdf icon and normal icon in one layer", + "render-tests/text-font-metrics/font-with-baseline-font-without-baseline": "mapbox-gl-js-internal#74", + "render-tests/text-font-metrics/font-with-image-vertical": "mapbox-gl-js-internal#75", + "render-tests/text-font-metrics/latin-alphabets-vertical": "mapbox-gl-js-internal#75", + "render-tests/text-font-metrics/latin-alphabets-vertical-no-ascender-descender": "mapbox-gl-js-internal#75", + "render-tests/text-font-metrics/line-placement-vertical-shaping-with-punctuations": "mapbox-gl-js-internal#76", + "render-tests/text-font-metrics/mixed-fonts-both-with-baseline": "mapbox-gl-js-internal#74", + "render-tests/text-font-metrics/mixed-fonts-with-image": "mapbox-gl-js-internal#74", + "render-tests/text-font-metrics/mixed-fonts-with-image-vertical": "mapbox-gl-js-internal#74", + "render-tests/text-font-metrics/mixed-fonts-with-images-vertical": "mapbox-gl-js-internal#75", + "render-tests/text-font-metrics/mixed-fonts-with-scales": "mapbox-gl-js-internal#74", + "render-tests/text-font-metrics/multiline-point-placement-vertical-shaping-with-punctuations": "mapbox-gl-js-internal#76", + "render-tests/text-font-metrics/point-placement-vertical-shaping-with-punctuations": "mapbox-gl-js-internal#76", + "render-tests/text-font-metrics/punctuations-vertical": "mapbox-gl-js-internal#75", + "render-tests/text-font-metrics/vertical-shaping-with-ZWSP": "mapbox-gl-js-internal#76", + "render-tests/text-variable-anchor/all-anchors-labels-priority-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", "render-tests/text-variable-anchor/all-anchors-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", - "render-tests/fill-pattern/update-feature-state": "https://github.com/mapbox/mapbox-gl-js/issues/7207", - "render-tests/text-size/zero": "https://github.com/mapbox/mapbox-gl-js/issues/9161", + "render-tests/text-variable-anchor/avoid-edges-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", "render-tests/text-variable-anchor/left-top-right-bottom-offset-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", + "render-tests/text-variable-anchor/pitched-rotated-debug": "skip - non-deterministic when rendered in browser", + "render-tests/text-variable-anchor/pitched-with-map": "skip - non-deterministic when rendered in browser", + "render-tests/text-variable-anchor/remember-last-placement": "skip - not sure this is correct behavior", + "render-tests/text-size/zero": "https://github.com/mapbox/mapbox-gl-js/issues/9161", "render-tests/tile-mode/streets-v11": "skip - mapbox-gl-js does not support tile-mode", "render-tests/within/paint-line": "https://github.com/mapbox/mapbox-gl-js/issues/7023", - "render-tests/icon-text-fit/text-variable-anchor-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", - "render-tests/text-variable-anchor/all-anchors-labels-priority-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", - "render-tests/fill-extrusion-pattern/1.5x-on-1x-add-image": "skip - non-deterministic on AMD graphics cards", - "render-tests/text-variable-anchor/avoid-edges-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode" + "render-tests/distance/layout-text-size": "skip - distance expression is not implemented" } diff --git a/test/integration/README.md b/test/integration/README.md index 6046e7c56e9..a64ecf32dda 100644 --- a/test/integration/README.md +++ b/test/integration/README.md @@ -29,7 +29,7 @@ yarn run test-render ``` or ``` -yarn run test-query-node +yarn run test-query ``` To run only the expression tests: @@ -42,7 +42,7 @@ yarn run test-expressions To run a subset of tests or an individual test, you can pass a specific subdirectory to the `test-render` script. For example, to run all the tests for a given property, e.g. `circle-radius`: ``` -$ yarn run test-render circle-radius +$ yarn run test-render tests=circle-radius ... * passed circle-radius/antimeridian * passed circle-radius/default @@ -56,7 +56,7 @@ Done in 2.71s. ``` Or to run a single test: ``` -$ yarn run test-render circle-radius/literal +$ yarn run test-render tests=circle-radius/literal ... * passed circle-radius/literal 1 passed (100.0%) @@ -66,7 +66,7 @@ Done in 2.32s. ### Viewing test results -During a test run, the test harness will use GL-JS to create an `actual.png` image from the given `style.json`, and will then use [pixelmatch](https://github.com/mapbox/pixelmatch) to compare that image to `expected.png`, generating a `diff.png` highlighting the mismatching pixels (if any) in red. +During a test run, the test harness will use GL-JS to create an `actual.png` image from the given `style.json`, and will then use [pixelmatch](https://github.com/mapbox/pixelmatch) to compare that image to `expected.png`, generating a `diff.png` highlighting the mismatched pixels (if any) in red. After the test(s) have run, you can view the results graphically by opening the `index.html` file generated by the harness: @@ -80,18 +80,13 @@ open ./test/integration/query-tests/index.html ## Running tests in the browser -Query tests can be run in the browser, the server for serving up the test page and test fixtures starts when you run +Render and query tests can be run in the browser. The server for serving up the test page and test fixtures starts when you run ``` -yarn run start -``` -OR -``` -yarn run start-debug +yarn run watch-query ``` - -If you want to run only the test server run: +or ``` -yarn run watch-query +yarn run watch-render ``` Then open the following url in the browser of your choice to start running the tests. @@ -107,6 +102,11 @@ A filter can be specified by using the `filter` query param in the url. E.g, add ``` to the end of the url will only run the tests that contain `circle-pitch` in the name. +You can run a specific test by as follows +``` +?filter=circle-radius/antimeridian +``` + ### Build Notifications The terminal window can be very noisy with both the build and the test servers running in the same session. @@ -115,7 +115,6 @@ So the server uses platform notifications to inform when the build has finished. DISABLE_BUILD_NOTIFICATIONS=true ``` - ## Writing new tests _Note: Expected results are always generated with the **js** implementation. This is merely for consistency and does not diff --git a/test/integration/expression-tests/to-number/basic/test.json b/test/integration/expression-tests/to-number/basic/test.json index 1d769c89f0d..ebceeb51f2a 100644 --- a/test/integration/expression-tests/to-number/basic/test.json +++ b/test/integration/expression-tests/to-number/basic/test.json @@ -7,7 +7,9 @@ [{}, {"properties": {"x": "Not a number"}}], [{}, {"properties": {"x": null}}], [{}, {"properties": {"x": [1, 2]}}], - [{}, {"properties": {"x": {"y": 1}}}] + [{}, {"properties": {"x": {"y": 1}}}], + [{}, {"properties": {"x": true}}], + [{}, {"properties": {"x": false}}] ], "expected": { "compiled": { @@ -23,7 +25,9 @@ {"error": "Could not convert \"Not a number\" to number."}, 0, {"error": "Could not convert [1,2] to number."}, - {"error": "Could not convert {\"y\":1} to number."} + {"error": "Could not convert {\"y\":1} to number."}, + 1, + 0 ], "serialized": ["to-number", ["get", "x"]] } diff --git a/test/integration/expression-tests/within/non-supported/test.json b/test/integration/expression-tests/within/non-supported/test.json index 16659a5fc16..5e12eb4b1a4 100644 --- a/test/integration/expression-tests/within/non-supported/test.json +++ b/test/integration/expression-tests/within/non-supported/test.json @@ -13,7 +13,7 @@ }, { "geometry": { "type": "Polygon", - "coordinates": [[[3, 3], [6, 6], [3, 3]]] + "coordinates": [[[3, 3], [4, 3], [4, 4], [3, 4], [3, 3]]] } }]], "expected": { diff --git a/test/integration/lib/generate-fixture-json.js b/test/integration/lib/generate-fixture-json.js index b4bf77377ff..355b013531b 100644 --- a/test/integration/lib/generate-fixture-json.js +++ b/test/integration/lib/generate-fixture-json.js @@ -4,8 +4,6 @@ const fs = require('fs'); const glob = require('glob'); const localizeURLs = require('./localize-urls'); -const OUTPUT_FILE = 'fixtures.json'; - exports.generateFixtureJson = generateFixtureJson; exports.getAllFixtureGlobs = getAllFixtureGlobs; @@ -47,7 +45,7 @@ function generateFixtureJson(rootDirectory, suiteDirectory, outputDirectory = 't allFiles[fixturePath] = json; } else if (extension === '.png') { - allFiles[fixturePath] = pngToBase64Str(fixturePath); + allFiles[fixturePath] = true; } else { throw new Error(`${extension} is incompatible , file path ${fixturePath}`); } @@ -64,7 +62,7 @@ function generateFixtureJson(rootDirectory, suiteDirectory, outputDirectory = 't //Skip if test is malformed if (malformedTests[testName]) { continue; } - //Lazily initaialize an object to store each file wihin a particular testName + //Lazily initialize an object to store each file wihin a particular testName if (result[testName] == null) { result[testName] = {}; } @@ -74,7 +72,8 @@ function generateFixtureJson(rootDirectory, suiteDirectory, outputDirectory = 't } const outputStr = JSON.stringify(result, null, 4); - const outputPath = path.join(outputDirectory, OUTPUT_FILE); + const outputFile = `${suiteDirectory.split('-')[0]}-fixtures.json`; + const outputPath = path.join(outputDirectory, outputFile); return new Promise((resolve, reject) => { fs.writeFile(outputPath, outputStr, {encoding: 'utf8'}, (err) => { @@ -87,7 +86,7 @@ function generateFixtureJson(rootDirectory, suiteDirectory, outputDirectory = 't function getAllFixtureGlobs(rootDirectory, suiteDirectory) { const basePath = path.join(rootDirectory, suiteDirectory); - const jsonPaths = path.join(basePath, '/**/*.json'); + const jsonPaths = path.join(basePath, '/**/[!actual]*.json'); const imagePaths = path.join(basePath, '/**/*.png'); return [jsonPaths, imagePaths]; @@ -97,10 +96,6 @@ function parseJsonFromFile(filePath) { return JSON.parse(fs.readFileSync(filePath, {encoding: 'utf8'})); } -function pngToBase64Str(filePath) { - return fs.readFileSync(filePath).toString('base64'); -} - function processStyle(testName, style) { const clone = JSON.parse(JSON.stringify(style)); // 7357 is testem's default port diff --git a/test/integration/lib/operation-handlers.js b/test/integration/lib/operation-handlers.js index d653c892943..d06130d2125 100644 --- a/test/integration/lib/operation-handlers.js +++ b/test/integration/lib/operation-handlers.js @@ -1,4 +1,9 @@ -function handleOperation(map, operations, opIndex, doneCb) { +/* eslint-env browser */ +/* global mapboxgl:readonly */ +import customLayerImplementations from '../custom_layer_implementations'; + +function handleOperation(map, options, opIndex, doneCb) { + const operations = options.operations; const operation = operations[opIndex]; const opName = operation[0]; //Delegate to special handler if one is available @@ -8,6 +13,11 @@ function handleOperation(map, operations, opIndex, doneCb) { }); } else { map[opName](...operation.slice(1)); + // Render one more frame with forceDrapeFirst + if (options.terrainDrapeFirst && map.painter.terrain) { + map.painter.terrain.forceDrapeFirst = true; + map._render(); + } doneCb(opIndex); } } @@ -15,10 +25,17 @@ function handleOperation(map, operations, opIndex, doneCb) { export const operationHandlers = { wait(map, params, doneCb) { const wait = function() { - if (map.loaded()) { + if (params.length) { + window._renderTestNow += params[0]; + mapboxgl.setNow(window._renderTestNow); + map._render(); doneCb(); } else { - map.once('render', wait); + if (map.loaded()) { + doneCb(); + } else { + map.once('render', wait); + } } }; wait(); @@ -32,12 +49,77 @@ export const operationHandlers = { } }; idle(); + }, + sleep(map, params, doneCb) { + setTimeout(doneCb, params[0]); + }, + addImage(map, params, doneCb) { + const image = new Image(); + image.onload = () => { + map.addImage(params[0], image, params[2] || {}); + doneCb(); + }; + + image.src = params[1].replace('./', ''); + image.onerror = () => { + throw new Error(`addImage opertation failed with src ${image.src}`); + }; + }, + addCustomLayer(map, params, doneCb) { + map.addLayer(new customLayerImplementations[params[0]](), params[1]); + map._render(); + doneCb(); + }, + updateFakeCanvas(map, params, doneCb) { + const updateFakeCanvas = async function() { + const canvasSource = map.getSource(params[0]); + canvasSource.play(); + // update before pause should be rendered + await updateCanvas(params[1]); + canvasSource.pause(); + await updateCanvas(params[2]); + map._render(); + doneCb(); + }; + updateFakeCanvas(); + }, + setStyle(map, params, doneCb) { + // Disable local ideograph generation (enabled by default) for + // consistent local ideograph rendering using fixtures in all runs of the test suite. + map.setStyle(params[0], {localIdeographFontFamily: false}); + doneCb(); + }, + pauseSource(map, params, doneCb) { + for (const sourceCache of map.style._getSourceCaches(params[0])) { + sourceCache.pause(); + } + doneCb(); + }, + setCameraPosition(map, params, doneCb) { + const options = map.getFreeCameraOptions(); + const location = params[0]; // lng, lat, altitude + options.position = mapboxgl.MercatorCoordinate.fromLngLat(new mapboxgl.LngLat(location[0], location[1]), location[2]); + map.setFreeCameraOptions(options); + doneCb(); + }, + lookAtPoint(map, params, doneCb) { + const options = map.getFreeCameraOptions(); + const location = params[0]; + const upVector = params[1]; + options.lookAtPoint(new mapboxgl.LngLat(location[0], location[1]), upVector); + map.setFreeCameraOptions(options); + doneCb(); } }; -export function applyOperations(map, operations, doneCb) { - // No operations specified, end immediately adn invoke doneCb. +export function applyOperations(map, options, doneCb) { + const operations = options.operations; + // No operations specified, end immediately and invoke doneCb. if (!operations || operations.length === 0) { + if (options.terrainDrapeFirst && map.painter.terrain) { + map.painter.terrain.forceDrapeFirst = true; + map._render(); // Render one more time with forceDrapeFirst. + } doneCb(); return; } @@ -50,7 +132,23 @@ export function applyOperations(map, operations, doneCb) { return; } - handleOperation(map, operations, ++lastOpIndex, scheduleNextOperation); + handleOperation(map, options, ++lastOpIndex, scheduleNextOperation); }; scheduleNextOperation(-1); } + +function updateCanvas(imagePath) { + return new Promise((resolve) => { + const canvas = window.document.getElementById('fake-canvas'); + const ctx = canvas.getContext('2d'); + const image = new Image(); + image.src = imagePath.replace('./', ''); + image.onload = () => { + resolve(ctx.drawImage(image, 0, 0, image.width, image.height)); + }; + + image.onerror = () => { + throw new Error(`updateFakeCanvas failed to load image at ${image.src}`); + }; + }); +} diff --git a/test/integration/lib/query-browser.js b/test/integration/lib/query-browser.js deleted file mode 100644 index 4eb275a70af..00000000000 --- a/test/integration/lib/query-browser.js +++ /dev/null @@ -1,86 +0,0 @@ -/* eslint-env browser */ -/* global tape:readonly, mapboxgl:readonly */ -/* eslint-disable import/no-unresolved */ -// fixtures.json is automatically generated before this file gets built -// refer testem.js#before_tests() -import fixtures from '../dist/fixtures.json'; -import ignores from '../../ignores.json'; -import {applyOperations} from './operation-handlers'; -import {deepEqual, generateDiffLog} from './json-diff'; - -for (const testName in fixtures) { - if (testName in ignores) { - tape.skip(testName, testFunc); - } else { - tape(testName, {timeout: 20000}, testFunc); - } -} - -function testFunc(t) { - // This needs to be read from the `t` object because this function runs async in a closure. - const currentTestName = t.name; - const style = fixtures[currentTestName].style; - const expected = fixtures[currentTestName].expected; - const options = style.metadata.test; - const skipLayerDelete = style.metadata.skipLayerDelete; - - window.devicePixelRatio = options.pixelRatio; - - //1. Create and position the container, floating at the bottom right - const container = document.createElement('div'); - container.style.position = 'fixed'; - container.style.bottom = '10px'; - container.style.right = '10px'; - container.style.width = `${options.width}px`; - container.style.height = `${options.height}px`; - document.body.appendChild(container); - - //2. Initialize the Map - const map = new mapboxgl.Map({ - container, - style, - classes: options.classes, - interactive: false, - attributionControl: false, - preserveDrawingBuffer: true, - axonometric: options.axonometric || false, - skew: options.skew || [0, 0], - fadeDuration: options.fadeDuration || 0, - localIdeographFontFamily: options.localIdeographFontFamily || false, - crossSourceCollisions: typeof options.crossSourceCollisions === "undefined" ? true : options.crossSourceCollisions - }); - map.repaint = true; - map.once('load', () => { - //3. Run the operations on the map - applyOperations(map, options.operations, () => { - - //4. Perform query operation and compare results from expected values - const results = options.queryGeometry ? - map.queryRenderedFeatures(options.queryGeometry, options.queryOptions || {}) : - []; - - const actual = results.map((feature) => { - const featureJson = JSON.parse(JSON.stringify(feature.toJSON())); - if (!skipLayerDelete) delete featureJson.layer; - return featureJson; - }); - - const testMetaData = { - name: t.name, - actual: map.getCanvas().toDataURL() - }; - const success = deepEqual(actual, expected); - if (success) { - t.pass(JSON.stringify(testMetaData)); - } else { - testMetaData['difference'] = generateDiffLog(expected, actual); - t.fail(JSON.stringify(testMetaData)); - } - //Cleanup WebGL context - map.remove(); - delete map.painter.context.gl; - document.body.removeChild(container); - t.end(); - }); - }); -} diff --git a/test/integration/lib/query.js b/test/integration/lib/query.js index 5485d39d078..8f71bfa6946 100644 --- a/test/integration/lib/query.js +++ b/test/integration/lib/query.js @@ -1,153 +1,127 @@ -import path from 'path'; -import fs from 'fs'; -import * as diff from 'diff'; -import {PNG} from 'pngjs'; -import harness from './harness'; - -function deepEqual(a, b) { - if (typeof a !== typeof b) - return false; - if (typeof a === 'number') - return Math.abs(a - b) < 1e-10; - if (a === null || typeof a !== 'object') - return a === b; - - const ka = Object.keys(a); - const kb = Object.keys(b); - - if (ka.length !== kb.length) - return false; - - ka.sort(); - kb.sort(); - - for (let i = 0; i < ka.length; i++) - if (ka[i] !== kb[i] || !deepEqual(a[ka[i]], b[ka[i]])) - return false; - - return true; +/* eslint-env browser */ +/* global tape:readonly, mapboxgl:readonly */ +/* eslint-disable import/no-unresolved */ +// query-fixtures.json is automatically generated before this file gets built +// refer testem.js#before_tests() +import fixtures from '../dist/query-fixtures.json'; +import ignores from '../../ignores.json'; +import {applyOperations} from './operation-handlers'; +import {deepEqual, generateDiffLog} from './json-diff'; +import {setupHTML, updateHTML} from '../../util/html_generator.js'; + +window._suiteName = 'query-tests'; +setupHTML(); + +const browserWriteFile = new Worker('../util/browser_write_file.js'); + +for (const testName in fixtures) { + const options = {timeout: 20000}; + if (testName in ignores) { + const ignoreType = ignores[testName]; + if (/^skip/.test(ignoreType)) { + options.skip = true; + } else { + options.todo = true; + } + } + + tape(testName, options, testFunc); } -/** - * Run the query suite. - * - * @param {string} implementation - identify the implementation under test; used to - * deal with implementation-specific test exclusions and fudge-factors - * @param {Object} options - * @param {Array} [options.tests] - array of test names to run; tests not in the - * array will be skipped - * @param {queryFn} query - a function that performs the query - * @returns {undefined} terminates the process when testing is complete - */ -export function run(implementation, options, query) { - const directory = path.join(__dirname, '../query-tests'); - harness(directory, implementation, options, (style, params, done) => { - query(style, params, (err, data, results) => { - if (err) return done(err); - - const dir = path.join(directory, params.id); +function testFunc(t) { + // This needs to be read from the `t` object because this function runs async in a closure. + const currentTestName = t.name; + const writeFileBasePath = `test/integration/${currentTestName}`; + const style = fixtures[currentTestName].style; + const expected = fixtures[currentTestName].expected || ''; + const options = style.metadata.test; + const skipLayerDelete = style.metadata.skipLayerDelete; + + window.devicePixelRatio = options.pixelRatio; + + //1. Create and position the container, floating at the bottom right + const container = document.createElement('div'); + container.style.position = 'fixed'; + container.style.bottom = '10px'; + container.style.right = '10px'; + container.style.width = `${options.width}px`; + container.style.height = `${options.height}px`; + document.body.appendChild(container); + + //2. Initialize the Map + const map = new mapboxgl.Map({ + container, + style, + classes: options.classes, + interactive: false, + attributionControl: false, + preserveDrawingBuffer: true, + axonometric: options.axonometric || false, + skew: options.skew || [0, 0], + fadeDuration: options.fadeDuration || 0, + localIdeographFontFamily: options.localIdeographFontFamily || false, + crossSourceCollisions: typeof options.crossSourceCollisions === "undefined" ? true : options.crossSourceCollisions + }); + map.repaint = true; + map.once('load', () => { + //3. Run the operations on the map + applyOperations(map, options, () => { + + //4. Perform query operation and compare results from expected values + const results = options.queryGeometry ? + map.queryRenderedFeatures(options.queryGeometry, options.queryOptions || {}) : + []; + + const actual = results.map((feature) => { + const featureJson = JSON.parse(JSON.stringify(feature.toJSON())); + if (!skipLayerDelete) delete featureJson.layer; + return featureJson; + }); - if (process.env.UPDATE) { - fs.writeFile(path.join(dir, 'expected.json'), JSON.stringify(results, null, 2), done); - return; - } + const testMetaData = { + name: t.name, + actual: map.getCanvas().toDataURL() + }; + const success = deepEqual(actual, expected); + const jsonDiff = generateDiffLog(expected, actual); - const expected = require(path.join(dir, 'expected.json')); - - params.ok = deepEqual(results, expected); - - if (!params.ok) { - const msg = diff.diffJson(expected, results) - .map((hunk) => { - if (hunk.added) { - return `+ ${hunk.value}`; - } else if (hunk.removed) { - return `- ${hunk.value}`; - } else { - return ` ${hunk.value}`; - } - }) - .join(''); - - params.difference = msg; - console.log(msg); + if (!success) { + testMetaData['jsonDiff'] = jsonDiff; } + t.ok(success || t._todo, t.name); + testMetaData.status = t._todo ? 'todo' : success ? 'passed' : 'failed'; - const width = params.width * params.pixelRatio; - const height = params.height * params.pixelRatio; - - const color = [255, 0, 0, 255]; + updateHTML(testMetaData); - function scaleByPixelRatio(x) { - return x * params.pixelRatio; - } + let fileInfo; - if (!Array.isArray(params.queryGeometry[0])) { - const p = params.queryGeometry.map(scaleByPixelRatio); - const d = 30; - drawAxisAlignedLine([p[0] - d, p[1]], [p[0] + d, p[1]], data, width, height, color); - drawAxisAlignedLine([p[0], p[1] - d], [p[0], p[1] + d], data, width, height, color); + if (process.env.UPDATE) { + fileInfo = [ + { + path: `${writeFileBasePath}/expected.json`, + data: jsonDiff.replace('+', '').trim() + } + ]; } else { - const a = params.queryGeometry[0].map(scaleByPixelRatio); - const b = params.queryGeometry[1].map(scaleByPixelRatio); - drawAxisAlignedLine([a[0], a[1]], [a[0], b[1]], data, width, height, color); - drawAxisAlignedLine([a[0], b[1]], [b[0], b[1]], data, width, height, color); - drawAxisAlignedLine([b[0], b[1]], [b[0], a[1]], data, width, height, color); - drawAxisAlignedLine([b[0], a[1]], [a[0], a[1]], data, width, height, color); + fileInfo = [ + { + path: `${writeFileBasePath}/actual.png`, + data: testMetaData.actual.split(',')[1] + }, + { + path: `${writeFileBasePath}/actual.json`, + data: jsonDiff.trim() + } + ]; } - const actualJSON = path.join(dir, 'actual.json'); - fs.writeFile(actualJSON, JSON.stringify(results, null, 2), () => {}); - - const actualPNG = path.join(dir, 'actual.png'); + browserWriteFile.postMessage(fileInfo); - const png = new PNG({ - width: params.width * params.pixelRatio, - height: params.height * params.pixelRatio - }); - - png.data = data; - - png.pack() - .pipe(fs.createWriteStream(actualPNG)) - .on('finish', () => { - params.actual = fs.readFileSync(actualPNG).toString('base64'); - done(); - }); + //Cleanup WebGL context + map.remove(); + delete map.painter.context.gl; + document.body.removeChild(container); + t.end(); }); }); } - -function drawAxisAlignedLine(a, b, pixels, width, height, color) { - const fromX = clamp(Math.min(a[0], b[0]), 0, width); - const toX = clamp(Math.max(a[0], b[0]), 0, width); - const fromY = clamp(Math.min(a[1], b[1]), 0, height); - const toY = clamp(Math.max(a[1], b[1]), 0, height); - - let index; - if (fromX === toX) { - for (let y = fromY; y <= toY; y++) { - index = getIndex(fromX, y); - pixels[index + 0] = color[0]; - pixels[index + 1] = color[1]; - pixels[index + 2] = color[2]; - pixels[index + 3] = color[3]; - } - } else { - for (let x = fromX; x <= toX; x++) { - index = getIndex(x, fromY); - pixels[index + 0] = color[0]; - pixels[index + 1] = color[1]; - pixels[index + 2] = color[2]; - pixels[index + 3] = color[3]; - } - } - - function getIndex(x, y) { - return (y * width + x) * 4; - } -} - -function clamp(x, a, b) { - return Math.max(a, Math.min(b, x)); -} diff --git a/test/integration/lib/render.js b/test/integration/lib/render.js index e2846a47c0b..8dd039893fb 100644 --- a/test/integration/lib/render.js +++ b/test/integration/lib/render.js @@ -1,183 +1,381 @@ -import path from 'path'; -import fs from 'fs'; -import {PNG} from 'pngjs'; -import harness from './harness'; +/* eslint-env browser */ +/* global tape:readonly, mapboxgl:readonly */ +/* eslint-disable import/no-unresolved */ +// render-fixtures.json is automatically generated before this file gets built +// refer testem.js#before_tests() +import fixtures from '../dist/render-fixtures.json'; +import ignores from '../../ignores.json'; +import config from '../../../src/util/config'; +import {clamp} from '../../../src/util/util'; +import {mercatorZfromAltitude} from '../../../src/geo/mercator_coordinate'; +import {setupHTML, updateHTML} from '../../util/html_generator.js'; +import {applyOperations} from './operation-handlers'; import pixelmatch from 'pixelmatch'; -import * as glob from 'glob'; - -/** - * Run the render test suite, compute differences to expected values (making exceptions based on - * implementation vagaries), print results to standard output, write test artifacts to the - * filesystem (optionally updating expected results), and exit the process with a success or - * failure code. - * - * Caller must supply a `render` function that does the actual rendering and passes the raw image - * result on to the `render` function's callback. - * - * A local server is launched that is capable of serving requests for the source, sprite, - * font, and tile assets needed by the tests, and the URLs within the test styles are - * rewritten to point to that server. - * - * As the tests run, results are printed to standard output, and test artifacts are written - * to the filesystem. If the environment variable `UPDATE` is set, the expected artifacts are - * updated in place based on the test rendering. - * - * If all the tests are successful, this function exits the process with exit code 0. Otherwise - * it exits with 1. If an unexpected error occurs, it exits with -1. - * - * @param {string} implementation - identify the implementation under test; used to - * deal with implementation-specific test exclusions and fudge-factors - * @param {Object} [ignores] - map of test names to disable. A key is the relative - * path to a test directory, e.g. `"render-tests/background-color/default"`. A value is a string - * that by convention links to an issue that explains why the test is currently disabled. By default, - * disabled tests will be run, but not fail the test run if the result does not match the expected - * result. If the value begins with "skip", the test will not be run at all -- use this for tests - * that would crash the test harness entirely if they were run. - * @param {renderFn} render - a function that performs the rendering - * @returns {undefined} terminates the process when testing is complete - */ -export function run(implementation, ignores, render) { - const options = {ignores, tests:[], shuffle:false, recycleMap:false, seed:makeHash()}; - - // https://stackoverflow.com/a/1349426/229714 - function makeHash() { - const array = []; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (let i = 0; i < 10; ++i) - array.push(possible.charAt(Math.floor(Math.random() * possible.length))); - - // join array elements without commas. - return array.join(''); - } +import {vec3, vec4} from 'gl-matrix'; + +const browserWriteFile = new Worker('../util/browser_write_file.js'); + +// We are self-hosting test files. +config.REQUIRE_ACCESS_TOKEN = false; +window._suiteName = 'render-tests'; + +mapboxgl.prewarm(); +mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.3/mapbox-gl-rtl-text.js'); + +//1. Create and position the container, floating at the bottom right +const container = document.createElement('div'); +container.style.position = 'fixed'; +container.style.bottom = '10px'; +container.style.right = '10px'; +document.body.appendChild(container); + +setupHTML(); + +const {canvas: expectedCanvas, ctx: expectedCtx} = createCanvas(); +const {canvas: diffCanvas, ctx: diffCtx} = createCanvas(); - function checkParameter(param) { - const index = options.tests.indexOf(param); - if (index === -1) - return false; - options.tests.splice(index, 1); - return true; +tape.onFinish(() => { + document.body.removeChild(container); + mapboxgl.clearPrewarmedResources(); +}); + +for (const testName in fixtures) { + const options = {timeout: 20000}; + if (testName in ignores) { + const ignoreType = ignores[testName]; + if (/^skip/.test(ignoreType)) { + options.skip = true; + } else { + options.todo = true; + } } - function checkValueParameter(defaultValue, param) { - const index = options.tests.findIndex((elem) => { return String(elem).startsWith(param); }); - if (index === -1) - return defaultValue; + tape(testName, options, testFunc); +} - const split = String(options.tests.splice(index, 1)).split('='); - if (split.length !== 2) - return defaultValue; +async function testFunc(t) { + // This needs to be read from the `t` object because this function runs async in a closure. + const currentTestName = t.name; + const writeFileBasePath = `test/integration/${currentTestName}`; + const currentFixture = fixtures[currentTestName]; + const style = currentFixture.style; + const options = style.metadata.test; - return split[1]; + // there may be multiple expected images, covering different platforms + const expectedPaths = []; + for (const prop in currentFixture) { + if (prop.indexOf('expected') > -1) { + let path = `${currentTestName}/${prop}.png`; + // regression tests with # in the name need to be sanitized + path = encodeURIComponent(path); + expectedPaths.push(path); + } } - if (process.argv.length > 2) { - options.tests = process.argv.slice(2).filter((value, index, self) => { return self.indexOf(value) === index; }) || []; - options.shuffle = checkParameter('--shuffle'); - options.recycleMap = checkParameter('--recycle-map'); - options.seed = checkValueParameter(options.seed, '--seed'); + const expectedImagePromises = Promise.all(expectedPaths.map((path) => drawImage(expectedCanvas, expectedCtx, path))); + + window.devicePixelRatio = options.pixelRatio; + + if (options.addFakeCanvas) { + const {canvas, ctx} = createCanvas(options.addFakeCanvas.id); + const src = options.addFakeCanvas.image.replace('./', ''); + await drawImage(canvas, ctx, src, false); + window.document.body.appendChild(canvas); } - const directory = path.join(__dirname, '../render-tests'); - harness(directory, implementation, options, (style, params, done) => { - render(style, params, (err, data) => { - if (err) return done(err); - - let stats; - const dir = path.join(directory, params.id); - try { - stats = fs.statSync(dir, fs.R_OK | fs.W_OK); - if (!stats.isDirectory()) throw new Error(); - } catch (e) { - fs.mkdirSync(dir); + container.style.width = `${options.width}px`; + container.style.height = `${options.height}px`; + + //2. Initialize the Map + const map = new mapboxgl.Map({ + container, + style, + classes: options.classes, + interactive: false, + attributionControl: false, + preserveDrawingBuffer: true, + axonometric: options.axonometric || false, + skew: options.skew || [0, 0], + fadeDuration: options.fadeDuration || 0, + localIdeographFontFamily: options.localIdeographFontFamily || false, + crossSourceCollisions: typeof options.crossSourceCollisions === "undefined" ? true : options.crossSourceCollisions, + transformRequest: (url, resourceType) => { + // some tests have the port hardcoded to 2900 + // this makes that backwards compatible + if (resourceType === 'Tile') { + const transformedUrl = new URL(url); + transformedUrl.port = '7357'; + return { + url: transformedUrl.toString() + }; + } + } + }); + map.repaint = true; + + // override internal timing to enable precise wait operations + window._renderTestNow = 0; + mapboxgl.setNow(window._renderTestNow); + + if (options.debug) map.showTileBoundaries = true; + if (options.showOverdrawInspector) map.showOverdrawInspector = true; + if (options.showPadding) map.showPadding = true; + if (options.collisionDebug) map.showCollisionBoxes = true; + if (options.fadeDuration) map._isInitialLoad = false; + + // Disable anisotropic filtering on render tests + map.painter.context.extTextureFilterAnisotropicForceOff = true; + + const gl = map.painter.context.gl; + map.once('load', async () => { + //3. Run the operations on the map + applyOperations(map, options, async () => { + map.repaint = false; + const viewport = gl.getParameter(gl.VIEWPORT); + const w = viewport[2]; + const h = viewport[3]; + let actualImageData; + + // 1. get pixel data from test canvas as Uint8Array + if (options.output === "terrainDepth") { + const pixels = drawTerrainDepth(map, w, h); + actualImageData = Uint8Array.from(pixels); } - const expectedPath = path.join(dir, 'expected.png'); - const actualPath = path.join(dir, 'actual.png'); - const diffPath = path.join(dir, 'diff.png'); - - const width = Math.floor(params.width * params.pixelRatio); - const height = Math.floor(params.height * params.pixelRatio); - const actualImg = new PNG({width, height}); - - // PNG data must be unassociated (not premultiplied) - for (let i = 0; i < data.length; i++) { - const a = data[i * 4 + 3] / 255; - if (a !== 0) { - data[i * 4 + 0] /= a; - data[i * 4 + 1] /= a; - data[i * 4 + 2] /= a; + if (!actualImageData) { + actualImageData = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4); + gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, actualImageData); + + // readPixels premultiplies the alpha channel so we need to + // undo that for comparison with the expected image pixels + for (let i = 0; i < actualImageData.length; i += 4) { + const alpha = actualImageData[i + 3] / 255; + actualImageData[i + 0] /= alpha; + actualImageData[i + 1] /= alpha; + actualImageData[i + 2] /= alpha; + } + + // readPixels starts at the bottom of the canvas + // so we need to flip the image data + const stride = w * 4; + const temp = new Uint8Array(w * 4); + for (let i = 0; i < (h / 2 | 0); ++i) { + const topOffset = i * stride; + const bottomOffset = (h - i - 1) * stride; + temp.set(actualImageData.subarray(topOffset, topOffset + stride)); + actualImageData.copyWithin(topOffset, bottomOffset, bottomOffset + stride); + actualImageData.set(temp, bottomOffset); } } - actualImg.data = data; - // there may be multiple expected images, covering different platforms - const expectedPaths = glob.sync(path.join(dir, 'expected*.png')); + // if we have multiple expected images, we'll compare against each one and pick the one with + // the least amount of difference; this is useful for covering features that render differently + // depending on platform, i.e. heatmaps use half-float textures for improved rendering where supported + const expectedImages = await expectedImagePromises; - if (!process.env.UPDATE && expectedPaths.length === 0) { - throw new Error('No expected*.png files found; did you mean to run tests with UPDATE=true?'); + if (!process.env.UPDATE && expectedImages.length === 0) { + console.warn('No expected*.png files found; did you mean to run tests with UPDATE=true?'); + t.end(); } - if (process.env.UPDATE) { - fs.writeFileSync(expectedPath, PNG.sync.write(actualImg)); + let fileInfo; + const actual = map.getCanvas().toDataURL(); + if (process.env.UPDATE) { + fileInfo = [ + { + path: `${writeFileBasePath}/expected.png`, + data: actual.split(',')[1] + } + ]; } else { - // if we have multiple expected images, we'll compare against each one and pick the one with - // the least amount of difference; this is useful for covering features that render differently - // depending on platform, i.e. heatmaps use half-float textures for improved rendering where supported + // 2. draw expected.png into a canvas and extract ImageData + let minDiffImage; + let minExpectedCanvas; let minDiff = Infinity; - let minDiffImg, minExpectedBuf; - for (const path of expectedPaths) { - const expectedBuf = fs.readFileSync(path); - const expectedImg = PNG.sync.read(expectedBuf); - const diffImg = new PNG({width, height}); + for (let i = 0; i < expectedImages.length; i++) { + // 3. set up Uint8ClampedArray to write diff into + const diffImage = new Uint8ClampedArray(w * h * 4); - const diff = pixelmatch( - actualImg.data, expectedImg.data, diffImg.data, - width, height, {threshold: 0.1285}) / (width * height); + // 4. Use pixelmatch to compare actual and expected images and write diff + // all inputs must be Uint8Array or Uint8ClampedArray + const currentDiff = pixelmatch(actualImageData, expectedImages[i].data, diffImage, w, h, {threshold: 0.1285}) / (w * h); - if (diff < minDiff) { - minDiff = diff; - minDiffImg = diffImg; - minExpectedBuf = expectedBuf; + if (currentDiff < minDiff) { + minDiff = currentDiff; + minDiffImage = diffImage; + minExpectedCanvas = expectedCanvas; } } - const diffBuf = PNG.sync.write(minDiffImg, {filterType: 4}); - const actualBuf = PNG.sync.write(actualImg, {filterType: 4}); + // 5. Convert diff Uint8Array to ImageData and write to canvas + // so we can get a base64 string to display the diff in the browser + diffCanvas.width = w; + diffCanvas.height = h; + const diffImageData = new ImageData(minDiffImage, w, h); + diffCtx.putImageData(diffImageData, 0, 0); - fs.writeFileSync(diffPath, diffBuf); - fs.writeFileSync(actualPath, actualBuf); + const expected = minExpectedCanvas.toDataURL(); + const imgDiff = diffCanvas.toDataURL(); - params.difference = minDiff; - params.ok = minDiff <= params.allowed; + // 6. use browserWriteFile to write actual and diff to disk (convert image back to base64) + fileInfo = [ + { + path: `${writeFileBasePath}/actual.png`, + data: actual.split(',')[1] + }, + { + path: `${writeFileBasePath}/diff.png`, + data: imgDiff.split(',')[1] + } + ]; + + // 7. pass image paths to testMetaData so the UI can load them from disk + const testMetaData = { + name: currentTestName, + actual, + expected, + imgDiff + }; - params.actual = actualBuf.toString('base64'); - params.expected = minExpectedBuf.toString('base64'); - params.diff = diffBuf.toString('base64'); + const pass = minDiff <= options.allowed; + t.ok(pass || t._todo, t.name); + testMetaData.status = t._todo ? 'todo' : pass ? 'passed' : 'failed'; + updateHTML(testMetaData); } - done(); + browserWriteFile.postMessage(fileInfo); + + //Cleanup WebGL context + map.remove(); + delete map.painter.context.gl; + expectedCtx.clearRect(0, 0, expectedCanvas.width, expectedCanvas.height); + diffCtx.clearRect(0, 0, diffCanvas.width, diffCanvas.height); + + if (options.addFakeCanvas) { + const canvas = window.document.getElementById(options.addFakeCanvas.id); + canvas.parentNode.removeChild(canvas); + } + + mapboxgl.restoreNow(); + t.end(); }); }); } -/** - * @callback renderFn - * @param {Object} style - style to render - * @param {Object} options - * @param {number} options.width - render this wide - * @param {number} options.height - render this high - * @param {number} options.pixelRatio - render with this pixel ratio - * @param {boolean} options.shuffle - shuffle tests sequence - * @param {String} options.seed - Shuffle seed - * @param {boolean} options.recycleMap - trigger map object recycling - * @param {renderCallback} callback - callback to call with the results of rendering - */ - -/** - * @callback renderCallback - * @param {?Error} error - * @param {Buffer} [result] - raw RGBA image data - */ +function drawImage(canvas, ctx, src, getImageData = true) { + return new Promise((resolve, reject) => { + const image = new Image(); + image.onload = () => { + canvas.height = image.height; + canvas.width = image.width; + if (!getImageData) { + resolve(ctx.drawImage(image, 0, 0)); + } + ctx.drawImage(image, 0, 0); + const result = ctx.getImageData(0, 0, image.width, image.height); + result.src = src; + resolve(result); + }; + image.onerror = reject; + image.src = src; + }); +} + +function createCanvas(id = 'fake-canvas') { + const canvas = window.document.createElement('canvas'); + canvas.id = id; + const ctx = canvas.getContext('2d'); + return {canvas, ctx}; +} + +function drawTerrainDepth(map, width, height) { + if (!map.painter.terrain) + return undefined; + + const terrain = map.painter.terrain; + const tr = map.transform; + const ws = tr.worldSize; + + // Compute frustum corner points in web mercator [0, 1] space where altitude is in meters + const clipSpaceCorners = [ + [-1, 1, -1, 1], + [ 1, 1, -1, 1], + [ 1, -1, -1, 1], + [-1, -1, -1, 1], + [-1, 1, 1, 1], + [ 1, 1, 1, 1], + [ 1, -1, 1, 1], + [-1, -1, 1, 1] + ]; + + const frustumCoords = clipSpaceCorners + .map(v => { + const s = vec4.transformMat4([], v, tr.invProjMatrix); + const k = 1.0 / s[3] / ws; + // Z scale in meters. + return vec4.mul(s, s, [k, k, 1.0 / s[3], k]); + }); + + const nearTL = frustumCoords[0]; + const nearTR = frustumCoords[1]; + const nearBL = frustumCoords[3]; + const farTL = frustumCoords[4]; + const farTR = frustumCoords[5]; + const farBL = frustumCoords[7]; + + // Compute basis vectors X & Y of near and far planes in transformed space. + // These vectors are then interpolated to find corresponding world rays for each screen pixel. + const nearRight = vec3.sub([], nearTR, nearTL); + const nearDown = vec3.sub([], nearBL, nearTL); + const farRight = vec3.sub([], farTR, farTL); + const farDown = vec3.sub([], farBL, farTL); + + const distances = []; + const data = []; + const metersToPixels = mercatorZfromAltitude(1.0, tr.center.lat); + let minDistance = Number.MAX_VALUE; + let maxDistance = 0; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + // Use uv-coordinates of the screen pixel to find positions on near and far planes + const u = (x + 0.5) / width; + const v = (y + 0.5) / height; + + const startPoint = vec3.add([], nearTL, vec3.add([], vec3.scale([], nearRight, u), vec3.scale([], nearDown, v))); + const endPoint = vec3.add([], farTL, vec3.add([], vec3.scale([], farRight, u), vec3.scale([], farDown, v))); + const dir = vec3.normalize([], vec3.sub([], endPoint, startPoint)); + const t = terrain.raycast(startPoint, dir, terrain.exaggeration()); + + if (t !== null) { + // The ray hit the terrain. Compute distance in world space and store to an intermediate array + const point = vec3.scaleAndAdd([], startPoint, dir, t); + const startToPoint = vec3.sub([], point, startPoint); + startToPoint[2] *= metersToPixels; + + const distance = vec3.length(startToPoint) * ws; + distances.push(distance); + minDistance = Math.min(distance, minDistance); + maxDistance = Math.max(distance, maxDistance); + } else { + distances.push(null); + } + } + } + + // Convert distance data to pixels; + for (let i = 0; i < width * height; i++) { + if (distances[i] === null) { + // Bright white pixel for non-intersections + data.push(255, 255, 255, 255); + } else { + let value = (distances[i] - minDistance) / (maxDistance - minDistance); + value = Math.floor((clamp(value, 0.0, 1.0)) * 255); + data.push(value, value, value, 255); + } + } + + return data; +} diff --git a/test/integration/lib/server.js b/test/integration/lib/server.js index b20eea261fc..a16adc4d2c7 100644 --- a/test/integration/lib/server.js +++ b/test/integration/lib/server.js @@ -20,7 +20,12 @@ module.exports = function () { //Write data to disk const {filePath, data} = JSON.parse(body); - fs.writeFile(path.join(process.cwd(), filePath), data, 'base64', () => { + + let encoding; + if (filePath.split('.')[1] !== 'json') { + encoding = 'base64'; + } + fs.writeFile(path.join(process.cwd(), filePath), data, encoding, () => { res.writeHead(200, {'Content-Type': 'text/html'}); res.end('ok'); }); diff --git a/test/integration/query-tests/fill-extrusion/above-horizon-fail/expected.json b/test/integration/query-tests/fill-extrusion/above-horizon-fail/expected.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/test/integration/query-tests/fill-extrusion/above-horizon-fail/expected.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/test/integration/query-tests/fill-extrusion/above-horizon-fail/style.json b/test/integration/query-tests/fill-extrusion/above-horizon-fail/style.json new file mode 100644 index 00000000000..e5a9b7da79b --- /dev/null +++ b/test/integration/query-tests/fill-extrusion/above-horizon-fail/style.json @@ -0,0 +1,141 @@ +{ + "version": 8, + "metadata": { + "test": { + "debug": true, + "width": 200, + "height": 400, + "queryGeometry": [ + 100, + 50 + ] + } + }, + "pitch": 85, + "center": [ + -79.46462631225589, + 43.64750394449096 + ], + "zoom": 13, + "sources": { + "zones": { + "type": "geojson", + "maxzoom": 13, + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "position": "middle" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -79.46462631225589, + 43.64750394449096 + ], + [ + -79.45793151855469, + 43.64750394449096 + ], + [ + -79.45793151855469, + 43.6545837335548 + ], + [ + -79.46462631225589, + 43.6545837335548 + ], + [ + -79.46462631225589, + 43.64750394449096 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "position": "bottom" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -79.46205139160156, + 43.635329724674484 + ], + [ + -79.45569992065428, + 43.635329724674484 + ], + [ + -79.45569992065428, + 43.642535173141056 + ], + [ + -79.46205139160156, + 43.642535173141056 + ], + [ + -79.46205139160156, + 43.635329724674484 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "position": "top" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -79.46788787841795, + 43.65557732137429 + ], + [ + -79.45700806884764, + 43.65557732137429 + ], + [ + -79.45700806884764, + 43.66290452383666 + ], + [ + -79.46788787841795, + 43.66290452383666 + ], + [ + -79.46788787841795, + 43.65557732137429 + ] + ] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "zones", + "type": "fill-extrusion", + "source": "zones", + "paint": { + "fill-extrusion-color": "#ccc", + "fill-extrusion-height": 1000 + } + } + ] +} diff --git a/test/integration/query-tests/fill-extrusion/above-horizon/expected.json b/test/integration/query-tests/fill-extrusion/above-horizon/expected.json new file mode 100644 index 00000000000..81de095678f --- /dev/null +++ b/test/integration/query-tests/fill-extrusion/above-horizon/expected.json @@ -0,0 +1,107 @@ +[ + { + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -79.46205139160156, + 43.63532972467448 + ], + [ + -79.46205139160156, + 43.642535173141056 + ], + [ + -79.4556999206543, + 43.642535173141056 + ], + [ + -79.4556999206543, + 43.63532972467448 + ], + [ + -79.46205139160156, + 43.63532972467448 + ] + ] + ] + }, + "type": "Feature", + "properties": { + "position": "bottom" + }, + "source": "zones", + "state": {} + }, + { + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -79.46462631225586, + 43.64750394449095 + ], + [ + -79.46462631225586, + 43.65458373355483 + ], + [ + -79.45793151855469, + 43.65458373355483 + ], + [ + -79.45793151855469, + 43.64750394449095 + ], + [ + -79.46462631225586, + 43.64750394449095 + ] + ] + ] + }, + "type": "Feature", + "properties": { + "position": "middle" + }, + "source": "zones", + "state": {} + }, + { + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -79.46788787841797, + 43.655577321374295 + ], + [ + -79.46788787841797, + 43.66290452383666 + ], + [ + -79.45700883865356, + 43.66290452383666 + ], + [ + -79.45700883865356, + 43.655577321374295 + ], + [ + -79.46788787841797, + 43.655577321374295 + ] + ] + ] + }, + "type": "Feature", + "properties": { + "position": "top" + }, + "source": "zones", + "state": {} + } +] \ No newline at end of file diff --git a/test/integration/query-tests/fill-extrusion/above-horizon/style.json b/test/integration/query-tests/fill-extrusion/above-horizon/style.json new file mode 100644 index 00000000000..2d788f428ea --- /dev/null +++ b/test/integration/query-tests/fill-extrusion/above-horizon/style.json @@ -0,0 +1,141 @@ +{ + "version": 8, + "metadata": { + "test": { + "debug": true, + "width": 200, + "height": 400, + "queryGeometry": [ + 156, + 131 + ] + } + }, + "pitch": 85, + "center": [ + -79.46462631225589, + 43.64750394449096 + ], + "zoom": 13, + "sources": { + "zones": { + "type": "geojson", + "maxzoom": 13, + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "position": "middle" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -79.46462631225589, + 43.64750394449096 + ], + [ + -79.45793151855469, + 43.64750394449096 + ], + [ + -79.45793151855469, + 43.6545837335548 + ], + [ + -79.46462631225589, + 43.6545837335548 + ], + [ + -79.46462631225589, + 43.64750394449096 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "position": "bottom" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -79.46205139160156, + 43.635329724674484 + ], + [ + -79.45569992065428, + 43.635329724674484 + ], + [ + -79.45569992065428, + 43.642535173141056 + ], + [ + -79.46205139160156, + 43.642535173141056 + ], + [ + -79.46205139160156, + 43.635329724674484 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "position": "top" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -79.46788787841795, + 43.65557732137429 + ], + [ + -79.45700806884764, + 43.65557732137429 + ], + [ + -79.45700806884764, + 43.66290452383666 + ], + [ + -79.46788787841795, + 43.66290452383666 + ], + [ + -79.46788787841795, + 43.65557732137429 + ] + ] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "zones", + "type": "fill-extrusion", + "source": "zones", + "paint": { + "fill-extrusion-color": "#ccc", + "fill-extrusion-height": 1000 + } + } + ] +} diff --git a/test/integration/query-tests/invisible-features/zero-opacity/expected.json b/test/integration/query-tests/invisible-features/zero-opacity/expected.json index 5c19c19f907..d933938da7b 100644 --- a/test/integration/query-tests/invisible-features/zero-opacity/expected.json +++ b/test/integration/query-tests/invisible-features/zero-opacity/expected.json @@ -1,7 +1,6 @@ [ { "geometry": { - "type": "Polygon", "coordinates": [ [ [ @@ -25,11 +24,111 @@ -0.99970513084196 ] ] - ] + ], + "type": "Polygon" }, - "type": "Feature", "properties": {}, "source": "geojson", - "state": {} + "state": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + -0.999755859375, + -0.99970513084196 + ], + [ + -0.999755859375, + 0.9997051308419742 + ], + [ + 0.999755859375, + 0.9997051308419742 + ], + [ + 0.999755859375, + -0.99970513084196 + ], + [ + -0.999755859375, + -0.99970513084196 + ] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "source": "geojson", + "state": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + -0.999755859375, + -0.99970513084196 + ], + [ + -0.999755859375, + 0.9997051308419742 + ], + [ + 0.999755859375, + 0.9997051308419742 + ], + [ + 0.999755859375, + -0.99970513084196 + ], + [ + -0.999755859375, + -0.99970513084196 + ] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "source": "geojson", + "state": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + -0.999755859375, + -0.99970513084196 + ], + [ + -0.999755859375, + 0.9997051308419742 + ], + [ + 0.999755859375, + 0.9997051308419742 + ], + [ + 0.999755859375, + -0.99970513084196 + ], + [ + -0.999755859375, + -0.99970513084196 + ] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "source": "geojson", + "state": {}, + "type": "Feature" } -] \ No newline at end of file + ] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/circle/map-aligned-miss/occluded/expected.json b/test/integration/query-tests/terrain/circle/map-aligned-miss/occluded/expected.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/test/integration/query-tests/terrain/circle/map-aligned-miss/occluded/expected.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/circle/map-aligned-miss/occluded/style.json b/test/integration/query-tests/terrain/circle/map-aligned-miss/occluded/style.json new file mode 100644 index 00000000000..a06d7eccb01 --- /dev/null +++ b/test/integration/query-tests/terrain/circle/map-aligned-miss/occluded/style.json @@ -0,0 +1,77 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200, + "queryGeometry": [42, 42] + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 60, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "Polygon", + "coordinates": [ + [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33341462261518, + 35.9294218694216 + ], + [ + -113.3220882006336, + 35.9418831745696 + ] + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": "#ff0000", + "circle-pitch-alignment": "map" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/query-tests/terrain/circle/map-aligned/occluded/expected.json b/test/integration/query-tests/terrain/circle/map-aligned/occluded/expected.json new file mode 100644 index 00000000000..62456936358 --- /dev/null +++ b/test/integration/query-tests/terrain/circle/map-aligned/occluded/expected.json @@ -0,0 +1,27 @@ +[ + { + "geometry": { + "coordinates": [ + [ + [ + -113.3269464969635, + 35.933558016730615 + ], + [ + -113.33341598510742, + 35.929422840516594 + ], + [ + -113.3269464969635, + 35.933558016730615 + ] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "source": "geojson", + "state": {}, + "type": "Feature" + } +] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/circle/map-aligned/occluded/style.json b/test/integration/query-tests/terrain/circle/map-aligned/occluded/style.json new file mode 100644 index 00000000000..146bea67f08 --- /dev/null +++ b/test/integration/query-tests/terrain/circle/map-aligned/occluded/style.json @@ -0,0 +1,77 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200, + "queryGeometry": [39, 50] + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 60, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "Polygon", + "coordinates": [ + [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33341462261518, + 35.9294218694216 + ], + [ + -113.3220882006336, + 35.9418831745696 + ] + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": "#ff0000", + "circle-pitch-alignment": "map" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/query-tests/terrain/circle/map-aligned/unoccluded/expected.json b/test/integration/query-tests/terrain/circle/map-aligned/unoccluded/expected.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/test/integration/query-tests/terrain/circle/map-aligned/unoccluded/expected.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/circle/map-aligned/unoccluded/style.json b/test/integration/query-tests/terrain/circle/map-aligned/unoccluded/style.json new file mode 100644 index 00000000000..24ec8439fe3 --- /dev/null +++ b/test/integration/query-tests/terrain/circle/map-aligned/unoccluded/style.json @@ -0,0 +1,77 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200, + "queryGeometry": [150, 5] + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 0, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "Polygon", + "coordinates": [ + [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33341462261518, + 35.9294218694216 + ], + [ + -113.3220882006336, + 35.9418831745696 + ] + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": "#ff0000", + "circle-pitch-alignment": "map" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/query-tests/terrain/circle/viewport-aligned/hit/expected.json b/test/integration/query-tests/terrain/circle/viewport-aligned/hit/expected.json new file mode 100644 index 00000000000..62456936358 --- /dev/null +++ b/test/integration/query-tests/terrain/circle/viewport-aligned/hit/expected.json @@ -0,0 +1,27 @@ +[ + { + "geometry": { + "coordinates": [ + [ + [ + -113.3269464969635, + 35.933558016730615 + ], + [ + -113.33341598510742, + 35.929422840516594 + ], + [ + -113.3269464969635, + 35.933558016730615 + ] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "source": "geojson", + "state": {}, + "type": "Feature" + } +] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/circle/viewport-aligned/hit/style.json b/test/integration/query-tests/terrain/circle/viewport-aligned/hit/style.json new file mode 100644 index 00000000000..5cad40e7ddf --- /dev/null +++ b/test/integration/query-tests/terrain/circle/viewport-aligned/hit/style.json @@ -0,0 +1,77 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200, + "queryGeometry": [40, 50] + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 60, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "Polygon", + "coordinates": [ + [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33341462261518, + 35.9294218694216 + ], + [ + -113.3220882006336, + 35.9418831745696 + ] + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": "#ff0000", + "circle-pitch-alignment": "viewport" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/query-tests/terrain/circle/viewport-aligned/miss/expected.json b/test/integration/query-tests/terrain/circle/viewport-aligned/miss/expected.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/test/integration/query-tests/terrain/circle/viewport-aligned/miss/expected.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/circle/viewport-aligned/miss/style.json b/test/integration/query-tests/terrain/circle/viewport-aligned/miss/style.json new file mode 100644 index 00000000000..8e2e6574c35 --- /dev/null +++ b/test/integration/query-tests/terrain/circle/viewport-aligned/miss/style.json @@ -0,0 +1,77 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200, + "queryGeometry": [41, 39] + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 60, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "Polygon", + "coordinates": [ + [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33341462261518, + 35.9294218694216 + ], + [ + -113.3220882006336, + 35.9418831745696 + ] + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": "#ff0000", + "circle-pitch-alignment": "viewport" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/fills/default/expected.json b/test/integration/query-tests/terrain/draped/fills/default/expected.json new file mode 100644 index 00000000000..0cd7ad98053 --- /dev/null +++ b/test/integration/query-tests/terrain/draped/fills/default/expected.json @@ -0,0 +1,55 @@ +[{ + "geometry": { + "coordinates": [ + [ + [ + -122.43267059326172, + 37.69487447234937 + ], + [ + -122.43252038955688, + 37.693855726015016 + ], + [ + -122.43140459060669, + 37.69439905913519 + ], + [ + -122.4310827255249, + 37.69309165707871 + ], + [ + -122.43022441864014, + 37.693634995797694 + ], + [ + -122.43033170700073, + 37.692446437178845 + ], + [ + -122.43159770965576, + 37.69195402874121 + ], + [ + -122.43181228637695, + 37.6931256158653 + ], + [ + -122.43333578109741, + 37.692667170934286 + ], + [ + -122.43267059326172, + 37.69487447234937 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "name": "p2" + }, + "source": "query-test-source", + "state": {}, + "type": "Feature" +}] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/fills/default/style.json b/test/integration/query-tests/terrain/draped/fills/default/style.json new file mode 100644 index 00000000000..f7d035a26ef --- /dev/null +++ b/test/integration/query-tests/terrain/draped/fills/default/style.json @@ -0,0 +1,136 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 500, + "height": 500, + "queryGeometry": [ + 257, + 242 + ] + } + }, + "center": [ -122.4329228784877, 37.69303037759488], + "zoom": 15.175358225195296, + "pitch": 74.49787460858354, + "bearing": -152.55854296672908, + "sources": { + "query-test-source": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "p1" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.44043827056883, + 37.6955366499607 + ], + [ + -122.44539499282835, + 37.693312388913455 + ], + [ + -122.44365692138672, + 37.68901648978227 + ], + [ + -122.43844270706177, + 37.69015416318515 + ], + [ + -122.43874311447144, + 37.69409343474501 + ], + [ + -122.44043827056883, + 37.6955366499607 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "p2" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.4326705932617, + 37.694874472349376 + ], + [ + -122.43333578109741, + 37.692667170934286 + ], + [ + -122.43181228637695, + 37.69312561586529 + ], + [ + -122.43159770965576, + 37.69195402874121 + ], + [ + -122.43033170700072, + 37.692446437178845 + ], + [ + -122.43022441864014, + 37.693634995797694 + ], + [ + -122.4310827255249, + 37.69309165707871 + ], + [ + -122.43140459060669, + 37.6943990591352 + ], + [ + -122.43252038955688, + 37.69385572601501 + ], + [ + -122.4326705932617, + 37.694874472349376 + ] + ] + ] + } + } + ] + } + }, + "mapbox-dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "tileSize": 256, + "maxzoom": 14 + } + }, + "layers": [ + { + "id": "query-test-polys", + "type": "fill", + "source": "query-test-source", + "paint": {} + } + ], + "terrain": { + "source": "mapbox-dem" + } + } \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/fills/slope-occlusion-box-query/expected.json b/test/integration/query-tests/terrain/draped/fills/slope-occlusion-box-query/expected.json new file mode 100644 index 00000000000..c946d749b5c --- /dev/null +++ b/test/integration/query-tests/terrain/draped/fills/slope-occlusion-box-query/expected.json @@ -0,0 +1,107 @@ +[{ + "geometry": { + "coordinates": [ + [ + [ + -122.43267059326172, + 37.69487447234937 + ], + [ + -122.43252038955688, + 37.693855726015016 + ], + [ + -122.43140459060669, + 37.69439905913519 + ], + [ + -122.4310827255249, + 37.69309165707871 + ], + [ + -122.43022441864014, + 37.693634995797694 + ], + [ + -122.43033170700073, + 37.692446437178845 + ], + [ + -122.43159770965576, + 37.69195402874121 + ], + [ + -122.43181228637695, + 37.6931256158653 + ], + [ + -122.43333578109741, + 37.692667170934286 + ], + [ + -122.43267059326172, + 37.69487447234937 + ] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "source": "query-test-source", + "state": {}, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + -122.43267059326172, + 37.69487447234937 + ], + [ + -122.43252038955688, + 37.693855726015016 + ], + [ + -122.43140459060669, + 37.69439905913519 + ], + [ + -122.4310827255249, + 37.69309165707871 + ], + [ + -122.43022441864014, + 37.693634995797694 + ], + [ + -122.43033170700073, + 37.692446437178845 + ], + [ + -122.43159770965576, + 37.69195402874121 + ], + [ + -122.43181228637695, + 37.6931256158653 + ], + [ + -122.43333578109741, + 37.692667170934286 + ], + [ + -122.43267059326172, + 37.69487447234937 + ] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "source": "query-test-source", + "state": {}, + "type": "Feature" + } +] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/fills/slope-occlusion-box-query/style.json b/test/integration/query-tests/terrain/draped/fills/slope-occlusion-box-query/style.json new file mode 100644 index 00000000000..d3a3cdbe46e --- /dev/null +++ b/test/integration/query-tests/terrain/draped/fills/slope-occlusion-box-query/style.json @@ -0,0 +1,132 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 500, + "height": 500, + "queryGeometry": [ + [ 308, 261 ], + [ 328, 291 ] + ] + } + }, + "center": [-122.43113081615442, 37.69518215671333], + "zoom": 14.873194705329965, + "pitch": 76.49787460858349, + "bearing": 28.241457033270535, + "sources": { + "query-test-source": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.44043827056883, + 37.6955366499607 + ], + [ + -122.44539499282835, + 37.693312388913455 + ], + [ + -122.44365692138672, + 37.68901648978227 + ], + [ + -122.43844270706177, + 37.69015416318515 + ], + [ + -122.43874311447144, + 37.69409343474501 + ], + [ + -122.44043827056883, + 37.6955366499607 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.4326705932617, + 37.694874472349376 + ], + [ + -122.43333578109741, + 37.692667170934286 + ], + [ + -122.43181228637695, + 37.69312561586529 + ], + [ + -122.43159770965576, + 37.69195402874121 + ], + [ + -122.43033170700072, + 37.692446437178845 + ], + [ + -122.43022441864014, + 37.693634995797694 + ], + [ + -122.4310827255249, + 37.69309165707871 + ], + [ + -122.43140459060669, + 37.6943990591352 + ], + [ + -122.43252038955688, + 37.69385572601501 + ], + [ + -122.4326705932617, + 37.694874472349376 + ] + ] + ] + } + } + ] + } + }, + "mapbox-dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "tileSize": 256, + "maxzoom": 14 + } + }, + "layers": [ + { + "id": "query-test-polys", + "type": "fill", + "source": "query-test-source", + "paint": {} + } + ], + "terrain": { + "source": "mapbox-dem" + } + } \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/fills/slope-occlusion/expected.json b/test/integration/query-tests/terrain/draped/fills/slope-occlusion/expected.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/test/integration/query-tests/terrain/draped/fills/slope-occlusion/expected.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/fills/slope-occlusion/style.json b/test/integration/query-tests/terrain/draped/fills/slope-occlusion/style.json new file mode 100644 index 00000000000..b96d897a414 --- /dev/null +++ b/test/integration/query-tests/terrain/draped/fills/slope-occlusion/style.json @@ -0,0 +1,132 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 500, + "height": 500, + "queryGeometry": [ + 318, + 271 + ] + } + }, + "center": [-122.43113081615442, 37.69518215671333], + "zoom": 14.873194705329965, + "pitch": 76.49787460858349, + "bearing": 28.241457033270535, + "sources": { + "query-test-source": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.44043827056883, + 37.6955366499607 + ], + [ + -122.44539499282835, + 37.693312388913455 + ], + [ + -122.44365692138672, + 37.68901648978227 + ], + [ + -122.43844270706177, + 37.69015416318515 + ], + [ + -122.43874311447144, + 37.69409343474501 + ], + [ + -122.44043827056883, + 37.6955366499607 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.4326705932617, + 37.694874472349376 + ], + [ + -122.43333578109741, + 37.692667170934286 + ], + [ + -122.43181228637695, + 37.69312561586529 + ], + [ + -122.43159770965576, + 37.69195402874121 + ], + [ + -122.43033170700072, + 37.692446437178845 + ], + [ + -122.43022441864014, + 37.693634995797694 + ], + [ + -122.4310827255249, + 37.69309165707871 + ], + [ + -122.43140459060669, + 37.6943990591352 + ], + [ + -122.43252038955688, + 37.69385572601501 + ], + [ + -122.4326705932617, + 37.694874472349376 + ] + ] + ] + } + } + ] + } + }, + "mapbox-dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "tileSize": 256, + "maxzoom": 14 + } + }, + "layers": [ + { + "id": "query-test-polys", + "type": "fill", + "source": "query-test-source", + "paint": {} + } + ], + "terrain": { + "source": "mapbox-dem" + } + } \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/lines/default/expected.json b/test/integration/query-tests/terrain/draped/lines/default/expected.json new file mode 100644 index 00000000000..87f803f9721 --- /dev/null +++ b/test/integration/query-tests/terrain/draped/lines/default/expected.json @@ -0,0 +1,37 @@ +[{ + "geometry": { + "coordinates": [ + [ + [ + -122.44043827056885, + 37.69553664996073 + ], + [ + -122.43874311447144, + 37.694093434745 + ], + [ + -122.43844270706177, + 37.69015416318514 + ], + [ + -122.44365692138672, + 37.68901648978226 + ], + [ + -122.44539499282837, + 37.69331238891348 + ], + [ + -122.44043827056885, + 37.69553664996073 + ] + ] + ], + "type": "Polygon" + }, + "properties": {}, + "source": "query-test-source", + "state": {}, + "type": "Feature" +}] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/lines/default/style.json b/test/integration/query-tests/terrain/draped/lines/default/style.json new file mode 100644 index 00000000000..d7d028ccc03 --- /dev/null +++ b/test/integration/query-tests/terrain/draped/lines/default/style.json @@ -0,0 +1,134 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 500, + "height": 500, + "queryGeometry": [ + 321, + 313 + ] + } + }, + "center": [-122.45446980870139, 37.6893087494564], + "zoom": 14.365824724643975, + "pitch": 75, + "bearing": -111.89999999999965, + "sources": { + "query-test-source": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.44043827056883, + 37.6955366499607 + ], + [ + -122.44539499282835, + 37.693312388913455 + ], + [ + -122.44365692138672, + 37.68901648978227 + ], + [ + -122.43844270706177, + 37.69015416318515 + ], + [ + -122.43874311447144, + 37.69409343474501 + ], + [ + -122.44043827056883, + 37.6955366499607 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.4326705932617, + 37.694874472349376 + ], + [ + -122.43333578109741, + 37.692667170934286 + ], + [ + -122.43181228637695, + 37.69312561586529 + ], + [ + -122.43159770965576, + 37.69195402874121 + ], + [ + -122.43033170700072, + 37.692446437178845 + ], + [ + -122.43022441864014, + 37.693634995797694 + ], + [ + -122.4310827255249, + 37.69309165707871 + ], + [ + -122.43140459060669, + 37.6943990591352 + ], + [ + -122.43252038955688, + 37.69385572601501 + ], + [ + -122.4326705932617, + 37.694874472349376 + ] + ] + ] + } + } + ] + } + }, + "mapbox-dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "tileSize": 256, + "maxzoom": 14 + } + }, + "layers": [ + { + "id": "query-test-lines", + "type": "line", + "source": "query-test-source", + "paint": { + "line-width": 5 + } + } + ], + "terrain": { + "source": "mapbox-dem" + } + } \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/lines/slope-occlusion-box-query/expected.json b/test/integration/query-tests/terrain/draped/lines/slope-occlusion-box-query/expected.json new file mode 100644 index 00000000000..04cef7fedd6 --- /dev/null +++ b/test/integration/query-tests/terrain/draped/lines/slope-occlusion-box-query/expected.json @@ -0,0 +1,27 @@ +[{ + "geometry": { + "coordinates": [ + [ + -122.44144678115845, + 37.68762408962763 + ], + [ + -122.44030952453613, + 37.68626562525917 + ], + [ + -122.43799209594727, + 37.68514487342378 + ], + [ + -122.43760585784912, + 37.68594298631518 + ] + ], + "type": "LineString" + }, + "properties": {}, + "source": "query-test-source", + "state": {}, + "type": "Feature" +}] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/lines/slope-occlusion-box-query/style.json b/test/integration/query-tests/terrain/draped/lines/slope-occlusion-box-query/style.json new file mode 100644 index 00000000000..7563dff2187 --- /dev/null +++ b/test/integration/query-tests/terrain/draped/lines/slope-occlusion-box-query/style.json @@ -0,0 +1,184 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 500, + "height": 500, + "queryGeometry": [ + [285, 270], + [315, 300] + ] + } + }, + "center": [-122.43837208778436, 37.68568404991683], + "zoom": 16.449998598933057, + "pitch": 69.5, + "bearing": -54.299999999999955, + "sources": { + "query-test-source": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -122.44144678115843, + 37.68762408962764 + ], + [ + -122.44030952453613, + 37.686265625259175 + ], + [ + -122.43799209594725, + 37.68514487342378 + ], + [ + -122.43760585784912, + 37.685942986315176 + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -122.43438720703125, + 37.68959381876486 + ], + [ + -122.43149042129517, + 37.68857499988068 + ], + [ + -122.43146896362306, + 37.68657128191147 + ], + [ + -122.42932319641112, + 37.68684297565604 + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.44043827056883, + 37.6955366499607 + ], + [ + -122.44539499282835, + 37.693312388913455 + ], + [ + -122.44365692138672, + 37.68901648978227 + ], + [ + -122.43844270706177, + 37.69015416318515 + ], + [ + -122.43874311447144, + 37.69409343474501 + ], + [ + -122.44043827056883, + 37.6955366499607 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.4326705932617, + 37.694874472349376 + ], + [ + -122.43333578109741, + 37.692667170934286 + ], + [ + -122.43181228637695, + 37.69312561586529 + ], + [ + -122.43159770965576, + 37.69195402874121 + ], + [ + -122.43033170700072, + 37.692446437178845 + ], + [ + -122.43022441864014, + 37.693634995797694 + ], + [ + -122.4310827255249, + 37.69309165707871 + ], + [ + -122.43140459060669, + 37.6943990591352 + ], + [ + -122.43252038955688, + 37.69385572601501 + ], + [ + -122.4326705932617, + 37.694874472349376 + ] + ] + ] + } + } + ] + } + }, + "mapbox-dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "tileSize": 256, + "maxzoom": 14 + } + }, + "layers": [ + { + "id": "query-test-lines", + "type": "line", + "source": "query-test-source", + "paint": { + "line-width": 15 + } + } + ], + "terrain": { + "source": "mapbox-dem" + } + } \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/lines/slope-occlusion/expected.json b/test/integration/query-tests/terrain/draped/lines/slope-occlusion/expected.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/test/integration/query-tests/terrain/draped/lines/slope-occlusion/expected.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/test/integration/query-tests/terrain/draped/lines/slope-occlusion/style.json b/test/integration/query-tests/terrain/draped/lines/slope-occlusion/style.json new file mode 100644 index 00000000000..42510991fbb --- /dev/null +++ b/test/integration/query-tests/terrain/draped/lines/slope-occlusion/style.json @@ -0,0 +1,184 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 500, + "height": 500, + "queryGeometry": [ + 300, + 285 + ] + } + }, + "center": [-122.43837208778436, 37.68568404991683], + "zoom": 16.449998598933057, + "pitch": 69.5, + "bearing": -54.299999999999955, + "sources": { + "query-test-source": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -122.44144678115843, + 37.68762408962764 + ], + [ + -122.44030952453613, + 37.686265625259175 + ], + [ + -122.43799209594725, + 37.68514487342378 + ], + [ + -122.43760585784912, + 37.685942986315176 + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -122.43438720703125, + 37.68959381876486 + ], + [ + -122.43149042129517, + 37.68857499988068 + ], + [ + -122.43146896362306, + 37.68657128191147 + ], + [ + -122.42932319641112, + 37.68684297565604 + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.44043827056883, + 37.6955366499607 + ], + [ + -122.44539499282835, + 37.693312388913455 + ], + [ + -122.44365692138672, + 37.68901648978227 + ], + [ + -122.43844270706177, + 37.69015416318515 + ], + [ + -122.43874311447144, + 37.69409343474501 + ], + [ + -122.44043827056883, + 37.6955366499607 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.4326705932617, + 37.694874472349376 + ], + [ + -122.43333578109741, + 37.692667170934286 + ], + [ + -122.43181228637695, + 37.69312561586529 + ], + [ + -122.43159770965576, + 37.69195402874121 + ], + [ + -122.43033170700072, + 37.692446437178845 + ], + [ + -122.43022441864014, + 37.693634995797694 + ], + [ + -122.4310827255249, + 37.69309165707871 + ], + [ + -122.43140459060669, + 37.6943990591352 + ], + [ + -122.43252038955688, + 37.69385572601501 + ], + [ + -122.4326705932617, + 37.694874472349376 + ] + ] + ] + } + } + ] + } + }, + "mapbox-dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "tileSize": 256, + "maxzoom": 14 + } + }, + "layers": [ + { + "id": "query-test-lines", + "type": "line", + "source": "query-test-source", + "paint": { + "line-width": 15 + } + } + ], + "terrain": { + "source": "mapbox-dem" + } + } \ No newline at end of file diff --git a/test/integration/render-tests/combinations/background-translucent--circle-translucent/expected.png b/test/integration/render-tests/combinations/background-translucent--circle-translucent/expected.png index de92554f48f..dcd0daf41af 100644 Binary files a/test/integration/render-tests/combinations/background-translucent--circle-translucent/expected.png and b/test/integration/render-tests/combinations/background-translucent--circle-translucent/expected.png differ diff --git a/test/integration/render-tests/combinations/symbol-translucent--hillshade-translucent-terrain/expected.png b/test/integration/render-tests/combinations/symbol-translucent--hillshade-translucent-terrain/expected.png new file mode 100644 index 00000000000..6c55aeccd95 Binary files /dev/null and b/test/integration/render-tests/combinations/symbol-translucent--hillshade-translucent-terrain/expected.png differ diff --git a/test/integration/render-tests/combinations/symbol-translucent--hillshade-translucent-terrain/style.json b/test/integration/render-tests/combinations/symbol-translucent--hillshade-translucent-terrain/style.json new file mode 100644 index 00000000000..f64c2726cc3 --- /dev/null +++ b/test/integration/render-tests/combinations/symbol-translucent--hillshade-translucent-terrain/style.json @@ -0,0 +1,88 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "center": [ + -113.26903, + 35.9654 + ], + "zoom": 11, + "terrain": { + "source": "dem" + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Polygon", + "coordinates": [ + [ + [ + -113.27384948730469, + 35.962 + ], + [ + -113.26421051269531, + 35.962 + ], + [ + -113.26421051269531, + 35.97 + ], + [ + -113.27384948730469, + 35.97 + ], + [ + -113.27384948730469, + 35.962 + ] + ] + ] + } + }, + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "symbol-translucent", + "type": "symbol", + "source": "geojson", + "layout": { + "icon-image": "dot.sdf", + "symbol-placement": "line" + }, + "paint": { + "icon-color": "red" + } + }, + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + } + ], + "sprite": "local://sprites/sprite" +} diff --git a/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain-draped/expected.png b/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain-draped/expected.png new file mode 100644 index 00000000000..d17bea7eff6 Binary files /dev/null and b/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain-draped/expected.png differ diff --git a/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain-draped/style.json b/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain-draped/style.json new file mode 100644 index 00000000000..ee60ef3579d --- /dev/null +++ b/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain-draped/style.json @@ -0,0 +1,82 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64, + "terrainDrapeFirst": true, + "description": "Terrain drape first mode is about reordering layers on render: all draped layers are rendered first followed by non draped (e.g. symbols, heatmap, circles)." + } + }, + "center": [ + -113.26903, + 35.9654 + ], + "zoom": 11, + "terrain": { + "source": "dem" + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Polygon", + "coordinates": [ + [ + [ + -113.27384948730469, + 35.962 + ], + [ + -113.26421051269531, + 35.962 + ], + [ + -113.26421051269531, + 35.97 + ], + [ + -113.27384948730469, + 35.97 + ], + [ + -113.27384948730469, + 35.962 + ] + ] + ] + } + }, + "dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/no/{z}-{x}-{y}.terrain.512.png" + ], + "maxzoom": 15, + "tileSize": 512 + } + }, + "layers": [ + { + "id": "symbol-translucent", + "type": "symbol", + "source": "geojson", + "layout": { + "icon-image": "dot.sdf", + "symbol-placement": "line" + }, + "paint": { + "icon-color": "red" + } + }, + { + "id": "line-translucent", + "type": "line", + "source": "geojson", + "paint": { + "line-color": "blue" + } + } + ], + "sprite": "local://sprites/sprite" +} diff --git a/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain/expected.png b/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain/expected.png new file mode 100644 index 00000000000..ab396ec1f07 Binary files /dev/null and b/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain/expected.png differ diff --git a/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain/style.json b/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain/style.json new file mode 100644 index 00000000000..4c9dfd31b6b --- /dev/null +++ b/test/integration/render-tests/combinations/symbol-translucent--line-translucent-terrain/style.json @@ -0,0 +1,80 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "center": [ + -113.26903, + 35.9654 + ], + "zoom": 11, + "terrain": { + "source": "dem" + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Polygon", + "coordinates": [ + [ + [ + -113.27384948730469, + 35.962 + ], + [ + -113.26421051269531, + 35.962 + ], + [ + -113.26421051269531, + 35.97 + ], + [ + -113.27384948730469, + 35.97 + ], + [ + -113.27384948730469, + 35.962 + ] + ] + ] + } + }, + "dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/no/{z}-{x}-{y}.terrain.512.png" + ], + "maxzoom": 15, + "tileSize": 512 + } + }, + "layers": [ + { + "id": "symbol-translucent", + "type": "symbol", + "source": "geojson", + "layout": { + "icon-image": "dot.sdf", + "symbol-placement": "line" + }, + "paint": { + "icon-color": "red" + } + }, + { + "id": "line-translucent", + "type": "line", + "source": "geojson", + "paint": { + "line-color": "blue" + } + } + ], + "sprite": "local://sprites/sprite" +} diff --git a/test/integration/render-tests/debug/collision-fractional-zoom/expected.png b/test/integration/render-tests/debug/collision-fractional-zoom/expected.png new file mode 100644 index 00000000000..cad306b74ed Binary files /dev/null and b/test/integration/render-tests/debug/collision-fractional-zoom/expected.png differ diff --git a/test/integration/render-tests/debug/collision-fractional-zoom/style.json b/test/integration/render-tests/debug/collision-fractional-zoom/style.json new file mode 100644 index 00000000000..c209ceb73ba --- /dev/null +++ b/test/integration/render-tests/debug/collision-fractional-zoom/style.json @@ -0,0 +1,62 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14.5, + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "symbol", + "source": "mapbox", + "source-layer": "road_label", + "layout": { + "icon-image": "triangle-stroked-12", + "text-field": "test test", + "text-size": [ + "interpolate", + ["linear"], + ["zoom"], + 14, + 10, + 15, + 20 + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "point", + "symbol-spacing": 20 + }, + "paint": { + "icon-opacity": 1 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/debug/collision-high-pitched-fractional-zoom/expected.png b/test/integration/render-tests/debug/collision-high-pitched-fractional-zoom/expected.png new file mode 100644 index 00000000000..1c3ebefe36b Binary files /dev/null and b/test/integration/render-tests/debug/collision-high-pitched-fractional-zoom/expected.png differ diff --git a/test/integration/render-tests/debug/collision-high-pitched-fractional-zoom/style.json b/test/integration/render-tests/debug/collision-high-pitched-fractional-zoom/style.json new file mode 100644 index 00000000000..567a7ec88ce --- /dev/null +++ b/test/integration/render-tests/debug/collision-high-pitched-fractional-zoom/style.json @@ -0,0 +1,57 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 500, + "height": 500, + "allowed": 0.005 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 3.2, + "pitch": 85, + "bearing": 90, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://data/places.geojson" + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "symbol", + "type": "symbol", + "source": "geojson", + "layout": { + "symbol-placement": "point", + "text-field": "test test test", + "text-size": [ + "interpolate", + ["linear"], + ["zoom"], + 3, + 10, + 4, + 20 + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/debug/collision-high-pitched-wrapped-fractional-zoom/expected.png b/test/integration/render-tests/debug/collision-high-pitched-wrapped-fractional-zoom/expected.png new file mode 100644 index 00000000000..15c2a8dc09d Binary files /dev/null and b/test/integration/render-tests/debug/collision-high-pitched-wrapped-fractional-zoom/expected.png differ diff --git a/test/integration/render-tests/debug/collision-high-pitched-wrapped-fractional-zoom/style.json b/test/integration/render-tests/debug/collision-high-pitched-wrapped-fractional-zoom/style.json new file mode 100644 index 00000000000..5937d27ef1e --- /dev/null +++ b/test/integration/render-tests/debug/collision-high-pitched-wrapped-fractional-zoom/style.json @@ -0,0 +1,57 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 500, + "height": 500, + "allowed": 0.005 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1.5, + "pitch": 85, + "bearing": 90, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://data/places.geojson" + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "symbol", + "type": "symbol", + "source": "geojson", + "layout": { + "symbol-placement": "point", + "text-field": "test test test", + "text-size": [ + "interpolate", + ["linear"], + ["zoom"], + 1, + 10, + 2, + 30 + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/debug/collision-high-pitched/expected.png b/test/integration/render-tests/debug/collision-high-pitched/expected.png new file mode 100644 index 00000000000..2f447581d6f Binary files /dev/null and b/test/integration/render-tests/debug/collision-high-pitched/expected.png differ diff --git a/test/integration/render-tests/debug/collision-high-pitched/style.json b/test/integration/render-tests/debug/collision-high-pitched/style.json new file mode 100644 index 00000000000..926a910a857 --- /dev/null +++ b/test/integration/render-tests/debug/collision-high-pitched/style.json @@ -0,0 +1,48 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 500, + "height": 500, + "allowed": 0.005 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 3, + "pitch": 85, + "bearing": 90, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://data/places.geojson" + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "symbol", + "type": "symbol", + "source": "geojson", + "layout": { + "symbol-placement": "point", + "text-field": "test test test", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/debug/collision-icon-text-line-translate-fractional-zoom/expected.png b/test/integration/render-tests/debug/collision-icon-text-line-translate-fractional-zoom/expected.png new file mode 100644 index 00000000000..00be47448f5 Binary files /dev/null and b/test/integration/render-tests/debug/collision-icon-text-line-translate-fractional-zoom/expected.png differ diff --git a/test/integration/render-tests/debug/collision-icon-text-line-translate-fractional-zoom/style.json b/test/integration/render-tests/debug/collision-icon-text-line-translate-fractional-zoom/style.json new file mode 100644 index 00000000000..0a7987b2fe6 --- /dev/null +++ b/test/integration/render-tests/debug/collision-icon-text-line-translate-fractional-zoom/style.json @@ -0,0 +1,99 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 256, + "width" : 256 + } + }, + "center": [ + 0.25, + 0.25 + ], + "zoom": 12.5, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "LineString", + "coordinates": [ + [ + 0, + 0 + ], + [ + 0.5, + 0.5 + ] + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "translate", + "type": "symbol", + "source": "geojson", + "layout": { + "icon-image": "fav-airport-18", + "icon-size": [ + "interpolate", + ["linear"], + ["zoom"], + 12, + 1, + 13, + 2 + ], + "text-field": "abc", + "text-size": [ + "interpolate", + ["linear"], + ["zoom"], + 12, + 10, + 13, + 20 + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "line", + "symbol-spacing": 50 + }, + "paint": { + "icon-opacity": 1, + "text-color": "hsl(0, 82%, 48%)", + "icon-translate": [ + "interpolate", + ["exponential", 2.0], + ["zoom"], + 9, + ["literal", [0, 0]], + 14, + ["literal", [-130, -130]] + ], + "text-translate": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 9, + ["literal", [0, 0]], + 13, + ["literal", [0, -130]] + ] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/debug/collision-icon-text-point-translate-fractional-zoom/expected.png b/test/integration/render-tests/debug/collision-icon-text-point-translate-fractional-zoom/expected.png new file mode 100644 index 00000000000..3498fd25fc5 Binary files /dev/null and b/test/integration/render-tests/debug/collision-icon-text-point-translate-fractional-zoom/expected.png differ diff --git a/test/integration/render-tests/debug/collision-icon-text-point-translate-fractional-zoom/style.json b/test/integration/render-tests/debug/collision-icon-text-point-translate-fractional-zoom/style.json new file mode 100644 index 00000000000..e54c9be550d --- /dev/null +++ b/test/integration/render-tests/debug/collision-icon-text-point-translate-fractional-zoom/style.json @@ -0,0 +1,84 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 256, + "width" : 256 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 12.4, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Point", + "coordinates": [ + 0, + 0 + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "translate", + "type": "symbol", + "source": "geojson", + "layout": { + "icon-image": "fav-airport-18", + "text-field": "abc", + "text-size": [ + "interpolate", + ["linear"], + ["zoom"], + 12, + 10, + 13, + 20 + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "point", + "symbol-spacing": 20 + }, + "paint": { + "icon-opacity": 1, + "text-color": "hsl(0, 82%, 48%)", + "icon-translate": [ + "interpolate", + ["exponential", 2.0], + ["zoom"], + 9, + ["literal", [0, 0]], + 14, + ["literal", [-130, -130]] + ], + "text-translate": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 9, + ["literal", [0, 0]], + 13, + ["literal", [0, -130]] + ] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/debug/collision-lines-high-pitched/expected.png b/test/integration/render-tests/debug/collision-lines-high-pitched/expected.png new file mode 100644 index 00000000000..9ba7ffcd857 Binary files /dev/null and b/test/integration/render-tests/debug/collision-lines-high-pitched/expected.png differ diff --git a/test/integration/render-tests/debug/collision-lines-high-pitched/style.json b/test/integration/render-tests/debug/collision-lines-high-pitched/style.json new file mode 100644 index 00000000000..e1f2b6454ad --- /dev/null +++ b/test/integration/render-tests/debug/collision-lines-high-pitched/style.json @@ -0,0 +1,56 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 500, + "height": 500 + } + }, + "center": [ + -74.522, + 39.92 + ], + "pitch": 85, + "zoom": 9, + "sources": { + "us-counties": { + "type": "vector", + "maxzoom": 7, + "minzoom": 7, + "tiles": [ + "local://tiles/counties-{z}-{x}-{y}.mvt" + ] + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "counties", + "type": "symbol", + "source": "us-counties", + "source-layer": "counties", + "layout": { + "text-field": "{name}", + "text-size": 11, + "symbol-spacing": 60, + "text-max-angle": 1000, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "line", + "text-pitch-alignment": "viewport" + }, + "interactive": true + } + ] +} diff --git a/test/integration/render-tests/debug/collision-lines-simple-words-high-pitched/expected.png b/test/integration/render-tests/debug/collision-lines-simple-words-high-pitched/expected.png new file mode 100644 index 00000000000..ac462bc2eb5 Binary files /dev/null and b/test/integration/render-tests/debug/collision-lines-simple-words-high-pitched/expected.png differ diff --git a/test/integration/render-tests/debug/collision-lines-simple-words-high-pitched/style.json b/test/integration/render-tests/debug/collision-lines-simple-words-high-pitched/style.json new file mode 100644 index 00000000000..05ad2d128ee --- /dev/null +++ b/test/integration/render-tests/debug/collision-lines-simple-words-high-pitched/style.json @@ -0,0 +1,128 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 512, + "height": 256 + } + }, + "center": [ + 0, + 2 + ], + "zoom": 6, + "pitch": 85, + "sources": { + "geojson": { + "type": "geojson", + "maxzoom": 1, + "data": { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": { + "name": "a" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -1, 1 ], [ 0, 1 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "ab" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -1, 0 ], [ 0, 0 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abc" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -1, -1 ], [ 0, -1 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abcd" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0, 1 ], [ 1, 1 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abcde" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0, 0 ], [ 1, 0 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abcdef" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0, -1 ], [ 1, -1 ] + ] + } + + }] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "symbol", + "source": "geojson", + "layout": { + "text-allow-overlap": true, + "text-field": "{name}", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "line-center", + "text-pitch-alignment": "viewport" + } + } + ] +} diff --git a/test/integration/render-tests/debug/collision-overscaled-fractional-zoom/expected.png b/test/integration/render-tests/debug/collision-overscaled-fractional-zoom/expected.png new file mode 100644 index 00000000000..ce8c19d3491 Binary files /dev/null and b/test/integration/render-tests/debug/collision-overscaled-fractional-zoom/expected.png differ diff --git a/test/integration/render-tests/debug/collision-overscaled-fractional-zoom/style.json b/test/integration/render-tests/debug/collision-overscaled-fractional-zoom/style.json new file mode 100644 index 00000000000..db23c8f3c93 --- /dev/null +++ b/test/integration/render-tests/debug/collision-overscaled-fractional-zoom/style.json @@ -0,0 +1,62 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 17.5, + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "symbol", + "source": "mapbox", + "source-layer": "road_label", + "layout": { + "icon-image": "triangle-stroked-12", + "text-field": "test test", + "text-size": [ + "interpolate", + ["linear"], + ["zoom"], + 17, + 10, + 18, + 30 + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "point", + "symbol-spacing": 20 + }, + "paint": { + "icon-opacity": 1 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/debug/collision-pitched-fractional-zoom/expected.png b/test/integration/render-tests/debug/collision-pitched-fractional-zoom/expected.png new file mode 100644 index 00000000000..dd04043293a Binary files /dev/null and b/test/integration/render-tests/debug/collision-pitched-fractional-zoom/expected.png differ diff --git a/test/integration/render-tests/debug/collision-pitched-fractional-zoom/style.json b/test/integration/render-tests/debug/collision-pitched-fractional-zoom/style.json new file mode 100644 index 00000000000..26fd13f7495 --- /dev/null +++ b/test/integration/render-tests/debug/collision-pitched-fractional-zoom/style.json @@ -0,0 +1,57 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 500, + "height": 500, + "allowed": 0.005 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 3.3, + "pitch": 60, + "bearing": 90, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://data/places.geojson" + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "symbol", + "type": "symbol", + "source": "geojson", + "layout": { + "symbol-placement": "point", + "text-field": "test test test", + "text-size": [ + "interpolate", + ["linear"], + ["zoom"], + 3, + 10, + 4, + 30 + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/debug/collision-pitched-wrapped-fractional-zoom/expected.png b/test/integration/render-tests/debug/collision-pitched-wrapped-fractional-zoom/expected.png new file mode 100644 index 00000000000..4f380c66896 Binary files /dev/null and b/test/integration/render-tests/debug/collision-pitched-wrapped-fractional-zoom/expected.png differ diff --git a/test/integration/render-tests/debug/collision-pitched-wrapped-fractional-zoom/style.json b/test/integration/render-tests/debug/collision-pitched-wrapped-fractional-zoom/style.json new file mode 100644 index 00000000000..941ee35553f --- /dev/null +++ b/test/integration/render-tests/debug/collision-pitched-wrapped-fractional-zoom/style.json @@ -0,0 +1,57 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 500, + "height": 500, + "allowed": 0.005 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1.5, + "pitch": 60, + "bearing": 90, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://data/places.geojson" + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "symbol", + "type": "symbol", + "source": "geojson", + "layout": { + "symbol-placement": "point", + "text-field": "test test test", + "text-size": [ + "interpolate", + ["linear"], + ["zoom"], + 1, + 10, + 2, + 30 + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/debug/collision-pitched-wrapped/expected.png b/test/integration/render-tests/debug/collision-pitched-wrapped/expected.png index d23243aa8ae..2ffc01ff310 100644 Binary files a/test/integration/render-tests/debug/collision-pitched-wrapped/expected.png and b/test/integration/render-tests/debug/collision-pitched-wrapped/expected.png differ diff --git a/test/integration/render-tests/debug/collision-pitched/expected.png b/test/integration/render-tests/debug/collision-pitched/expected.png index d1eb3bf6a38..8af943f108b 100644 Binary files a/test/integration/render-tests/debug/collision-pitched/expected.png and b/test/integration/render-tests/debug/collision-pitched/expected.png differ diff --git a/test/integration/render-tests/debug/collision/expected.png b/test/integration/render-tests/debug/collision/expected.png index 6dc313d9531..03c3ee67c64 100644 Binary files a/test/integration/render-tests/debug/collision/expected.png and b/test/integration/render-tests/debug/collision/expected.png differ diff --git a/test/integration/render-tests/debug/raster/expected.png b/test/integration/render-tests/debug/raster/expected.png index 04f45dc70c9..e0ed7f8ac69 100644 Binary files a/test/integration/render-tests/debug/raster/expected.png and b/test/integration/render-tests/debug/raster/expected.png differ diff --git a/test/integration/render-tests/debug/raster/style.json b/test/integration/render-tests/debug/raster/style.json index 9cc0d03cf9f..388f9dc4e77 100644 --- a/test/integration/render-tests/debug/raster/style.json +++ b/test/integration/render-tests/debug/raster/style.json @@ -4,7 +4,7 @@ "test": { "debug": true, "height": 256, - "allowed": 0.0062 + "allowed": 0.02975 } }, "center": [ diff --git a/test/integration/render-tests/debug/terrain/camera-under-terrain/expected.png b/test/integration/render-tests/debug/terrain/camera-under-terrain/expected.png new file mode 100644 index 00000000000..26326dd44cf Binary files /dev/null and b/test/integration/render-tests/debug/terrain/camera-under-terrain/expected.png differ diff --git a/test/integration/render-tests/debug/terrain/camera-under-terrain/style.json b/test/integration/render-tests/debug/terrain/camera-under-terrain/style.json new file mode 100644 index 00000000000..1eafce26211 --- /dev/null +++ b/test/integration/render-tests/debug/terrain/camera-under-terrain/style.json @@ -0,0 +1,48 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 16.4, + "pitch": 85, + "bearing": -25, + "terrain": { + "source": "rgbterrain", + "exaggeration": 1.5 + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/12-759-1609.terrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "gray" + } + }, + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "rgbterrain", + "paint": { + "hillshade-exaggeration": 1 + } + } + ] +} diff --git a/test/integration/render-tests/debug/terrain/collision-align-line-viewport/expected.png b/test/integration/render-tests/debug/terrain/collision-align-line-viewport/expected.png new file mode 100644 index 00000000000..e484c5c7ded Binary files /dev/null and b/test/integration/render-tests/debug/terrain/collision-align-line-viewport/expected.png differ diff --git a/test/integration/render-tests/debug/terrain/collision-align-line-viewport/style.json b/test/integration/render-tests/debug/terrain/collision-align-line-viewport/style.json new file mode 100644 index 00000000000..d63d29f0d72 --- /dev/null +++ b/test/integration/render-tests/debug/terrain/collision-align-line-viewport/style.json @@ -0,0 +1,80 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "collisionDebug": true, + "operations": [ + ["wait"], + ["setTerrain", { + "source": "rgbterrain", + "exaggeration": 1.5 + }], + ["wait"] + ] + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 16.4, + "pitch": 55, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/12-759-1609.terrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + }, + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-color": "#888", + "line-width": 1 + } + }, + { + "id": "text", + "type": "symbol", + "source": "mapbox", + "source-layer": "road_label", + "layout": { + "symbol-placement": "line", + "symbol-spacing": 60, + "text-rotation-alignment": "map", + "text-pitch-alignment": "viewport", + "text-field": "The {class}", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-opacity": 1 + } + } + ] +} diff --git a/test/integration/render-tests/debug/terrain/collision-lines-occlusion/expected.png b/test/integration/render-tests/debug/terrain/collision-lines-occlusion/expected.png new file mode 100644 index 00000000000..26bdc824337 Binary files /dev/null and b/test/integration/render-tests/debug/terrain/collision-lines-occlusion/expected.png differ diff --git a/test/integration/render-tests/debug/terrain/collision-lines-occlusion/style.json b/test/integration/render-tests/debug/terrain/collision-lines-occlusion/style.json new file mode 100644 index 00000000000..0e9149cf3e1 --- /dev/null +++ b/test/integration/render-tests/debug/terrain/collision-lines-occlusion/style.json @@ -0,0 +1,70 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 200, + "height": 200, + "collisionDebug": true, + "description": "Covers placement and collision for elevated text. There is also text completely behind terrain that shouldn't be visible.", + "operations": [ + ["wait", 1500] + ] + } + }, + "center": [-74.7546, 39.1667], + "pitch": 35, + "zoom": 9.1, + "bearing": -80, + "terrain": { + "source": "rgbterrain", + "exaggeration": 33 + }, + "sources": { + "us-counties": { + "type": "vector", + "maxzoom": 7, + "minzoom": 7, + "tiles": [ + "local://tiles/counties-{z}-{x}-{y}.mvt" + ] + }, + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/const/{z}-{x}-{y}.terrain.512.png" + ], + "minzoom": 6, + "maxzoom": 7, + "tileSize": 512 + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "counties", + "type": "symbol", + "source": "us-counties", + "source-layer": "counties", + "layout": { + "text-field": "{name}", + "text-size": 11, + "symbol-spacing": 60, + "text-max-angle": 1000, + "text-pitch-alignment": "viewport", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "line" + }, + "interactive": true + } + ] +} diff --git a/test/integration/render-tests/debug/terrain/collision-occlusion/expected.png b/test/integration/render-tests/debug/terrain/collision-occlusion/expected.png new file mode 100644 index 00000000000..6671ab09828 Binary files /dev/null and b/test/integration/render-tests/debug/terrain/collision-occlusion/expected.png differ diff --git a/test/integration/render-tests/debug/terrain/collision-occlusion/style.json b/test/integration/render-tests/debug/terrain/collision-occlusion/style.json new file mode 100644 index 00000000000..2bc0e0b0dc2 --- /dev/null +++ b/test/integration/render-tests/debug/terrain/collision-occlusion/style.json @@ -0,0 +1,73 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 160, + "width": 160, + "allowed": 0.0005, + "description": "Collision debug and occlusion test for text and icons.", + "operations": [ + ["wait", 1500] + ] + } + }, + "center": [ + 13.421, + 52.499168 + ], + "bearing": 36.8, + "pitch": 41, + "zoom": 14, + "terrain": { + "source": "rgbterrain", + "exaggeration": 3 + }, + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + }, + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/const/{z}-{x}-{y}.terrain.512.png" + ], + "maxzoom": 15, + "tileSize": 512 + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "symbol", + "source": "mapbox", + "source-layer": "road_label", + "layout": { + "icon-image": "triangle-stroked-12", + "text-field": "test test", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "point", + "symbol-spacing": 20 + }, + "paint": { + "icon-opacity": 1 + } + } + ] +} diff --git a/test/integration/render-tests/debug/terrain/collision-pitch-with-map-text-and-icon/expected.png b/test/integration/render-tests/debug/terrain/collision-pitch-with-map-text-and-icon/expected.png new file mode 100644 index 00000000000..45a120926d4 Binary files /dev/null and b/test/integration/render-tests/debug/terrain/collision-pitch-with-map-text-and-icon/expected.png differ diff --git a/test/integration/render-tests/debug/terrain/collision-pitch-with-map-text-and-icon/style.json b/test/integration/render-tests/debug/terrain/collision-pitch-with-map-text-and-icon/style.json new file mode 100644 index 00000000000..52602ba5a53 --- /dev/null +++ b/test/integration/render-tests/debug/terrain/collision-pitch-with-map-text-and-icon/style.json @@ -0,0 +1,107 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "collisionDebug": true, + "operations": [ + ["wait"], + ["setTerrain", { + "source": "rgbterrain" + }], + ["wait"] + ] + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14.9, + "pitch": 45, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/12-759-1609.terrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + }, + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-color": "#888", + "line-width": 1 + } + }, + { + "id": "text-and-icon", + "type": "symbol", + "source": "mapbox", + "source-layer": "poi_label", + "filter": [ + "==", + "maki", + "restaurant" + ], + "layout": { + "symbol-placement": "point", + "icon-image": "building-12", + "text-allow-overlap": false, + "text-pitch-alignment": "map", + "icon-pitch-alignment": "map", + "text-field": "Test", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "icon-opacity": 1, + "text-opacity": 1 + } + }, + { + "id": "text", + "type": "symbol", + "source": "mapbox", + "source-layer": "road_label", + "layout": { + "symbol-placement": "line", + "symbol-spacing": 60, + "text-rotation-alignment": "map", + "text-pitch-alignment": "map", + "text-field": "{class}", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-opacity": 1 + } + } + ] +} diff --git a/test/integration/render-tests/debug/terrain/collision-pitch-with-map/expected.png b/test/integration/render-tests/debug/terrain/collision-pitch-with-map/expected.png new file mode 100644 index 00000000000..947840d6d8a Binary files /dev/null and b/test/integration/render-tests/debug/terrain/collision-pitch-with-map/expected.png differ diff --git a/test/integration/render-tests/debug/terrain/collision-pitch-with-map/style.json b/test/integration/render-tests/debug/terrain/collision-pitch-with-map/style.json new file mode 100644 index 00000000000..a85aa58a791 --- /dev/null +++ b/test/integration/render-tests/debug/terrain/collision-pitch-with-map/style.json @@ -0,0 +1,97 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "collisionDebug": true, + "operations": [ + ["wait"], + ["setTerrain", { + "source": "rgbterrain" + }], + ["wait"] + ] + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14.9, + "pitch": 45, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/12-759-1609.terrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + }, + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-color": "#888", + "line-width": 1 + } + }, + { + "id": "icon", + "type": "symbol", + "source": "mapbox", + "source-layer": "poi_label", + "filter": [ + "==", + "maki", + "restaurant" + ], + "layout": { + "symbol-placement": "point", + "icon-rotation-alignment": "map", + "icon-image": "building-12" + }, + "paint": {} + }, + { + "id": "text", + "type": "symbol", + "source": "mapbox", + "source-layer": "road_label", + "layout": { + "symbol-placement": "line", + "symbol-spacing": 60, + "text-rotation-alignment": "map", + "text-pitch-alignment": "map", + "text-field": "{class}", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-opacity": 1 + } + } + ] +} diff --git a/test/integration/render-tests/debug/terrain/collision-pitched/expected.png b/test/integration/render-tests/debug/terrain/collision-pitched/expected.png new file mode 100644 index 00000000000..3c055081e0d Binary files /dev/null and b/test/integration/render-tests/debug/terrain/collision-pitched/expected.png differ diff --git a/test/integration/render-tests/debug/terrain/collision-pitched/style.json b/test/integration/render-tests/debug/terrain/collision-pitched/style.json new file mode 100644 index 00000000000..d46c8fd074c --- /dev/null +++ b/test/integration/render-tests/debug/terrain/collision-pitched/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 500, + "height": 500, + "allowed": 0.005, + "description": "Tiles in front get elevation (TODO use exaggeration later) of >700km (defined by 1-0-0.terrain.512.png). This pushes terrain and symbols over tiles in back.", + "operations": [ + ["sleep", 1500] + ] + } + }, + "center": [ + 0, + 0 + ], + "zoom": 3, + "pitch": 60, + "bearing": 90, + "terrain": { + "source": "dem" + }, + "sources": { + "dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/const/{z}-{x}-{y}.terrain.512.png" + ], + "maxzoom": 15, + "tileSize": 512 + }, + "geojson": { + "type": "geojson", + "data": "local://data/places.geojson" + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "symbol", + "type": "symbol", + "source": "geojson", + "layout": { + "symbol-placement": "point", + "text-field": "test test test", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/debug/tile-overscaled/expected.png b/test/integration/render-tests/debug/tile-overscaled/expected.png index 8ea8baa3839..8daae795152 100644 Binary files a/test/integration/render-tests/debug/tile-overscaled/expected.png and b/test/integration/render-tests/debug/tile-overscaled/expected.png differ diff --git a/test/integration/render-tests/debug/tile/expected.png b/test/integration/render-tests/debug/tile/expected.png index 12c0ba92834..fedbdc89213 100644 Binary files a/test/integration/render-tests/debug/tile/expected.png and b/test/integration/render-tests/debug/tile/expected.png differ diff --git a/test/integration/render-tests/distance/layout-text-size/expected.png b/test/integration/render-tests/distance/layout-text-size/expected.png new file mode 100644 index 00000000000..0853fc2a1c4 Binary files /dev/null and b/test/integration/render-tests/distance/layout-text-size/expected.png differ diff --git a/test/integration/render-tests/distance/layout-text-size/style.json b/test/integration/render-tests/distance/layout-text-size/style.json new file mode 100644 index 00000000000..3f354a7fa59 --- /dev/null +++ b/test/integration/render-tests/distance/layout-text-size/style.json @@ -0,0 +1,75 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "center": [ 0.0005, 0 ], + "zoom": 15, + "sources": { + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "Near" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + }, + { + "type": "Feature", + "properties": { + "name": "Far" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0.001, 0 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "text", + "type": "symbol", + "source": "point", + "layout": { + "text-field": ["get", "name"], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": [ + "case", + [ + ">", + [ + "distance", + { + "type": "Point", + "coordinates": [ + 0, + 0 + ] + } + ], + 100 + ], + 16, + 24 + ] + } + } + ] + } diff --git a/test/integration/render-tests/feature-state/change-zoom/expected.png b/test/integration/render-tests/feature-state/change-zoom/expected.png new file mode 100644 index 00000000000..d91b8b95aac Binary files /dev/null and b/test/integration/render-tests/feature-state/change-zoom/expected.png differ diff --git a/test/integration/render-tests/feature-state/change-zoom/style.json b/test/integration/render-tests/feature-state/change-zoom/style.json new file mode 100644 index 00000000000..eb6a76836de --- /dev/null +++ b/test/integration/render-tests/feature-state/change-zoom/style.json @@ -0,0 +1,69 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "operations": [ + [ + "setFeatureState", + { + "source": "mapbox", + "sourceLayer": "poi_label", + "id": "1000059876748" + }, + { + "color": "red" + } + ], + ["setZoom", 10.0 ], + [ + "wait" + ], + ["setZoom", 14.0 ], + [ + "wait" + ] + ] + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14, + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "poi_label", + "type": "circle", + "source": "mapbox", + "source-layer": "poi_label", + "paint": { + "circle-radius": 5, + "circle-color": [ + "coalesce", + [ + "feature-state", + "color" + ], + "black" + ] + } + } + ] +} diff --git a/test/integration/render-tests/fill-extrusion-base/property-function-terrain-flat/expected.png b/test/integration/render-tests/fill-extrusion-base/property-function-terrain-flat/expected.png new file mode 100644 index 00000000000..e9503bfd251 Binary files /dev/null and b/test/integration/render-tests/fill-extrusion-base/property-function-terrain-flat/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-base/property-function-terrain-flat/style.json b/test/integration/render-tests/fill-extrusion-base/property-function-terrain-flat/style.json new file mode 100644 index 00000000000..3968c4885da --- /dev/null +++ b/test/integration/render-tests/fill-extrusion-base/property-function-terrain-flat/style.json @@ -0,0 +1,156 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512 + } + }, + "center": [0.0005, 0.0005], + "terrain": { + "source": "rgbterrain", + "exaggeration": 0.05 + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/11-378-803.terrain.png" + ], + "maxzoom": 13, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 30, + "type": "building", + "height": 0 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 0.0001, + 0.0001 + ], + [ + 0.0001, + 0.0007 + ], + [ + 0.0007, + 0.0007 + ], + [ + 0.0007, + 0.0001 + ], + [ + 0.0001, + 0.0001 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "property": 20, + "type": "garage", + "height": 0 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 0.0002, + 0.0004 + ], + [ + 0.0004, + 0.0006 + ], + [ + 0.0006, + 0.0004 + ], + [ + 0.0004, + 0.0002 + ], + [ + 0.0002, + 0.0004 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "property": 10, + "type": "stable", + "height": 0 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 0.0003, + 0.0003 + ], + [ + 0.0003, + 0.0005 + ], + [ + 0.0005, + 0.0005 + ], + [ + 0.0005, + 0.0003 + ], + [ + 0.0003, + 0.0003 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 66, + "zoom": 18, + "bearing": 80, + "layers": [ + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 40, + "fill-extrusion-color": "red", + "fill-extrusion-opacity": 0.5, + "fill-extrusion-base": { + "stops": [[0,0],[100,100]], + "type": "exponential", + "property": "property" + } + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/fill-extrusion-base/property-function-terrain/expected.png b/test/integration/render-tests/fill-extrusion-base/property-function-terrain/expected.png new file mode 100644 index 00000000000..9076bb8d15c Binary files /dev/null and b/test/integration/render-tests/fill-extrusion-base/property-function-terrain/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-base/property-function-terrain/style.json b/test/integration/render-tests/fill-extrusion-base/property-function-terrain/style.json new file mode 100644 index 00000000000..2ed73b7e2e6 --- /dev/null +++ b/test/integration/render-tests/fill-extrusion-base/property-function-terrain/style.json @@ -0,0 +1,148 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512 + } + }, + "terrain": { + "source": "rgbterrain", + "exaggeration": 0.01 + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/11-378-804.terrain.png" + ], + "maxzoom": 13, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 30 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0003, + -0.0003 + ], + [ + -0.0003, + 0.0003 + ], + [ + 0.0003, + 0.0003 + ], + [ + 0.0003, + -0.0003 + ], + [ + -0.0003, + -0.0003 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "property": 20 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0002, + 0 + ], + [ + 0, + 0.0002 + ], + [ + 0.0002, + 0 + ], + [ + 0, + -0.0002 + ], + [ + -0.0002, + 0 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "property": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0001, + -0.0001 + ], + [ + -0.0001, + 0.0001 + ], + [ + 0.0001, + 0.0001 + ], + [ + 0.0001, + -0.0001 + ], + [ + -0.0001, + -0.0001 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 60, + "zoom": 18, + "layers": [ + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 40, + "fill-extrusion-color": "red", + "fill-extrusion-opacity": 0.5, + "fill-extrusion-base": { + "stops": [[0,0],[100,100]], + "type": "exponential", + "property": "property" + } + } + } + ] +} diff --git a/test/integration/render-tests/fill-extrusion-base/tile-border/expected.png b/test/integration/render-tests/fill-extrusion-base/tile-border/expected.png new file mode 100644 index 00000000000..bed2b18db87 Binary files /dev/null and b/test/integration/render-tests/fill-extrusion-base/tile-border/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-base/tile-border/style.json b/test/integration/render-tests/fill-extrusion-base/tile-border/style.json new file mode 100644 index 00000000000..0cd9837ad99 --- /dev/null +++ b/test/integration/render-tests/fill-extrusion-base/tile-border/style.json @@ -0,0 +1,84 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512 + } + }, + "sources": { + "rect": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 0.01, + 0.01 + ], + [ + 0.01, + -0.01 + ], + [ + 0.02, + -0.01 + ], + [ + 0.02, + 0.01 + ], + [ + 0.01, + 0.01 + ] + ], + [ + [ + 0.012, + -0.002 + ], + [ + 0.012, + -0.008 + ], + [ + 0.018, + -0.008 + ], + [ + 0.018, + -0.002 + ], + [ + 0.012, + -0.002 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 0, + "zoom": 13, + "center": [0.015, 0.0], + "layers": [ + { + "id": "rect", + "type": "fill-extrusion", + "source": "rect", + "paint": { + "fill-extrusion-height": 30 + } + } + ] +} diff --git a/test/integration/render-tests/fill-extrusion-pattern/multiple-layers-flat/expected.png b/test/integration/render-tests/fill-extrusion-pattern/multiple-layers-flat/expected.png new file mode 100644 index 00000000000..2f6723215a7 Binary files /dev/null and b/test/integration/render-tests/fill-extrusion-pattern/multiple-layers-flat/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-pattern/multiple-layers-flat/style.json b/test/integration/render-tests/fill-extrusion-pattern/multiple-layers-flat/style.json new file mode 100644 index 00000000000..4d91263fa92 --- /dev/null +++ b/test/integration/render-tests/fill-extrusion-pattern/multiple-layers-flat/style.json @@ -0,0 +1,211 @@ +{ + "version": 8, + "metadata": { + "description": "Buildings spanning to > 2 tiles don't get flat roofs.", + "test": { + "height": 256, + "operations": [ + ["wait"], + ["setTerrain", { + "source": "rgbterrain", + "exaggeration": 0.03 + }], + ["wait"] + ] + } + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/11-378-804.terrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "buffer": 0, + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "type": "building", + "height": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -0.0010, 0 ], + [ -0.0007, 0.0003 ], + [ -0.0003999, 0 ], + [ -0.0007, -0.0003 ], + [ -0.0010, 0 ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -0.0004, 0 ], + [ -0.0002, 0.0002 ], + [ 0, 0 ], + [ -0.0002, -0.0002 ], + [ -0.0004, 0 ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 5 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -0.0001, 0.00035 ], + [ -0.0001, 0.0004 ], + [ 0.0001, 0.0004 ], + [ 0.0001, 0.00035 ], + [ -0.0001, 0.00035 ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "conservatory", + "height": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -0.00003, 0.00036 ], + [ -0.00003, 0.00039 ], + [ 0.00003, 0.00039 ], + [ 0.00003, 0.00036 ], + [ -0.00003, 0.00036 ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 5 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ 0, 0 ], + [ -0.0001, 0.0003 ], + [ 0.0002, 0.0003 ], + [ 0.0002, -0.0001 ], + [ 0, 0 ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 5 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -0.0002, 0.0005 ], + [ -0.0002, 0.0007 ], + [ 0.0002, 0.0007 ], + [ 0.0002, 0.0005 ], + [ -0.0002, 0.0005 ] + ] + ] + } + } + ] + } + } + }, + "sprite": "local://sprites/emerald", + "pitch": 62.5, + "zoom": 18, + "bearing": -45, + "center": [-0.0002, 0.00012], + "layers": [ + { + "id": "extrusion", + "filter": [ + "==", + "type", + "building" + ], + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-pattern": "generic_icon", + "fill-extrusion-opacity": 0.5, + "fill-extrusion-height": { + "stops": [[0, 0], [100, 100]], + "property": "height" + } + } + }, + { + "id": "extrusion-copy", + "filter": [ + "==", + "type", + "building" + ], + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-pattern": "generic_icon", + "fill-extrusion-opacity": 0.5, + "fill-extrusion-height": { + "stops": [[0, 0], [100, 100]], + "property": "height" + } + } + }, + { + "id": "extrusion-overlap-in-different-layer-gets-flat", + "filter": [ + "==", + "type", + "conservatory" + ], + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-color": "green", + "fill-extrusion-opacity": 0.7, + "fill-extrusion-height": { + "stops": [[0, 0], [100, 100]], + "property": "height" + } + } + } + ] +} diff --git a/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat-on-border/expected.png b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat-on-border/expected.png new file mode 100644 index 00000000000..780135a6417 Binary files /dev/null and b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat-on-border/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat-on-border/style.json b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat-on-border/style.json new file mode 100644 index 00000000000..2044cb9153e --- /dev/null +++ b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat-on-border/style.json @@ -0,0 +1,170 @@ +{ + "version": 8, + "metadata": { + "description": "Tests various cases of flatRoofsUpdate().", + "test": { + "height": 256, + "operations": [ + ["wait"], + ["setTerrain", { + "source": "rgbterrain", + "exaggeration": 0.03 + }], + ["wait"] + ] + } + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/11-378-804.terrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "buffer": 0, + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "type": "building", + "height": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -0.0010, 0 ], + [ -0.0007, 0.0003 ], + [ -0.0003999, 0 ], + [ -0.0007, -0.0003 ], + [ -0.0010, 0 ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -0.0004, 0 ], + [ -0.0002, 0.0002 ], + [ 0, 0 ], + [ -0.0002, -0.0002 ], + [ -0.0004, 0 ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 5 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -0.0001, 0.00035 ], + [ -0.0001, 0.0004 ], + [ 0.0001, 0.0004 ], + [ 0.0001, 0.00035 ], + [ -0.0001, 0.00035 ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -0.00003, 0.00036 ], + [ -0.00003, 0.00039 ], + [ 0.00003, 0.00039 ], + [ 0.00003, 0.00036 ], + [ -0.00003, 0.00036 ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 5 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -0.0002, 0.0002 ], + [ -0.0002, 0.0003 ], + [ 0.0002, 0.0003 ], + [ 0.0002, 0.0002 ], + [ -0.0002, 0.0002 ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 5 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -0.0002, 0.0005 ], + [ -0.0002, 0.0007 ], + [ 0.0002, 0.0007 ], + [ 0.0002, 0.0005 ], + [ -0.0002, 0.0005 ] + ] + ] + } + } + ] + } + } + }, + "sprite": "local://sprites/emerald", + "pitch": 62.5, + "zoom": 18, + "bearing": -45, + "center": [-0.0002, 0.00012], + "layers": [ + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-pattern": "generic_icon", + "fill-extrusion-opacity": 0.5, + "fill-extrusion-height": { + "stops": [[0, 0], [100, 100]], + "property": "height" + } + } + } + ] +} diff --git a/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat/expected.png b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat/expected.png new file mode 100644 index 00000000000..1404d39a2ac Binary files /dev/null and b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat/style.json b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat/style.json new file mode 100644 index 00000000000..3c98e2f7c45 --- /dev/null +++ b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain-flat/style.json @@ -0,0 +1,120 @@ +{ + "version": 8, + "metadata": { + "description": "Flat roof centroid attribute array is uploaded after terrain gets enabled.", + "test": { + "height": 256, + "operations": [ + ["wait"], + ["setTerrain", { + "source": "rgbterrain", + "exaggeration": 0.05 + }], + ["wait"] + ] + } + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/12-759-1609.terrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "buffer": 0, + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "type": "building", + "height": 20 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0004, + -0.0002 + ], + [ + -0.0002, + 0 + ], + [ + 0, + -0.0002 + ], + [ + -0.0002, + -0.0004 + ], + [ + -0.0004, + -0.0002 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 20 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 0.0001, + 0.0001 + ], + [ + 0.0001, + 0.0005 + ], + [ + 0.0004, + 0.0005 + ], + [ + 0.0004, + 0.0001 + ], + [ + 0.0001, + 0.0001 + ] + ] + ] + } + } + ] + } + } + }, + "sprite": "local://sprites/emerald", + "pitch": 66, + "bearing": -60, + "zoom": 18, + "layers": [ + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-pattern": "generic_icon", + "fill-extrusion-opacity": 0.5, + "fill-extrusion-height": 10 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain/expected.png b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain/expected.png new file mode 100644 index 00000000000..72d416fc2a4 Binary files /dev/null and b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain/style.json b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain/style.json new file mode 100644 index 00000000000..bc5b9b152c6 --- /dev/null +++ b/test/integration/render-tests/fill-extrusion-pattern/opacity-terrain/style.json @@ -0,0 +1,106 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "terrain": { + "source": "rgbterrain", + "exaggeration": 0.02 + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/11-378-804.terrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "buffer": 0, + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0004, + 0 + ], + [ + -0.0002, + 0.0002 + ], + [ + 0, + 0 + ], + [ + -0.0002, + -0.0002 + ], + [ + -0.0004, + 0 + ] + ] + ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 0, + -0.0002 + ], + [ + 0, + 0.0002 + ], + [ + 0.0003, + 0.0002 + ], + [ + 0.0003, + -0.0002 + ], + [ + 0, + -0.0002 + ] + ] + ] + } + } + ] + } + } + }, + "sprite": "local://sprites/emerald", + "pitch": 60, + "zoom": 18, + "layers": [ + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-pattern": "generic_icon", + "fill-extrusion-opacity": 0.5, + "fill-extrusion-height": 10 + } + } + ] +} diff --git a/test/integration/render-tests/fill-extrusion-pattern/tile-buffer/expected.png b/test/integration/render-tests/fill-extrusion-pattern/tile-buffer/expected.png index 8b08b54ea97..5787258e6a0 100644 Binary files a/test/integration/render-tests/fill-extrusion-pattern/tile-buffer/expected.png and b/test/integration/render-tests/fill-extrusion-pattern/tile-buffer/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border/expected.png b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border/expected.png new file mode 100644 index 00000000000..46f46a66cb2 Binary files /dev/null and b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border/style.json b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border/style.json new file mode 100644 index 00000000000..e159f263482 --- /dev/null +++ b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border/style.json @@ -0,0 +1,70 @@ +{ + "version": 8, + "metadata": { + "description": "Tests various cases of flatRoofsUpdate().", + "test": { + "height": 256, + "operations": [ + ["wait"] + ] + } + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}-terrain-512.png" + ], + "maxzoom": 14, + "tileSize": 512 + }, + "mapbox": { + "type": "vector", + "maxzoom": 16, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "terrain": { + "source": "rgbterrain" + }, + "pitch": 43, + "bearing": -154.2, + "zoom": 19.6, + "center": [ + -122.4645969, + 37.7563445 + ], + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "blue" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-color": "#888", + "line-width": 10 + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "mapbox", + "source-layer": "building", + "filter": ["==", "extrude", "true"], + "paint": { + "fill-extrusion-color": "gray", + "fill-extrusion-opacity": 0.7, + "fill-extrusion-height": ["get", "height"] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/fill-extrusion-terrain/flat-roof/expected.png b/test/integration/render-tests/fill-extrusion-terrain/flat-roof/expected.png new file mode 100644 index 00000000000..4cdd0801637 Binary files /dev/null and b/test/integration/render-tests/fill-extrusion-terrain/flat-roof/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-terrain/flat-roof/style.json b/test/integration/render-tests/fill-extrusion-terrain/flat-roof/style.json new file mode 100644 index 00000000000..041e6099b17 --- /dev/null +++ b/test/integration/render-tests/fill-extrusion-terrain/flat-roof/style.json @@ -0,0 +1,69 @@ +{ + "version": 8, + "metadata": { + "description": "Verifies there is no assert mapbox-gl-js-internal#134", + "test": { + "height": 256, + "operations": [ + ["wait"] + ] + } + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}-terrain-512.png" + ], + "maxzoom": 14, + "tileSize": 512 + }, + "mapbox": { + "type": "vector", + "maxzoom": 16, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "terrain": { + "source": "rgbterrain" + }, + "pitch": 65, + "zoom": 17.5, + "center": [ + -122.464761, + 37.753219 + ], + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "green" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-color": "#888", + "line-width": 10 + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "mapbox", + "source-layer": "building", + "filter": ["==", "extrude", "true"], + "paint": { + "fill-extrusion-color": "gray", + "fill-extrusion-opacity": 0.5, + "fill-extrusion-height": ["get", "height"] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/fit-screen-coordinates/flat/expected.png b/test/integration/render-tests/fit-screen-coordinates/flat/expected.png new file mode 100644 index 00000000000..352bf9f9244 Binary files /dev/null and b/test/integration/render-tests/fit-screen-coordinates/flat/expected.png differ diff --git a/test/integration/render-tests/fit-screen-coordinates/flat/style.json b/test/integration/render-tests/fit-screen-coordinates/flat/style.json new file mode 100644 index 00000000000..992c1afb61d --- /dev/null +++ b/test/integration/render-tests/fit-screen-coordinates/flat/style.json @@ -0,0 +1,88 @@ +{ + "version": 8, + "metadata": { + "test": { + "description": "Validates the bounds of initial rect is within frustum, unset operation to visualize initial frame", + "height": 512, + "width": 512, + "operations": [ + ["fitScreenCoordinates", { + "x": 320, + "y": 200 + }, + { + "x": 400, + "y": 240 + }, + 0, + { + "duration":0 + }], + ["wait", 100] + ] + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0001, + -0.0001 + ], + [ + -0.0001, + 0.0001 + ], + [ + 0.0001, + 0.0001 + ], + [ + 0.0001, + -0.0001 + ], + [ + -0.0001, + -0.0001 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 60, + "zoom": 18, + "center": [ + -0.0003, + -0.0002 + ], + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "beige" + } + }, + { + "id": "fill", + "type": "fill", + "source": "geojson", + "paint": { + "fill-color": "red" + } + } + ] +} diff --git a/test/integration/render-tests/fit-screen-coordinates/horizon/expected.png b/test/integration/render-tests/fit-screen-coordinates/horizon/expected.png new file mode 100644 index 00000000000..98b4ab148d1 Binary files /dev/null and b/test/integration/render-tests/fit-screen-coordinates/horizon/expected.png differ diff --git a/test/integration/render-tests/fit-screen-coordinates/horizon/style.json b/test/integration/render-tests/fit-screen-coordinates/horizon/style.json new file mode 100644 index 00000000000..8ae622031c9 --- /dev/null +++ b/test/integration/render-tests/fit-screen-coordinates/horizon/style.json @@ -0,0 +1,46 @@ +{ + "version": 8, + "metadata": { + "test": { + "description": "Points over horizon should be no-op", + "height": 256, + "width": 512, + "operations": [ + ["fitScreenCoordinates", { + "x": 250, + "y": 30 + }, + { + "x": 260, + "y": 40 + }, + 0, + { + "duration":0 + }], + ["wait", 100] + ] + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [0, 90], + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/fit-screen-coordinates/terrain/expected.png b/test/integration/render-tests/fit-screen-coordinates/terrain/expected.png new file mode 100644 index 00000000000..a5c91282bf9 Binary files /dev/null and b/test/integration/render-tests/fit-screen-coordinates/terrain/expected.png differ diff --git a/test/integration/render-tests/fit-screen-coordinates/terrain/style.json b/test/integration/render-tests/fit-screen-coordinates/terrain/style.json new file mode 100644 index 00000000000..69c9077fb2d --- /dev/null +++ b/test/integration/render-tests/fit-screen-coordinates/terrain/style.json @@ -0,0 +1,58 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512, + "operations": [ + ["fitScreenCoordinates", { + "x": 115, + "y": 240 + }, + { + "x": 130, + "y": 260 + }, + 0, + { + "duration":0 + }], + ["wait", 100] + ] + } + }, + "center": [-113.26903, 35.9554], + "zoom": 11, + "pitch": 45, + "terrain": { + "source": "hillshade" + }, + "sources": { + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} diff --git a/test/integration/render-tests/free-camera/default/expected.png b/test/integration/render-tests/free-camera/default/expected.png new file mode 100644 index 00000000000..9df2c5ce82a Binary files /dev/null and b/test/integration/render-tests/free-camera/default/expected.png differ diff --git a/test/integration/render-tests/free-camera/default/style.json b/test/integration/render-tests/free-camera/default/style.json new file mode 100644 index 00000000000..e0d3c50c9f2 --- /dev/null +++ b/test/integration/render-tests/free-camera/default/style.json @@ -0,0 +1,18 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "operations": [ + ["setStyle", "local://styles/chicago.json"], + ["setZoom", 13 ], + ["setCenter", [-87.6942445, 41.8703965]], + ["lookAtPoint", [-87.6942445, 41.8703965], [-1, -1, 0]], + ["wait"] + ] + } + }, + "sources": {}, + "layers": [] +} diff --git a/test/integration/render-tests/free-camera/invalid-orientation/expected.png b/test/integration/render-tests/free-camera/invalid-orientation/expected.png new file mode 100644 index 00000000000..156e545b143 Binary files /dev/null and b/test/integration/render-tests/free-camera/invalid-orientation/expected.png differ diff --git a/test/integration/render-tests/free-camera/invalid-orientation/style.json b/test/integration/render-tests/free-camera/invalid-orientation/style.json new file mode 100644 index 00000000000..686892aa59a --- /dev/null +++ b/test/integration/render-tests/free-camera/invalid-orientation/style.json @@ -0,0 +1,18 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "operations": [ + ["setStyle", "local://styles/chicago.json"], + ["setZoom", 13 ], + ["setCenter", [-87.6942445, 41.9703965] ], + ["lookAtPoint", [-87.6942445, 41.8703965], [1, 0, 0]], + ["wait"] + ] + } + }, + "sources": {}, + "layers": [] +} diff --git a/test/integration/render-tests/free-camera/pitch-bearing/expected.png b/test/integration/render-tests/free-camera/pitch-bearing/expected.png new file mode 100644 index 00000000000..2d17d33d641 Binary files /dev/null and b/test/integration/render-tests/free-camera/pitch-bearing/expected.png differ diff --git a/test/integration/render-tests/free-camera/pitch-bearing/style.json b/test/integration/render-tests/free-camera/pitch-bearing/style.json new file mode 100644 index 00000000000..464a7da0920 --- /dev/null +++ b/test/integration/render-tests/free-camera/pitch-bearing/style.json @@ -0,0 +1,18 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "operations": [ + ["setStyle", "local://styles/chicago.json"], + ["setCameraPosition", [-87.7942445, 41.7703965, 10000.0]], + ["lookAtPoint", [-87.6942445, 41.8703965]], + ["setZoom", 13 ], + ["wait"] + ] + } + }, + "sources": {}, + "layers": [] +} diff --git a/test/integration/render-tests/free-camera/pixels-per-meter-scaling/expected.png b/test/integration/render-tests/free-camera/pixels-per-meter-scaling/expected.png new file mode 100644 index 00000000000..00541936881 Binary files /dev/null and b/test/integration/render-tests/free-camera/pixels-per-meter-scaling/expected.png differ diff --git a/test/integration/render-tests/free-camera/pixels-per-meter-scaling/style.json b/test/integration/render-tests/free-camera/pixels-per-meter-scaling/style.json new file mode 100644 index 00000000000..47d799f7504 --- /dev/null +++ b/test/integration/render-tests/free-camera/pixels-per-meter-scaling/style.json @@ -0,0 +1,65 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -1, + -1 + ], + [ + -1, + 1 + ], + [ + 1, + 1 + ], + [ + 1, + -1 + ], + [ + -1, + -1 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 60, + "zoom": 4, + "bearing": -35, + "layers": [ + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 500000, + "fill-extrusion-color": "blue" + } + } + ] +} diff --git a/test/integration/render-tests/free-camera/roll/expected.png b/test/integration/render-tests/free-camera/roll/expected.png new file mode 100644 index 00000000000..6b23a77f77e Binary files /dev/null and b/test/integration/render-tests/free-camera/roll/expected.png differ diff --git a/test/integration/render-tests/free-camera/roll/style.json b/test/integration/render-tests/free-camera/roll/style.json new file mode 100644 index 00000000000..db69bd11e38 --- /dev/null +++ b/test/integration/render-tests/free-camera/roll/style.json @@ -0,0 +1,19 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "operations": [ + ["setStyle", "local://styles/chicago.json"], + ["setZoom", 13 ], + ["setCenter", [-87.7942445, 41.8703965]], + ["lookAtPoint", [-87.6942445, 41.8703965], [1, 1, 1]], + ["setZoom", 13 ], + ["wait"] + ] + } + }, + "sources": {}, + "layers": [] +} diff --git a/test/integration/render-tests/free-camera/terrain/expected.png b/test/integration/render-tests/free-camera/terrain/expected.png new file mode 100644 index 00000000000..12c26186b7e Binary files /dev/null and b/test/integration/render-tests/free-camera/terrain/expected.png differ diff --git a/test/integration/render-tests/free-camera/terrain/style.json b/test/integration/render-tests/free-camera/terrain/style.json new file mode 100644 index 00000000000..a1826d48b3a --- /dev/null +++ b/test/integration/render-tests/free-camera/terrain/style.json @@ -0,0 +1,48 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512, + "operations": [ + ["lookAtPoint", [-113.336142, 35.937623]], + ["setZoom", 13], + ["wait"] + ] + } + }, + "center": [-113.26903, 35.9554], + "zoom": 12, + "pitch": 45, + "terrain": { + "source": "hillshade" + }, + "sources": { + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} diff --git a/test/integration/render-tests/geojson/clustered-properties-alt-expression/expected.png b/test/integration/render-tests/geojson/clustered-properties-alt-expression/expected.png new file mode 100644 index 00000000000..a1fb2060a2c Binary files /dev/null and b/test/integration/render-tests/geojson/clustered-properties-alt-expression/expected.png differ diff --git a/test/integration/render-tests/geojson/clustered-properties-alt-expression/style.json b/test/integration/render-tests/geojson/clustered-properties-alt-expression/style.json new file mode 100644 index 00000000000..014c8d43c75 --- /dev/null +++ b/test/integration/render-tests/geojson/clustered-properties-alt-expression/style.json @@ -0,0 +1,78 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 256, + "height": 128 + } + }, + "center": [ + -10, + -5 + ], + "zoom": 0, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://data/places.geojson", + "cluster": true, + "clusterRadius": 50, + "clusterProperties": { + "max": ["max", ["get", "scalerank"]], + "sum": [["+", ["number", ["accumulated"]], ["number", ["get", "sum"]]], ["get", "scalerank"]], + "has_island": ["any", ["==", ["get", "featureclass"], "island"]] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "cluster", + "type": "circle", + "source": "geojson", + "filter": [ + "==", + "cluster", + true + ], + "paint": { + "circle-color": ["case", ["get", "has_island"], "orange", "rgba(0, 200, 0, 1)"], + "circle-radius": 20 + } + }, + { + "id": "cluster_label", + "type": "symbol", + "source": "geojson", + "filter": [ + "==", + "cluster", + true + ], + "layout": { + "text-field": "{sum},{max}", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": 12, + "text-allow-overlap": true, + "text-ignore-placement": true + } + }, + { + "id": "unclustered_point", + "type": "circle", + "source": "geojson", + "filter": [ + "!=", + "cluster", + true + ], + "paint": { + "circle-color": "rgba(0, 0, 200, 1)", + "circle-radius": 10 + } + } + ] +} diff --git a/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-0/expected.png b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-0/expected.png new file mode 100644 index 00000000000..5d0f834b4b7 Binary files /dev/null and b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-0/expected.png differ diff --git a/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-0/style.json b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-0/style.json new file mode 100644 index 00000000000..a1395faef7b --- /dev/null +++ b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-0/style.json @@ -0,0 +1,33 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "Verify behavior on tile borders" + } + }, + "center": [-113.2935, 35.9529], + "zoom": 11.2, + "pitch": 30, + "sources": { + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-0/{z}-{x}-{y}.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + } + ] +} diff --git a/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-1/expected.png b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-1/expected.png new file mode 100644 index 00000000000..5d0f834b4b7 Binary files /dev/null and b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-1/expected.png differ diff --git a/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-1/style.json b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-1/style.json new file mode 100644 index 00000000000..ebb3523d173 --- /dev/null +++ b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-1/style.json @@ -0,0 +1,33 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "Verify behavior on tile borders" + } + }, + "center": [-113.2935, 35.9529], + "zoom": 11.2, + "pitch": 30, + "sources": { + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-1/{z}-{x}-{y}-1.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + } + ] +} diff --git a/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-2/expected.png b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-2/expected.png new file mode 100644 index 00000000000..5d0f834b4b7 Binary files /dev/null and b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-2/expected.png differ diff --git a/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-2/style.json b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-2/style.json new file mode 100644 index 00000000000..a4766259095 --- /dev/null +++ b/test/integration/render-tests/hillshade-buffer/tile-edge-buffer-2/style.json @@ -0,0 +1,33 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "Verify behavior on tile borders" + } + }, + "center": [-113.2935, 35.9529], + "zoom": 11.2, + "pitch": 30, + "sources": { + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-2/{z}-{x}-{y}-2.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + } + ] +} diff --git a/test/integration/render-tests/icon-rotate/with-offset/expected.png b/test/integration/render-tests/icon-rotate/with-offset/expected.png index dd68e281351..d3ef7202794 100644 Binary files a/test/integration/render-tests/icon-rotate/with-offset/expected.png and b/test/integration/render-tests/icon-rotate/with-offset/expected.png differ diff --git a/test/integration/render-tests/icon-rotate/with-offset/style.json b/test/integration/render-tests/icon-rotate/with-offset/style.json index f1048abf36d..a45ac195905 100644 --- a/test/integration/render-tests/icon-rotate/with-offset/style.json +++ b/test/integration/render-tests/icon-rotate/with-offset/style.json @@ -4,6 +4,7 @@ "test": { "width": 128, "height": 256, + "allowed": 0.005, "collisionDebug": true } }, diff --git a/test/integration/render-tests/icon-text-fit/text-variable-anchor-overlap/expected.png b/test/integration/render-tests/icon-text-fit/text-variable-anchor-overlap/expected.png index abc7219feb3..00d239d9eac 100644 Binary files a/test/integration/render-tests/icon-text-fit/text-variable-anchor-overlap/expected.png and b/test/integration/render-tests/icon-text-fit/text-variable-anchor-overlap/expected.png differ diff --git a/test/integration/render-tests/image/default-terrain/expected.png b/test/integration/render-tests/image/default-terrain/expected.png new file mode 100644 index 00000000000..7a388217eb8 Binary files /dev/null and b/test/integration/render-tests/image/default-terrain/expected.png differ diff --git a/test/integration/render-tests/image/default-terrain/style.json b/test/integration/render-tests/image/default-terrain/style.json new file mode 100644 index 00000000000..b6debab6a4d --- /dev/null +++ b/test/integration/render-tests/image/default-terrain/style.json @@ -0,0 +1,61 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512 + } + }, + "center": [ + -122.514426, + 37.562984 + ], + "zoom": 17, + "bearing": 120, + "pitch": 60, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "image": { + "type": "image", + "coordinates": [ + [ + -122.51596391201019, + 37.56238816766053 + ], + [ + -122.51467645168304, + 37.56410183312965 + ], + [ + -122.51309394836426, + 37.563391708549425 + ], + [ + -122.51423120498657, + 37.56161849366671 + ] + ], + "url": "local://image/0.png" + }, + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}-terrain-512.png" + ], + "maxzoom": 14, + "tileSize": 512 + } + }, + "layers": [ + { + "id": "image", + "type": "raster", + "source": "image", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/image/terrain-single-world/expected.png b/test/integration/render-tests/image/terrain-single-world/expected.png new file mode 100644 index 00000000000..118414ca216 Binary files /dev/null and b/test/integration/render-tests/image/terrain-single-world/expected.png differ diff --git a/test/integration/render-tests/image/terrain-single-world/style.json b/test/integration/render-tests/image/terrain-single-world/style.json new file mode 100644 index 00000000000..f9728724d4b --- /dev/null +++ b/test/integration/render-tests/image/terrain-single-world/style.json @@ -0,0 +1,91 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "description": "Single world copy verifies also that there are no tile skirts visible", + "operations": [ + ["wait"], + ["setRenderWorldCopies", false], + ["wait"] + ] + } + }, + "zoom": 0.99, + "center": [-180, 0], + "pitch": 85, + "bearing": 45, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + }, + "image": { + "type": "image", + "coordinates": [ + [ + -210, + 70 + ], + [ + 190, + 80 + ], + [ + 210, + -70 + ], + [ + -190, + -80 + ] + ], + "url": "local://image/1.png" + }, + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}-terrain-512.png" + ], + "maxzoom": 14, + "tileSize": 512 + } + }, + "transition": { + "duration": 0 + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "brown" + } + }, + { + "id": "land", + "type": "fill", + "source": "mapbox", + "source-layer": "water", + "paint": { + "fill-color": "purple" + } + }, + { + "id": "image", + "type": "raster", + "source": "image", + "paint": { + "raster-fade-duration": 0, + "raster-opacity": 0.3 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/image/wrap-terrain/expected.png b/test/integration/render-tests/image/wrap-terrain/expected.png new file mode 100644 index 00000000000..a2ced28c824 Binary files /dev/null and b/test/integration/render-tests/image/wrap-terrain/expected.png differ diff --git a/test/integration/render-tests/image/wrap-terrain/style.json b/test/integration/render-tests/image/wrap-terrain/style.json new file mode 100644 index 00000000000..db9789bf55b --- /dev/null +++ b/test/integration/render-tests/image/wrap-terrain/style.json @@ -0,0 +1,83 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512 + } + }, + "zoom": 0, + "center": [-180, 0], + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + }, + "image": { + "type": "image", + "coordinates": [ + [ + -210, + 70 + ], + [ + 190, + 80 + ], + [ + 210, + -70 + ], + [ + -190, + -80 + ] + ], + "url": "local://image/1.png" + }, + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}-terrain-512.png" + ], + "maxzoom": 14, + "tileSize": 512 + } + }, + "transition": { + "duration": 0 + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "land", + "type": "fill", + "source": "mapbox", + "source-layer": "water", + "paint": { + "fill-color": "lightblue" + } + }, + { + "id": "image", + "type": "raster", + "source": "image", + "paint": { + "raster-fade-duration": 0, + "raster-opacity": 0.3 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/image/wrap/expected.png b/test/integration/render-tests/image/wrap/expected.png new file mode 100644 index 00000000000..a2ced28c824 Binary files /dev/null and b/test/integration/render-tests/image/wrap/expected.png differ diff --git a/test/integration/render-tests/image/wrap/style.json b/test/integration/render-tests/image/wrap/style.json new file mode 100644 index 00000000000..8d838821ab8 --- /dev/null +++ b/test/integration/render-tests/image/wrap/style.json @@ -0,0 +1,72 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512 + } + }, + "zoom": 0, + "center": [-180, 0], + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + }, + "image": { + "type": "image", + "coordinates": [ + [ + -210, + 70 + ], + [ + 190, + 80 + ], + [ + 210, + -70 + ], + [ + -190, + -80 + ] + ], + "url": "local://image/1.png" + } + }, + "transition": { + "duration": 0 + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "land", + "type": "fill", + "source": "mapbox", + "source-layer": "water", + "paint": { + "fill-color": "lightblue" + } + }, + { + "id": "image", + "type": "raster", + "source": "image", + "paint": { + "raster-fade-duration": 0, + "raster-opacity": 0.3 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/line-dasharray/case/round/expected.png b/test/integration/render-tests/line-dasharray/case/round/expected.png index 701bdce7e06..310c9429d12 100644 Binary files a/test/integration/render-tests/line-dasharray/case/round/expected.png and b/test/integration/render-tests/line-dasharray/case/round/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/line-metrics/expected.png b/test/integration/render-tests/line-dasharray/line-metrics/expected.png new file mode 100644 index 00000000000..d253a63d133 Binary files /dev/null and b/test/integration/render-tests/line-dasharray/line-metrics/expected.png differ diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#3682/style.json b/test/integration/render-tests/line-dasharray/line-metrics/style.json similarity index 55% rename from test/integration/render-tests/regressions/mapbox-gl-js#3682/style.json rename to test/integration/render-tests/line-dasharray/line-metrics/style.json index ed5b8bb3ff0..87751487997 100644 --- a/test/integration/render-tests/regressions/mapbox-gl-js#3682/style.json +++ b/test/integration/render-tests/line-dasharray/line-metrics/style.json @@ -2,49 +2,56 @@ "version": 8, "metadata": { "test": { - "width": 64, - "height": 64 + "height": 256 } }, + "center": [ + 49.9604, + 16.6676 + ], + "zoom": 7, "sources": { "geojson": { "type": "geojson", "data": { "type": "Feature", - "properties": { - "width": 5 - }, "geometry": { "type": "LineString", "coordinates": [ [ - 0, - -20 + 46.9604, + 13.6676 ], [ - 0, - 20 + 54.8909, + 21.0603 ] ] } - } + }, + "lineMetrics": true } }, "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#FFF" + } + }, { "id": "line", "type": "line", "source": "geojson", "paint": { + "line-color": "#000", "line-dasharray": [ - 1, - 1 + 2, + 4 ], - "line-width": { - "type": "identity", - "property": "width" - } + "line-width": 15 } } ] -} \ No newline at end of file +} diff --git a/test/integration/render-tests/line-dasharray/overscaled-terrain/expected.png b/test/integration/render-tests/line-dasharray/overscaled-terrain/expected.png new file mode 100644 index 00000000000..73e81e47618 Binary files /dev/null and b/test/integration/render-tests/line-dasharray/overscaled-terrain/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/overscaled-terrain/style.json b/test/integration/render-tests/line-dasharray/overscaled-terrain/style.json new file mode 100644 index 00000000000..7419e1f48c5 --- /dev/null +++ b/test/integration/render-tests/line-dasharray/overscaled-terrain/style.json @@ -0,0 +1,56 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 17, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-width": 2, + "line-color": "#000", + "line-dasharray": [ + 2.5, + 2.5 + ] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/line-dasharray/overscaled/expected.png b/test/integration/render-tests/line-dasharray/overscaled/expected.png index 7b495d58ee7..e58dddd4be0 100644 Binary files a/test/integration/render-tests/line-dasharray/overscaled/expected.png and b/test/integration/render-tests/line-dasharray/overscaled/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/slant/expected.png b/test/integration/render-tests/line-dasharray/slant/expected.png index a96453abda0..896a9de6cf4 100644 Binary files a/test/integration/render-tests/line-dasharray/slant/expected.png and b/test/integration/render-tests/line-dasharray/slant/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/unusual-cases/empty-array/expected.png b/test/integration/render-tests/line-dasharray/unusual-cases/empty-array/expected.png new file mode 100644 index 00000000000..981f140d098 Binary files /dev/null and b/test/integration/render-tests/line-dasharray/unusual-cases/empty-array/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/unusual-cases/empty-array/style.json b/test/integration/render-tests/line-dasharray/unusual-cases/empty-array/style.json new file mode 100644 index 00000000000..01436328f8f --- /dev/null +++ b/test/integration/render-tests/line-dasharray/unusual-cases/empty-array/style.json @@ -0,0 +1,42 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "LineString", + "coordinates": [ + [ + -10, + 0 + ], + [ + 10, + 0 + ] + ] + } + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "geojson", + "layout": { + "line-cap": "round" + }, + "paint": { + "line-width": 8, + "line-dasharray": [], + "line-color": "blue" + } + } + ] +} diff --git a/test/integration/render-tests/line-dasharray/unusual-cases/negative-values/expected.png b/test/integration/render-tests/line-dasharray/unusual-cases/negative-values/expected.png new file mode 100644 index 00000000000..525b18def2b Binary files /dev/null and b/test/integration/render-tests/line-dasharray/unusual-cases/negative-values/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/unusual-cases/negative-values/style.json b/test/integration/render-tests/line-dasharray/unusual-cases/negative-values/style.json new file mode 100644 index 00000000000..4895823bb06 --- /dev/null +++ b/test/integration/render-tests/line-dasharray/unusual-cases/negative-values/style.json @@ -0,0 +1,39 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "LineString", + "coordinates": [ + [ + -10, + 0 + ], + [ + 10, + 0 + ] + ] + } + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "geojson", + "paint": { + "line-width": 8, + "line-dasharray": [1, -2, -1, 1], + "line-color": "blue" + } + } + ] +} diff --git a/test/integration/render-tests/line-dasharray/unusual-cases/zero-values/expected.png b/test/integration/render-tests/line-dasharray/unusual-cases/zero-values/expected.png new file mode 100644 index 00000000000..f00b82e0fda Binary files /dev/null and b/test/integration/render-tests/line-dasharray/unusual-cases/zero-values/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/unusual-cases/zero-values/style.json b/test/integration/render-tests/line-dasharray/unusual-cases/zero-values/style.json new file mode 100644 index 00000000000..c216c7e1a85 --- /dev/null +++ b/test/integration/render-tests/line-dasharray/unusual-cases/zero-values/style.json @@ -0,0 +1,42 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "LineString", + "coordinates": [ + [ + -10, + 0 + ], + [ + 10, + 0 + ] + ] + } + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "geojson", + "layout": { + "line-cap": "round" + }, + "paint": { + "line-width": 8, + "line-dasharray": [0, 0, 0, 0], + "line-color": "blue" + } + } + ] +} diff --git a/test/integration/render-tests/line-dasharray/zoom-history-line-metrics/expected.png b/test/integration/render-tests/line-dasharray/zoom-history-line-metrics/expected.png new file mode 100644 index 00000000000..70d853e0fcf Binary files /dev/null and b/test/integration/render-tests/line-dasharray/zoom-history-line-metrics/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/zoom-history-line-metrics/style.json b/test/integration/render-tests/line-dasharray/zoom-history-line-metrics/style.json new file mode 100644 index 00000000000..2af7c8f9967 --- /dev/null +++ b/test/integration/render-tests/line-dasharray/zoom-history-line-metrics/style.json @@ -0,0 +1,54 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64, + "operations": [ + [ + "wait" + ], + [ + "setZoom", + 1 + ], + [ + "wait" + ] + ] + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "LineString", + "coordinates": [ + [ + -150, + -75 + ], + [ + 150, + 75 + ] + ] + }, + "lineMetrics": true + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "geojson", + "paint": { + "line-dasharray": [ + 1, + 2 + ], + "line-width": 16 + } + } + ] +} diff --git a/test/integration/render-tests/line-gradient/gradient-tile-boundaries-terrain/expected.png b/test/integration/render-tests/line-gradient/gradient-tile-boundaries-terrain/expected.png new file mode 100644 index 00000000000..af0313f6045 Binary files /dev/null and b/test/integration/render-tests/line-gradient/gradient-tile-boundaries-terrain/expected.png differ diff --git a/test/integration/render-tests/line-gradient/gradient-tile-boundaries-terrain/style.json b/test/integration/render-tests/line-gradient/gradient-tile-boundaries-terrain/style.json new file mode 100644 index 00000000000..a7671acf8c1 --- /dev/null +++ b/test/integration/render-tests/line-gradient/gradient-tile-boundaries-terrain/style.json @@ -0,0 +1,68 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 128 + } + }, + "center": [ + -77.02803308586635, + 38.891047607560125 + ], + "zoom": 18, + "terrain": { + "source": "dem" + }, + "sources": { + "dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/no/{z}-{x}-{y}.terrain.512.png" + ], + "maxzoom": 15, + "tileSize": 512 + }, + "gradient": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-77.028035, 38.890600 ], + [-77.028035, 38.891088 ] + ] + } + }, + "lineMetrics": true + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "gradient", + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": 15, + "line-opacity": 0.5, + "line-gradient": [ + "interpolate", + ["linear"], + ["line-progress"], + 0, "rgba(0, 0, 255, 0)", + 0.1, "royalblue", + 0.3, "cyan", + 0.5, "lime", + 0.7, "yellow", + 1, "red" + ] + } + } + ] +} diff --git a/test/integration/render-tests/line-pattern/line-metrics/expected.png b/test/integration/render-tests/line-pattern/line-metrics/expected.png new file mode 100644 index 00000000000..582d5712606 Binary files /dev/null and b/test/integration/render-tests/line-pattern/line-metrics/expected.png differ diff --git a/test/integration/render-tests/line-pattern/line-metrics/style.json b/test/integration/render-tests/line-pattern/line-metrics/style.json new file mode 100644 index 00000000000..9e263009dce --- /dev/null +++ b/test/integration/render-tests/line-pattern/line-metrics/style.json @@ -0,0 +1,55 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "center": [ + 49.9604, + 16.6676 + ], + "zoom": 7, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 46.9604, + 13.6676 + ], + [ + 54.8909, + 21.0603 + ] + ] + } + }, + "lineMetrics": true + } + }, + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#FFF" + } + }, + { + "id": "line", + "type": "line", + "source": "geojson", + "paint": { + "line-color": "#000", + "line-pattern": "generic_icon", + "line-width": 25 + } + } + ] +} diff --git a/test/integration/render-tests/line-pattern/pitch-terrain/expected.png b/test/integration/render-tests/line-pattern/pitch-terrain/expected.png new file mode 100644 index 00000000000..b2325dbd05a Binary files /dev/null and b/test/integration/render-tests/line-pattern/pitch-terrain/expected.png differ diff --git a/test/integration/render-tests/line-pattern/pitch-terrain/style.json b/test/integration/render-tests/line-pattern/pitch-terrain/style.json new file mode 100644 index 00000000000..e28cd882951 --- /dev/null +++ b/test/integration/render-tests/line-pattern/pitch-terrain/style.json @@ -0,0 +1,55 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 17, + "pitch": 60, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-width": 20, + "line-pattern": "generic_icon", + "line-opacity": 0.5 + } + } + ] +} diff --git a/test/integration/render-tests/line-pattern/pitch/expected.png b/test/integration/render-tests/line-pattern/pitch/expected.png index b3f8eebeda0..efd96984e09 100644 Binary files a/test/integration/render-tests/line-pattern/pitch/expected.png and b/test/integration/render-tests/line-pattern/pitch/expected.png differ diff --git a/test/integration/render-tests/line-translate-anchor/viewport-terrain/expected.png b/test/integration/render-tests/line-translate-anchor/viewport-terrain/expected.png new file mode 100644 index 00000000000..85f5b76a3f9 Binary files /dev/null and b/test/integration/render-tests/line-translate-anchor/viewport-terrain/expected.png differ diff --git a/test/integration/render-tests/line-translate-anchor/viewport-terrain/style.json b/test/integration/render-tests/line-translate-anchor/viewport-terrain/style.json new file mode 100644 index 00000000000..4cc7ae64e52 --- /dev/null +++ b/test/integration/render-tests/line-translate-anchor/viewport-terrain/style.json @@ -0,0 +1,58 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14, + "bearing": 90, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-width": 2, + "line-color": "#000", + "line-translate": [ + 10, + 10 + ], + "line-translate-anchor": "viewport" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/line-translate/literal-rotated-terrain/expected.png b/test/integration/render-tests/line-translate/literal-rotated-terrain/expected.png new file mode 100644 index 00000000000..09f2143ee31 Binary files /dev/null and b/test/integration/render-tests/line-translate/literal-rotated-terrain/expected.png differ diff --git a/test/integration/render-tests/line-translate/literal-rotated-terrain/style.json b/test/integration/render-tests/line-translate/literal-rotated-terrain/style.json new file mode 100644 index 00000000000..97042c770dd --- /dev/null +++ b/test/integration/render-tests/line-translate/literal-rotated-terrain/style.json @@ -0,0 +1,58 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14, + "pitch": 60, + "bearing": 45, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-width": 2, + "line-color": "#000", + "line-translate": [ + 5, + 5 + ] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/raster-masking/terrain/overlapping-zoom/expected.png b/test/integration/render-tests/raster-masking/terrain/overlapping-zoom/expected.png new file mode 100644 index 00000000000..b4cdd24619c Binary files /dev/null and b/test/integration/render-tests/raster-masking/terrain/overlapping-zoom/expected.png differ diff --git a/test/integration/render-tests/raster-masking/terrain/overlapping-zoom/style.json b/test/integration/render-tests/raster-masking/terrain/overlapping-zoom/style.json new file mode 100644 index 00000000000..33c4c405755 --- /dev/null +++ b/test/integration/render-tests/raster-masking/terrain/overlapping-zoom/style.json @@ -0,0 +1,62 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "operations": [ + [ + "setZoom", + 13, + null + ], + [ + "wait" + ] + ] + } + }, + "center": [ + -122.48, + 37.84 + ], + "zoom": 14, + "terrain": { + "source": "dem" + }, + "sources": { + "contour": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.contour.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/no/{z}-{x}-{y}.terrain.512.png" + ], + "maxzoom": 15, + "tileSize": 512 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "raster", + "type": "raster", + "source": "contour", + "paint": { + "raster-fade-duration": 0, + "raster-opacity": 0.5 + } + } + ] +} diff --git a/test/integration/render-tests/real-world/sanfrancisco-terrain-draped-movelayer/expected.png b/test/integration/render-tests/real-world/sanfrancisco-terrain-draped-movelayer/expected.png new file mode 100644 index 00000000000..3f6ccc7a276 Binary files /dev/null and b/test/integration/render-tests/real-world/sanfrancisco-terrain-draped-movelayer/expected.png differ diff --git a/test/integration/render-tests/real-world/sanfrancisco-terrain-draped-movelayer/style.json b/test/integration/render-tests/real-world/sanfrancisco-terrain-draped-movelayer/style.json new file mode 100644 index 00000000000..51cfff4c93e --- /dev/null +++ b/test/integration/render-tests/real-world/sanfrancisco-terrain-draped-movelayer/style.json @@ -0,0 +1,39 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "terrainDrapeFirst": true, + "description": ["terrainDrapeFirst causes that we render cache is used when rendering.", + "There is no terrain data available and the expected.png (for zoom 15) is copied from 2D (sanfrancisco render test).", + " It is noticeable that actual.png here is a bit different from expected.png: `A` in 'Masonic Ave' is rendered over contour line", + "as expected in drape first and cached mode"], + "operations": [ + ["setStyle", "local://styles/sanfrancisco.json"], + ["setZoom", 15.1 ], + ["setCenter", [-122.448635, 37.7669995] ], + ["wait"], + ["addSource", "rgbterrain", { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.noterrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + }], + ["setTerrain", {"source": "rgbterrain"}], + ["wait"], + ["setZoom", 15.2], + ["moveLayer", "road_major", "road_minor"], + ["moveLayer", "contour-line", "building"], + ["wait"], + ["setZoom", 15], + ["wait"], + ["wait"] + ] + } + }, + "sources": {}, + "layers": [] +} diff --git a/test/integration/render-tests/real-world/sanfrancisco-terrain-draped/expected.png b/test/integration/render-tests/real-world/sanfrancisco-terrain-draped/expected.png new file mode 100644 index 00000000000..3ef52e44dd9 Binary files /dev/null and b/test/integration/render-tests/real-world/sanfrancisco-terrain-draped/expected.png differ diff --git a/test/integration/render-tests/real-world/sanfrancisco-terrain-draped/style.json b/test/integration/render-tests/real-world/sanfrancisco-terrain-draped/style.json new file mode 100644 index 00000000000..d49ce7ac794 --- /dev/null +++ b/test/integration/render-tests/real-world/sanfrancisco-terrain-draped/style.json @@ -0,0 +1,37 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "terrainDrapeFirst": true, + "description": ["terrainDrapeFirst causes that we render cache is used when rendering.", + "There is no terrain data available and the expected.png (for zoom 15) is copied from 2D (sanfrancisco render test).", + " It is noticeable that actual.png here is a bit different from expected.png: `A` in 'Masonic Ave' is rendered over contour line", + "as expected in drape first and cached mode"], + "operations": [ + ["setStyle", "local://styles/sanfrancisco.json"], + ["setZoom", 15.1 ], + ["setCenter", [-122.448635, 37.7669995] ], + ["wait"], + ["addSource", "rgbterrain", { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.noterrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + }], + ["setTerrain", {"source": "rgbterrain"}], + ["wait"], + ["setZoom", 15.2], + ["wait"], + ["setZoom", 15], + ["wait"], + ["wait"] + ] + } + }, + "sources": {}, + "layers": [] +} diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#2467/style.json b/test/integration/render-tests/regressions/mapbox-gl-js#2467/style.json index ffb9d77b467..668bdd2e12e 100644 --- a/test/integration/render-tests/regressions/mapbox-gl-js#2467/style.json +++ b/test/integration/render-tests/regressions/mapbox-gl-js#2467/style.json @@ -2,6 +2,7 @@ "version": 8, "metadata": { "test": { + "fadeDuration": 1000, "width": 64, "height": 64, "description": "Tests that raster tiles are retained for cross-fading. The first pair of wait operations ensures that z1 tiles are fully faded in. The third wait ensures that v0 tiles are loaded, and the last two waits fade them halfway in.", diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#3682/expected.png b/test/integration/render-tests/regressions/mapbox-gl-js#3682/expected.png deleted file mode 100644 index f5be29d3660..00000000000 Binary files a/test/integration/render-tests/regressions/mapbox-gl-js#3682/expected.png and /dev/null differ diff --git a/test/integration/render-tests/runtime-styling/image-update-icon/expected.png b/test/integration/render-tests/runtime-styling/image-update-icon/expected.png index 027baead205..20a306d63c2 100644 Binary files a/test/integration/render-tests/runtime-styling/image-update-icon/expected.png and b/test/integration/render-tests/runtime-styling/image-update-icon/expected.png differ diff --git a/test/integration/render-tests/runtime-styling/image-update-pattern/expected.png b/test/integration/render-tests/runtime-styling/image-update-pattern/expected.png index 0fc06575a26..662b8aebb6c 100644 Binary files a/test/integration/render-tests/runtime-styling/image-update-pattern/expected.png and b/test/integration/render-tests/runtime-styling/image-update-pattern/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-blend/fill-opaque/expected.png b/test/integration/render-tests/skybox/atmosphere-blend/fill-opaque/expected.png new file mode 100644 index 00000000000..959a3486c9c Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere-blend/fill-opaque/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-blend/fill-opaque/style.json b/test/integration/render-tests/skybox/atmosphere-blend/fill-opaque/style.json new file mode 100644 index 00000000000..ec9a35e3a0e --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere-blend/fill-opaque/style.json @@ -0,0 +1,86 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ [ -0.0003, -0.00047 ], + [ -0.0003, -0.00017 ], + [ 0, -0.00017 ], + [ 0, -0.00047 ], + [ -0.0003, -0.00047 ] ] + ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ [ -0.00015, -0.00015 ], + [ -0.00015, 0.00015 ], + [ 0.00015, 0.00015 ], + [ 0.00015, -0.00015 ], + [ -0.00015, -0.00015 ] ] + ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ [ 0, 0.00017 ], + [ 0, 0.00047 ], + [ 0.0003, 0.00047 ], + [ 0.0003, 0.00017 ], + [ 0, 0.00017 ] ] + ] + } + } + ] + } + } + }, + "zoom": 19, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun-intensity": 30, + "sky-atmosphere-sun": [0, 0] + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 10 + } + } + ] +} diff --git a/test/integration/render-tests/skybox/atmosphere-blend/fill-transparent/expected.png b/test/integration/render-tests/skybox/atmosphere-blend/fill-transparent/expected.png new file mode 100644 index 00000000000..9a63c5cc9aa Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere-blend/fill-transparent/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-blend/fill-transparent/style.json b/test/integration/render-tests/skybox/atmosphere-blend/fill-transparent/style.json new file mode 100644 index 00000000000..70c0f5e4e88 --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere-blend/fill-transparent/style.json @@ -0,0 +1,87 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ [ -0.0003, -0.00047 ], + [ -0.0003, -0.00017 ], + [ 0, -0.00017 ], + [ 0, -0.00047 ], + [ -0.0003, -0.00047 ] ] + ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ [ -0.00015, -0.00015 ], + [ -0.00015, 0.00015 ], + [ 0.00015, 0.00015 ], + [ 0.00015, -0.00015 ], + [ -0.00015, -0.00015 ] ] + ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ [ 0, 0.00017 ], + [ 0, 0.00047 ], + [ 0.0003, 0.00047 ], + [ 0.0003, 0.00017 ], + [ 0, 0.00017 ] ] + ] + } + } + ] + } + } + }, + "zoom": 19, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun-intensity": 30, + "sky-atmosphere-sun": [0, 0] + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "rgba(255,255,255,0.8)" + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 10, + "fill-extrusion-opacity": 0.2 + } + } + ] +} diff --git a/test/integration/render-tests/skybox/atmosphere-color/expected.png b/test/integration/render-tests/skybox/atmosphere-color/expected.png new file mode 100644 index 00000000000..96075032652 Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere-color/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-color/style.json b/test/integration/render-tests/skybox/atmosphere-color/style.json new file mode 100644 index 00000000000..fbb00715c47 --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere-color/style.json @@ -0,0 +1,33 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": {}, + "center": [-113.26903, 35.9654], + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-color": "rgba(200,150,100,0.5)", + "sky-atmosphere-halo-color": "rgba(255,255,0,0.5)", + "sky-atmosphere-sun": [0, 90], + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/atmosphere-horizon/expected.png b/test/integration/render-tests/skybox/atmosphere-horizon/expected.png new file mode 100644 index 00000000000..0f69731ed57 Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere-horizon/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-horizon/style.json b/test/integration/render-tests/skybox/atmosphere-horizon/style.json new file mode 100644 index 00000000000..882078dc88f --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere-horizon/style.json @@ -0,0 +1,30 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [0, 90], + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/atmosphere-intensity/high/expected.png b/test/integration/render-tests/skybox/atmosphere-intensity/high/expected.png new file mode 100644 index 00000000000..8cbda31b8b4 Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere-intensity/high/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-intensity/high/style.json b/test/integration/render-tests/skybox/atmosphere-intensity/high/style.json new file mode 100644 index 00000000000..fef923a01e4 --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere-intensity/high/style.json @@ -0,0 +1,30 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [0, 0], + "sky-atmosphere-sun-intensity": 25 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/atmosphere-intensity/low/expected.png b/test/integration/render-tests/skybox/atmosphere-intensity/low/expected.png new file mode 100644 index 00000000000..742ac47d0c1 Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere-intensity/low/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-intensity/low/style.json b/test/integration/render-tests/skybox/atmosphere-intensity/low/style.json new file mode 100644 index 00000000000..0219144bfa5 --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere-intensity/low/style.json @@ -0,0 +1,30 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [0, 0], + "sky-atmosphere-sun-intensity": 5 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/atmosphere-intensity/medium/expected.png b/test/integration/render-tests/skybox/atmosphere-intensity/medium/expected.png new file mode 100644 index 00000000000..a6b8008858c Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere-intensity/medium/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-intensity/medium/style.json b/test/integration/render-tests/skybox/atmosphere-intensity/medium/style.json new file mode 100644 index 00000000000..69667f73de1 --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere-intensity/medium/style.json @@ -0,0 +1,30 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [0, 0], + "sky-atmosphere-sun-intensity": 15 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/atmosphere-mie/expected.png b/test/integration/render-tests/skybox/atmosphere-mie/expected.png new file mode 100644 index 00000000000..b47e74100fd Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere-mie/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-mie/style.json b/test/integration/render-tests/skybox/atmosphere-mie/style.json new file mode 100644 index 00000000000..13d071d9332 --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere-mie/style.json @@ -0,0 +1,32 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-color": "rgba(0,0,0,0)", + "sky-atmosphere-halo-color": "rgba(255,255,255,0.1)", + "sky-atmosphere-sun": [0, 90], + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "gray" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/atmosphere-rayleigh/expected.png b/test/integration/render-tests/skybox/atmosphere-rayleigh/expected.png new file mode 100644 index 00000000000..6d64b3fa2eb Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere-rayleigh/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-rayleigh/style.json b/test/integration/render-tests/skybox/atmosphere-rayleigh/style.json new file mode 100644 index 00000000000..0f9e72b83d1 --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere-rayleigh/style.json @@ -0,0 +1,24 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-halo-color": "rgba(0,0,0,0)", + "sky-atmosphere-sun": [0, 90], + "sky-atmosphere-sun-intensity": 30 + } + } + ] +} diff --git a/test/integration/render-tests/skybox/atmosphere-terrain/expected.png b/test/integration/render-tests/skybox/atmosphere-terrain/expected.png new file mode 100644 index 00000000000..6d0242182e1 Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere-terrain/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-terrain/style.json b/test/integration/render-tests/skybox/atmosphere-terrain/style.json new file mode 100644 index 00000000000..4af7a4a646c --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere-terrain/style.json @@ -0,0 +1,62 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512, + "operations": [ + ["wait"] + ] + } + }, + "center": [-113.27903, 35.98], + "zoom": 13.5, + "pitch": 85, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "red" + } + }, + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [0, 0], + "sky-atmosphere-sun-intensity": 30 + } + } + ] +} diff --git a/test/integration/render-tests/skybox/atmosphere-update/expected.png b/test/integration/render-tests/skybox/atmosphere-update/expected.png new file mode 100644 index 00000000000..1a062048287 Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere-update/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere-update/style.json b/test/integration/render-tests/skybox/atmosphere-update/style.json new file mode 100644 index 00000000000..ab086ddb73b --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere-update/style.json @@ -0,0 +1,46 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 256, + "height": 256, + "operations": [ + [ + "setPaintProperty", + "sky", + "sky-atmosphere-halo-color", + "rgba(0,0,0,0)" + ], + [ + "setPaintProperty", + "sky", + "sky-atmosphere-sun-intensity", + 30.0 + ], + [ + "wait" + ] + ] + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [0, 90] + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/atmosphere/expected.png b/test/integration/render-tests/skybox/atmosphere/expected.png new file mode 100644 index 00000000000..b4e80a72aa7 Binary files /dev/null and b/test/integration/render-tests/skybox/atmosphere/expected.png differ diff --git a/test/integration/render-tests/skybox/atmosphere/style.json b/test/integration/render-tests/skybox/atmosphere/style.json new file mode 100644 index 00000000000..c32e131da8c --- /dev/null +++ b/test/integration/render-tests/skybox/atmosphere/style.json @@ -0,0 +1,30 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [0, 0], + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/compositing/alpha-blend/expected.png b/test/integration/render-tests/skybox/compositing/alpha-blend/expected.png new file mode 100644 index 00000000000..c8c4cf73881 Binary files /dev/null and b/test/integration/render-tests/skybox/compositing/alpha-blend/expected.png differ diff --git a/test/integration/render-tests/skybox/compositing/alpha-blend/style.json b/test/integration/render-tests/skybox/compositing/alpha-blend/style.json new file mode 100644 index 00000000000..c078c1a23c1 --- /dev/null +++ b/test/integration/render-tests/skybox/compositing/alpha-blend/style.json @@ -0,0 +1,59 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "blue-fill", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient-center": [0, 90], + "sky-gradient": [ + "interpolate", + ["linear"], + ["sky-radial-progress"], + 0, + "green", + 1, + "green" + ] + } + }, + { + "id": "red-fill", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient-center": [0, 90], + "sky-gradient": [ + "interpolate", + ["linear"], + ["sky-radial-progress"], + 0, + "red", + 0.99, + "red", + 1, + "rgba(0,0,0,0)" + ], + "sky-gradient-radius": 20, + "sky-opacity": 0.5 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/compositing/base/expected.png b/test/integration/render-tests/skybox/compositing/base/expected.png new file mode 100644 index 00000000000..646e30873a6 Binary files /dev/null and b/test/integration/render-tests/skybox/compositing/base/expected.png differ diff --git a/test/integration/render-tests/skybox/compositing/base/style.json b/test/integration/render-tests/skybox/compositing/base/style.json new file mode 100644 index 00000000000..5ee703436e7 --- /dev/null +++ b/test/integration/render-tests/skybox/compositing/base/style.json @@ -0,0 +1,60 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 512 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [0, 0], + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "rainbow", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient-center": [0, 90], + "sky-gradient": [ + "interpolate", + ["linear"], + ["sky-radial-progress"], + 0.49, + "rgba(0,0,0,0)", + 0.5, + "blue", + 0.6, + "royalblue", + 0.7, + "cyan", + 0.8, + "lime", + 0.9, + "yellow", + 0.99, + "red", + 1, + "rgba(0,0,0,0)" + ], + "sky-gradient-radius": 20 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/above/expected.png b/test/integration/render-tests/skybox/fill-extrusion-light/above/expected.png new file mode 100644 index 00000000000..f6a0639914e Binary files /dev/null and b/test/integration/render-tests/skybox/fill-extrusion-light/above/expected.png differ diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/above/style.json b/test/integration/render-tests/skybox/fill-extrusion-light/above/style.json new file mode 100644 index 00000000000..e2c74672464 --- /dev/null +++ b/test/integration/render-tests/skybox/fill-extrusion-light/above/style.json @@ -0,0 +1,87 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "light": { + "intensity": 1, + "position": [1, 0, 0], + "anchor": "map" + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 0 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0002, + -0.0002 + ], + [ + -0.0002, + 0.0002 + ], + [ + 0.0002, + 0.0002 + ], + [ + 0.0002, + -0.0002 + ], + [ + -0.0002, + -0.0002 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 85, + "bearing": 45, + "zoom": 18, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 3, + "fill-extrusion-base": ["get", "property"], + "fill-extrusion-color": "#999" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/horizon-north-east/expected.png b/test/integration/render-tests/skybox/fill-extrusion-light/horizon-north-east/expected.png new file mode 100644 index 00000000000..b09fe999ef3 Binary files /dev/null and b/test/integration/render-tests/skybox/fill-extrusion-light/horizon-north-east/expected.png differ diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/horizon-north-east/style.json b/test/integration/render-tests/skybox/fill-extrusion-light/horizon-north-east/style.json new file mode 100644 index 00000000000..72d8eca4427 --- /dev/null +++ b/test/integration/render-tests/skybox/fill-extrusion-light/horizon-north-east/style.json @@ -0,0 +1,87 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "light": { + "intensity": 1, + "position": [1, 45, 90], + "anchor": "map" + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 0 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0002, + -0.0002 + ], + [ + -0.0002, + 0.0002 + ], + [ + 0.0002, + 0.0002 + ], + [ + 0.0002, + -0.0002 + ], + [ + -0.0002, + -0.0002 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 85, + "bearing": 45, + "zoom": 18, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 3, + "fill-extrusion-base": ["get", "property"], + "fill-extrusion-color": "#999" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/horizon-south-west/expected.png b/test/integration/render-tests/skybox/fill-extrusion-light/horizon-south-west/expected.png new file mode 100644 index 00000000000..2782ca21e1c Binary files /dev/null and b/test/integration/render-tests/skybox/fill-extrusion-light/horizon-south-west/expected.png differ diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/horizon-south-west/style.json b/test/integration/render-tests/skybox/fill-extrusion-light/horizon-south-west/style.json new file mode 100644 index 00000000000..e4a99e768aa --- /dev/null +++ b/test/integration/render-tests/skybox/fill-extrusion-light/horizon-south-west/style.json @@ -0,0 +1,87 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "light": { + "intensity": 1, + "position": [1, 225, 90], + "anchor": "map" + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 0 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0002, + -0.0002 + ], + [ + -0.0002, + 0.0002 + ], + [ + 0.0002, + 0.0002 + ], + [ + 0.0002, + -0.0002 + ], + [ + -0.0002, + -0.0002 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 85, + "bearing": 45, + "zoom": 18, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 3, + "fill-extrusion-base": ["get", "property"], + "fill-extrusion-color": "#999" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/north-east/expected.png b/test/integration/render-tests/skybox/fill-extrusion-light/north-east/expected.png new file mode 100644 index 00000000000..4122f95ff9e Binary files /dev/null and b/test/integration/render-tests/skybox/fill-extrusion-light/north-east/expected.png differ diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/north-east/style.json b/test/integration/render-tests/skybox/fill-extrusion-light/north-east/style.json new file mode 100644 index 00000000000..5d07530b4ee --- /dev/null +++ b/test/integration/render-tests/skybox/fill-extrusion-light/north-east/style.json @@ -0,0 +1,87 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "light": { + "intensity": 1, + "position": [1, 45, 45], + "anchor": "map" + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 0 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0002, + -0.0002 + ], + [ + -0.0002, + 0.0002 + ], + [ + 0.0002, + 0.0002 + ], + [ + 0.0002, + -0.0002 + ], + [ + -0.0002, + -0.0002 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 85, + "bearing": 45, + "zoom": 18, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 3, + "fill-extrusion-base": ["get", "property"], + "fill-extrusion-color": "#999" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/north-west/expected.png b/test/integration/render-tests/skybox/fill-extrusion-light/north-west/expected.png new file mode 100644 index 00000000000..c06a75cf422 Binary files /dev/null and b/test/integration/render-tests/skybox/fill-extrusion-light/north-west/expected.png differ diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/north-west/style.json b/test/integration/render-tests/skybox/fill-extrusion-light/north-west/style.json new file mode 100644 index 00000000000..a0648eb32e1 --- /dev/null +++ b/test/integration/render-tests/skybox/fill-extrusion-light/north-west/style.json @@ -0,0 +1,87 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "light": { + "intensity": 1, + "position": [1, 315, 45], + "anchor": "map" + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 0 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0002, + -0.0002 + ], + [ + -0.0002, + 0.0002 + ], + [ + 0.0002, + 0.0002 + ], + [ + 0.0002, + -0.0002 + ], + [ + -0.0002, + -0.0002 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 85, + "bearing": 45, + "zoom": 18, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 3, + "fill-extrusion-base": ["get", "property"], + "fill-extrusion-color": "#999" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/south-east/expected.png b/test/integration/render-tests/skybox/fill-extrusion-light/south-east/expected.png new file mode 100644 index 00000000000..7a8042c8234 Binary files /dev/null and b/test/integration/render-tests/skybox/fill-extrusion-light/south-east/expected.png differ diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/south-east/style.json b/test/integration/render-tests/skybox/fill-extrusion-light/south-east/style.json new file mode 100644 index 00000000000..1aed34c43a0 --- /dev/null +++ b/test/integration/render-tests/skybox/fill-extrusion-light/south-east/style.json @@ -0,0 +1,87 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "light": { + "intensity": 1, + "position": [1, 135, 45], + "anchor": "map" + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 0 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0002, + -0.0002 + ], + [ + -0.0002, + 0.0002 + ], + [ + 0.0002, + 0.0002 + ], + [ + 0.0002, + -0.0002 + ], + [ + -0.0002, + -0.0002 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 85, + "bearing": 45, + "zoom": 18, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 3, + "fill-extrusion-base": ["get", "property"], + "fill-extrusion-color": "#999" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/south-west/expected.png b/test/integration/render-tests/skybox/fill-extrusion-light/south-west/expected.png new file mode 100644 index 00000000000..2ec6b3ba89a Binary files /dev/null and b/test/integration/render-tests/skybox/fill-extrusion-light/south-west/expected.png differ diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/south-west/style.json b/test/integration/render-tests/skybox/fill-extrusion-light/south-west/style.json new file mode 100644 index 00000000000..53431e678ae --- /dev/null +++ b/test/integration/render-tests/skybox/fill-extrusion-light/south-west/style.json @@ -0,0 +1,87 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "light": { + "intensity": 1, + "position": [1.0, 225, 45], + "anchor": "map" + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 0 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0002, + -0.0002 + ], + [ + -0.0002, + 0.0002 + ], + [ + 0.0002, + 0.0002 + ], + [ + 0.0002, + -0.0002 + ], + [ + -0.0002, + -0.0002 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 85, + "bearing": 45, + "zoom": 18, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 3, + "fill-extrusion-base": ["get", "property"], + "fill-extrusion-color": "#999" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/sun-override/expected.png b/test/integration/render-tests/skybox/fill-extrusion-light/sun-override/expected.png new file mode 100644 index 00000000000..e8523801b8c Binary files /dev/null and b/test/integration/render-tests/skybox/fill-extrusion-light/sun-override/expected.png differ diff --git a/test/integration/render-tests/skybox/fill-extrusion-light/sun-override/style.json b/test/integration/render-tests/skybox/fill-extrusion-light/sun-override/style.json new file mode 100644 index 00000000000..c270e987078 --- /dev/null +++ b/test/integration/render-tests/skybox/fill-extrusion-light/sun-override/style.json @@ -0,0 +1,88 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "light": { + "intensity": 1, + "position": [1, 0, 0], + "anchor": "map" + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 0 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0002, + -0.0002 + ], + [ + -0.0002, + 0.0002 + ], + [ + 0.0002, + 0.0002 + ], + [ + 0.0002, + -0.0002 + ], + [ + -0.0002, + -0.0002 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 85, + "bearing": 45, + "zoom": 18, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [225, 90], + "sky-atmosphere-sun-intensity": 30 + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 3, + "fill-extrusion-base": ["get", "property"], + "fill-extrusion-color": "#999" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/gradient/default/expected.png b/test/integration/render-tests/skybox/gradient/default/expected.png new file mode 100644 index 00000000000..340f01cb05a Binary files /dev/null and b/test/integration/render-tests/skybox/gradient/default/expected.png differ diff --git a/test/integration/render-tests/skybox/gradient/default/style.json b/test/integration/render-tests/skybox/gradient/default/style.json new file mode 100644 index 00000000000..e6d1218d029 --- /dev/null +++ b/test/integration/render-tests/skybox/gradient/default/style.json @@ -0,0 +1,28 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "gradient" + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/gradient/east/expected.png b/test/integration/render-tests/skybox/gradient/east/expected.png new file mode 100644 index 00000000000..a68e6211865 Binary files /dev/null and b/test/integration/render-tests/skybox/gradient/east/expected.png differ diff --git a/test/integration/render-tests/skybox/gradient/east/style.json b/test/integration/render-tests/skybox/gradient/east/style.json new file mode 100644 index 00000000000..0b1fdb0a794 --- /dev/null +++ b/test/integration/render-tests/skybox/gradient/east/style.json @@ -0,0 +1,39 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient-center": [20, 90], + "sky-gradient-radius": 30, + "sky-gradient": [ + "interpolate", + ["linear"], + ["sky-radial-progress"], + 0.0, + "red", + 1, + "white" + ] + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/gradient/linear/expected.png b/test/integration/render-tests/skybox/gradient/linear/expected.png new file mode 100644 index 00000000000..3db1fdf5ee5 Binary files /dev/null and b/test/integration/render-tests/skybox/gradient/linear/expected.png differ diff --git a/test/integration/render-tests/skybox/gradient/linear/style.json b/test/integration/render-tests/skybox/gradient/linear/style.json new file mode 100644 index 00000000000..baa3d4e2594 --- /dev/null +++ b/test/integration/render-tests/skybox/gradient/linear/style.json @@ -0,0 +1,39 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient-center": [0, 0], + "sky-gradient-radius": 90, + "sky-gradient": [ + "interpolate", + ["linear"], + ["sky-radial-progress"], + 0.8, + "white", + 1.0, + "red" + ] + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/gradient/north/expected.png b/test/integration/render-tests/skybox/gradient/north/expected.png new file mode 100644 index 00000000000..384534c2b9a Binary files /dev/null and b/test/integration/render-tests/skybox/gradient/north/expected.png differ diff --git a/test/integration/render-tests/skybox/gradient/north/style.json b/test/integration/render-tests/skybox/gradient/north/style.json new file mode 100644 index 00000000000..e8dc6ef6ced --- /dev/null +++ b/test/integration/render-tests/skybox/gradient/north/style.json @@ -0,0 +1,39 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient-center": [0, 90], + "sky-gradient-radius": 30, + "sky-gradient": [ + "interpolate", + ["linear"], + ["sky-radial-progress"], + 0.0, + "red", + 1, + "white" + ] + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/gradient/south/expected.png b/test/integration/render-tests/skybox/gradient/south/expected.png new file mode 100644 index 00000000000..5e200a29b86 Binary files /dev/null and b/test/integration/render-tests/skybox/gradient/south/expected.png differ diff --git a/test/integration/render-tests/skybox/gradient/south/style.json b/test/integration/render-tests/skybox/gradient/south/style.json new file mode 100644 index 00000000000..88d18810573 --- /dev/null +++ b/test/integration/render-tests/skybox/gradient/south/style.json @@ -0,0 +1,40 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "bearing": 180, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient-center": [180, 90], + "sky-gradient-radius": 30, + "sky-gradient": [ + "interpolate", + ["linear"], + ["sky-radial-progress"], + 0.0, + "white", + 1, + "red" + ] + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/gradient/update/expected.png b/test/integration/render-tests/skybox/gradient/update/expected.png new file mode 100644 index 00000000000..cdea4a3304c Binary files /dev/null and b/test/integration/render-tests/skybox/gradient/update/expected.png differ diff --git a/test/integration/render-tests/skybox/gradient/update/style.json b/test/integration/render-tests/skybox/gradient/update/style.json new file mode 100644 index 00000000000..df9bab783a6 --- /dev/null +++ b/test/integration/render-tests/skybox/gradient/update/style.json @@ -0,0 +1,50 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "operations": [ + [ + "setPaintProperty", + "sky", + "sky-gradient-center", + [20, 70] + ], + [ + "wait" + ] + ] + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient-center": [0, 90], + "sky-gradient-radius": 30, + "sky-gradient": [ + "interpolate", + ["linear"], + ["sky-radial-progress"], + 0.0, + "red", + 1, + "white" + ] + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/gradient/west/expected.png b/test/integration/render-tests/skybox/gradient/west/expected.png new file mode 100644 index 00000000000..7a81cb91339 Binary files /dev/null and b/test/integration/render-tests/skybox/gradient/west/expected.png differ diff --git a/test/integration/render-tests/skybox/gradient/west/style.json b/test/integration/render-tests/skybox/gradient/west/style.json new file mode 100644 index 00000000000..538fc26606f --- /dev/null +++ b/test/integration/render-tests/skybox/gradient/west/style.json @@ -0,0 +1,39 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "sources": {}, + "zoom": 16, + "pitch": 85, + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient-center": [340, 90], + "sky-gradient-radius": 30, + "sky-gradient": [ + "interpolate", + ["linear"], + ["sky-radial-progress"], + 0.0, + "red", + 1, + "white" + ] + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/horizon-visibility/base/expected.png b/test/integration/render-tests/skybox/horizon-visibility/base/expected.png new file mode 100644 index 00000000000..71e41664eba Binary files /dev/null and b/test/integration/render-tests/skybox/horizon-visibility/base/expected.png differ diff --git a/test/integration/render-tests/skybox/horizon-visibility/base/style.json b/test/integration/render-tests/skybox/horizon-visibility/base/style.json new file mode 100644 index 00000000000..555b4895d85 --- /dev/null +++ b/test/integration/render-tests/skybox/horizon-visibility/base/style.json @@ -0,0 +1,31 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "sources": {}, + "zoom": 14, + "pitch": 72, + "bearing": 60, + "layers": [ + { + "id": "blue-fill", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient-center": [0, 0], + "sky-gradient": "green" + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "red" + } + } + ] +} diff --git a/test/integration/render-tests/skybox/horizon-visibility/padding/expected.png b/test/integration/render-tests/skybox/horizon-visibility/padding/expected.png new file mode 100644 index 00000000000..6c3044faeba Binary files /dev/null and b/test/integration/render-tests/skybox/horizon-visibility/padding/expected.png differ diff --git a/test/integration/render-tests/skybox/horizon-visibility/padding/style.json b/test/integration/render-tests/skybox/horizon-visibility/padding/style.json new file mode 100644 index 00000000000..bd4d60a4141 --- /dev/null +++ b/test/integration/render-tests/skybox/horizon-visibility/padding/style.json @@ -0,0 +1,45 @@ +{ + "version": 8, + "metadata": { + "test": { + "showPadding": true, + "height": 128, + "width": 128, + "operations": [ + [ + "setPadding", + { + "top": 100, + "left": 0, + "bottom": 0, + "right": 0 + } + ], + [ + "wait" + ] + ] + } + }, + "sources": {}, + "zoom": 14, + "pitch": 60, + "layers": [ + { + "id": "blue-fill", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient-center": [0, 0], + "sky-gradient": "green" + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "blue" + } + } + ] +} diff --git a/test/integration/render-tests/symbol-cross-fade/chinese-disable-fade-in/expected.png b/test/integration/render-tests/symbol-cross-fade/chinese-disable-fade-in/expected.png new file mode 100644 index 00000000000..42f2d8d0a8d Binary files /dev/null and b/test/integration/render-tests/symbol-cross-fade/chinese-disable-fade-in/expected.png differ diff --git a/test/integration/render-tests/symbol-cross-fade/chinese-disable-fade-in/style.json b/test/integration/render-tests/symbol-cross-fade/chinese-disable-fade-in/style.json new file mode 100644 index 00000000000..7ad05e8046c --- /dev/null +++ b/test/integration/render-tests/symbol-cross-fade/chinese-disable-fade-in/style.json @@ -0,0 +1,220 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "description": "Line symbols are placed differently at different zoom levels: this test crosses a zoom level. It renders immediately to test `disableFadeInOnLoad`. Cross-fade should be ignored. The 'wait' logic is tricky here: 1st 'wait 100' is to trigger placement at z2 after tiles have loaded, 2nd 'wait 100' is to allow z2 labels to fade in, 'wait' after 'setZoom' runs until the z3 tiles load (but doesn't elapse any time on the fake test clock), next 'wait 100' triggers a placement using the z3 and z2 tiles together, final 'wait 80' actually exercises the cross fade.", + "operations": [ + ["wait", 100], + ["wait", 100], + [ + "setZoom", + 3 + ], + ["wait"], + [ + "wait", + 100 + ], + [ + "wait", + 80 + ] + ] + } + }, + "zoom": 2, + "center": [-14.41400, 39.09187], + "sources": { + "mapbox": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "氣到身什戰只白質位歡" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -14.4195556640625, + 39.091699613104595 + ], + [ + 102.3046875, + 39.36827914916014 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "電局今情再夜面造" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -14.403076171875, + 39.10022600175347 + ], + [ + 103.35937499999999, + 65.80277639340238 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "有究往極他生血通育" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -14.414062499999998, + 39.091699613104595 + ], + [ + -14.765625, + 82.21421714106776 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "不示有電親界因來終" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -14.408569335937498, + 39.091699613104595 + ], + [ + -130.78125, + 39.095962936305476 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "後再學全看素力來:不車" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -14.414062499999998, + 39.095962936305476 + ], + [ + -16.5234375, + -58.81374171570779 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "有下人費也家了清,黨光她保過每心" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -14.4195556640625, + 39.10022600175347 + ], + [ + -130.4296875, + 64.47279382008166 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "醫公藝說就公和有" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -14.4195556640625, + 39.0831721934762 + ], + [ + 33.75, + 81.87364125482827 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "光中輪的態指那差車" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -14.447021484374998, + 39.104488809440475 + ], + [ + -66.4453125, + 82.26169873683153 + ] + ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "lines-symbol", + "type": "symbol", + "source": "mapbox", + "layout": { + "text-field": "{name}", + "symbol-placement": "line", + "symbol-spacing": 100, + "text-allow-overlap": false, + "text-font": [ "NotoCJK" ] + } + }, { + "id": "lines", + "type": "line", + "source": "mapbox", + "paint": { + "line-opacity": 0.25 + } + } + ] +} diff --git a/test/integration/render-tests/symbol-cross-fade/chinese/expected.png b/test/integration/render-tests/symbol-cross-fade/chinese-fade-in/expected.png similarity index 100% rename from test/integration/render-tests/symbol-cross-fade/chinese/expected.png rename to test/integration/render-tests/symbol-cross-fade/chinese-fade-in/expected.png diff --git a/test/integration/render-tests/symbol-cross-fade/chinese/style.json b/test/integration/render-tests/symbol-cross-fade/chinese-fade-in/style.json similarity index 100% rename from test/integration/render-tests/symbol-cross-fade/chinese/style.json rename to test/integration/render-tests/symbol-cross-fade/chinese-fade-in/style.json diff --git a/test/integration/render-tests/symbol-distance-fade/expected.png b/test/integration/render-tests/symbol-distance-fade/expected.png new file mode 100644 index 00000000000..78a3da536a4 Binary files /dev/null and b/test/integration/render-tests/symbol-distance-fade/expected.png differ diff --git a/test/integration/render-tests/symbol-distance-fade/style.json b/test/integration/render-tests/symbol-distance-fade/style.json new file mode 100644 index 00000000000..da44253a1fa --- /dev/null +++ b/test/integration/render-tests/symbol-distance-fade/style.json @@ -0,0 +1,181 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 400, + "height": 256 + } + }, + "center": [ + -18, + 62 + ], + "zoom": 8.3, + "pitch": 85, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -11, + 68.31986144668052 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -12, + 67.71986144668052 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -13, + 67.31986144668052 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -14, + 66.71986144668052 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -15, + 66.31986144668052 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -16, + 65.71986144668052 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -17, + 65.31986144668052 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -18, + 64.71986144668052 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -19, + 64.31986144668052 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -20, + 63.71986144668052 + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + -21, + 63.31986144668052 + ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "sky", + "type": "sky", + "paint": { + "sky-type": "gradient", + "sky-gradient": "lightblue" + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "beige" + } + }, + { + "id": "symbol", + "type": "symbol", + "source": "geojson", + "layout": { + "symbol-placement": "point", + "text-field": "O", + "text-allow-overlap": false, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": 40 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/buildings-on-raster/expected.png b/test/integration/render-tests/terrain/buildings-on-raster/expected.png new file mode 100644 index 00000000000..6ac723a567e Binary files /dev/null and b/test/integration/render-tests/terrain/buildings-on-raster/expected.png differ diff --git a/test/integration/render-tests/terrain/buildings-on-raster/style.json b/test/integration/render-tests/terrain/buildings-on-raster/style.json new file mode 100644 index 00000000000..37bb9679b43 --- /dev/null +++ b/test/integration/render-tests/terrain/buildings-on-raster/style.json @@ -0,0 +1,120 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.27423, 35.964761], + "zoom": 17.426, + "pitch": 60, + "bearing": -10, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "buffer": 0, + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "type": "building", + "height": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-113.2743, 35.96453], + [-113.2745, 35.96453], + [-113.2745, 35.96463], + [-113.2743, 35.96463], + [-113.2743, 35.96453] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-113.2739, 35.96453], + [-113.2742, 35.96453], + [-113.2742, 35.96463], + [-113.2739, 35.96463], + [-113.2739, 35.96453] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "type": "building", + "height": 10 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-113.27489, 35.96489], + [-113.27479, 35.96489], + [-113.27479, 35.96499], + [-113.27489, 35.96499], + [-113.27489, 35.96489] + ] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-opacity": 0.5, + "fill-extrusion-height": 20, + "fill-extrusion-color": "red" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/camera-placement/elevation-not-yet-available/expected.png b/test/integration/render-tests/terrain/camera-placement/elevation-not-yet-available/expected.png new file mode 100644 index 00000000000..1ee593fa0ea Binary files /dev/null and b/test/integration/render-tests/terrain/camera-placement/elevation-not-yet-available/expected.png differ diff --git a/test/integration/render-tests/terrain/camera-placement/elevation-not-yet-available/style.json b/test/integration/render-tests/terrain/camera-placement/elevation-not-yet-available/style.json new file mode 100644 index 00000000000..dad992d89a6 --- /dev/null +++ b/test/integration/render-tests/terrain/camera-placement/elevation-not-yet-available/style.json @@ -0,0 +1,50 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "operations": [ + ["setCenter", [-113.231881, 35.99282]], + ["wait"], + ["setZoom", 15.28] + ] + } + }, + "center": [-122.45814, 37.76159], + "zoom": 16.02, + "pitch": 56, + "bearing": 98.1, + "terrain": { + "source": "rgbterrain", + "exaggeration": 2.5 + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/circle/map-aligned/occluded/expected.png b/test/integration/render-tests/terrain/circle/map-aligned/occluded/expected.png new file mode 100644 index 00000000000..f656e73462f Binary files /dev/null and b/test/integration/render-tests/terrain/circle/map-aligned/occluded/expected.png differ diff --git a/test/integration/render-tests/terrain/circle/map-aligned/occluded/style.json b/test/integration/render-tests/terrain/circle/map-aligned/occluded/style.json new file mode 100644 index 00000000000..a9f78ddadba --- /dev/null +++ b/test/integration/render-tests/terrain/circle/map-aligned/occluded/style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200 + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 60, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "MultiPoint", + "coordinates": [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33341462261518, + 35.9294218694216 + ], + [ + -113.3220882006336, + 35.9418831745696 + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": "#ff0000", + "circle-pitch-alignment": "map" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/circle/map-aligned/partly-occluded/expected.png b/test/integration/render-tests/terrain/circle/map-aligned/partly-occluded/expected.png new file mode 100644 index 00000000000..ee0d366e002 Binary files /dev/null and b/test/integration/render-tests/terrain/circle/map-aligned/partly-occluded/expected.png differ diff --git a/test/integration/render-tests/terrain/circle/map-aligned/partly-occluded/style.json b/test/integration/render-tests/terrain/circle/map-aligned/partly-occluded/style.json new file mode 100644 index 00000000000..f9eb75858c9 --- /dev/null +++ b/test/integration/render-tests/terrain/circle/map-aligned/partly-occluded/style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200 + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 65, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "MultiPoint", + "coordinates": [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33641462261518, + 35.9294218694216 + ], + [ + -113.3170882006336, + 35.9258831745696 + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 15, + "circle-color": "#ff0000", + "circle-pitch-alignment": "map" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/circle/map-aligned/unoccluded/expected.png b/test/integration/render-tests/terrain/circle/map-aligned/unoccluded/expected.png new file mode 100644 index 00000000000..56e3e716491 Binary files /dev/null and b/test/integration/render-tests/terrain/circle/map-aligned/unoccluded/expected.png differ diff --git a/test/integration/render-tests/terrain/circle/map-aligned/unoccluded/style.json b/test/integration/render-tests/terrain/circle/map-aligned/unoccluded/style.json new file mode 100644 index 00000000000..704e6ed0464 --- /dev/null +++ b/test/integration/render-tests/terrain/circle/map-aligned/unoccluded/style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200 + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 0, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "MultiPoint", + "coordinates": [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33341462261518, + 35.9294218694216 + ], + [ + -113.3220882006336, + 35.9418831745696 + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": "#ff0000", + "circle-pitch-alignment": "map" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/circle/pitch-scaling/expected.png b/test/integration/render-tests/terrain/circle/pitch-scaling/expected.png new file mode 100644 index 00000000000..346140623c3 Binary files /dev/null and b/test/integration/render-tests/terrain/circle/pitch-scaling/expected.png differ diff --git a/test/integration/render-tests/terrain/circle/pitch-scaling/style.json b/test/integration/render-tests/terrain/circle/pitch-scaling/style.json new file mode 100644 index 00000000000..63817ef190f --- /dev/null +++ b/test/integration/render-tests/terrain/circle/pitch-scaling/style.json @@ -0,0 +1,75 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200 + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 0, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "MultiPoint", + "coordinates": [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33341462261518, + 35.9294218694216 + ], + [ + -113.3250882006336, + 35.9418831745696 + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 10, + "circle-color": "#ff0000", + "circle-pitch-alignment": "map", + "circle-pitch-scale": "viewport" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/circle/viewport-aligned/occluded/expected.png b/test/integration/render-tests/terrain/circle/viewport-aligned/occluded/expected.png new file mode 100644 index 00000000000..50cd8d5f50b Binary files /dev/null and b/test/integration/render-tests/terrain/circle/viewport-aligned/occluded/expected.png differ diff --git a/test/integration/render-tests/terrain/circle/viewport-aligned/occluded/style.json b/test/integration/render-tests/terrain/circle/viewport-aligned/occluded/style.json new file mode 100644 index 00000000000..bd805c28eff --- /dev/null +++ b/test/integration/render-tests/terrain/circle/viewport-aligned/occluded/style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200 + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 60, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "MultiPoint", + "coordinates": [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33341462261518, + 35.9294218694216 + ], + [ + -113.3220882006336, + 35.9418831745696 + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": "#ff0000", + "circle-pitch-alignment": "viewport" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/circle/viewport-aligned/partly-occluded/expected.png b/test/integration/render-tests/terrain/circle/viewport-aligned/partly-occluded/expected.png new file mode 100644 index 00000000000..1d232732ff7 Binary files /dev/null and b/test/integration/render-tests/terrain/circle/viewport-aligned/partly-occluded/expected.png differ diff --git a/test/integration/render-tests/terrain/circle/viewport-aligned/partly-occluded/style.json b/test/integration/render-tests/terrain/circle/viewport-aligned/partly-occluded/style.json new file mode 100644 index 00000000000..0804c37a624 --- /dev/null +++ b/test/integration/render-tests/terrain/circle/viewport-aligned/partly-occluded/style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200 + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 0, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "MultiPoint", + "coordinates": [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33341462261518, + 35.9294218694216 + ], + [ + -113.3250882006336, + 35.9418831745696 + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 15, + "circle-color": "#ff0000", + "circle-pitch-alignment": "viewport" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/circle/viewport-aligned/unoccluded/expected.png b/test/integration/render-tests/terrain/circle/viewport-aligned/unoccluded/expected.png new file mode 100644 index 00000000000..37e569410ff Binary files /dev/null and b/test/integration/render-tests/terrain/circle/viewport-aligned/unoccluded/expected.png differ diff --git a/test/integration/render-tests/terrain/circle/viewport-aligned/unoccluded/style.json b/test/integration/render-tests/terrain/circle/viewport-aligned/unoccluded/style.json new file mode 100644 index 00000000000..a34ea0de712 --- /dev/null +++ b/test/integration/render-tests/terrain/circle/viewport-aligned/unoccluded/style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 200 + } + }, + "center": [-113.32694547094238, 35.93455626259847], + "zoom": 12, + "pitch": 0, + "bearing": -20, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "MultiPoint", + "coordinates": [ + [ + -113.32694547094238, + 35.93355626259847 + ], + [ + -113.33341462261518, + 35.9294218694216 + ], + [ + -113.3250882006336, + 35.9418831745696 + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": "#ff0000", + "circle-pitch-alignment": "viewport" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/error-overlap/initializing-no-terrain-at-center/expected.png b/test/integration/render-tests/terrain/error-overlap/initializing-no-terrain-at-center/expected.png new file mode 100644 index 00000000000..906f4de4eff Binary files /dev/null and b/test/integration/render-tests/terrain/error-overlap/initializing-no-terrain-at-center/expected.png differ diff --git a/test/integration/render-tests/terrain/error-overlap/initializing-no-terrain-at-center/style.json b/test/integration/render-tests/terrain/error-overlap/initializing-no-terrain-at-center/style.json new file mode 100644 index 00000000000..06dcff1efbc --- /dev/null +++ b/test/integration/render-tests/terrain/error-overlap/initializing-no-terrain-at-center/style.json @@ -0,0 +1,59 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 1024, + "width": 256, + "description": "On terrain init, before center gets terrain data, keep map in 2D.", + "operations": [ + [ + "wait" + ] + ] + } + }, + "center": [0.005, 0.01], + "zoom": 14.51, + "pitch": 60, + "bearing": -45, + "sprite": "local://sprites/emerald", + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/const/{z}-{x}-{y}.terrain.128.png" + ], + "maxzoom": 15, + "tileSize": 128 + }, + "color": { + "type": "raster", + "tiles": [ + "local://tiles/const/{z}-{x}-{y}.color.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-pattern": "airport_icon" + } + }, + { + "id": "raster", + "type": "raster", + "source": "color", + "paint": { + "raster-fade-duration": 0, + "raster-opacity": 0.5 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/error-overlap/opaque-zoom14.5/expected.png b/test/integration/render-tests/terrain/error-overlap/opaque-zoom14.5/expected.png new file mode 100644 index 00000000000..581fabb673e Binary files /dev/null and b/test/integration/render-tests/terrain/error-overlap/opaque-zoom14.5/expected.png differ diff --git a/test/integration/render-tests/terrain/error-overlap/opaque-zoom14.5/style.json b/test/integration/render-tests/terrain/error-overlap/opaque-zoom14.5/style.json new file mode 100644 index 00000000000..75203219b04 --- /dev/null +++ b/test/integration/render-tests/terrain/error-overlap/opaque-zoom14.5/style.json @@ -0,0 +1,57 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 1024, + "width": 256, + "operations": [ + [ + "wait", 1500 + ] + ] + } + }, + "center": [-0.01, 0.01], + "zoom": 14.51, + "pitch": 60, + "bearing": -45, + "sprite": "local://sprites/emerald", + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/const/{z}-{x}-{y}.terrain.128.png" + ], + "maxzoom": 15, + "tileSize": 128 + }, + "color": { + "type": "raster", + "tiles": [ + "local://tiles/const/{z}-{x}-{y}.color.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-pattern": "airport_icon" + } + }, + { + "id": "raster", + "type": "raster", + "source": "color", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/error-overlap/transparent-zoom14.5/expected.png b/test/integration/render-tests/terrain/error-overlap/transparent-zoom14.5/expected.png new file mode 100644 index 00000000000..5060d44ab01 Binary files /dev/null and b/test/integration/render-tests/terrain/error-overlap/transparent-zoom14.5/expected.png differ diff --git a/test/integration/render-tests/terrain/error-overlap/transparent-zoom14.5/style.json b/test/integration/render-tests/terrain/error-overlap/transparent-zoom14.5/style.json new file mode 100644 index 00000000000..d3f42b6fb08 --- /dev/null +++ b/test/integration/render-tests/terrain/error-overlap/transparent-zoom14.5/style.json @@ -0,0 +1,58 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 1024, + "width": 256, + "operations": [ + [ + "wait", 1500 + ] + ] + } + }, + "center": [-0.01, 0.01], + "zoom": 14.51, + "pitch": 60, + "bearing": -45, + "sprite": "local://sprites/emerald", + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/const/{z}-{x}-{y}.terrain.128.png" + ], + "maxzoom": 15, + "tileSize": 128 + }, + "color": { + "type": "raster", + "tiles": [ + "local://tiles/const/{z}-{x}-{y}.color.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-pattern": "airport_icon" + } + }, + { + "id": "raster", + "type": "raster", + "source": "color", + "paint": { + "raster-fade-duration": 0, + "raster-opacity": 0.5 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/fill-border/expected.png b/test/integration/render-tests/terrain/fill-border/expected.png new file mode 100644 index 00000000000..624ff821357 Binary files /dev/null and b/test/integration/render-tests/terrain/fill-border/expected.png differ diff --git a/test/integration/render-tests/terrain/fill-border/style.json b/test/integration/render-tests/terrain/fill-border/style.json new file mode 100644 index 00000000000..de0c9411cf2 --- /dev/null +++ b/test/integration/render-tests/terrain/fill-border/style.json @@ -0,0 +1,95 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 200, + "width": 200, + "terrainDrapeFirst": false, + "operations": [ + [ + "wait", 1500 + ] + ] + } + }, + "center": [-113.2697514325975, 35.962383122262054], + "zoom": 12.5, + "pitch": 50, + "bearing": -40, + "terrain": { + "source": "rgbterrain" + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "Polygon", + "coordinates": [ + [ + [ + -113.27384948730469, + 35.962 + ], + [ + -113.26421051269531, + 35.962 + ], + [ + -113.26421051269531, + 35.97 + ], + [ + -113.27384948730469, + 35.97 + ], + [ + -113.28412743652345, + 35.964 + ], + [ + -113.27384948730469, + 35.962 + ] + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "fill-translucent", + "type": "fill", + "source": "geojson", + "paint": { + "fill-color": "yellow", + "fill-outline-color": "blue", + "fill-opacity": 0.95 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/heatmap/expected.png b/test/integration/render-tests/terrain/heatmap/expected.png new file mode 100644 index 00000000000..d7b3a5567fe Binary files /dev/null and b/test/integration/render-tests/terrain/heatmap/expected.png differ diff --git a/test/integration/render-tests/terrain/heatmap/style.json b/test/integration/render-tests/terrain/heatmap/style.json new file mode 100644 index 00000000000..351d30b7745 --- /dev/null +++ b/test/integration/render-tests/terrain/heatmap/style.json @@ -0,0 +1,92 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 200, + "width": 200, + "operations": [ + [ + "wait", 1500 + ] + ] + } + }, + "center": [-113.2697514325975, 35.962383122262054], + "zoom": 12.49, + "pitch": 64, + "bearing": -40, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "geojson": { + "type": "geojson", + "data": { + "type": "Polygon", + "coordinates": [ + [ + [ + -113.27384948730469, + 35.962 + ], + [ + -113.26421051269531, + 35.962 + ], + [ + -113.26421051269531, + 35.97 + ], + [ + -113.27384948730469, + 35.97 + ], + [ + -113.28412743652345, + 35.964 + ], + [ + -113.27384948730469, + 35.962 + ] + ] + ] + } + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "heatmap-translucent", + "type": "heatmap", + "source": "geojson", + "paint": { + "heatmap-opacity": 0.7, + "heatmap-radius": 100 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/hillshade--raster-interpolate-exaggeration/expected.png b/test/integration/render-tests/terrain/hillshade--raster-interpolate-exaggeration/expected.png new file mode 100644 index 00000000000..c5930f1b4c3 Binary files /dev/null and b/test/integration/render-tests/terrain/hillshade--raster-interpolate-exaggeration/expected.png differ diff --git a/test/integration/render-tests/terrain/hillshade--raster-interpolate-exaggeration/style.json b/test/integration/render-tests/terrain/hillshade--raster-interpolate-exaggeration/style.json new file mode 100644 index 00000000000..0c2bb7ce114 --- /dev/null +++ b/test/integration/render-tests/terrain/hillshade--raster-interpolate-exaggeration/style.json @@ -0,0 +1,66 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9954], + "zoom": 11.2, + "pitch": 45, + "terrain": { + "source": "terrain", + "exaggeration": [ + "interpolate", + ["linear"], + ["zoom"], + 10.5, 0.1, + 11.5, 5 + ] + }, + "sources": { + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "terrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + }, + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/hillshade--raster-runtime-styling/expected.png b/test/integration/render-tests/terrain/hillshade--raster-runtime-styling/expected.png new file mode 100644 index 00000000000..a96d5ce97ec Binary files /dev/null and b/test/integration/render-tests/terrain/hillshade--raster-runtime-styling/expected.png differ diff --git a/test/integration/render-tests/terrain/hillshade--raster-runtime-styling/style.json b/test/integration/render-tests/terrain/hillshade--raster-runtime-styling/style.json new file mode 100644 index 00000000000..8807517c265 --- /dev/null +++ b/test/integration/render-tests/terrain/hillshade--raster-runtime-styling/style.json @@ -0,0 +1,85 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "terrainDrapeFirst": true, + "description": "terrainDrapeFirst ensures that cache is used all the time. Cache is invalidated on setPaintProperty and altered color is rendered.", + "operations": [ + [ + "wait" + ], + [ + "setPaintProperty", + "background", + "background-color", + "pink" + ], + [ + "wait", 100 + ], + [ + "setPaintProperty", + "background", + "background-color", + "green" + ] + ] + } + }, + "transition": { + "duration": 0 + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "pitch": 45, + "terrain": { + "source": "hillshade" + }, + "sources": { + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "red", + "background-opacity": 0.5 + } + }, + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0, + "raster-opacity": 0.5 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/hillshade--raster-seams-alpha/expected.png b/test/integration/render-tests/terrain/hillshade--raster-seams-alpha/expected.png new file mode 100644 index 00000000000..4e5f139d21e Binary files /dev/null and b/test/integration/render-tests/terrain/hillshade--raster-seams-alpha/expected.png differ diff --git a/test/integration/render-tests/terrain/hillshade--raster-seams-alpha/style.json b/test/integration/render-tests/terrain/hillshade--raster-seams-alpha/style.json new file mode 100644 index 00000000000..93a95ac6946 --- /dev/null +++ b/test/integration/render-tests/terrain/hillshade--raster-seams-alpha/style.json @@ -0,0 +1,59 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "pitch": 45, + "terrain": { + "source": "hillshade" + }, + "sources": { + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "red" + } + }, + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0, + "raster-opacity": 0.5 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/hillshade--raster-seams/expected.png b/test/integration/render-tests/terrain/hillshade--raster-seams/expected.png new file mode 100644 index 00000000000..1ba0e5139cc Binary files /dev/null and b/test/integration/render-tests/terrain/hillshade--raster-seams/expected.png differ diff --git a/test/integration/render-tests/terrain/hillshade--raster-seams/style.json b/test/integration/render-tests/terrain/hillshade--raster-seams/style.json new file mode 100644 index 00000000000..1600ef1b124 --- /dev/null +++ b/test/integration/render-tests/terrain/hillshade--raster-seams/style.json @@ -0,0 +1,58 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "pitch": 45, + "terrain": { + "source": "hillshade" + }, + "sources": { + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + }, + { + "id": "background", + "type": "background", + "paint": { + "background-color": "red" + } + }, + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/hillshade-buffer-0/expected.png b/test/integration/render-tests/terrain/hillshade-buffer-0/expected.png new file mode 100644 index 00000000000..3c58b416b07 Binary files /dev/null and b/test/integration/render-tests/terrain/hillshade-buffer-0/expected.png differ diff --git a/test/integration/render-tests/terrain/hillshade-buffer-0/style.json b/test/integration/render-tests/terrain/hillshade-buffer-0/style.json new file mode 100644 index 00000000000..24d8134940d --- /dev/null +++ b/test/integration/render-tests/terrain/hillshade-buffer-0/style.json @@ -0,0 +1,44 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "Separate sources for hillshade and terrain make full resolution hillshade." + } + }, + "center": [-113.2935, 35.9529], + "zoom": 11.2, + "pitch": 30, + "terrain": { + "source": "terrain" + }, + "sources": { + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-0/{z}-{x}-{y}.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "terrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-0/{z}-{x}-{y}.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/hillshade-buffer-1/expected.png b/test/integration/render-tests/terrain/hillshade-buffer-1/expected.png new file mode 100644 index 00000000000..3c58b416b07 Binary files /dev/null and b/test/integration/render-tests/terrain/hillshade-buffer-1/expected.png differ diff --git a/test/integration/render-tests/terrain/hillshade-buffer-1/style.json b/test/integration/render-tests/terrain/hillshade-buffer-1/style.json new file mode 100644 index 00000000000..1370a6a5ac4 --- /dev/null +++ b/test/integration/render-tests/terrain/hillshade-buffer-1/style.json @@ -0,0 +1,44 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "Separate sources for hillshade and terrain make full resolution hillshade." + } + }, + "center": [-113.2935, 35.9529], + "zoom": 11.2, + "pitch": 30, + "terrain": { + "source": "terrain" + }, + "sources": { + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-1/{z}-{x}-{y}-1.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "terrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-1/{z}-{x}-{y}-1.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/hillshade-buffer-2/expected.png b/test/integration/render-tests/terrain/hillshade-buffer-2/expected.png new file mode 100644 index 00000000000..3c58b416b07 Binary files /dev/null and b/test/integration/render-tests/terrain/hillshade-buffer-2/expected.png differ diff --git a/test/integration/render-tests/terrain/hillshade-buffer-2/style.json b/test/integration/render-tests/terrain/hillshade-buffer-2/style.json new file mode 100644 index 00000000000..dc937417582 --- /dev/null +++ b/test/integration/render-tests/terrain/hillshade-buffer-2/style.json @@ -0,0 +1,44 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "Separate sources for hillshade and terrain make full resolution hillshade." + } + }, + "center": [-113.2935, 35.9529], + "zoom": 11.2, + "pitch": 30, + "terrain": { + "source": "terrain" + }, + "sources": { + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-2/{z}-{x}-{y}-2.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "terrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-2/{z}-{x}-{y}-2.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/raster--hillshade-buffer-0/expected.png b/test/integration/render-tests/terrain/raster--hillshade-buffer-0/expected.png new file mode 100644 index 00000000000..d9a88446ca6 Binary files /dev/null and b/test/integration/render-tests/terrain/raster--hillshade-buffer-0/expected.png differ diff --git a/test/integration/render-tests/terrain/raster--hillshade-buffer-0/style.json b/test/integration/render-tests/terrain/raster--hillshade-buffer-0/style.json new file mode 100644 index 00000000000..f70c6366bc9 --- /dev/null +++ b/test/integration/render-tests/terrain/raster--hillshade-buffer-0/style.json @@ -0,0 +1,52 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "Shared source for hillshade and terrain." + } + }, + "center": [-113.26903, 35.9954], + "zoom": 10.9, + "pitch": 45, + "terrain": { + "source": "hillshade" + }, + "sources": { + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-0/{z}-{x}-{y}.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/raster--hillshade-buffer-1/expected.png b/test/integration/render-tests/terrain/raster--hillshade-buffer-1/expected.png new file mode 100644 index 00000000000..d9a88446ca6 Binary files /dev/null and b/test/integration/render-tests/terrain/raster--hillshade-buffer-1/expected.png differ diff --git a/test/integration/render-tests/terrain/raster--hillshade-buffer-1/style.json b/test/integration/render-tests/terrain/raster--hillshade-buffer-1/style.json new file mode 100644 index 00000000000..7787143b403 --- /dev/null +++ b/test/integration/render-tests/terrain/raster--hillshade-buffer-1/style.json @@ -0,0 +1,52 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "Shared source for hillshade and terrain." + } + }, + "center": [-113.26903, 35.9954], + "zoom": 10.9, + "pitch": 45, + "terrain": { + "source": "hillshade" + }, + "sources": { + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-1/{z}-{x}-{y}-1.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/raster--hillshade-buffer-2/expected.png b/test/integration/render-tests/terrain/raster--hillshade-buffer-2/expected.png new file mode 100644 index 00000000000..d9a88446ca6 Binary files /dev/null and b/test/integration/render-tests/terrain/raster--hillshade-buffer-2/expected.png differ diff --git a/test/integration/render-tests/terrain/raster--hillshade-buffer-2/style.json b/test/integration/render-tests/terrain/raster--hillshade-buffer-2/style.json new file mode 100644 index 00000000000..faa641c91b2 --- /dev/null +++ b/test/integration/render-tests/terrain/raster--hillshade-buffer-2/style.json @@ -0,0 +1,52 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "Shared source for hillshade and terrain." + } + }, + "center": [-113.26903, 35.9954], + "zoom": 10.9, + "pitch": 45, + "terrain": { + "source": "hillshade" + }, + "sources": { + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "hillshade": { + "type": "raster-dem", + "tiles": [ + "local://tiles/terrain-buffer-2/{z}-{x}-{y}-2.png" + ], + "maxzoom": 15, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + }, + { + "id": "hillshade-translucent", + "type": "hillshade", + "source": "hillshade", + "paint": { + "hillshade-exaggeration": 1 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/raster-fade/expected.png b/test/integration/render-tests/terrain/raster-fade/expected.png new file mode 100644 index 00000000000..cd6b3b59492 Binary files /dev/null and b/test/integration/render-tests/terrain/raster-fade/expected.png differ diff --git a/test/integration/render-tests/terrain/raster-fade/style.json b/test/integration/render-tests/terrain/raster-fade/style.json new file mode 100644 index 00000000000..df7a7ad4add --- /dev/null +++ b/test/integration/render-tests/terrain/raster-fade/style.json @@ -0,0 +1,57 @@ +{ + "version": 8, + "metadata": { + "test": { + "fadeDuration": 100, + "height": 256, + "width": 256, + "description": "In addition to terrain raster, verifies also toggling terrain off / on", + "operations": [ + ["wait"], + ["setTerrain", null], + ["wait"], + ["setTerrain", { + "source": "rgbterrain" + }], + [ + "wait", + 500 + ] + ] + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "pitch": 45, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 200 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/raster/expected.png b/test/integration/render-tests/terrain/raster/expected.png new file mode 100644 index 00000000000..c7213a05560 Binary files /dev/null and b/test/integration/render-tests/terrain/raster/expected.png differ diff --git a/test/integration/render-tests/terrain/raster/style.json b/test/integration/render-tests/terrain/raster/style.json new file mode 100644 index 00000000000..8e01a2779e6 --- /dev/null +++ b/test/integration/render-tests/terrain/raster/style.json @@ -0,0 +1,53 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "description": "In addition to terrain raster, verifies also toggling terrain off / on", + "operations": [ + ["wait"], + ["setTerrain", null], + ["wait"], + ["setTerrain", { + "source": "rgbterrain" + }], + ["wait"] + ] + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "pitch": 45, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 15, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} diff --git a/test/integration/render-tests/terrain/raycast/depth-buffer-0/expected.png b/test/integration/render-tests/terrain/raycast/depth-buffer-0/expected.png new file mode 100644 index 00000000000..270944a26aa Binary files /dev/null and b/test/integration/render-tests/terrain/raycast/depth-buffer-0/expected.png differ diff --git a/test/integration/render-tests/terrain/raycast/depth-buffer-0/style.json b/test/integration/render-tests/terrain/raycast/depth-buffer-0/style.json new file mode 100644 index 00000000000..4905c49dc4e --- /dev/null +++ b/test/integration/render-tests/terrain/raycast/depth-buffer-0/style.json @@ -0,0 +1,45 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "output": "terrainDepth" + } + }, + "center": [-113.32042, 35.9535], + "zoom": 14.41, + "pitch": 59, + "bearing": -165.5, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/raycast/depth-buffer-1/expected.png b/test/integration/render-tests/terrain/raycast/depth-buffer-1/expected.png new file mode 100644 index 00000000000..f4f572aa415 Binary files /dev/null and b/test/integration/render-tests/terrain/raycast/depth-buffer-1/expected.png differ diff --git a/test/integration/render-tests/terrain/raycast/depth-buffer-1/style.json b/test/integration/render-tests/terrain/raycast/depth-buffer-1/style.json new file mode 100644 index 00000000000..3e024c71fd5 --- /dev/null +++ b/test/integration/render-tests/terrain/raycast/depth-buffer-1/style.json @@ -0,0 +1,45 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "output": "terrainDepth" + } + }, + "center": [-113.32296, 35.94662], + "zoom": 12.1, + "pitch": 62, + "bearing": 64.5, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/raycast/depth-buffer-2/expected.png b/test/integration/render-tests/terrain/raycast/depth-buffer-2/expected.png new file mode 100644 index 00000000000..3513e5abbe5 Binary files /dev/null and b/test/integration/render-tests/terrain/raycast/depth-buffer-2/expected.png differ diff --git a/test/integration/render-tests/terrain/raycast/depth-buffer-2/style.json b/test/integration/render-tests/terrain/raycast/depth-buffer-2/style.json new file mode 100644 index 00000000000..78aac82d984 --- /dev/null +++ b/test/integration/render-tests/terrain/raycast/depth-buffer-2/style.json @@ -0,0 +1,45 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "output": "terrainDepth" + } + }, + "center": [-113.3188, 35.955], + "zoom": 10.85, + "pitch": 51, + "bearing": 75.1, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/raycast/depth-buffer-3/expected.png b/test/integration/render-tests/terrain/raycast/depth-buffer-3/expected.png new file mode 100644 index 00000000000..fe8a1c750af Binary files /dev/null and b/test/integration/render-tests/terrain/raycast/depth-buffer-3/expected.png differ diff --git a/test/integration/render-tests/terrain/raycast/depth-buffer-3/style.json b/test/integration/render-tests/terrain/raycast/depth-buffer-3/style.json new file mode 100644 index 00000000000..af76adad547 --- /dev/null +++ b/test/integration/render-tests/terrain/raycast/depth-buffer-3/style.json @@ -0,0 +1,46 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "output": "terrainDepth" + } + }, + "center": [-113.32296, 35.94662], + "zoom": 12.1, + "pitch": 62, + "bearing": 64.5, + "terrain": { + "source": "rgbterrain", + "exaggeration": 0.5 + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/raycast/depth-buffer-4/expected.png b/test/integration/render-tests/terrain/raycast/depth-buffer-4/expected.png new file mode 100644 index 00000000000..009d1e897ef Binary files /dev/null and b/test/integration/render-tests/terrain/raycast/depth-buffer-4/expected.png differ diff --git a/test/integration/render-tests/terrain/raycast/depth-buffer-4/style.json b/test/integration/render-tests/terrain/raycast/depth-buffer-4/style.json new file mode 100644 index 00000000000..1dba06312b1 --- /dev/null +++ b/test/integration/render-tests/terrain/raycast/depth-buffer-4/style.json @@ -0,0 +1,46 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256, + "output": "terrainDepth" + } + }, + "center": [-113.32296, 35.94662], + "zoom": 12.1, + "pitch": 62, + "bearing": 64.5, + "terrain": { + "source": "rgbterrain", + "exaggeration": 2.5 + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ], + "maxzoom": 12, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/terrain/terrarium/expected.png b/test/integration/render-tests/terrain/terrarium/expected.png new file mode 100644 index 00000000000..7f4a7cecd31 Binary files /dev/null and b/test/integration/render-tests/terrain/terrarium/expected.png differ diff --git a/test/integration/render-tests/terrain/terrarium/style.json b/test/integration/render-tests/terrain/terrarium/style.json new file mode 100644 index 00000000000..478151781ed --- /dev/null +++ b/test/integration/render-tests/terrain/terrarium/style.json @@ -0,0 +1,44 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [-113.26903, 35.9654], + "zoom": 11, + "pitch": 45, + "terrain": { + "source": "terrarium" + }, + "sources": { + "satellite": { + "type": "raster", + "tiles": [ + "local://tiles/{z}-{x}-{y}.satellite.png" + ], + "maxzoom": 17, + "tileSize": 256 + }, + "terrarium" : { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrarium.png" + ], + "maxzoom": 15, + "tileSize": 256, + "encoding": "terrarium" + } + }, + "layers": [ + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-fade-duration": 0 + } + } + ] +} diff --git a/test/integration/render-tests/text-field/terrain/formatted-images-vertical/expected.png b/test/integration/render-tests/text-field/terrain/formatted-images-vertical/expected.png new file mode 100644 index 00000000000..db1901df9e2 Binary files /dev/null and b/test/integration/render-tests/text-field/terrain/formatted-images-vertical/expected.png differ diff --git a/test/integration/render-tests/text-field/terrain/formatted-images-vertical/style.json b/test/integration/render-tests/text-field/terrain/formatted-images-vertical/style.json new file mode 100644 index 00000000000..ddd38e27e7c --- /dev/null +++ b/test/integration/render-tests/text-field/terrain/formatted-images-vertical/style.json @@ -0,0 +1,57 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 168, + "width": 128 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "terrain": { + "source": "dem" + }, + "sources": { + "dem": { + "type": "raster-dem", + "tiles": [ + "local://tiles/no/{z}-{x}-{y}.terrain.512.png" + ], + "maxzoom": 15, + "tileSize": 512 + }, + "vertical": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "vertical", + "type": "symbol", + "source": "vertical", + "layout": { + "text-writing-mode": ["vertical"], + "text-field": ["format", "London", ["image", "london-overground.london-underground.national-rail"], + "IH", ["image", "interstate_1"], ["image", "government_icon"], "ッ",{"font-scale": 1.8}], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/text-font-metrics/font-with-baseline-font-without-baseline/expected.png b/test/integration/render-tests/text-font-metrics/font-with-baseline-font-without-baseline/expected.png new file mode 100644 index 00000000000..09b3914fd7c Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/font-with-baseline-font-without-baseline/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/font-with-baseline-font-without-baseline/style.json b/test/integration/render-tests/text-font-metrics/font-with-baseline-font-without-baseline/style.json new file mode 100644 index 00000000000..823b479ffc5 --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/font-with-baseline-font-without-baseline/style.json @@ -0,0 +1,50 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 100, + "width": 100 + } + }, + "center": [ + 0, + 1 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [0, 2] + } + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-size": 15, + "text-max-width": 20, + "text-font": ["literal", ["ArialAscenderDescender"]], + "text-field": ["format", "重ルg",{}, "重ルg", { "text-font": ["literal",["NotoCJK"]]}], + "symbol-placement": "point" + } + } + ] +} diff --git a/test/integration/render-tests/text-font-metrics/font-with-image-vertical/expected.png b/test/integration/render-tests/text-font-metrics/font-with-image-vertical/expected.png new file mode 100644 index 00000000000..2107dce7ebf Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/font-with-image-vertical/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/font-with-image-vertical/style.json b/test/integration/render-tests/text-font-metrics/font-with-image-vertical/style.json new file mode 100644 index 00000000000..c029a4e690b --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/font-with-image-vertical/style.json @@ -0,0 +1,65 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 180, + "width": 150 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 2 + ] + } + } + } + }, + "sprite": "local://sprites/emerald", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": [ + "literal", + [ + "NotoCJKAscenderDescender" + ] + ], + "text-field": [ + "format", + "g", + ["image", "interstate_1"], + "f重yルp", + ["image", "government_icon"] + ], + "text-writing-mode": ["vertical"], + "symbol-placement": "point" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical-no-ascender-descender/expected.png b/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical-no-ascender-descender/expected.png new file mode 100644 index 00000000000..047dc41920d Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical-no-ascender-descender/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical-no-ascender-descender/style.json b/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical-no-ascender-descender/style.json new file mode 100644 index 00000000000..335439039aa --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical-no-ascender-descender/style.json @@ -0,0 +1,64 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 170, + "width": 120 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 2 + ] + } + } + } + }, + "sprite": "local://sprites/emerald", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": [ + "literal", + [ + "NotoCJK" + ] + ], + "text-field": [ + "format", + "pgル", + {"font-scale": 1.5}, + "pqgjy" + ], + "text-writing-mode": ["vertical"], + "symbol-placement": "point" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical/expected.png b/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical/expected.png new file mode 100644 index 00000000000..1963bce4948 Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical/style.json b/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical/style.json new file mode 100644 index 00000000000..5d81ba07150 --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/latin-alphabets-vertical/style.json @@ -0,0 +1,69 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 170, + "width": 120 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 2 + ] + } + } + } + }, + "sprite": "local://sprites/emerald", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": [ + "literal", + [ + "NotoCJKAscenderDescender" + ] + ], + "text-field": [ + "format", + "pgル", + {"font-scale": 1.5}, + "pqgjy", + { + "text-font": [ + "literal", ["ArialAscenderDescender"] + ] + } + ], + "text-writing-mode": ["vertical"], + "symbol-placement": "point" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-font-metrics/line-placement-vertical-shaping-with-punctuations/expected.png b/test/integration/render-tests/text-font-metrics/line-placement-vertical-shaping-with-punctuations/expected.png new file mode 100644 index 00000000000..ab80f08a941 Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/line-placement-vertical-shaping-with-punctuations/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/line-placement-vertical-shaping-with-punctuations/style.json b/test/integration/render-tests/text-font-metrics/line-placement-vertical-shaping-with-punctuations/style.json new file mode 100644 index 00000000000..66d46ce808c --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/line-placement-vertical-shaping-with-punctuations/style.json @@ -0,0 +1,61 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 200, + "width": 120 + } + }, + "center": [ + 0, + 30 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [[0, 0], [0, 60]] + } + } + } + }, + "sprite": "local://sprites/emerald", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": [ + "literal", + [ + "NotoCJK" + ] + ], + "text-field": "“重M”A(ル)M-B", + "symbol-placement": "line" + } + }, { + "id": "lines", + "type": "line", + "source": "geojson", + "paint": { + "line-opacity": 0.25 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-font-metrics/mixed-fonts-both-with-baseline/expected.png b/test/integration/render-tests/text-font-metrics/mixed-fonts-both-with-baseline/expected.png new file mode 100644 index 00000000000..595d72267f5 Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/mixed-fonts-both-with-baseline/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/mixed-fonts-both-with-baseline/style.json b/test/integration/render-tests/text-font-metrics/mixed-fonts-both-with-baseline/style.json new file mode 100644 index 00000000000..6e146a8dee8 --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/mixed-fonts-both-with-baseline/style.json @@ -0,0 +1,48 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 100, + "width": 100 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [0, 2] + } + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": ["literal", ["NotoCJKAscenderDescender"]], + "text-field": ["format", "重ルg",{}, "重ルg", { "text-font": ["literal",["ArialAscenderDescender"]]}], + "symbol-placement": "point" + } + } + ] +} diff --git a/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image-vertical/expected.png b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image-vertical/expected.png new file mode 100644 index 00000000000..d55bdd4feeb Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image-vertical/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image-vertical/style.json b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image-vertical/style.json new file mode 100644 index 00000000000..d106f167e8d --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image-vertical/style.json @@ -0,0 +1,69 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 100, + "width": 100 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 2 + ] + } + } + } + }, + "sprite": "local://sprites/emerald", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": [ + "literal", + [ + "NotoCJKAscenderDescender" + ] + ], + "text-field": [ + "format", + "重ル", + { + "text-font": [ + "literal", ["ArialAscenderDescender"] + ], + "font-scale": 1.6 + }, + ["image", "government_icon"] + ], + "text-writing-mode": ["vertical"], + "symbol-placement": "point" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image/expected.png b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image/expected.png new file mode 100644 index 00000000000..c836f6073a0 Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image/style.json b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image/style.json new file mode 100644 index 00000000000..b2dc5afce4c --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-image/style.json @@ -0,0 +1,69 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 100, + "width": 200 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 2 + ] + } + } + } + }, + "sprite": "local://sprites/emerald", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": [ + "literal", + [ + "NotoCJKAscenderDescender" + ] + ], + "text-field": [ + "format", + "重ルg",{}, + "重ルg", + { + "text-font": [ + "literal", ["ArialAscenderDescender"] + ], + "font-scale": 1.6 + }, + ["image", "interstate_1"], ["image", "government_icon"] + ], + "symbol-placement": "point" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-font-metrics/mixed-fonts-with-images-vertical/expected.png b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-images-vertical/expected.png new file mode 100644 index 00000000000..f9a34db54c3 Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-images-vertical/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/mixed-fonts-with-images-vertical/style.json b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-images-vertical/style.json new file mode 100644 index 00000000000..87f8f7f014b --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-images-vertical/style.json @@ -0,0 +1,79 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 250, + "width": 200 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 2 + ] + } + } + } + }, + "sprite": "local://sprites/emerald", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": [ + "literal", + [ + "NotoCJKAscenderDescender" + ] + ], + "text-field": [ + "format", + "g", + {"font-scale": 2.0}, + ["image", "interstate_1"], + "重f", + { + "text-font": [ + "literal", ["ArialAscenderDescender"] + ], + "font-scale": 1.2 + }, + "ルp", + { + "text-font": [ + "literal", ["ArialAscenderDescender"] + ], + "font-scale": 1.8 + }, + ["image", "government_icon"] + ], + "text-writing-mode": ["vertical"], + "symbol-placement": "point" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-font-metrics/mixed-fonts-with-scales/expected.png b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-scales/expected.png new file mode 100644 index 00000000000..259ba05a335 Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-scales/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/mixed-fonts-with-scales/style.json b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-scales/style.json new file mode 100644 index 00000000000..19f5d033c3d --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/mixed-fonts-with-scales/style.json @@ -0,0 +1,49 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 120, + "width": 120 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [0, 1.5] + } + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": ["literal", ["NotoCJKAscenderDescender"]], + "text-field": ["format", "重ルg",{"font-scale": 1.6}, "重ルg", { "text-font": ["literal",["ArialAscenderDescender"]]}], + "symbol-placement": "point" + } + } + ] +} diff --git a/test/integration/render-tests/text-font-metrics/multiline-point-placement-vertical-shaping-with-punctuations/expected.png b/test/integration/render-tests/text-font-metrics/multiline-point-placement-vertical-shaping-with-punctuations/expected.png new file mode 100644 index 00000000000..545e6642641 Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/multiline-point-placement-vertical-shaping-with-punctuations/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/multiline-point-placement-vertical-shaping-with-punctuations/style.json b/test/integration/render-tests/text-font-metrics/multiline-point-placement-vertical-shaping-with-punctuations/style.json new file mode 100644 index 00000000000..14861c3187a --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/multiline-point-placement-vertical-shaping-with-punctuations/style.json @@ -0,0 +1,58 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 150, + "width": 120 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 2 + ] + } + } + } + }, + "sprite": "local://sprites/emerald", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": [ + "literal", + [ + "NotoCJK" + ] + ], + "text-field": "“\n重M\n”A(\nルB\n)M", + "text-writing-mode": ["vertical"], + "symbol-placement": "point" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-font-metrics/point-placement-vertical-shaping-with-punctuations/expected.png b/test/integration/render-tests/text-font-metrics/point-placement-vertical-shaping-with-punctuations/expected.png new file mode 100644 index 00000000000..6e7bca6306e Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/point-placement-vertical-shaping-with-punctuations/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/point-placement-vertical-shaping-with-punctuations/style.json b/test/integration/render-tests/text-font-metrics/point-placement-vertical-shaping-with-punctuations/style.json new file mode 100644 index 00000000000..c4482638fa7 --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/point-placement-vertical-shaping-with-punctuations/style.json @@ -0,0 +1,59 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 200, + "width": 120 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 2 + ] + } + } + } + }, + "sprite": "local://sprites/emerald", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": [ + "literal", + [ + "NotoCJK" + ] + ], + "text-field": "“重M”A(ル)M-B", + "text-writing-mode": ["vertical"], + "symbol-placement": "point" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-font-metrics/punctuations-vertical/expected.png b/test/integration/render-tests/text-font-metrics/punctuations-vertical/expected.png new file mode 100644 index 00000000000..6f1b6f4adc9 Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/punctuations-vertical/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/punctuations-vertical/style.json b/test/integration/render-tests/text-font-metrics/punctuations-vertical/style.json new file mode 100644 index 00000000000..b8b0b7f7f8f --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/punctuations-vertical/style.json @@ -0,0 +1,70 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 200, + "width": 150 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 2 + ] + } + } + } + }, + "sprite": "local://sprites/emerald", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": [ + "literal", + [ + "NotoCJKAscenderDescender" + ] + ], + "text-field": [ + "format", + ["image", "interstate_1"], + "(重)", + "(ル)", + { + "text-font": [ + "literal", ["ArialAscenderDescender"] + ] + }, + ["image", "government_icon"] + ], + "text-writing-mode": ["vertical"], + "symbol-placement": "point" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-font-metrics/vertical-shaping-with-ZWSP/expected.png b/test/integration/render-tests/text-font-metrics/vertical-shaping-with-ZWSP/expected.png new file mode 100644 index 00000000000..9dda2fdeb78 Binary files /dev/null and b/test/integration/render-tests/text-font-metrics/vertical-shaping-with-ZWSP/expected.png differ diff --git a/test/integration/render-tests/text-font-metrics/vertical-shaping-with-ZWSP/style.json b/test/integration/render-tests/text-font-metrics/vertical-shaping-with-ZWSP/style.json new file mode 100644 index 00000000000..7499c1fa4d5 --- /dev/null +++ b/test/integration/render-tests/text-font-metrics/vertical-shaping-with-ZWSP/style.json @@ -0,0 +1,59 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 150, + "width": 100 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 2 + ] + } + } + } + }, + "sprite": "local://sprites/emerald", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "mixed-fonts", + "type": "symbol", + "source": "geojson", + "layout": { + "text-max-width": 20, + "text-font": [ + "literal", + [ + "NotoCJK" + ] + ], + "text-field": "重M\u200bAル\u200bMB", + "text-writing-mode": ["vertical"], + "symbol-placement": "point" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-icon-high-pitch/expected.png b/test/integration/render-tests/text-icon-high-pitch/expected.png new file mode 100644 index 00000000000..cd8c152493d Binary files /dev/null and b/test/integration/render-tests/text-icon-high-pitch/expected.png differ diff --git a/test/integration/render-tests/text-icon-high-pitch/style.json b/test/integration/render-tests/text-icon-high-pitch/style.json new file mode 100644 index 00000000000..1ff358ada61 --- /dev/null +++ b/test/integration/render-tests/text-icon-high-pitch/style.json @@ -0,0 +1,54 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": false, + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "pitch": 85, + "zoom": 14, + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/sprite", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "symbol", + "source": "mapbox", + "source-layer": "road_label", + "layout": { + "icon-image": "triangle-stroked-12", + "text-field": "test test", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "point", + "symbol-spacing": 20 + }, + "paint": { + "icon-opacity": 1 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map-terrain/expected.png b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map-terrain/expected.png new file mode 100644 index 00000000000..bc7a1f001b7 Binary files /dev/null and b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map-terrain/expected.png differ diff --git a/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map-terrain/style.json b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map-terrain/style.json new file mode 100644 index 00000000000..9c8be7765f6 --- /dev/null +++ b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map-terrain/style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 17, + "pitch": 45, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/12-759-1609.terrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + }, + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-color": "#888", + "line-width": 1 + } + }, + { + "id": "text", + "type": "symbol", + "source": "mapbox", + "source-layer": "road_label", + "layout": { + "symbol-placement": "line", + "symbol-spacing": 60, + "text-rotation-alignment": "map", + "text-pitch-alignment": "map", + "text-field": "{class}", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-opacity": 1 + } + } + ] +} diff --git a/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-viewport-terrain/expected.png b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-viewport-terrain/expected.png new file mode 100644 index 00000000000..f9ef08997bc Binary files /dev/null and b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-viewport-terrain/expected.png differ diff --git a/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-viewport-terrain/style.json b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-viewport-terrain/style.json new file mode 100644 index 00000000000..804f4bf8e71 --- /dev/null +++ b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-viewport-terrain/style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14.9, + "pitch": 45, + "terrain": { + "source": "rgbterrain" + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/12-759-1609.terrain.png" + ], + "maxzoom": 11, + "tileSize": 256 + }, + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-color": "#888", + "line-width": 1 + } + }, + { + "id": "text", + "type": "symbol", + "source": "mapbox", + "source-layer": "road_label", + "layout": { + "symbol-placement": "line", + "symbol-spacing": 60, + "text-rotation-alignment": "viewport", + "text-pitch-alignment": "map", + "text-field": "{class}", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-opacity": 1 + } + } + ] +} diff --git a/test/integration/render-tests/text-rotate/with-offset/expected.png b/test/integration/render-tests/text-rotate/with-offset/expected.png index c27be9e099a..f1ae274732a 100644 Binary files a/test/integration/render-tests/text-rotate/with-offset/expected.png and b/test/integration/render-tests/text-rotate/with-offset/expected.png differ diff --git a/test/integration/render-tests/text-rotate/with-offset/style.json b/test/integration/render-tests/text-rotate/with-offset/style.json index 8c281360c5c..26bd59f4f8a 100644 --- a/test/integration/render-tests/text-rotate/with-offset/style.json +++ b/test/integration/render-tests/text-rotate/with-offset/style.json @@ -4,6 +4,7 @@ "test": { "width": 128, "height": 256, + "allowed": 0.005, "collisionDebug": true } }, diff --git a/test/integration/render-tests/text-variable-anchor/icon-text-fit-collision-box/expected.png b/test/integration/render-tests/text-variable-anchor/icon-text-fit-collision-box/expected.png index deb6c2b27ba..187aa945958 100644 Binary files a/test/integration/render-tests/text-variable-anchor/icon-text-fit-collision-box/expected.png and b/test/integration/render-tests/text-variable-anchor/icon-text-fit-collision-box/expected.png differ diff --git a/test/integration/render-tests/text-variable-anchor/rotated-offset/expected.png b/test/integration/render-tests/text-variable-anchor/rotated-offset/expected.png index d056626fe08..d0b72b7ecbc 100644 Binary files a/test/integration/render-tests/text-variable-anchor/rotated-offset/expected.png and b/test/integration/render-tests/text-variable-anchor/rotated-offset/expected.png differ diff --git a/test/integration/render-tests/text-variable-anchor/rotated-with-map/expected.png b/test/integration/render-tests/text-variable-anchor/rotated-with-map/expected.png index a7598d9d6fd..1fa69a6ecb8 100644 Binary files a/test/integration/render-tests/text-variable-anchor/rotated-with-map/expected.png and b/test/integration/render-tests/text-variable-anchor/rotated-with-map/expected.png differ diff --git a/test/integration/render-tests/text-variable-anchor/rotated/expected.png b/test/integration/render-tests/text-variable-anchor/rotated/expected.png index 63403973742..62699a478c2 100644 Binary files a/test/integration/render-tests/text-variable-anchor/rotated/expected.png and b/test/integration/render-tests/text-variable-anchor/rotated/expected.png differ diff --git a/test/integration/render-tests/text-writing-mode/point_label/cjk-variable-anchors-vertical-horizontal-mode-icon-text-fit-terrain/expected.png b/test/integration/render-tests/text-writing-mode/point_label/cjk-variable-anchors-vertical-horizontal-mode-icon-text-fit-terrain/expected.png new file mode 100644 index 00000000000..8923aee2f53 Binary files /dev/null and b/test/integration/render-tests/text-writing-mode/point_label/cjk-variable-anchors-vertical-horizontal-mode-icon-text-fit-terrain/expected.png differ diff --git a/test/integration/render-tests/text-writing-mode/point_label/cjk-variable-anchors-vertical-horizontal-mode-icon-text-fit-terrain/style.json b/test/integration/render-tests/text-writing-mode/point_label/cjk-variable-anchors-vertical-horizontal-mode-icon-text-fit-terrain/style.json new file mode 100644 index 00000000000..8a0d2a56cf3 --- /dev/null +++ b/test/integration/render-tests/text-writing-mode/point_label/cjk-variable-anchors-vertical-horizontal-mode-icon-text-fit-terrain/style.json @@ -0,0 +1,99 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 256 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "bearing": -66.4, + "pitch": 75, + "terrain": { + "source": "rgbterrain", + "exaggeration": 3 + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/const/{z}-{x}-{y}.terrain1.512.png" + ], + "maxzoom": 3, + "tileSize": 512 + }, + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name_jp": "マップLineボックス" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, 14 ] + } + }, + { + "type": "Feature", + "properties": { + "name_jp": "マップボNewックス" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, 14 ] + } + }, + { + "type": "Feature", + "properties": { + "name_jp": "マップボックス" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, 14 ] + } + }, + { + "type": "Feature", + "properties": { + "name_jp": "マップボNewックス" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, -32 ] + } + } + ] + } + } + }, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "text", + "type": "symbol", + "source": "point", + "layout": { + "icon-image": "motorway_lg_6", + "icon-text-fit": "both", + "icon-text-fit-padding": [5, 5, 5, 5], + "text-field": "{name_jp}", + "text-writing-mode": ["vertical", "horizontal"], + "text-radial-offset": 3, + "text-variable-anchor": ["center", "left", "right", "bottom"], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": 10, + "text-max-width": 8 + } + } + ] +} diff --git a/test/integration/render-tests/video/default/style.json b/test/integration/render-tests/video/default/style.json index 53cca09964f..5e633b67fa9 100644 --- a/test/integration/render-tests/video/default/style.json +++ b/test/integration/render-tests/video/default/style.json @@ -34,7 +34,8 @@ ] ], "urls": [ - "local://video/0.png" + "https://static-assets.mapbox.com/mapbox-gl-js/drone.mp4", + "https://static-assets.mapbox.com/mapbox-gl-js/drone.webm" ] } }, @@ -48,4 +49,4 @@ } } ] -} \ No newline at end of file +} diff --git a/test/integration/render-tests/within/layout-text-size/expected.png b/test/integration/render-tests/within/layout-text-size/expected.png new file mode 100644 index 00000000000..2e95630e7ce Binary files /dev/null and b/test/integration/render-tests/within/layout-text-size/expected.png differ diff --git a/test/integration/render-tests/within/layout-text-size/style.json b/test/integration/render-tests/within/layout-text-size/style.json new file mode 100644 index 00000000000..1c9c8b2eed2 --- /dev/null +++ b/test/integration/render-tests/within/layout-text-size/style.json @@ -0,0 +1,118 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "zoom": 2, + "center": [3.05, 3.25], + "sources": { + "points": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "In" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.5775390625, + 2.3284603685731593 + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "Out" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.021484375, + 7.298078531355303 + ] + } + } + ] + } + }, + "polygon": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], + [0, 5], + [5, 5], + [5, 0], + [0, 0] + ] + ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "border", + "type": "line", + "source": "polygon" + }, + { + "id": "symbol", + "type": "symbol", + "source": "points", + "layout": { + "text-field": "{name}", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": ["case", ["within", { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], + [0, 5], + [5, 5], + [5, 0], + [0, 0] + ] + ] + }], 24, 12] + }, + "paint" : { + "text-color": ["case", ["within", { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], + [0, 5], + [5, 5], + [5, 0], + [0, 0] + ] + ] + }], "green", "red"] + } + } + ] + } + \ No newline at end of file diff --git a/test/integration/rollup.config.test.js b/test/integration/rollup.config.test.js index f8e347cea28..4007dff75ff 100644 --- a/test/integration/rollup.config.test.js +++ b/test/integration/rollup.config.test.js @@ -1,14 +1,16 @@ import {plugins} from '../../build/rollup_plugins'; +const suiteName = process.env.SUITE_NAME; + export default { - input: 'test/integration/lib/query-browser.js', + input: `test/integration/lib/${suiteName}.js`, output: { - name: 'queryTests', + name: `${suiteName}Tests`, format: 'iife', sourcemap: 'inline', indent: false, - file: 'test/integration/dist/query-test.js' + file: `test/integration/dist/integration-test.js` }, - plugins: plugins(false, false), + plugins: plugins(false, false, true), external: [ 'tape', 'mapboxgl' ] }; diff --git a/test/integration/testem.js b/test/integration/testem.js index ec262ff7b0d..bd9601c2ec1 100644 --- a/test/integration/testem.js +++ b/test/integration/testem.js @@ -13,7 +13,10 @@ const rollupDevConfig = require('../../rollup.config').default; const rollupTestConfig = require('./rollup.config.test').default; const rootFixturePath = 'test/integration/'; -const suitePath = 'query-tests'; +const outputPath = `${rootFixturePath}dist`; +const suiteName = process.env.SUITE_NAME; +const suitePath = `${suiteName}-tests`; +const ciOutputFile = `${rootFixturePath}${suitePath}/test-results.xml`; const fixtureBuildInterval = 2000; let beforeHookInvoked = false; @@ -22,21 +25,32 @@ let server; let fixtureWatcher; const rollupWatchers = {}; -module.exports = { - "test_page": "test/integration/testem_page.html", - "src_files": [ - "dist/mapbox-gl-dev.js", - "test/integration/dist/query-test.js" - ], - "launch_in_dev": [], - "launch_in_ci": [ "Chrome" ], - "browser_args": { - "Chrome": { - "mode": "ci", - "args": [ "--headless", "--disable-gpu", "--remote-debugging-port=9222" ] +function getQueryParams() { + const params = process.argv.slice(2).filter((value, index, self) => { return self.indexOf(value) === index; }) || []; + const filterIndex = params.findIndex((elem) => { return String(elem).startsWith("tests="); }); + const queryParams = {}; + if (filterIndex !== -1) { + const split = String(params.splice(filterIndex, 1)).split('='); + if (split.length === 2) { + queryParams.filter = split[1]; } - }, + } + return queryParams; +} + +const defaultTestemConfig = { + "test_page": "test/integration/testem_page.html", + "query_params": getQueryParams(), "proxies": { + "/image":{ + "target": "http://localhost:2900" + }, + "/geojson":{ + "target": "http://localhost:2900" + }, + "/video":{ + "target": "http://localhost:2900" + }, "/tiles":{ "target": "http://localhost:2900" }, @@ -54,6 +68,9 @@ module.exports = { }, "/write-file":{ "target": "http://localhost:2900" + }, + "/mvt-fixtures":{ + "target": "http://localhost:2900" } }, "before_tests"(config, data, callback) { @@ -76,15 +93,31 @@ module.exports = { } }; +const ciTestemConfig = { + "launch_in_ci": [ "Chrome" ], + "reporter": "xunit", + "report_file": ciOutputFile, + "xunit_intermediate_output": true, + "browser_args": { + "Chrome": { + "ci": [ "--disable-backgrounding-occluded-windows" ] + } + } +}; + +const testemConfig = process.env.CI ? Object.assign({}, defaultTestemConfig, ciTestemConfig) : defaultTestemConfig; + +module.exports = testemConfig; + // helper method that builds test artifacts when in CI mode. // Retuns a promise that resolves when all artifacts are built function buildArtifactsCi() { //1. Compile fixture data into a json file, so it can be bundled - generateFixtureJson(rootFixturePath, suitePath); + generateFixtureJson(rootFixturePath, suitePath, outputPath, suitePath === 'render-tests'); //2. Build tape const tapePromise = buildTape(); //3. Build test artifacts in parallel - const rollupPromise = runAll(['build-query-suite', 'build-dev'], {parallel: true}); + const rollupPromise = runAll([`build-test-suite`, 'build-dev'], {parallel: true}); return Promise.all([tapePromise, rollupPromise]); } @@ -95,21 +128,21 @@ function buildArtifactsDev() { return buildTape().then(() => { // A promise that resolves on the first build of fixtures.json return new Promise((resolve, reject) => { - fixtureWatcher = chokidar.watch(getAllFixtureGlobs(rootFixturePath, suitePath)); + fixtureWatcher = chokidar.watch(getAllFixtureGlobs(rootFixturePath, suitePath), {ignored: (path) => path.includes('actual.png') || path.includes('actual.json') || path.includes('diff.png')}); let needsRebuild = false; fixtureWatcher.on('ready', () => { - generateFixtureJson(rootFixturePath, suitePath); + generateFixtureJson(rootFixturePath, suitePath, outputPath, suitePath === 'render-tests'); //Throttle calls to `generateFixtureJson` to run every 2s setInterval(() => { if (needsRebuild) { - generateFixtureJson(rootFixturePath, suitePath); + generateFixtureJson(rootFixturePath, suitePath, outputPath, suitePath === 'render-tests'); needsRebuild = false; } }, fixtureBuildInterval); //Flag needs rebuild when anything changes - fixtureWatcher.on('all', () => { + fixtureWatcher.on('change', () => { needsRebuild = true; }); // Resolve promise once chokidar has finished first scan of fixtures @@ -144,7 +177,7 @@ function buildArtifactsDev() { return Promise.all([ startRollupWatcher('mapbox-gl', rollupDevConfig), - startRollupWatcher('query-suite', rollupTestConfig), + startRollupWatcher(suitePath, rollupTestConfig) ]); }); } diff --git a/test/integration/testem_page.html b/test/integration/testem_page.html index 7cdc390cfa0..96a17e47300 100644 --- a/test/integration/testem_page.html +++ b/test/integration/testem_page.html @@ -3,12 +3,19 @@ Mapbox GL JS Integration Tests + - + + - \ No newline at end of file + diff --git a/test/integration/tiles/10-162-396.terrain.png b/test/integration/tiles/10-162-396.terrain.png new file mode 100644 index 00000000000..9c2378fc312 Binary files /dev/null and b/test/integration/tiles/10-162-396.terrain.png differ diff --git a/test/integration/tiles/10-162-397.terrain.png b/test/integration/tiles/10-162-397.terrain.png new file mode 100644 index 00000000000..12eecdfef03 Binary files /dev/null and b/test/integration/tiles/10-162-397.terrain.png differ diff --git a/test/integration/tiles/10-163-394.terrain.png b/test/integration/tiles/10-163-394.terrain.png new file mode 100644 index 00000000000..c2281cb84f9 Binary files /dev/null and b/test/integration/tiles/10-163-394.terrain.png differ diff --git a/test/integration/tiles/10-163-395.terrain.png b/test/integration/tiles/10-163-395.terrain.png new file mode 100644 index 00000000000..09dc01c56f8 Binary files /dev/null and b/test/integration/tiles/10-163-395.terrain.png differ diff --git a/test/integration/tiles/10-163-396.terrain.png b/test/integration/tiles/10-163-396.terrain.png new file mode 100644 index 00000000000..470106fdb48 Binary files /dev/null and b/test/integration/tiles/10-163-396.terrain.png differ diff --git a/test/integration/tiles/10-163-397.terrain.png b/test/integration/tiles/10-163-397.terrain.png new file mode 100644 index 00000000000..41e4e82f52a Binary files /dev/null and b/test/integration/tiles/10-163-397.terrain.png differ diff --git a/test/integration/tiles/10-189-402.terrain.png b/test/integration/tiles/10-189-402.terrain.png new file mode 100644 index 00000000000..241675b7d1e Binary files /dev/null and b/test/integration/tiles/10-189-402.terrain.png differ diff --git a/test/integration/tiles/10-189-402.terrarium.png b/test/integration/tiles/10-189-402.terrarium.png new file mode 100644 index 00000000000..4f58a76e55e Binary files /dev/null and b/test/integration/tiles/10-189-402.terrarium.png differ diff --git a/test/integration/tiles/10-190-402.terrain.png b/test/integration/tiles/10-190-402.terrain.png new file mode 100644 index 00000000000..e05ccea2b90 Binary files /dev/null and b/test/integration/tiles/10-190-402.terrain.png differ diff --git a/test/integration/tiles/10-190-402.terrarium.png b/test/integration/tiles/10-190-402.terrarium.png new file mode 100644 index 00000000000..a79748f1c9e Binary files /dev/null and b/test/integration/tiles/10-190-402.terrarium.png differ diff --git a/test/integration/tiles/11-326-790.terrain.png b/test/integration/tiles/11-326-790.terrain.png new file mode 100644 index 00000000000..ac263ad874e Binary files /dev/null and b/test/integration/tiles/11-326-790.terrain.png differ diff --git a/test/integration/tiles/11-326-792.terrain.png b/test/integration/tiles/11-326-792.terrain.png new file mode 100644 index 00000000000..bbf0a9acf4f Binary files /dev/null and b/test/integration/tiles/11-326-792.terrain.png differ diff --git a/test/integration/tiles/11-326-793.terrain.png b/test/integration/tiles/11-326-793.terrain.png new file mode 100644 index 00000000000..9ac9144efae Binary files /dev/null and b/test/integration/tiles/11-326-793.terrain.png differ diff --git a/test/integration/tiles/11-327-790.terrain.png b/test/integration/tiles/11-327-790.terrain.png new file mode 100644 index 00000000000..b4c29e7be86 Binary files /dev/null and b/test/integration/tiles/11-327-790.terrain.png differ diff --git a/test/integration/tiles/11-327-791.terrain.png b/test/integration/tiles/11-327-791.terrain.png new file mode 100644 index 00000000000..65ab7a7ce7f Binary files /dev/null and b/test/integration/tiles/11-327-791.terrain.png differ diff --git a/test/integration/tiles/11-327-792.terrain.png b/test/integration/tiles/11-327-792.terrain.png new file mode 100644 index 00000000000..4fe21cc6a8f Binary files /dev/null and b/test/integration/tiles/11-327-792.terrain.png differ diff --git a/test/integration/tiles/11-327-793.terrain.png b/test/integration/tiles/11-327-793.terrain.png new file mode 100644 index 00000000000..28759f5eacd Binary files /dev/null and b/test/integration/tiles/11-327-793.terrain.png differ diff --git a/test/integration/tiles/11-378-803.terrain.png b/test/integration/tiles/11-378-803.terrain.png new file mode 100644 index 00000000000..2bc7870944f Binary files /dev/null and b/test/integration/tiles/11-378-803.terrain.png differ diff --git a/test/integration/tiles/11-378-803.terrarium.png b/test/integration/tiles/11-378-803.terrarium.png new file mode 100644 index 00000000000..11064a7e76e Binary files /dev/null and b/test/integration/tiles/11-378-803.terrarium.png differ diff --git a/test/integration/tiles/11-378-804.terrain.png b/test/integration/tiles/11-378-804.terrain.png new file mode 100644 index 00000000000..bf5cc125b78 Binary files /dev/null and b/test/integration/tiles/11-378-804.terrain.png differ diff --git a/test/integration/tiles/11-378-804.terrarium.png b/test/integration/tiles/11-378-804.terrarium.png new file mode 100644 index 00000000000..fcf15647b16 Binary files /dev/null and b/test/integration/tiles/11-378-804.terrarium.png differ diff --git a/test/integration/tiles/11-379-803.terrain.png b/test/integration/tiles/11-379-803.terrain.png new file mode 100644 index 00000000000..37f46f7824b Binary files /dev/null and b/test/integration/tiles/11-379-803.terrain.png differ diff --git a/test/integration/tiles/11-379-803.terrarium.png b/test/integration/tiles/11-379-803.terrarium.png new file mode 100644 index 00000000000..3578d41e4e8 Binary files /dev/null and b/test/integration/tiles/11-379-803.terrarium.png differ diff --git a/test/integration/tiles/11-379-804.terrain.png b/test/integration/tiles/11-379-804.terrain.png new file mode 100644 index 00000000000..4e049c80d08 Binary files /dev/null and b/test/integration/tiles/11-379-804.terrain.png differ diff --git a/test/integration/tiles/11-379-804.terrarium.png b/test/integration/tiles/11-379-804.terrarium.png new file mode 100644 index 00000000000..efb63550d16 Binary files /dev/null and b/test/integration/tiles/11-379-804.terrarium.png differ diff --git a/test/integration/tiles/11-380-803.terrain.png b/test/integration/tiles/11-380-803.terrain.png new file mode 100644 index 00000000000..bf5cc125b78 Binary files /dev/null and b/test/integration/tiles/11-380-803.terrain.png differ diff --git a/test/integration/tiles/11-380-803.terrarium.png b/test/integration/tiles/11-380-803.terrarium.png new file mode 100644 index 00000000000..fb82bb868d1 Binary files /dev/null and b/test/integration/tiles/11-380-803.terrarium.png differ diff --git a/test/integration/tiles/11-380-804.terrain.png b/test/integration/tiles/11-380-804.terrain.png new file mode 100644 index 00000000000..c8b5b675f14 Binary files /dev/null and b/test/integration/tiles/11-380-804.terrain.png differ diff --git a/test/integration/tiles/11-380-804.terrarium.png b/test/integration/tiles/11-380-804.terrarium.png new file mode 100644 index 00000000000..3eebf86015b Binary files /dev/null and b/test/integration/tiles/11-380-804.terrarium.png differ diff --git a/test/integration/tiles/12-653-1586-terrain-512.png b/test/integration/tiles/12-653-1586-terrain-512.png new file mode 100644 index 00000000000..347fb3c80ae Binary files /dev/null and b/test/integration/tiles/12-653-1586-terrain-512.png differ diff --git a/test/integration/tiles/12-654-1584.terrain.png b/test/integration/tiles/12-654-1584.terrain.png new file mode 100644 index 00000000000..4570f8cb321 Binary files /dev/null and b/test/integration/tiles/12-654-1584.terrain.png differ diff --git a/test/integration/tiles/12-654-1585.terrain.png b/test/integration/tiles/12-654-1585.terrain.png new file mode 100644 index 00000000000..2410e6176a6 Binary files /dev/null and b/test/integration/tiles/12-654-1585.terrain.png differ diff --git a/test/integration/tiles/12-654-1586-terrain-512.png b/test/integration/tiles/12-654-1586-terrain-512.png new file mode 100644 index 00000000000..8c2e5455d9e Binary files /dev/null and b/test/integration/tiles/12-654-1586-terrain-512.png differ diff --git a/test/integration/tiles/12-655-1584.terrain.png b/test/integration/tiles/12-655-1584.terrain.png new file mode 100644 index 00000000000..e0a3b177fe1 Binary files /dev/null and b/test/integration/tiles/12-655-1584.terrain.png differ diff --git a/test/integration/tiles/12-655-1585.terrain.png b/test/integration/tiles/12-655-1585.terrain.png new file mode 100644 index 00000000000..de016fb4a6d Binary files /dev/null and b/test/integration/tiles/12-655-1585.terrain.png differ diff --git a/test/integration/tiles/13-1307-3172-terrain-512.png b/test/integration/tiles/13-1307-3172-terrain-512.png new file mode 100644 index 00000000000..7fd09cf52e6 Binary files /dev/null and b/test/integration/tiles/13-1307-3172-terrain-512.png differ diff --git a/test/integration/tiles/13-1308-3168.terrain.png b/test/integration/tiles/13-1308-3168.terrain.png new file mode 100644 index 00000000000..cd3703d82cf Binary files /dev/null and b/test/integration/tiles/13-1308-3168.terrain.png differ diff --git a/test/integration/tiles/13-1308-3169.terrain.png b/test/integration/tiles/13-1308-3169.terrain.png new file mode 100644 index 00000000000..20599d8859d Binary files /dev/null and b/test/integration/tiles/13-1308-3169.terrain.png differ diff --git a/test/integration/tiles/13-1308-3172-terrain-512.png b/test/integration/tiles/13-1308-3172-terrain-512.png new file mode 100644 index 00000000000..ade72f2b2f0 Binary files /dev/null and b/test/integration/tiles/13-1308-3172-terrain-512.png differ diff --git a/test/integration/tiles/13-1309-3168.terrain.png b/test/integration/tiles/13-1309-3168.terrain.png new file mode 100644 index 00000000000..d0ca2f80b98 Binary files /dev/null and b/test/integration/tiles/13-1309-3168.terrain.png differ diff --git a/test/integration/tiles/13-1309-3169.terrain.png b/test/integration/tiles/13-1309-3169.terrain.png new file mode 100644 index 00000000000..2b8258c58a6 Binary files /dev/null and b/test/integration/tiles/13-1309-3169.terrain.png differ diff --git a/test/integration/tiles/13-1310-3168.terrain.png b/test/integration/tiles/13-1310-3168.terrain.png new file mode 100644 index 00000000000..d81955fa735 Binary files /dev/null and b/test/integration/tiles/13-1310-3168.terrain.png differ diff --git a/test/integration/tiles/13-1310-3169.terrain.png b/test/integration/tiles/13-1310-3169.terrain.png new file mode 100644 index 00000000000..b5e694eefa7 Binary files /dev/null and b/test/integration/tiles/13-1310-3169.terrain.png differ diff --git a/test/integration/tiles/13-1311-3168.terrain.png b/test/integration/tiles/13-1311-3168.terrain.png new file mode 100644 index 00000000000..0eddc8259a4 Binary files /dev/null and b/test/integration/tiles/13-1311-3168.terrain.png differ diff --git a/test/integration/tiles/14-2615-6344-terrain-512.png b/test/integration/tiles/14-2615-6344-terrain-512.png new file mode 100644 index 00000000000..957e346b725 Binary files /dev/null and b/test/integration/tiles/14-2615-6344-terrain-512.png differ diff --git a/test/integration/tiles/14-2615-6345-terrain-512.png b/test/integration/tiles/14-2615-6345-terrain-512.png new file mode 100644 index 00000000000..a8f43cef887 Binary files /dev/null and b/test/integration/tiles/14-2615-6345-terrain-512.png differ diff --git a/test/integration/tiles/14-2616-6344-terrain-512.png b/test/integration/tiles/14-2616-6344-terrain-512.png new file mode 100644 index 00000000000..95084114e3a Binary files /dev/null and b/test/integration/tiles/14-2616-6344-terrain-512.png differ diff --git a/test/integration/tiles/14-2616-6345-terrain-512.png b/test/integration/tiles/14-2616-6345-terrain-512.png new file mode 100644 index 00000000000..bcf0a441f1d Binary files /dev/null and b/test/integration/tiles/14-2616-6345-terrain-512.png differ diff --git a/test/integration/tiles/14-2617-6344-terrain-512.png b/test/integration/tiles/14-2617-6344-terrain-512.png new file mode 100644 index 00000000000..17a42db3099 Binary files /dev/null and b/test/integration/tiles/14-2617-6344-terrain-512.png differ diff --git a/test/integration/tiles/14-2617-6345-terrain-512.png b/test/integration/tiles/14-2617-6345-terrain-512.png new file mode 100644 index 00000000000..f1cba4a9fd6 Binary files /dev/null and b/test/integration/tiles/14-2617-6345-terrain-512.png differ diff --git a/test/integration/tiles/14-2618-6333-terrain-512.png b/test/integration/tiles/14-2618-6333-terrain-512.png new file mode 100644 index 00000000000..0b96878cc9a Binary files /dev/null and b/test/integration/tiles/14-2618-6333-terrain-512.png differ diff --git a/test/integration/tiles/14-2618-6334-terrain-512.png b/test/integration/tiles/14-2618-6334-terrain-512.png new file mode 100644 index 00000000000..021068bee8e Binary files /dev/null and b/test/integration/tiles/14-2618-6334-terrain-512.png differ diff --git a/test/integration/tiles/14-2618-6336.terrain.png b/test/integration/tiles/14-2618-6336.terrain.png new file mode 100644 index 00000000000..8bce7406f11 Binary files /dev/null and b/test/integration/tiles/14-2618-6336.terrain.png differ diff --git a/test/integration/tiles/14-2618-6337.terrain.png b/test/integration/tiles/14-2618-6337.terrain.png new file mode 100644 index 00000000000..d4c0443746f Binary files /dev/null and b/test/integration/tiles/14-2618-6337.terrain.png differ diff --git a/test/integration/tiles/14-2618-6338.terrain.png b/test/integration/tiles/14-2618-6338.terrain.png new file mode 100644 index 00000000000..e987ccf5565 Binary files /dev/null and b/test/integration/tiles/14-2618-6338.terrain.png differ diff --git a/test/integration/tiles/14-2619-6336.terrain.png b/test/integration/tiles/14-2619-6336.terrain.png new file mode 100644 index 00000000000..2f5536fc87e Binary files /dev/null and b/test/integration/tiles/14-2619-6336.terrain.png differ diff --git a/test/integration/tiles/14-2619-6337.terrain.png b/test/integration/tiles/14-2619-6337.terrain.png new file mode 100644 index 00000000000..1595b2f5176 Binary files /dev/null and b/test/integration/tiles/14-2619-6337.terrain.png differ diff --git a/test/integration/tiles/14-2619-6338.terrain.png b/test/integration/tiles/14-2619-6338.terrain.png new file mode 100644 index 00000000000..30d43b79fce Binary files /dev/null and b/test/integration/tiles/14-2619-6338.terrain.png differ diff --git a/test/integration/tiles/14-2620-6336.terrain.png b/test/integration/tiles/14-2620-6336.terrain.png new file mode 100644 index 00000000000..a6a05f9f60f Binary files /dev/null and b/test/integration/tiles/14-2620-6336.terrain.png differ diff --git a/test/integration/tiles/14-2620-6337.terrain.png b/test/integration/tiles/14-2620-6337.terrain.png new file mode 100644 index 00000000000..30f3cabd37a Binary files /dev/null and b/test/integration/tiles/14-2620-6337.terrain.png differ diff --git a/test/integration/tiles/16-10473-25335.mvt b/test/integration/tiles/16-10473-25335.mvt new file mode 100644 index 00000000000..10720d115d1 Binary files /dev/null and b/test/integration/tiles/16-10473-25335.mvt differ diff --git a/test/integration/tiles/16-10473-25336.mvt b/test/integration/tiles/16-10473-25336.mvt new file mode 100644 index 00000000000..07447abbdba Binary files /dev/null and b/test/integration/tiles/16-10473-25336.mvt differ diff --git a/test/integration/tiles/16-10474-25335.mvt b/test/integration/tiles/16-10474-25335.mvt new file mode 100644 index 00000000000..fa993a0d2b9 Binary files /dev/null and b/test/integration/tiles/16-10474-25335.mvt differ diff --git a/test/integration/tiles/16-10474-25336.mvt b/test/integration/tiles/16-10474-25336.mvt new file mode 100644 index 00000000000..4e7c3f63278 Binary files /dev/null and b/test/integration/tiles/16-10474-25336.mvt differ diff --git a/test/integration/tiles/7-20-48.terrain.png b/test/integration/tiles/7-20-48.terrain.png new file mode 100644 index 00000000000..2ca3611f69a Binary files /dev/null and b/test/integration/tiles/7-20-48.terrain.png differ diff --git a/test/integration/tiles/8-40-98.terrain.png b/test/integration/tiles/8-40-98.terrain.png new file mode 100644 index 00000000000..1d5034f2456 Binary files /dev/null and b/test/integration/tiles/8-40-98.terrain.png differ diff --git a/test/integration/tiles/8-41-98.terrain.png b/test/integration/tiles/8-41-98.terrain.png new file mode 100644 index 00000000000..e75ea62cd91 Binary files /dev/null and b/test/integration/tiles/8-41-98.terrain.png differ diff --git a/test/integration/tiles/9-80-198.terrain.png b/test/integration/tiles/9-80-198.terrain.png new file mode 100644 index 00000000000..12eecdfef03 Binary files /dev/null and b/test/integration/tiles/9-80-198.terrain.png differ diff --git a/test/integration/tiles/9-81-196.terrain.png b/test/integration/tiles/9-81-196.terrain.png new file mode 100644 index 00000000000..405d49b43fc Binary files /dev/null and b/test/integration/tiles/9-81-196.terrain.png differ diff --git a/test/integration/tiles/9-81-198.terrain.png b/test/integration/tiles/9-81-198.terrain.png new file mode 100644 index 00000000000..bf7a982d226 Binary files /dev/null and b/test/integration/tiles/9-81-198.terrain.png differ diff --git a/test/integration/tiles/9-81-199.terrain.png b/test/integration/tiles/9-81-199.terrain.png new file mode 100644 index 00000000000..b540465f826 Binary files /dev/null and b/test/integration/tiles/9-81-199.terrain.png differ diff --git a/test/integration/tiles/9-82-196.terrain.png b/test/integration/tiles/9-82-196.terrain.png new file mode 100644 index 00000000000..3c2a745d7b9 Binary files /dev/null and b/test/integration/tiles/9-82-196.terrain.png differ diff --git a/test/integration/tiles/9-82-197.terrain.png b/test/integration/tiles/9-82-197.terrain.png new file mode 100644 index 00000000000..f61a231ac10 Binary files /dev/null and b/test/integration/tiles/9-82-197.terrain.png differ diff --git a/test/integration/tiles/9-94-200.terrain.png b/test/integration/tiles/9-94-200.terrain.png new file mode 100644 index 00000000000..38b598a8e80 Binary files /dev/null and b/test/integration/tiles/9-94-200.terrain.png differ diff --git a/test/integration/tiles/9-94-200.terrarium.png b/test/integration/tiles/9-94-200.terrarium.png new file mode 100644 index 00000000000..5dcaf293679 Binary files /dev/null and b/test/integration/tiles/9-94-200.terrarium.png differ diff --git a/test/integration/tiles/9-94-201.terrain.png b/test/integration/tiles/9-94-201.terrain.png new file mode 100644 index 00000000000..41374e6aa41 Binary files /dev/null and b/test/integration/tiles/9-94-201.terrain.png differ diff --git a/test/integration/tiles/9-94-201.terrarium.png b/test/integration/tiles/9-94-201.terrarium.png new file mode 100644 index 00000000000..580d22062d3 Binary files /dev/null and b/test/integration/tiles/9-94-201.terrarium.png differ diff --git a/test/integration/tiles/9-95-200.terrain.png b/test/integration/tiles/9-95-200.terrain.png new file mode 100644 index 00000000000..7e4485867d5 Binary files /dev/null and b/test/integration/tiles/9-95-200.terrain.png differ diff --git a/test/integration/tiles/9-95-200.terrarium.png b/test/integration/tiles/9-95-200.terrarium.png new file mode 100644 index 00000000000..d0922455c89 Binary files /dev/null and b/test/integration/tiles/9-95-200.terrarium.png differ diff --git a/test/integration/tiles/9-95-201.terrain.png b/test/integration/tiles/9-95-201.terrain.png new file mode 100644 index 00000000000..85b2d21d4c6 Binary files /dev/null and b/test/integration/tiles/9-95-201.terrain.png differ diff --git a/test/integration/tiles/9-95-201.terrarium.png b/test/integration/tiles/9-95-201.terrarium.png new file mode 100644 index 00000000000..7bf16207a2d Binary files /dev/null and b/test/integration/tiles/9-95-201.terrarium.png differ diff --git a/test/integration/tiles/const/0-0-0.terrain.512.png b/test/integration/tiles/const/0-0-0.terrain.512.png new file mode 100644 index 00000000000..be39d9aad1f Binary files /dev/null and b/test/integration/tiles/const/0-0-0.terrain.512.png differ diff --git a/test/integration/tiles/const/0-0-0.terrain1.512.png b/test/integration/tiles/const/0-0-0.terrain1.512.png new file mode 100644 index 00000000000..0eb98f91608 Binary files /dev/null and b/test/integration/tiles/const/0-0-0.terrain1.512.png differ diff --git a/test/integration/tiles/const/1-0-0.terrain.512.png b/test/integration/tiles/const/1-0-0.terrain.512.png new file mode 100644 index 00000000000..06fda014637 Binary files /dev/null and b/test/integration/tiles/const/1-0-0.terrain.512.png differ diff --git a/test/integration/tiles/const/10-512-511.color.png b/test/integration/tiles/const/10-512-511.color.png new file mode 100644 index 00000000000..537f72ac548 Binary files /dev/null and b/test/integration/tiles/const/10-512-511.color.png differ diff --git a/test/integration/tiles/const/11-1023-1024.color.png b/test/integration/tiles/const/11-1023-1024.color.png new file mode 100644 index 00000000000..e514468d2f6 Binary files /dev/null and b/test/integration/tiles/const/11-1023-1024.color.png differ diff --git a/test/integration/tiles/const/12-2047-2047.color.png b/test/integration/tiles/const/12-2047-2047.color.png new file mode 100644 index 00000000000..a76deeafb7a Binary files /dev/null and b/test/integration/tiles/const/12-2047-2047.color.png differ diff --git a/test/integration/tiles/const/12-2047-2047.terrain.128.png b/test/integration/tiles/const/12-2047-2047.terrain.128.png new file mode 100644 index 00000000000..d5287574f0c Binary files /dev/null and b/test/integration/tiles/const/12-2047-2047.terrain.128.png differ diff --git a/test/integration/tiles/const/12-2047-2048.terrain.128.png b/test/integration/tiles/const/12-2047-2048.terrain.128.png new file mode 100644 index 00000000000..be2699de25b Binary files /dev/null and b/test/integration/tiles/const/12-2047-2048.terrain.128.png differ diff --git a/test/integration/tiles/const/12-2048-2048.terrain.128.png b/test/integration/tiles/const/12-2048-2048.terrain.128.png new file mode 100644 index 00000000000..be2699de25b Binary files /dev/null and b/test/integration/tiles/const/12-2048-2048.terrain.128.png differ diff --git a/test/integration/tiles/const/12-2200-1343.terrain.512.png b/test/integration/tiles/const/12-2200-1343.terrain.512.png new file mode 100644 index 00000000000..0eb98f91608 Binary files /dev/null and b/test/integration/tiles/const/12-2200-1343.terrain.512.png differ diff --git a/test/integration/tiles/const/13-4095-4095.color.png b/test/integration/tiles/const/13-4095-4095.color.png new file mode 100644 index 00000000000..ddb78b6f08e Binary files /dev/null and b/test/integration/tiles/const/13-4095-4095.color.png differ diff --git a/test/integration/tiles/const/13-4095-4095.terrain.128.png b/test/integration/tiles/const/13-4095-4095.terrain.128.png new file mode 100644 index 00000000000..14f7d62a0d2 Binary files /dev/null and b/test/integration/tiles/const/13-4095-4095.terrain.128.png differ diff --git a/test/integration/tiles/const/14-8189-8190.color.png b/test/integration/tiles/const/14-8189-8190.color.png new file mode 100644 index 00000000000..ccd6c9f5c63 Binary files /dev/null and b/test/integration/tiles/const/14-8189-8190.color.png differ diff --git a/test/integration/tiles/const/14-8191-8191.color.png b/test/integration/tiles/const/14-8191-8191.color.png new file mode 100644 index 00000000000..65041f6b478 Binary files /dev/null and b/test/integration/tiles/const/14-8191-8191.color.png differ diff --git a/test/integration/tiles/const/14-8191-8191.terrain.128.png b/test/integration/tiles/const/14-8191-8191.terrain.128.png new file mode 100644 index 00000000000..f3faa15f755 Binary files /dev/null and b/test/integration/tiles/const/14-8191-8191.terrain.128.png differ diff --git a/test/integration/tiles/const/15-16380-16380.color.png b/test/integration/tiles/const/15-16380-16380.color.png new file mode 100644 index 00000000000..0ea48c6fc37 Binary files /dev/null and b/test/integration/tiles/const/15-16380-16380.color.png differ diff --git a/test/integration/tiles/const/15-16383-16383.color.png b/test/integration/tiles/const/15-16383-16383.color.png new file mode 100644 index 00000000000..289d8434b92 Binary files /dev/null and b/test/integration/tiles/const/15-16383-16383.color.png differ diff --git a/test/integration/tiles/const/15-16384-16383.color.png b/test/integration/tiles/const/15-16384-16383.color.png new file mode 100644 index 00000000000..a6b2bc3e930 Binary files /dev/null and b/test/integration/tiles/const/15-16384-16383.color.png differ diff --git a/test/integration/tiles/const/6-18-24.terrain.512.png b/test/integration/tiles/const/6-18-24.terrain.512.png new file mode 100644 index 00000000000..6bd66c47acb Binary files /dev/null and b/test/integration/tiles/const/6-18-24.terrain.512.png differ diff --git a/test/integration/tiles/const/7-37-48.terrain.512.png b/test/integration/tiles/const/7-37-48.terrain.512.png new file mode 100644 index 00000000000..7d294f18aa8 Binary files /dev/null and b/test/integration/tiles/const/7-37-48.terrain.512.png differ diff --git a/test/integration/tiles/terrain-buffer-0/10-189-402.png b/test/integration/tiles/terrain-buffer-0/10-189-402.png new file mode 100644 index 00000000000..6ec85d652ac Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/10-189-402.png differ diff --git a/test/integration/tiles/terrain-buffer-0/10-190-402.png b/test/integration/tiles/terrain-buffer-0/10-190-402.png new file mode 100644 index 00000000000..c18d711df20 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/10-190-402.png differ diff --git a/test/integration/tiles/terrain-buffer-0/11-378-803.png b/test/integration/tiles/terrain-buffer-0/11-378-803.png new file mode 100644 index 00000000000..c3e309b0b6b Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/11-378-803.png differ diff --git a/test/integration/tiles/terrain-buffer-0/11-378-804.png b/test/integration/tiles/terrain-buffer-0/11-378-804.png new file mode 100644 index 00000000000..8c615ae194b Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/11-378-804.png differ diff --git a/test/integration/tiles/terrain-buffer-0/11-379-803.png b/test/integration/tiles/terrain-buffer-0/11-379-803.png new file mode 100644 index 00000000000..bc2125f1cab Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/11-379-803.png differ diff --git a/test/integration/tiles/terrain-buffer-0/11-379-804.png b/test/integration/tiles/terrain-buffer-0/11-379-804.png new file mode 100644 index 00000000000..5bcc6ec49f7 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/11-379-804.png differ diff --git a/test/integration/tiles/terrain-buffer-0/11-380-803.png b/test/integration/tiles/terrain-buffer-0/11-380-803.png new file mode 100644 index 00000000000..1b0238ec016 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/11-380-803.png differ diff --git a/test/integration/tiles/terrain-buffer-0/11-380-804.png b/test/integration/tiles/terrain-buffer-0/11-380-804.png new file mode 100644 index 00000000000..cdd5086af3a Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/11-380-804.png differ diff --git a/test/integration/tiles/terrain-buffer-0/12-758-1608.png b/test/integration/tiles/terrain-buffer-0/12-758-1608.png new file mode 100644 index 00000000000..02665d30bb9 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/12-758-1608.png differ diff --git a/test/integration/tiles/terrain-buffer-0/12-758-1609.png b/test/integration/tiles/terrain-buffer-0/12-758-1609.png new file mode 100644 index 00000000000..0ba426e84c9 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/12-758-1609.png differ diff --git a/test/integration/tiles/terrain-buffer-0/12-759-1608.png b/test/integration/tiles/terrain-buffer-0/12-759-1608.png new file mode 100644 index 00000000000..5f95b5577fb Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/12-759-1608.png differ diff --git a/test/integration/tiles/terrain-buffer-0/12-759-1609.png b/test/integration/tiles/terrain-buffer-0/12-759-1609.png new file mode 100644 index 00000000000..45f39a5f1f9 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/12-759-1609.png differ diff --git a/test/integration/tiles/terrain-buffer-0/9-94-200.png b/test/integration/tiles/terrain-buffer-0/9-94-200.png new file mode 100644 index 00000000000..bf73f97bff2 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/9-94-200.png differ diff --git a/test/integration/tiles/terrain-buffer-0/9-94-201.png b/test/integration/tiles/terrain-buffer-0/9-94-201.png new file mode 100644 index 00000000000..4bfcb19ace5 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/9-94-201.png differ diff --git a/test/integration/tiles/terrain-buffer-0/9-95-200.png b/test/integration/tiles/terrain-buffer-0/9-95-200.png new file mode 100644 index 00000000000..b7a0bea0774 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/9-95-200.png differ diff --git a/test/integration/tiles/terrain-buffer-0/9-95-201.png b/test/integration/tiles/terrain-buffer-0/9-95-201.png new file mode 100644 index 00000000000..8e5a7daa16f Binary files /dev/null and b/test/integration/tiles/terrain-buffer-0/9-95-201.png differ diff --git a/test/integration/tiles/terrain-buffer-1/10-189-402-1.png b/test/integration/tiles/terrain-buffer-1/10-189-402-1.png new file mode 100644 index 00000000000..f9c6eff1f0e Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/10-189-402-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/10-190-402-1.png b/test/integration/tiles/terrain-buffer-1/10-190-402-1.png new file mode 100644 index 00000000000..3a378e8f058 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/10-190-402-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/11-378-803-1.png b/test/integration/tiles/terrain-buffer-1/11-378-803-1.png new file mode 100644 index 00000000000..3c98472e862 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/11-378-803-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/11-378-804-1.png b/test/integration/tiles/terrain-buffer-1/11-378-804-1.png new file mode 100644 index 00000000000..db287c9b340 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/11-378-804-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/11-379-803-1.png b/test/integration/tiles/terrain-buffer-1/11-379-803-1.png new file mode 100644 index 00000000000..c716bb5fb4b Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/11-379-803-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/11-379-804-1.png b/test/integration/tiles/terrain-buffer-1/11-379-804-1.png new file mode 100644 index 00000000000..43088dbe0be Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/11-379-804-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/11-380-803-1.png b/test/integration/tiles/terrain-buffer-1/11-380-803-1.png new file mode 100644 index 00000000000..6f98630a466 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/11-380-803-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/11-380-804-1.png b/test/integration/tiles/terrain-buffer-1/11-380-804-1.png new file mode 100644 index 00000000000..9ad005033c6 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/11-380-804-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/12-758-1608-1.png b/test/integration/tiles/terrain-buffer-1/12-758-1608-1.png new file mode 100644 index 00000000000..f92075a67db Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/12-758-1608-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/12-758-1609-1.png b/test/integration/tiles/terrain-buffer-1/12-758-1609-1.png new file mode 100644 index 00000000000..5b3438ae35a Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/12-758-1609-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/12-759-1608-1.png b/test/integration/tiles/terrain-buffer-1/12-759-1608-1.png new file mode 100644 index 00000000000..db1cbfdb9b7 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/12-759-1608-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/12-759-1609-1.png b/test/integration/tiles/terrain-buffer-1/12-759-1609-1.png new file mode 100644 index 00000000000..0783d563052 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/12-759-1609-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/9-94-200-1.png b/test/integration/tiles/terrain-buffer-1/9-94-200-1.png new file mode 100644 index 00000000000..883413a2f44 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/9-94-200-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/9-94-201-1.png b/test/integration/tiles/terrain-buffer-1/9-94-201-1.png new file mode 100644 index 00000000000..3690ce2ee29 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/9-94-201-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/9-95-200-1.png b/test/integration/tiles/terrain-buffer-1/9-95-200-1.png new file mode 100644 index 00000000000..eb27525ff2a Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/9-95-200-1.png differ diff --git a/test/integration/tiles/terrain-buffer-1/9-95-201-1.png b/test/integration/tiles/terrain-buffer-1/9-95-201-1.png new file mode 100644 index 00000000000..1b608d19d8a Binary files /dev/null and b/test/integration/tiles/terrain-buffer-1/9-95-201-1.png differ diff --git a/test/integration/tiles/terrain-buffer-2/10-189-402-2.png b/test/integration/tiles/terrain-buffer-2/10-189-402-2.png new file mode 100644 index 00000000000..5e0c775558b Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/10-189-402-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/10-190-402-2.png b/test/integration/tiles/terrain-buffer-2/10-190-402-2.png new file mode 100644 index 00000000000..401d457d48b Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/10-190-402-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/11-378-803-2.png b/test/integration/tiles/terrain-buffer-2/11-378-803-2.png new file mode 100644 index 00000000000..87cbe2f6ade Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/11-378-803-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/11-378-804-2.png b/test/integration/tiles/terrain-buffer-2/11-378-804-2.png new file mode 100644 index 00000000000..7cdb31ff99c Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/11-378-804-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/11-379-803-2.png b/test/integration/tiles/terrain-buffer-2/11-379-803-2.png new file mode 100644 index 00000000000..fdd978a8137 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/11-379-803-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/11-379-804-2.png b/test/integration/tiles/terrain-buffer-2/11-379-804-2.png new file mode 100644 index 00000000000..ac3ed1d937a Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/11-379-804-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/11-380-803-2.png b/test/integration/tiles/terrain-buffer-2/11-380-803-2.png new file mode 100644 index 00000000000..7fbfc013118 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/11-380-803-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/11-380-804-2.png b/test/integration/tiles/terrain-buffer-2/11-380-804-2.png new file mode 100644 index 00000000000..c0e4a33855b Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/11-380-804-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/12-758-1608-2.png b/test/integration/tiles/terrain-buffer-2/12-758-1608-2.png new file mode 100644 index 00000000000..f9df0841ae4 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/12-758-1608-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/12-758-1609-2.png b/test/integration/tiles/terrain-buffer-2/12-758-1609-2.png new file mode 100644 index 00000000000..d8b359a9647 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/12-758-1609-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/12-759-1608-2.png b/test/integration/tiles/terrain-buffer-2/12-759-1608-2.png new file mode 100644 index 00000000000..f0654527cf4 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/12-759-1608-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/12-759-1609-2.png b/test/integration/tiles/terrain-buffer-2/12-759-1609-2.png new file mode 100644 index 00000000000..9c8cf3638c0 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/12-759-1609-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/9-94-200-2.png b/test/integration/tiles/terrain-buffer-2/9-94-200-2.png new file mode 100644 index 00000000000..bbcae5d7bef Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/9-94-200-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/9-94-201-2.png b/test/integration/tiles/terrain-buffer-2/9-94-201-2.png new file mode 100644 index 00000000000..d3d7f818058 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/9-94-201-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/9-95-200-2.png b/test/integration/tiles/terrain-buffer-2/9-95-200-2.png new file mode 100644 index 00000000000..3a329e27193 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/9-95-200-2.png differ diff --git a/test/integration/tiles/terrain-buffer-2/9-95-201-2.png b/test/integration/tiles/terrain-buffer-2/9-95-201-2.png new file mode 100644 index 00000000000..da2a89870b8 Binary files /dev/null and b/test/integration/tiles/terrain-buffer-2/9-95-201-2.png differ diff --git a/test/query.test.js b/test/query.test.js deleted file mode 100644 index 2ac5e6584a4..00000000000 --- a/test/query.test.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable import/unambiguous, import/no-commonjs, no-global-assign */ - -require('./stub_loader'); -require('@mapbox/flow-remove-types/register'); -require = require("esm")(module, true); - -const querySuite = require('./integration/lib/query'); -const suiteImplementation = require('./suite_implementation'); -const ignores = require('./ignores.json'); - -let tests; - -if (process.argv[1] === __filename && process.argv.length > 2) { - tests = process.argv.slice(2); -} - -querySuite.run('js', {tests, ignores}, suiteImplementation); diff --git a/test/render.test.js b/test/render.test.js deleted file mode 100644 index 3c594a37a71..00000000000 --- a/test/render.test.js +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable import/unambiguous, import/no-commonjs, no-global-assign */ - -require('./stub_loader'); -require('@mapbox/flow-remove-types/register'); -const {registerFont} = require('canvas'); -require = require("esm")(module, true); - -const suite = require('./integration/lib/render'); -const suiteImplementation = require('./suite_implementation'); -const ignores = require('./ignores.json'); -registerFont('./node_modules/npm-font-open-sans/fonts/Bold/OpenSans-Bold.ttf', {family: 'Open Sans', weight: 'bold'}); - -suite.run('js', ignores, suiteImplementation); diff --git a/test/stub_loader.js b/test/stub_loader.js deleted file mode 100644 index 0fc4534a341..00000000000 --- a/test/stub_loader.js +++ /dev/null @@ -1,16 +0,0 @@ -// Load our stubbed ajax module for the integration suite implementation -/* eslint-disable import/unambiguous, import/no-commonjs */ -const fs = require('fs'); -const assert = require('assert'); -const pirates = require('pirates'); - -process.env["ESM_OPTIONS"] = '{ "cache": "node_modules/.cache/esm-stubbed"}'; - -pirates.addHook((code, filename) => { - assert(filename.endsWith('/ajax.js')); - return fs.readFileSync(`${__dirname}/ajax_stubs.js`, 'utf-8'); -}, { - exts: ['.js'], - matcher: filename => filename.endsWith('/ajax.js') -}); - diff --git a/test/suite_implementation.js b/test/suite_implementation.js index 0b33c0803dc..1f2338eca7d 100644 --- a/test/suite_implementation.js +++ b/test/suite_implementation.js @@ -8,6 +8,10 @@ import rtlText from '@mapbox/mapbox-gl-rtl-text'; import fs from 'fs'; import path from 'path'; import customLayerImplementations from './integration/custom_layer_implementations'; +import MercatorCoordinate, {mercatorZfromAltitude} from '../src/geo/mercator_coordinate'; +import LngLat from '../src/geo/lng_lat'; +import {clamp} from '../src/util/util'; +import {vec3, vec4} from 'gl-matrix'; rtlTextPlugin['applyArabicShaping'] = rtlText.applyArabicShaping; rtlTextPlugin['processBidirectionalText'] = rtlText.processBidirectionalText; @@ -67,37 +71,39 @@ module.exports = function(style, options, _callback) { // eslint-disable-line im if (options.debug) map.showTileBoundaries = true; if (options.showOverdrawInspector) map.showOverdrawInspector = true; if (options.showPadding) map.showPadding = true; + if (options.collisionDebug) map.showCollisionBoxes = true; - const gl = map.painter.context.gl; + // Disable anisotropic filtering on render tests + map.painter.context.extTextureFilterAnisotropicForceOff = true; + const gl = map.painter.context.gl; map.once('load', () => { - if (options.collisionDebug) { - map.showCollisionBoxes = true; - if (options.operations) { - options.operations.push(["wait"]); - } else { - options.operations = [["wait"]]; - } - } applyOperations(map, options.operations, () => { const viewport = gl.getParameter(gl.VIEWPORT); const w = viewport[2]; const h = viewport[3]; + let data; - const pixels = new Uint8Array(w * h * 4); - gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + if (options.output === "terrainDepth") { + const pixels = drawTerrainDepth(map, w, h); + data = new Buffer(pixels); + } - const data = new Buffer(pixels); + if (!data) { + const pixels = new Uint8Array(w * h * 4); + gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + data = new Buffer(pixels); - // Flip the scanlines. - const stride = w * 4; - const tmp = new Buffer(stride); - for (let i = 0, j = h - 1; i < j; i++, j--) { - const start = i * stride; - const end = j * stride; - data.copy(tmp, 0, start, start + stride); - data.copy(data, start, end, end + stride); - tmp.copy(data, end); + // Flip the scanlines. + const stride = w * 4; + const tmp = new Buffer(stride); + for (let i = 0, j = h - 1; i < j; i++, j--) { + const start = i * stride; + const end = j * stride; + data.copy(tmp, 0, start, start + stride); + data.copy(data, start, end, end + stride); + tmp.copy(data, end); + } } const results = options.queryGeometry ? @@ -125,8 +131,11 @@ module.exports = function(style, options, _callback) { // eslint-disable-line im function applyOperations(map, operations, callback) { const operation = operations && operations[0]; if (!operations || operations.length === 0) { + if (options.terrainDrapeFirst && map.painter.terrain) { + map.painter.terrain.forceDrapeFirst = true; + map._render(); // Render one more time with forceDrapeFirst. + } callback(); - } else if (operation[0] === 'wait') { if (operation.length > 1) { now += operation[1]; @@ -176,10 +185,26 @@ module.exports = function(style, options, _callback) { // eslint-disable-line im } else if (operation[0] === 'pauseSource') { map.style.sourceCaches[operation[1]].pause(); applyOperations(map, operations.slice(1), callback); + } else if (operation[0] === 'setCameraPosition') { + const options = map.getFreeCameraOptions(); + const location = operation[1]; // lng, lat, altitude + options.position = MercatorCoordinate.fromLngLat(new LngLat(location[0], location[1]), location[2]); + map.setFreeCameraOptions(options); + applyOperations(map, operations.slice(1), callback); + } else if (operation[0] === 'lookAtPoint') { + const options = map.getFreeCameraOptions(); + const location = operation[1]; + const upVector = operation[2]; + options.lookAtPoint(new LngLat(location[0], location[1]), upVector); + map.setFreeCameraOptions(options); + applyOperations(map, operations.slice(1), callback); } else { if (typeof map[operation[0]] === 'function') { map[operation[0]](...operation.slice(1)); } + if (options.terrainDrapeFirst && map.painter.terrain) { + map.painter.terrain.forceDrapeFirst = true; + } applyOperations(map, operations.slice(1), callback); } } @@ -200,3 +225,93 @@ function updateFakeCanvas(document, id, imagePath) { const image = PNG.sync.read(fs.readFileSync(path.join(__dirname, './integration', imagePath))); fakeCanvas.data = image.data; } + +function drawTerrainDepth(map, width, height) { + if (!map.painter.terrain) + return undefined; + + const terrain = map.painter.terrain; + const tr = map.transform; + const ws = tr.worldSize; + + // Compute frustum corner points in web mercator [0, 1] space where altitude is in meters + const clipSpaceCorners = [ + [-1, 1, -1, 1], + [ 1, 1, -1, 1], + [ 1, -1, -1, 1], + [-1, -1, -1, 1], + [-1, 1, 1, 1], + [ 1, 1, 1, 1], + [ 1, -1, 1, 1], + [-1, -1, 1, 1] + ]; + + const frustumCoords = clipSpaceCorners + .map(v => { + const s = vec4.transformMat4([], v, tr.invProjMatrix); + const k = 1.0 / s[3] / ws; + // Z scale in meters. + return vec4.mul(s, s, [k, k, 1.0 / s[3], k]); + }); + + const nearTL = frustumCoords[0]; + const nearTR = frustumCoords[1]; + const nearBL = frustumCoords[3]; + const farTL = frustumCoords[4]; + const farTR = frustumCoords[5]; + const farBL = frustumCoords[7]; + + // Compute basis vectors X & Y of near and far planes in transformed space. + // These vectors are then interpolated to find corresponding world rays for each screen pixel. + const nearRight = vec3.sub([], nearTR, nearTL); + const nearDown = vec3.sub([], nearBL, nearTL); + const farRight = vec3.sub([], farTR, farTL); + const farDown = vec3.sub([], farBL, farTL); + + const distances = []; + const data = []; + const metersToPixels = mercatorZfromAltitude(1.0, tr.center.lat); + let minDistance = Number.MAX_VALUE; + let maxDistance = 0; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + // Use uv-coordinates of the screen pixel to find positions on near and far planes + const u = (x + 0.5) / width; + const v = (y + 0.5) / height; + + const startPoint = vec3.add([], nearTL, vec3.add([], vec3.scale([], nearRight, u), vec3.scale([], nearDown, v))); + const endPoint = vec3.add([], farTL, vec3.add([], vec3.scale([], farRight, u), vec3.scale([], farDown, v))); + const dir = vec3.normalize([], vec3.sub([], endPoint, startPoint)); + const t = terrain.raycast(startPoint, dir, terrain.exaggeration()); + + if (t !== null) { + // The ray hit the terrain. Compute distance in world space and store to an intermediate array + const point = vec3.scaleAndAdd([], startPoint, dir, t); + const startToPoint = vec3.sub([], point, startPoint); + startToPoint[2] *= metersToPixels; + + const distance = vec3.length(startToPoint) * ws; + distances.push(distance); + minDistance = Math.min(distance, minDistance); + maxDistance = Math.max(distance, maxDistance); + } else { + distances.push(null); + } + } + } + + // Convert distance data to pixels; + for (let i = 0; i < width * height; i++) { + if (distances[i] === null) { + // Bright white pixel for non-intersections + data.push(255, 255, 255, 255); + } else { + let value = (distances[i] - minDistance) / (maxDistance - minDistance); + value = Math.floor((clamp(value, 0.0, 1.0)) * 255); + data.push(value, value, value, 255); + } + } + + return data; +} diff --git a/test/unit/data/dem_data.test.js b/test/unit/data/dem_data.test.js index 4f9627d37e2..2bc8b05f3ba 100644 --- a/test/unit/data/dem_data.test.js +++ b/test/unit/data/dem_data.test.js @@ -141,7 +141,8 @@ test('DEMData#backfillBorder', (t) => { dim: 4, stride: 6, data: dem0.data, - encoding: 'mapbox' + encoding: 'mapbox', + borderReady: false }, 'serializes DEM'); const transferrables = []; diff --git a/test/unit/data/dem_tree.test.js b/test/unit/data/dem_tree.test.js new file mode 100644 index 00000000000..e9abe2acf54 --- /dev/null +++ b/test/unit/data/dem_tree.test.js @@ -0,0 +1,418 @@ +import {test} from '../../util/test'; +import DEMData from '../../../src/data/dem_data'; +import DemMinMaxQuadTree, {buildDemMipmap, sampleElevation} from '../../../src/data/dem_tree'; +import {fixedNum} from '../../util/fixed'; + +const unpackVector = [65536, 256, 1]; + +function encodeElevation(elevation) { + const result = []; + elevation = (elevation + 10000) * 10; + for (let i = 0; i < 3; i++) { + result[i] = Math.floor(elevation / unpackVector[i]); + elevation -= result[i] * unpackVector[i]; + } + return result; +} + +function fillElevation(size, padding, value) { + const paddedSize = size + padding * 2; + const result = []; + for (let i = 0; i < paddedSize * paddedSize; i++) + result[i] = value; + return result; +} + +function mockDEMfromElevation(size, padding, elevation) { + const paddedSize = size + padding * 2; + const pixels = new Uint8Array(paddedSize * paddedSize * 4); + for (let i = 0; i < elevation.length; i++) { + const bytes = encodeElevation(elevation[i]); + pixels[i * 4 + 0] = bytes[0]; + pixels[i * 4 + 1] = bytes[1]; + pixels[i * 4 + 2] = bytes[2]; + pixels[i * 4 + 3] = 0; + } + + return new DEMData(0, {width: paddedSize, height: paddedSize, data: pixels}); +} + +function idx(x, y, size, padding) { + return (y + 1) * (size + 2 * padding) + (x + 1); +} + +test('DEM mip map generation', (t) => { + const leafCount = (mip) => { + let count = 0; + for (let i = 0; i < mip.leaves.length; i++) + count += mip.leaves[i]; + return count; + }; + + t.test('Flat DEM', (t) => { + const size = 256; + const elevation = fillElevation(size, 1, 0); + const dem = mockDEMfromElevation(size, 1, elevation); + + // No elevation differences. 6 levels expected and all marked as leaves + const demMips = buildDemMipmap(dem); + const expectedSizes = [32, 16, 8, 4, 2, 1]; + const expectedLeaves = [1024, 256, 64, 16, 4, 1]; + + t.equal(demMips.length, 6); + for (let i = 0; i < 6; i++) { + t.equal(demMips[i].size, expectedSizes[i]); + t.equal(leafCount(demMips[i]), expectedLeaves[i]); + } + + t.end(); + }); + + t.test('Small DEM', (t) => { + const size = 4; + const elevation = fillElevation(size, 1, 100); + const dem = mockDEMfromElevation(size, 1, elevation); + const demMips = buildDemMipmap(dem); + + t.equal(demMips.length, 1); + t.equal(demMips[0].size, 1); + t.equal(demMips[0].maximums.length, 1); + t.equal(demMips[0].minimums.length, 1); + t.equal(demMips[0].maximums[0], 100); + t.equal(demMips[0].minimums[0], 100); + t.equal(demMips[0].leaves[0], 1); + + t.end(); + }); + + t.test('Elevation sampling', (t) => { + const size = 16; + const padding = 1; + const elevation = fillElevation(size, padding, 0); + + // Fill the elevation data with 4 blocks (8x8) texels with elevations 0, 100, and 200 + for (let y = 0; y < size; y++) { + const yBlock = Math.floor(y / 8); + for (let x = 0; x < size; x++) { + const xBlock = Math.floor(x / 8); + elevation[idx(x, y, size, padding)] = (xBlock + yBlock) * 100; + } + } + + const dem = mockDEMfromElevation(size, 1, elevation); + + // Check each 9 corners and expect to find interpolated values (except on borders) + t.equal(sampleElevation(0, 0, dem), 0); + t.equal(sampleElevation(0.5, 0, dem), 50); + t.equal(sampleElevation(1.0, 0, dem), 100); + + t.equal(sampleElevation(0, 0.5, dem), 50); + t.equal(sampleElevation(0.5, 0.5, dem), 100); + t.equal(sampleElevation(1.0, 0.5, dem), 150); + + t.equal(sampleElevation(0, 1.0, dem), 100); + t.equal(sampleElevation(0.5, 1.0, dem), 150); + t.equal(sampleElevation(1.0, 1.0, dem), 200); + + t.end(); + }); + + t.test('Merge nodes with little elevation difference', (t) => { + const size = 32; + const padding = 1; + const elevation = fillElevation(size, padding, 0); + /* + Construct elevation data with following expected mips + mip 0 (max, leaves): + 5 0 0 1000 | 1 1 1 1 + 0 0 0 0 | 1 1 1 1 + 0 0 0 0 | 1 1 1 1 + 101 0 0 97 | 1 1 1 1 + + mip 1 (max, leaves): + 5 1000 | 1 0 + 102 97 | 0 0 + + mip 2 (max, leaves): + 1000 | 0 + */ + + const idx = (x, y) => (y + 1) * (size + 2 * padding) + (x + 1); + + // Set elevation values of sampled corner points. (One block is 8x8 texels) + elevation[idx(0, 0)] = 5; + elevation[idx(size - 1, 0)] = 1000; + elevation[idx(0, size - 1)] = 101; + elevation[idx(size - 1, size - 1)] = 97; + + const dem = mockDEMfromElevation(size, 1, elevation); + const demMips = buildDemMipmap(dem); + + // mip 0 + let expectedMaximums = [ + 5, 0, 0, 1000, + 0, 0, 0, 0, + 0, 0, 0, 0, + 101, 0, 0, 97 + ]; + + let expectedLeaves = [ + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 1 + ]; + + t.equal(demMips.length, 3); + t.equal(demMips[0].size, 4); + t.deepEqual(demMips[0].maximums, expectedMaximums); + t.deepEqual(demMips[0].leaves, expectedLeaves); + + // mip 1 + expectedMaximums = [ + 5, 1000, + 101, 97 + ]; + + expectedLeaves = [ + 1, 0, + 0, 0 + ]; + + t.equal(demMips[1].size, 2); + t.deepEqual(demMips[1].maximums, expectedMaximums); + t.deepEqual(demMips[1].leaves, expectedLeaves); + + // mip 2 + t.equal(demMips[2].size, 1); + t.deepEqual(demMips[2].minimums, [0]); + t.deepEqual(demMips[2].maximums, [1000]); + t.deepEqual(demMips[2].leaves, [0]); + + t.end(); + }); + + t.end(); +}); + +test('DemMinMaxQuadTree', (t) => { + t.test('Construct', (t) => { + t.test('Flat DEM', (t) => { + const size = 256; + const elevation = fillElevation(size, 1, 12345); + const dem = mockDEMfromElevation(size, 1, elevation); + + // No elevation differences. 6 levels expected and all marked as leaves + const tree = new DemMinMaxQuadTree(dem); + t.equal(tree.nodeCount, 1); + t.equal(tree.maximums[0], 12345); + t.equal(tree.minimums[0], 12345); + t.equal(tree.leaves[1]); + + t.end(); + }); + + t.test('Sparse tree', (t) => { + const size = 32; + const padding = 1; + const elevation = fillElevation(size, padding, 0); + /* + 5 0 0 1000 + 0 0 0 0 + 0 0 0 0 + 101 0 0 97 + */ + + const idx = (x, y) => (y + 1) * (size + 2 * padding) + (x + 1); + + // Set elevation values of sampled corner points. (One block is 8x8 texels) + elevation[idx(0, 0)] = 5; + elevation[idx(size - 1, 0)] = 1000; + elevation[idx(0, size - 1)] = 101; + elevation[idx(size - 1, size - 1)] = 97; + + const dem = mockDEMfromElevation(size, 1, elevation); + const tree = new DemMinMaxQuadTree(dem); + + t.equal(tree.nodeCount, 17); + + const expectedMaximums = [ + // Root + 1000, + + // Mip 1 + 5, 1000, + 101, 97, + + // Mip 2 + 0, 1000, 0, 0, + 0, 0, 101, 0, + 0, 0, 0, 97 + ]; + + const expectedMinimums = [ + // Root + 0, + + // Mip 1 + 0, 0, + 0, 0, + + // Mip 2 + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + ]; + + const expectedLeaves = [ + // Root + 0, + + // Mip 1 + 1, 0, + 0, 0, + + // Mip 2 + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 1 + ]; + + t.deepEqual(tree.maximums, expectedMaximums); + t.deepEqual(tree.minimums, expectedMinimums); + t.deepEqual(tree.leaves, expectedLeaves); + + t.end(); + }); + + t.end(); + }); + + t.test('Raycasting', (t) => { + t.test('Flat plane', (t) => { + const size = 32; + const padding = 1; + const elevation = fillElevation(size, padding, 10); + const dem = mockDEMfromElevation(size, padding, elevation); + const tree = new DemMinMaxQuadTree(dem); + + const minx = -1; + const maxx = 1; + const miny = -1; + const maxy = 1; + + let dist = tree.raycast(minx, miny, maxx, maxy, [0, 0, 11], [0, 0, -1]); + t.ok(dist); + t.equal(dist, 1.0); + + dist = tree.raycast(minx, miny, maxx, maxy, [1.001, 0, 5], [0, 0, -1]); + t.notOk(dist); + + dist = tree.raycast(minx, miny, maxx, maxy, [-1, 0, 11], [1, 0, 0]); + t.notOk(dist); + + // Ray should not be allowed to pass the dem chunk below the surface + dist = tree.raycast(minx, miny, maxx, maxy, [-2.5, 0, 1], [1, 0, 0]); + t.ok(dist); + t.equal(dist, 1.5); + + t.end(); + }); + + t.test('Gradient', (t) => { + const size = 32; + const padding = 1; + const elevation = fillElevation(size, padding, 0); + + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + elevation[idx(x, y, size, padding)] = x; + } + } + + const dem = mockDEMfromElevation(size, padding, elevation); + const tree = new DemMinMaxQuadTree(dem); + const minx = -16; + const maxx = 16; + const miny = -16; + const maxy = 16; + + let dist = tree.raycast(minx, miny, maxx, maxy, [0, 0, 50], [0, 0, -1]); + t.ok(dist); + t.equal(dist, 34.5); + + dist = tree.raycast(minx, miny, maxx, maxy, [-32, 0, 32], [0.707, 0, -0.707]); + t.ok(dist); + t.equal(fixedNum(dist, 3), 34.3); + + dist = tree.raycast(minx, miny, maxx, maxy, [16, 0, 32.01], [-0.707, 0, -0.707]); + t.notOk(dist); + + dist = tree.raycast(minx, miny, maxx, maxy, [16, 0, 31], [-0.707, 0, -0.707]); + t.equal(dist, 0); + + t.end(); + }); + + t.test('Flat plane with exaggeration', (t) => { + const size = 32; + const padding = 1; + const elevation = fillElevation(size, padding, 10); + const dem = mockDEMfromElevation(size, padding, elevation); + const tree = new DemMinMaxQuadTree(dem); + + const minx = -1; + const maxx = 1; + const miny = -1; + const maxy = 1; + + let dist = tree.raycast(minx, miny, maxx, maxy, [0, 0, 11], [0, 0, -1], 0.5); + t.ok(dist); + t.equal(dist, 6.0); + + dist = tree.raycast(minx, miny, maxx, maxy, [0, 0, 11], [0, 0, -1], 0.1); + t.ok(dist); + t.equal(dist, 10.0); + + t.end(); + }); + + t.test('Gradient with 0.5 exaggeration', (t) => { + const size = 32; + const padding = 1; + const elevation = fillElevation(size, padding, 0); + + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + elevation[idx(x, y, size, padding)] = x; + } + } + + const dem = mockDEMfromElevation(size, padding, elevation); + const tree = new DemMinMaxQuadTree(dem); + const minx = -16; + const maxx = 16; + const miny = -16; + const maxy = 16; + + let dist = tree.raycast(minx, miny, maxx, maxy, [0, 0, 50], [0, 0, -1], 0.5); + t.ok(dist); + t.equal(dist, 42.25); + t.true(dist > tree.raycast(minx, miny, maxx, maxy, [0, 0, 50], [0, 0, -1]), 1); + + dist = tree.raycast(minx, miny, maxx, maxy, [-32, 0, 32], [0.707, 0, -0.707], 0.5); + t.ok(dist); + t.equal(fixedNum(dist, 3), 37.954); + t.true(dist > tree.raycast(minx, miny, maxx, maxy, [-32, 0, 32], [0.707, 0, -0.707]), 1); + + dist = tree.raycast(minx, miny, maxx, maxy, [16, 0, 32.01], [-0.707, 0, -0.707], 0.5); + t.notOk(dist); + + t.end(); + }); + + t.end(); + }); + + t.end(); +}); diff --git a/test/unit/geo/transform.test.js b/test/unit/geo/transform.test.js index 8a9632c516e..8612df08a08 100644 --- a/test/unit/geo/transform.test.js +++ b/test/unit/geo/transform.test.js @@ -3,7 +3,12 @@ import Point from '@mapbox/point-geometry'; import Transform from '../../../src/geo/transform'; import LngLat from '../../../src/geo/lng_lat'; import {OverscaledTileID, CanonicalTileID} from '../../../src/source/tile_id'; -import {fixedLngLat, fixedCoord} from '../../util/fixed'; +import {fixedNum, fixedLngLat, fixedCoord, fixedPoint, fixedVec3, fixedVec4} from '../../util/fixed'; +import {FreeCameraOptions} from '../../../src/ui/free_camera'; +import MercatorCoordinate, {mercatorZfromAltitude} from '../../../src/geo/mercator_coordinate'; +import {vec3, quat} from 'gl-matrix'; +import LngLatBounds from '../../../src/geo/lng_lat_bounds'; +import {extend, degToRad} from '../../../src/util/util'; test('transform', (t) => { @@ -37,8 +42,11 @@ test('transform', (t) => { t.equal(transform.height, 500); t.deepEqual(fixedLngLat(transform.pointLocation(new Point(250, 250))), {lng: 0, lat: 0}); t.deepEqual(fixedCoord(transform.pointCoordinate(new Point(250, 250))), {x: 0.5, y: 0.5, z: 0}); - t.deepEqual(transform.locationPoint(new LngLat(0, 0)), {x: 250, y: 250}); + t.deepEqual(fixedPoint(transform.locationPoint(new LngLat(0, 0))), {x: 250, y: 250}); t.deepEqual(transform.locationCoordinate(new LngLat(0, 0)), {x: 0.5, y: 0.5, z: 0}); + t.deepEqual(fixedLngLat(transform.pointLocation3D(new Point(250, 250))), {lng: 0, lat: 0}); + t.deepEqual(fixedCoord(transform.pointCoordinate3D(new Point(250, 250))), {x: 0.5, y: 0.5, z: 0}); + t.deepEqual(fixedPoint(transform.locationPoint3D(new LngLat(0, 0))), {x: 250, y: 250}); t.end(); }); @@ -109,6 +117,66 @@ test('transform', (t) => { t.end(); }); + t.test('_minZoomForBounds respects latRange and lngRange', (t) => { + t.test('it returns 0 when latRange and lngRange are undefined', (t) => { + const transform = new Transform(); + transform.center = new LngLat(0, 0); + transform.zoom = 10; + transform.resize(500, 500); + + t.equal(transform._minZoomForBounds(), 0); + t.end(); + }); + + t.test('it results in equivalent minZoom as _constrain()', (t) => { + const transform = new Transform(); + transform.center = new LngLat(0, 0); + transform.zoom = 10; + transform.resize(500, 500); + transform.lngRange = [-5, 5]; + transform.latRange = [-5, 5]; + + const preComputedMinZoom = transform._minZoomForBounds(); + transform.zoom = 0; + const constrainedMinZoom = transform.zoom; + + t.equal(preComputedMinZoom, constrainedMinZoom); + t.end(); + }); + + t.end(); + }); + + test('mapbox-gl-js-internal#373', (t) => { + const options = { + minzoom: 3, + maxzoom: 22, + tileSize: 512 + }; + + const transform = new Transform(); + transform.resize(512, 512); + transform.center = {lng: -0.01, lat: 0.01}; + transform.zoom = 3; + transform.pitch = 65; + transform.bearing = 45; + + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(3, 0, 3, 3, 3), + new OverscaledTileID(3, 0, 3, 3, 4), + new OverscaledTileID(3, 0, 3, 4, 3), + new OverscaledTileID(3, 0, 3, 4, 4), + new OverscaledTileID(3, 0, 3, 4, 2), + new OverscaledTileID(3, 0, 3, 5, 3), + new OverscaledTileID(3, 0, 3, 5, 2), + new OverscaledTileID(3, 0, 3, 4, 1), + new OverscaledTileID(3, 0, 3, 6, 3), + new OverscaledTileID(3, 0, 3, 5, 1), + new OverscaledTileID(3, 0, 3, 6, 2)]); + + t.end(); + }); + test('coveringTiles', (t) => { const options = { minzoom: 1, @@ -206,7 +274,7 @@ test('transform', (t) => { transform.bearing = 0; transform.resize(300, 300); t.test('calculates tile coverage at w > 0', (t) => { - transform.center = {lng: 630.01, lat: 0.01}; + transform.center = {lng: 630.02, lat: 0.01}; t.deepEqual(transform.coveringTiles(options), [ new OverscaledTileID(2, 2, 2, 1, 1), new OverscaledTileID(2, 2, 2, 1, 2), @@ -217,28 +285,339 @@ test('transform', (t) => { }); t.test('calculates tile coverage at w = -1', (t) => { - transform.center = {lng: -360.01, lat: 0.01}; + transform.center = {lng: -360.01, lat: 0.02}; t.deepEqual(transform.coveringTiles(options), [ new OverscaledTileID(2, -1, 2, 1, 1), - new OverscaledTileID(2, -1, 2, 1, 2), new OverscaledTileID(2, -1, 2, 2, 1), + new OverscaledTileID(2, -1, 2, 1, 2), new OverscaledTileID(2, -1, 2, 2, 2) ]); t.end(); }); t.test('calculates tile coverage across meridian', (t) => { + transform.zoom = 1; + transform.center = {lng: -180.01, lat: 0.02}; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(1, -1, 1, 1, 0), + new OverscaledTileID(1, 0, 1, 0, 0), + new OverscaledTileID(1, -1, 1, 1, 1), + new OverscaledTileID(1, 0, 1, 0, 1) + ]); + t.end(); + }); + + t.test('only includes tiles for a single world, if renderWorldCopies is set to false', (t) => { transform.zoom = 1; transform.center = {lng: -180.01, lat: 0.01}; + transform.renderWorldCopies = false; t.deepEqual(transform.coveringTiles(options), [ new OverscaledTileID(1, 0, 1, 0, 0), + new OverscaledTileID(1, 0, 1, 0, 1) + ]); + t.end(); + }); + + t.test('mapbox-gl-js-internal#86', (t) => { + transform.renderWorldCopies = true; + transform.maxPitch = 85; + transform.zoom = 1.28; + transform.bearing = -81.6; + transform.pitch = 81; + transform.center = {lng: -153.3, lat: 0.0}; + transform.resize(2759, 1242); + t.deepEqual(transform.coveringTiles({tileSize: 512}), [ new OverscaledTileID(1, 0, 1, 0, 1), - new OverscaledTileID(1, -1, 1, 1, 0), - new OverscaledTileID(1, -1, 1, 1, 1) + new OverscaledTileID(1, 0, 1, 0, 0), + new OverscaledTileID(0, -1, 0, 0, 0), + new OverscaledTileID(1, 0, 1, 1, 1), + new OverscaledTileID(1, 0, 1, 1, 0), + new OverscaledTileID(1, 1, 1, 0, 1), + new OverscaledTileID(1, 1, 1, 0, 0), + new OverscaledTileID(0, -2, 0, 0, 0), + new OverscaledTileID(0, -3, 0, 0, 0) + ]); + t.end(); + }); + + t.end(); + }); + + const createCollisionElevation = (elevation) => { + return { + getAtPoint(p) { + if (p.x === 0.5 && p.y === 0.5) + return 0; + return elevation; + }, + getForTilePoints(tileID, points) { + for (const p of points) { + p[2] = elevation; + } + return true; + }, + }; + }; + + const createConstantElevation = (elevation) => { + return { + getAtPoint(_) { + return elevation; + }, + getForTilePoints(tileID, points) { + for (const p of points) { + p[2] = elevation; + } + return true; + }, + }; + }; + + const createRampElevation = (scale) => { + return { + getAtPoint(p) { + return scale * (p.x + p.y - 1.0); + }, + getForTilePoints(tileID, points) { + for (const p of points) { + p[2] = scale * (p.x + p.y - 1.0); + } + return true; + }, + }; + }; + + test('Constrained camera height over terrain', (t) => { + const transform = new Transform(); + transform.resize(200, 200); + transform.maxPitch = 85; + + transform.elevation = createCollisionElevation(10); + transform.constantCameraHeight = false; + transform.bearing = -45; + transform.pitch = 85; + + // Set camera altitude to 5 meters + const altitudeZ = mercatorZfromAltitude(5, transform.center.lat) / Math.cos(degToRad(85)); + const zoom = transform._zoomFromMercatorZ(altitudeZ); + transform.zoom = zoom; + + // Pitch should have been adjusted so that the camera isn't under the terrain + const pixelsPerMeter = mercatorZfromAltitude(1, transform.center.lat) * transform.worldSize; + const updatedAltitude = transform.cameraToCenterDistance / pixelsPerMeter * Math.cos(degToRad(transform.pitch)); + + t.true(updatedAltitude > 10); + t.equal(fixedNum(transform.zoom), fixedNum(zoom)); + t.equal(fixedNum(transform.bearing), -45); + + t.end(); + }); + + test('Compute zoom from camera height', (t) => { + const transform = new Transform(); + transform.resize(200, 200); + transform.center = {lng: 0, lat: 0}; + transform.zoom = 16; + transform.elevation = createRampElevation(500); + t.equal(transform.elevation.getAtPoint(new MercatorCoordinate(1.0, 0.5)), 250); + + t.equal(transform.zoom, 16); + t.equal(transform._cameraZoom, 16); + + // zoom should remain unchanged + transform.cameraElevationReference = "ground"; + transform.center = new LngLat(180, 0); + t.equal(transform.zoom, 16); + + transform.center = new LngLat(0, 0); + t.equal(transform.zoom, 16); + + // zoom should change so that the altitude remains constant + transform.cameraElevationReference = "sea"; + transform.center = new LngLat(180, 0); + t.equal(transform._cameraZoom, 16); + + const altitudeZ = transform.cameraToCenterDistance / (Math.pow(2.0, transform._cameraZoom) * transform.tileSize); + const heightZ = transform.cameraToCenterDistance / (Math.pow(2.0, transform.zoom) * transform.tileSize); + const elevationZ = mercatorZfromAltitude(250, 0); + t.equal(fixedNum(elevationZ + heightZ), fixedNum(altitudeZ)); + + t.end(); + }); + + test('Constant camera height over terrain', (t) => { + const transform = new Transform(); + transform.resize(200, 200); + transform.center = {lng: 0, lat: 0}; + transform.zoom = 16; + + transform.elevation = createConstantElevation(0); + t.equal(transform.zoom, transform._cameraZoom); + + // Camera zoom should change so that the standard zoom value describes distance between the camera and the terrain + transform.elevation = createConstantElevation(10000); + t.equal(fixedNum(transform._cameraZoom), 11.1449615644); + + // Camera height over terrain should remain constant + const altitudeZ = transform.cameraToCenterDistance / (Math.pow(2.0, transform._cameraZoom) * transform.tileSize); + const heightZ = transform.cameraToCenterDistance / (Math.pow(2.0, transform.zoom) * transform.tileSize); + const elevationZ = mercatorZfromAltitude(10000, 0); + t.equal(elevationZ + heightZ, altitudeZ); + + transform.pitch = 32; + t.equal(fixedNum(transform._cameraZoom), 11.1449615644); + t.equal(transform.zoom, 16); + + t.end(); + }); + + test('coveringTiles for terrain', (t) => { + const options2D = { + minzoom: 1, + maxzoom: 10, + tileSize: 512 + }; + + const options = extend({ + useElevationData: true + }, options2D); + + const transform = new Transform(); + let centerElevation = 0; + let tilesDefaultElevation = 0; + const tileElevation = {}; + const elevation = { + getAtPoint(_) { + return this.exaggeration() * centerElevation; + }, + getMinMaxForTile(tileID) { + const ele = tileElevation[tileID.key] !== undefined ? tileElevation[tileID.key] : tilesDefaultElevation; + if (ele === null) return null; + return {min: this.exaggeration() * ele, max: this.exaggeration() * ele}; + }, + exaggeration() { + return 10; // Low tile zoom used, exaggerate elevation to make impact. + } + }; + transform.elevation = elevation; + transform.resize(200, 200); + + // make slightly off center so that sort order is not subject to precision issues + transform.center = {lng: -0.01, lat: 0.01}; + + transform.zoom = 0; + t.deepEqual(transform.coveringTiles(options), []); + + transform.zoom = 1; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(1, 0, 1, 0, 0), + new OverscaledTileID(1, 0, 1, 1, 0), + new OverscaledTileID(1, 0, 1, 0, 1), + new OverscaledTileID(1, 0, 1, 1, 1)]); + + transform.zoom = 2.4; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(2, 0, 2, 1, 1), + new OverscaledTileID(2, 0, 2, 2, 1), + new OverscaledTileID(2, 0, 2, 1, 2), + new OverscaledTileID(2, 0, 2, 2, 2)]); + + transform.zoom = 10; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(10, 0, 10, 511, 511), + new OverscaledTileID(10, 0, 10, 512, 511), + new OverscaledTileID(10, 0, 10, 511, 512), + new OverscaledTileID(10, 0, 10, 512, 512)]); + + transform.zoom = 11; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(10, 0, 10, 511, 511), + new OverscaledTileID(10, 0, 10, 512, 511), + new OverscaledTileID(10, 0, 10, 511, 512), + new OverscaledTileID(10, 0, 10, 512, 512)]); + + transform.zoom = 9.1; + transform.pitch = 60.0; + transform.bearing = 32.0; + transform.center = new LngLat(56.90, 48.20); + transform.resize(1024, 768); + transform.elevation = null; + const cover2D = transform.coveringTiles(options2D); + // No LOD as there is no elevation data. + t.true(cover2D[0].overscaledZ === cover2D[cover2D.length - 1].overscaledZ); + + transform.pitch = 65.0; + transform.elevation = elevation; + const cover = transform.coveringTiles(options); + // First part of the cover should be the same as for 60 degrees no elevation case. + t.deepEqual(cover.slice(0, 6), cover2D.slice(0, 6)); + + // Even though it is larger pitch, less tiles are expected as LOD kicks in. + t.true(cover.length < cover2D.length); + t.true(cover[0].overscaledZ > cover[cover.length - 1].overscaledZ); + + // Elevated LOD with elevated center returns the same + tilesDefaultElevation = centerElevation = 10000; + + transform.elevation = null; + transform.elevation = elevation; + const cover10k = transform.coveringTiles(options); + + t.deepEqual(cover, cover10k); + + // Lower tiles on side get clipped. + const lowTiles = [ + new OverscaledTileID(9, 0, 9, 335, 178).key, + new OverscaledTileID(9, 0, 9, 337, 178).key + ]; + t.true(cover.filter(t => lowTiles.includes(t.key)).length === lowTiles.length); + + for (const t of lowTiles) { + tileElevation[t] = 0; + } + const coverLowSide = transform.coveringTiles(options); + t.true(coverLowSide.filter(t => lowTiles.includes(t.key)).length === 0); + + tileElevation[lowTiles[0]] = null; // missing elevation information gets to cover. + t.ok(transform.coveringTiles(options).find(t => t.key === lowTiles[0])); + + transform.zoom = 2; + transform.pitch = 0; + transform.bearing = 0; + transform.resize(300, 300); + t.test('calculates tile coverage at w > 0', (t) => { + transform.center = {lng: 630.02, lat: 0.01}; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(2, 2, 2, 1, 1), + new OverscaledTileID(2, 2, 2, 1, 2), + new OverscaledTileID(2, 2, 2, 0, 1), + new OverscaledTileID(2, 2, 2, 0, 2) + ]); + t.end(); + }); + + t.test('calculates tile coverage at w = -1', (t) => { + transform.center = {lng: -360.01, lat: 0.02}; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(2, -1, 2, 1, 1), + new OverscaledTileID(2, -1, 2, 2, 1), + new OverscaledTileID(2, -1, 2, 1, 2), + new OverscaledTileID(2, -1, 2, 2, 2) ]); t.end(); }); + t.test('calculates tile coverage across meridian', (t) => { + transform.zoom = 1; + transform.center = {lng: -180.01, lat: 0.02}; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(1, -1, 1, 1, 0), + new OverscaledTileID(1, 0, 1, 0, 0), + new OverscaledTileID(1, -1, 1, 1, 1), + new OverscaledTileID(1, 0, 1, 0, 1) + ]); + t.end(); + }); t.test('only includes tiles for a single world, if renderWorldCopies is set to false', (t) => { transform.zoom = 1; transform.center = {lng: -180.01, lat: 0.01}; @@ -249,6 +628,17 @@ test('transform', (t) => { ]); t.end(); }); + t.test('proper distance to center with wrap. Zoom drop at the end.', (t) => { + transform.resize(2000, 2000); + transform.zoom = 3.29; + transform.pitch = 57; + transform.bearing = 91.8; + transform.center = {lng: -134.66, lat: 20.52}; + const cover = transform.coveringTiles(options); + t.assert(cover[0].overscaledZ === 3); + t.assert(cover[cover.length - 1].overscaledZ <= 2); + t.end(); + }); t.end(); }); @@ -353,5 +743,497 @@ test('transform', (t) => { t.end(); }); + t.test('isHorizonVisible', (t) => { + + t.test('isHorizonVisibleForPoints', (t) => { + const transform = new Transform(); + transform.maxPitch = 85; + transform.resize(800, 800); + transform.zoom = 10; + transform.center = {lng: 0, lat: 0}; + transform.pitch = 85; + let p0, p1; + + t.true(transform.isHorizonVisible()); + + p0 = new Point(0, 0); p1 = new Point(10, 10); + t.true(transform.isHorizonVisibleForPoints(p0, p1)); + + p0 = new Point(0, 250); p1 = new Point(10, 350); + t.true(transform.isHorizonVisibleForPoints(p0, p1)); + + p0 = new Point(0, transform.horizonLineFromTop() - 10); + p1 = new Point(10, transform.horizonLineFromTop() + 10); + t.true(transform.isHorizonVisibleForPoints(p0, p1)); + + p0 = new Point(0, 700); p1 = new Point(10, 710); + t.false(transform.isHorizonVisibleForPoints(p0, p1)); + + p0 = new Point(0, transform.horizonLineFromTop()); + p1 = new Point(10, transform.horizonLineFromTop() + 10); + t.false(transform.isHorizonVisibleForPoints(p0, p1)); + + t.end(); + }); + + t.test('high pitch', (t) => { + const transform = new Transform(); + transform.maxPitch = 85; + transform.resize(300, 300); + transform.zoom = 10; + transform.center = {lng: 0, lat: 0}; + transform.pitch = 0; + + t.false(transform.isHorizonVisible()); + transform.pitch = 85; + t.true(transform.isHorizonVisible()); + + t.end(); + }); + + t.test('with large top padding', (t) => { + const transform = new Transform(); + transform.resize(200, 200); + transform.zoom = 10; + transform.center = {lng: 0, lat: 0}; + transform.pitch = 60; + + t.false(transform.isHorizonVisible()); + transform.padding = {top: 180}; + t.true(transform.isHorizonVisible()); + + t.end(); + }); + + t.test('lower zoom level, rotated map making background visible', (t) => { + const transform = new Transform(); + transform.resize(1300, 1300); + transform.zoom = 3; + transform.center = {lng: 0, lat: 0}; + transform.pitch = 0; + + t.false(transform.isHorizonVisible()); + transform.zoom = 0; + transform.bearing = 45; + t.true(transform.isHorizonVisible()); + + t.end(); + }); + + t.test('accounts for renderWorldCopies', (t) => { + const transform = new Transform(); + transform.resize(1300, 1300); + transform.zoom = 2; + transform.center = {lng: -135, lat: 0}; + transform.pitch = 0; + transform.bearing = -45; + transform.renderWorldCopies = true; + + t.false(transform.isHorizonVisible()); + transform.renderWorldCopies = false; + t.true(transform.isHorizonVisible()); + + t.end(); + }); + + t.end(); + }); + + t.test('freeCamera', (t) => { + const rotatedFrame = (quaternion) => { + return { + up: vec3.transformQuat([], [0, -1, 0], quaternion), + forward: vec3.transformQuat([], [0, 0, -1], quaternion), + right: vec3.transformQuat([], [1, 0, 0], quaternion) + }; + }; + + t.test('invalid height', (t) => { + const transform = new Transform(); + const options = new FreeCameraOptions(); + + options.orientation = [1, 1, 1, 1]; + options.position = new MercatorCoordinate(0.1, 0.2, 0.3); + transform.setFreeCameraOptions(options); + + const updatedOrientation = transform.getFreeCameraOptions().orientation; + const updatedPosition = transform.getFreeCameraOptions().position; + + // Expect default state as height is invalid + t.deepEqual(updatedOrientation, [0, 0, 0, 1]); + t.deepEqual(updatedPosition, new MercatorCoordinate(0, 0, 0)); + t.end(); + }); + + t.test('invalid z', (t) => { + const transform = new Transform(); + transform.resize(100, 100); + const options = new FreeCameraOptions(); + + // Invalid z-value (<= 0.0 || > 1) should be clamped to respect both min & max zoom values + options.position = new MercatorCoordinate(0.1, 0.1, 0.0); + transform.setFreeCameraOptions(options); + t.equal(transform.zoom, transform.maxZoom); + t.true(transform.getFreeCameraOptions().position.z > 0.0); + + options.position = new MercatorCoordinate(0.5, 0.2, 123.456); + transform.setFreeCameraOptions(options); + t.equal(transform.zoom, transform.minZoom); + t.true(transform.getFreeCameraOptions().position.z <= 1.0); + + t.end(); + }); + + t.test('orientation', (t) => { + const transform = new Transform(); + transform.resize(100, 100); + const options = new FreeCameraOptions(); + + // Default orientation + options.orientation = [0, 0, 0, 1]; + transform.setFreeCameraOptions(options); + t.equal(transform.bearing, 0); + t.equal(transform.pitch, 0); + t.deepEqual(transform.center, new LngLat(0, 0)); + + // 60 pitch + options.orientation = [0, 0, 0, 1]; + quat.rotateX(options.orientation, options.orientation, -60.0 * Math.PI / 180.0); + transform.setFreeCameraOptions(options); + t.equal(transform.bearing, 0.0); + t.equal(transform.pitch, 60.0); + t.deepEqual(fixedPoint(transform.point, 5), new Point(256, 50)); + + // 56 bearing + options.orientation = [0, 0, 0, 1]; + quat.rotateZ(options.orientation, options.orientation, 56.0 * Math.PI / 180.0); + transform.setFreeCameraOptions(options); + t.equal(fixedNum(transform.bearing), 56.0); + t.equal(fixedNum(transform.pitch), 0.0); + t.deepEqual(fixedPoint(transform.point, 5), new Point(512, 359.80761)); + + // 30 pitch and -179 bearing + options.orientation = [0, 0, 0, 1]; + quat.rotateZ(options.orientation, options.orientation, -179.0 * Math.PI / 180.0); + quat.rotateX(options.orientation, options.orientation, -30.0 * Math.PI / 180.0); + transform.setFreeCameraOptions(options); + t.equal(fixedNum(transform.bearing), -179.0); + t.equal(fixedNum(transform.pitch), 30.0); + t.deepEqual(fixedPoint(transform.point, 5), new Point(442.09608, 386.59111)); + + t.end(); + }); + + t.test('invalid orientation', (t) => { + const transform = new Transform(); + transform.resize(100, 100); + const options = new FreeCameraOptions(); + + // Zero length quaternion + options.orientation = [0, 0, 0, 0]; + transform.setFreeCameraOptions(options); + t.deepEqual(transform.getFreeCameraOptions().orientation, [0, 0, 0, 1]); + + // up vector is on the xy-plane. Right vector can't be computed + options.orientation = [0, 0, 0, 1]; + quat.rotateY(options.orientation, options.orientation, Math.PI * 0.5); + transform.setFreeCameraOptions(options); + t.deepEqual(transform.getFreeCameraOptions().orientation, [0, 0, 0, 1]); + + // Camera is upside down + options.orientation = [0, 0, 0, 1]; + quat.rotateX(options.orientation, options.orientation, Math.PI * 0.75); + transform.setFreeCameraOptions(options); + t.deepEqual(transform.getFreeCameraOptions().orientation, [0, 0, 0, 1]); + + t.end(); + }); + + t.test('wraps coordinates when renderWorldCopies is true', (t) => { + const transform = new Transform(); + transform.resize(100, 100); + const options = new FreeCameraOptions(); + options._renderWorldCopies = true; + + const lngLatLike = [-482.44, 37.83]; + options.position = MercatorCoordinate.fromLngLat(lngLatLike); + transform.setFreeCameraOptions(options); + + t.equal(parseFloat(options.position.toLngLat().lng.toFixed(2)), -122.44); + t.end(); + }); + + t.test('does not wrap coordinates when renderWorldCopies is falsey', (t) => { + const transform = new Transform(); + transform.resize(100, 100); + const options = new FreeCameraOptions(); + + const lngLatLike = [-482.44, 37.83]; + options.position = MercatorCoordinate.fromLngLat(lngLatLike); + transform.setFreeCameraOptions(options); + + t.equal(parseFloat(options.position.toLngLat().lng.toFixed(2)), lngLatLike[0]); + t.end(); + }); + + t.test('clamp pitch', (t) => { + const transform = new Transform(); + transform.resize(100, 100); + const options = new FreeCameraOptions(); + let frame = null; + + options.orientation = [0, 0, 0, 1]; + quat.rotateX(options.orientation, options.orientation, -85.0 * Math.PI / 180.0); + transform.setFreeCameraOptions(options); + t.equal(transform.pitch, transform.maxPitch); + frame = rotatedFrame(transform.getFreeCameraOptions().orientation); + + t.deepEqual(fixedVec3(frame.right, 5), [1, 0, 0]); + t.deepEqual(fixedVec3(frame.up, 5), [0, -0.5, 0.86603]); + t.deepEqual(fixedVec3(frame.forward, 5), [0, -0.86603, -0.5]); + + t.end(); + }); + + t.test('clamp to bounds', (t) => { + const transform = new Transform(); + transform.resize(100, 100); + transform.setMaxBounds(new LngLatBounds(new LngLat(-180, -transform.maxValidLatitude), new LngLat(180, transform.maxValidLatitude))); + transform.zoom = 8.56; + const options = new FreeCameraOptions(); + + // Place the camera to an arbitrary position looking away from the map + options.position = new MercatorCoordinate(-100.0, -10000.0, 1000.0); + options.orientation = quat.rotateX([], [0, 0, 0, 1], -45.0 * Math.PI / 180.0); + transform.setFreeCameraOptions(options); + + t.deepEqual(fixedPoint(transform.point, 5), new Point(50, 50)); + t.equal(fixedNum(transform.bearing), 0.0); + t.equal(fixedNum(transform.pitch), 45.0); + + t.end(); + }); + + t.test('invalid state', (t) => { + const transform = new Transform(); + + t.equal(transform.pitch, 0); + t.equal(transform.bearing, 0); + t.deepEqual(transform.point, new Point(256, 256)); + + t.deepEqual(transform.getFreeCameraOptions().position, new MercatorCoordinate(0, 0, 0)); + t.deepEqual(transform.getFreeCameraOptions().orientation, [0, 0, 0, 1]); + + t.end(); + }); + + t.test('orientation roll', (t) => { + const transform = new Transform(); + transform.resize(100, 100); + let options = new FreeCameraOptions(); + + const orientationWithoutRoll = quat.rotateX([], [0, 0, 0, 1], -Math.PI / 4); + const orientationWithRoll = quat.rotateZ([], orientationWithoutRoll, Math.PI / 4); + + options.orientation = orientationWithRoll; + transform.setFreeCameraOptions(options); + options = transform.getFreeCameraOptions(); + + t.deepEqual(fixedVec4(options.orientation, 5), fixedVec4(orientationWithoutRoll, 5)); + t.equal(fixedNum(transform.pitch), 45.0); + t.equal(fixedNum(transform.bearing), 0.0); + t.deepEqual(fixedPoint(transform.point), new Point(256, 106)); + + t.end(); + }); + + t.test('state synchronization', (t) => { + const transform = new Transform(); + transform.resize(100, 100); + let frame = null; + + transform.pitch = 0.0; + transform.bearing = 0.0; + frame = rotatedFrame(transform.getFreeCameraOptions().orientation); + t.deepEqual(transform.getFreeCameraOptions().position, new MercatorCoordinate(0.5, 0.5, 0.29296875)); + t.deepEqual(frame.right, [1, 0, 0]); + t.deepEqual(frame.up, [0, -1, 0]); + t.deepEqual(frame.forward, [0, 0, -1]); + + transform.center = new LngLat(24.9384, 60.1699); + t.deepEqual(fixedCoord(transform.getFreeCameraOptions().position, 5), new MercatorCoordinate(0.56927, 0.28945, 0.29297)); + + transform.center = new LngLat(20, -20); + transform.pitch = 20; + transform.bearing = 77; + t.deepEqual(fixedCoord(transform.getFreeCameraOptions().position, 5), new MercatorCoordinate(0.45792, 0.57926, 0.27530)); + + transform.pitch = 0; + transform.bearing = 90; + frame = rotatedFrame(transform.getFreeCameraOptions().orientation); + t.deepEqual(fixedVec3(frame.right), [0, 1, 0]); + t.deepEqual(fixedVec3(frame.up), [1, 0, 0]); + t.deepEqual(fixedVec3(frame.forward), [0, 0, -1]); + + // Invalid pitch + transform.bearing = 0; + transform.pitch = -10; + frame = rotatedFrame(transform.getFreeCameraOptions().orientation); + t.deepEqual(fixedCoord(transform.getFreeCameraOptions().position, 5), new MercatorCoordinate(0.55556, 0.55672, 0.29297)); + t.deepEqual(frame.right, [1, 0, 0]); + t.deepEqual(frame.up, [0, -1, 0]); + t.deepEqual(frame.forward, [0, 0, -1]); + + transform.bearing = 0; + transform.pitch = 85; + transform.center = new LngLat(0, -80); + frame = rotatedFrame(transform.getFreeCameraOptions().orientation); + t.deepEqual(fixedCoord(transform.getFreeCameraOptions().position, 5), new MercatorCoordinate(0.5, 1.14146, 0.14648)); + t.deepEqual(fixedVec3(frame.right, 5), [1, 0, 0]); + t.deepEqual(fixedVec3(frame.up, 5), [0, -0.5, 0.86603]); + t.deepEqual(fixedVec3(frame.forward, 5), [0, -0.86603, -0.5]); + + t.end(); + }); + + t.test('Position should ignore the camera elevation reference mode', (t) => { + let groundElevation = 200; + const transform = new Transform(0, 22, 0, 85); + transform.resize(100, 100); + transform._elevation = { + getAtPoint: () => groundElevation, + exaggeration: () => 1.0, + raycast: () => undefined + }; + + const expected = new FreeCameraOptions(); + expected.position = new MercatorCoordinate(0.1596528750412326, 0.3865452936454495, 0.00007817578881907832); + expected.orientation = [-0.35818916989938915, -0.3581891698993891, 0.6096724682702889, 0.609672468270289]; + + transform.cameraElevationReference = "sea"; + transform.setFreeCameraOptions(expected); + let actual = transform.getFreeCameraOptions(); + t.deepEqual(fixedCoord(actual.position), fixedCoord(expected.position)); + t.deepEqual(fixedVec4(actual.orientation), fixedVec4(expected.orientation)); + + transform.cameraElevationReference = "ground"; + groundElevation = 300; + expected.position = new MercatorCoordinate(0.16, 0.39, 0.000078); + transform.setFreeCameraOptions(expected); + actual = transform.getFreeCameraOptions(); + t.deepEqual(fixedCoord(actual.position), fixedCoord(expected.position)); + t.deepEqual(fixedVec4(actual.orientation), fixedVec4(expected.orientation)); + + t.end(); + }); + + t.test('_translateCameraConstrained', (t) => { + t.test('it clamps at zoom 0 when lngRange and latRange are not defined', (t) => { + const transform = new Transform(); + transform.center = new LngLat(0, 0); + transform.zoom = 10; + transform.resize(500, 500); + + transform._updateCameraState(); + transform._translateCameraConstrained([0.2, 0.3, 1000]); + + t.equal(transform.zoom, 0); + t.end(); + }); + + t.test('it performs no clamping if camera z movementis not upwards', (t) => { + const transform = new Transform(); + transform.center = new LngLat(0, 0); + transform.zoom = 10; + transform.resize(500, 500); + + transform._updateCameraState(); + const initialPos = transform._camera.position; + transform._translateCameraConstrained([0.2, 0.3, 0]); + const finalPos = transform._camera.position; + + t.equal(initialPos[0] + 0.2, finalPos[0]); + t.equal(initialPos[1] + 0.3, finalPos[1]); + t.equal(initialPos[2], finalPos[2]); + t.end(); + }); + + t.test('it clamps at a height equivalent to _constrain', (t) => { + const transform = new Transform(); + transform.center = new LngLat(0, 0); + transform.zoom = 20; + transform.resize(500, 500); + transform.lngRange = [-5, 5]; + transform.latRange = [-5, 5]; + + //record constrained zoom + transform.zoom = 0; + const minZoom = transform.zoom; + + //zoom back in and update camera position + transform.zoom = 20; + transform._updateCameraState(); + transform._translateCameraConstrained([0.1, 0.2, 1]); + t.equal(transform.zoom, minZoom); + + t.end(); + }); + + t.end(); + }); + + t.end(); + }); + + t.test("pointRayIntersection with custom altitude", (t) => { + const transform = new Transform(); + transform.resize(100, 100); + transform.pitch = 45; + + let result = transform.rayIntersectionCoordinate(transform.pointRayIntersection(transform.centerPoint)); + t.deepEqual(fixedCoord(result), new MercatorCoordinate(0.5, 0.5, 0.0)); + + result = transform.rayIntersectionCoordinate(transform.pointRayIntersection(transform.centerPoint, 1000)); + const diff = mercatorZfromAltitude(1000, 0); + t.deepEqual(fixedCoord(result), fixedCoord(new MercatorCoordinate(0.5, 0.5 + diff, diff))); + + t.end(); + }); + + t.test("ZoomDeltaToMovement", (t) => { + const transform = new Transform(); + transform.resize(100, 100); + + // Incrementing zoom by 1 is expected reduce distance by half + let foundMovement = transform.zoomDeltaToMovement([0.5, 0.5, 0.0], 1.0); + let expectedMovement = transform.cameraToCenterDistance / transform.worldSize * 0.5; + t.equal(foundMovement, expectedMovement); + + foundMovement = transform.zoomDeltaToMovement([0.5, 0.5, 0.0], 2.0); + expectedMovement = transform.cameraToCenterDistance / transform.worldSize * 0.75; + t.equal(foundMovement, expectedMovement); + + t.end(); + }); + + t.test("ComputeZoomRelativeTo", (t) => { + const transform = new Transform(); + transform.resize(100, 100); + transform.zoom = 0; + + const height = transform._camera.position[2]; + t.equal(transform.computeZoomRelativeTo(new MercatorCoordinate(0.5, 0.5, 0.0)), 0); + t.equal(transform.computeZoomRelativeTo(new MercatorCoordinate(0.0, 0.0, 0.0)), 0); + t.equal(transform.computeZoomRelativeTo(new MercatorCoordinate(0.5, 0.5, height * 0.5)), 1); + t.equal(transform.computeZoomRelativeTo(new MercatorCoordinate(0.5, 0.5, height * 0.75)), 2); + + transform.zoom += 1; + t.equal(transform.computeZoomRelativeTo(new MercatorCoordinate(0.5, 0.5, 0.0)), 1); + t.equal(transform.computeZoomRelativeTo(new MercatorCoordinate(0.5, 0.5, height * 0.25)), 2); + t.equal(transform.computeZoomRelativeTo(new MercatorCoordinate(0.5, 0.5, height * 0.375)), 3); + + t.end(); + }); + t.end(); }); diff --git a/test/unit/source/query_features.test.js b/test/unit/source/query_features.test.js index a2cf20d60c4..f23fa218ec5 100644 --- a/test/unit/source/query_features.test.js +++ b/test/unit/source/query_features.test.js @@ -4,13 +4,14 @@ import { querySourceFeatures } from '../../../src/source/query_features.js'; import SourceCache from '../../../src/source/source_cache.js'; +import {create} from '../../../src/source/source.js'; import Transform from '../../../src/geo/transform.js'; test('QueryFeatures#rendered', (t) => { t.test('returns empty object if source returns no tiles', (t) => { const mockSourceCache = {tilesIn () { return []; }}; const transform = new Transform(); - const result = queryRenderedFeatures(mockSourceCache, {}, undefined, {}, undefined, transform); + const result = queryRenderedFeatures(mockSourceCache, {}, undefined, {}, undefined, undefined, transform); t.deepEqual(result, []); t.end(); }); @@ -20,7 +21,7 @@ test('QueryFeatures#rendered', (t) => { test('QueryFeatures#source', (t) => { t.test('returns empty result when source has no features', (t) => { - const sourceCache = new SourceCache('test', { + const source = create('test', { type: 'geojson', data: {type: 'FeatureCollection', features: []} }, { @@ -29,7 +30,8 @@ test('QueryFeatures#source', (t) => { send(type, params, callback) { return callback(); } }; } - }); + }, this); + const sourceCache = new SourceCache('test', source); const result = querySourceFeatures(sourceCache, {}); t.deepEqual(result, []); t.end(); diff --git a/test/unit/source/raster_dem_tile_source.test.js b/test/unit/source/raster_dem_tile_source.test.js index f7fef0d6d5e..05d2ec20c09 100644 --- a/test/unit/source/raster_dem_tile_source.test.js +++ b/test/unit/source/raster_dem_tile_source.test.js @@ -79,7 +79,8 @@ test('RasterTileSource', (t) => { }); window.server.respond(); }); - t.test('populates neighboringTiles', (t) => { + + t.test('getNeighboringTiles', (t) => { window.server.respondWith('/source.json', JSON.stringify({ minzoom: 0, maxzoom: 22, @@ -87,67 +88,35 @@ test('RasterTileSource', (t) => { tiles: ["http://example.com/{z}/{x}/{y}.png"] })); const source = createSource({url: "/source.json"}); - source.on('data', (e) => { - if (e.sourceDataType === 'metadata') { - const tile = { - tileID: new OverscaledTileID(10, 0, 10, 5, 5), - state: 'loading', - loadVectorData () {}, - setExpiryData() {} - }; - source.loadTile(tile, () => {}); - t.deepEqual(Object.keys(tile.neighboringTiles), [ - new OverscaledTileID(10, 0, 10, 4, 5).key, - new OverscaledTileID(10, 0, 10, 6, 5).key, - new OverscaledTileID(10, 0, 10, 4, 4).key, - new OverscaledTileID(10, 0, 10, 5, 4).key, - new OverscaledTileID(10, 0, 10, 6, 4).key, - new OverscaledTileID(10, 0, 10, 4, 6).key, - new OverscaledTileID(10, 0, 10, 5, 6).key, - new OverscaledTileID(10, 0, 10, 6, 6).key - ]); - - t.end(); - - } + t.test('getNeighboringTiles', (t) => { + t.deepEqual(Uint32Array.from(Object.keys(source._getNeighboringTiles(new OverscaledTileID(10, 0, 10, 5, 5)))).sort(), Uint32Array.from([ + new OverscaledTileID(10, 0, 10, 4, 5).key, + new OverscaledTileID(10, 0, 10, 6, 5).key, + new OverscaledTileID(10, 0, 10, 4, 4).key, + new OverscaledTileID(10, 0, 10, 5, 4).key, + new OverscaledTileID(10, 0, 10, 6, 4).key, + new OverscaledTileID(10, 0, 10, 4, 6).key, + new OverscaledTileID(10, 0, 10, 5, 6).key, + new OverscaledTileID(10, 0, 10, 6, 6).key + ]).sort()); + t.end(); }); - window.server.respond(); - }); - t.test('populates neighboringTiles with wrapped tiles', (t) => { - window.server.respondWith('/source.json', JSON.stringify({ - minzoom: 0, - maxzoom: 22, - attribution: "Mapbox", - tiles: ["http://example.com/{z}/{x}/{y}.png"] - })); - const source = createSource({url: "/source.json"}); - source.on('data', (e) => { - if (e.sourceDataType === 'metadata') { - const tile = { - tileID: new OverscaledTileID(5, 0, 5, 31, 5), - state: 'loading', - loadVectorData () {}, - setExpiryData() {} - }; - source.loadTile(tile, () => {}); - - t.deepEqual(Object.keys(tile.neighboringTiles), [ - new OverscaledTileID(5, 0, 5, 30, 6).key, - new OverscaledTileID(5, 0, 5, 31, 6).key, - new OverscaledTileID(5, 0, 5, 30, 5).key, - new OverscaledTileID(5, 1, 5, 0, 5).key, - new OverscaledTileID(5, 0, 5, 30, 4).key, - new OverscaledTileID(5, 0, 5, 31, 4).key, - new OverscaledTileID(5, 1, 5, 0, 4).key, - new OverscaledTileID(5, 1, 5, 0, 6).key - ]); - t.end(); - } + t.test('getNeighboringTiles with wrapped tiles', (t) => { + t.deepEqual(Uint32Array.from(Object.keys(source._getNeighboringTiles(new OverscaledTileID(5, 0, 5, 31, 5)))).sort(), Uint32Array.from([ + new OverscaledTileID(5, 0, 5, 30, 6).key, + new OverscaledTileID(5, 0, 5, 31, 6).key, + new OverscaledTileID(5, 0, 5, 30, 5).key, + new OverscaledTileID(5, 1, 5, 0, 5).key, + new OverscaledTileID(5, 0, 5, 30, 4).key, + new OverscaledTileID(5, 0, 5, 31, 4).key, + new OverscaledTileID(5, 1, 5, 0, 4).key, + new OverscaledTileID(5, 1, 5, 0, 6).key + ]).sort()); + t.end(); }); - window.server.respond(); + t.end(); }); t.end(); - }); diff --git a/test/unit/source/raster_dem_tile_worker_source.test.js b/test/unit/source/raster_dem_tile_worker_source.test.js index febeaa32b27..fd12dc2b6fc 100644 --- a/test/unit/source/raster_dem_tile_worker_source.test.js +++ b/test/unit/source/raster_dem_tile_worker_source.test.js @@ -14,32 +14,10 @@ test('loadTile', (t) => { dim: 256 }, (err, data) => { if (err) t.fail(); - t.deepEqual(Object.keys(source.loaded), [0]); t.ok(data instanceof DEMData, 'returns DEM data'); - t.end(); }); }); t.end(); }); - -test('removeTile', (t) => { - t.test('removes loaded tile', (t) => { - const source = new RasterDEMTileWorkerSource(null, new StyleLayerIndex()); - - source.loaded = { - '0': {} - }; - - source.removeTile({ - source: 'source', - uid: 0 - }); - - t.deepEqual(source.loaded, {}); - t.end(); - }); - - t.end(); -}); diff --git a/test/unit/source/raster_tile_source.test.js b/test/unit/source/raster_tile_source.test.js index 2b77fa7db9e..04a88526899 100644 --- a/test/unit/source/raster_tile_source.test.js +++ b/test/unit/source/raster_tile_source.test.js @@ -1,6 +1,7 @@ import {test} from '../../util/test'; import RasterTileSource from '../../../src/source/raster_tile_source'; import window from '../../../src/util/window'; +import config from '../../../src/util/config'; import {OverscaledTileID} from '../../../src/source/tile_id'; import {RequestManager} from '../../../src/util/mapbox'; @@ -132,6 +133,56 @@ test('RasterTileSource', (t) => { window.server.respond(); }); + t.test('adds @2x to requests on hidpi devices', (t) => { + // helper function that makes a mock mapbox raster source and makes it load a tile + function makeMapboxSource(url, extension, loadCb, accessToken) { + window.devicePixelRatio = 2; + config.API_URL = 'http://path.png'; + config.REQUIRE_ACCESS_TOKEN = !!accessToken; + if (accessToken) { + config.ACCESS_TOKEN = accessToken; + } + + const source = createSource({url}); + source.tiles = [`${url}/{z}/{x}/{y}.${extension}`]; + const urlNormalizerSpy = t.spy(source.map._requestManager, 'normalizeTileURL'); + const tile = { + tileID: new OverscaledTileID(10, 0, 10, 5, 5), + state: 'loading', + loadVectorData () {}, + setExpiryData() {} + }; + source.loadTile(tile, () => {}); + loadCb(urlNormalizerSpy); + } + + t.test('png extension', (t) => { + makeMapboxSource('mapbox://path.png', 'png', (spy) => { + t.ok(spy.calledOnce); + t.equal(spy.getCall(0).args[0], 'mapbox://path.png/10/5/5.png'); + t.equal(spy.getCall(0).args[1], true); + t.end(); + }); + }); + t.test('png32 extension', (t) => { + makeMapboxSource('mapbox://path.png', 'png32', (spy) => { + t.ok(spy.calledOnce); + t.equal(spy.getCall(0).args[0], 'mapbox://path.png/10/5/5.png32'); + t.equal(spy.getCall(0).args[1], true); + t.end(); + }); + }); + t.test('jpg70 extension', (t) => { + makeMapboxSource('mapbox://path.png', 'jpg70', (spy) => { + t.ok(spy.calledOnce); + t.equal(spy.getCall(0).args[0], 'mapbox://path.png/10/5/5.jpg70'); + t.equal(spy.getCall(0).args[1], true); + t.end(); + }); + }); + t.end(); + }); + t.test('cancels TileJSON request if removed', (t) => { const source = createSource({url: "/source.json"}); source.onRemove(); diff --git a/test/unit/source/source_cache.test.js b/test/unit/source/source_cache.test.js index 2d8fc7790b0..1391a82982f 100644 --- a/test/unit/source/source_cache.test.js +++ b/test/unit/source/source_cache.test.js @@ -1,7 +1,8 @@ import {test} from '../../util/test'; import SourceCache from '../../../src/source/source_cache'; -import {setType} from '../../../src/source/source'; +import {create, setType} from '../../../src/source/source'; import Tile from '../../../src/source/tile'; +import {QueryGeometry} from '../../../src/style/query_geometry'; import {OverscaledTileID} from '../../../src/source/tile_id'; import Transform from '../../../src/geo/transform'; import LngLat from '../../../src/geo/lng_lat'; @@ -57,20 +58,24 @@ function MockSourceType(id, sourceOptions, _dispatcher, eventedParent) { setType('mock-source-type', MockSourceType); function createSourceCache(options, used) { - const sc = new SourceCache('id', extend({ + const spec = options || {}; + spec['minzoom'] = spec['minzoom'] || 0; + spec['maxzoom'] = spec['maxzoom'] || 14; + + const eventedParent = new Evented(); + const sc = new SourceCache('id', create('id', extend({ tileSize: 512, - minzoom: 0, - maxzoom: 14, type: 'mock-source-type' - }, options), /* dispatcher */ {}); + }, spec), /* dispatcher */ {}, eventedParent)); sc.used = typeof used === 'boolean' ? used : true; - return sc; + sc.transform = {tileZoom: 0}; + return {sourceCache: sc, eventedParent}; } test('SourceCache#addTile', (t) => { t.test('loads tile when uncached', (t) => { const tileID = new OverscaledTileID(0, 0, 0, 0, 0); - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile) { t.deepEqual(tile.tileID, tileID); t.equal(tile.uses, 0); @@ -83,7 +88,8 @@ test('SourceCache#addTile', (t) => { t.test('adds tile when uncached', (t) => { const tileID = new OverscaledTileID(0, 0, 0, 0, 0); - const sourceCache = createSourceCache({}).on('dataloading', (data) => { + const {sourceCache, eventedParent} = createSourceCache({}); + eventedParent.on('dataloading', (data) => { t.deepEqual(data.tile.tileID, tileID); t.equal(data.tile.uses, 1); t.end(); @@ -95,9 +101,9 @@ test('SourceCache#addTile', (t) => { t.test('updates feature state on added uncached tile', (t) => { const tileID = new OverscaledTileID(0, 0, 0, 0, 0); let updateFeaturesSpy; - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { - sourceCache.on('data', () => { + eventedParent.on('data', () => { t.equal(updateFeaturesSpy.getCalls().length, 1); t.end(); }); @@ -115,13 +121,14 @@ test('SourceCache#addTile', (t) => { let load = 0, add = 0; - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loaded'; load++; callback(); } - }).on('dataloading', () => { add++; }); + }); + eventedParent.on('dataloading', () => { add++; }); const tr = new Transform(); tr.width = 512; @@ -140,7 +147,7 @@ test('SourceCache#addTile', (t) => { t.test('updates feature state on cached tile', (t) => { const tileID = new OverscaledTileID(0, 0, 0, 0, 0); - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loaded'; callback(); @@ -168,7 +175,7 @@ test('SourceCache#addTile', (t) => { const time = new Date(); time.setSeconds(time.getSeconds() + 5); - const sourceCache = createSourceCache(); + const {sourceCache} = createSourceCache(); sourceCache._setTileReloadTimer = (id) => { sourceCache._timers[id] = setTimeout(() => {}, 0); }; @@ -211,13 +218,14 @@ test('SourceCache#addTile', (t) => { let load = 0, add = 0; - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loaded'; load++; callback(); } - }).on('dataloading', () => { add++; }); + }); + eventedParent.on('dataloading', () => { add++; }); const t1 = sourceCache._addTile(tileID); const t2 = sourceCache._addTile(new OverscaledTileID(0, 1, 0, 0, 0)); @@ -230,7 +238,7 @@ test('SourceCache#addTile', (t) => { }); t.test('should load tiles with identical overscaled Z but different canonical Z', (t) => { - const sourceCache = createSourceCache(); + const {sourceCache} = createSourceCache(); const tileIDs = [ new OverscaledTileID(1, 0, 0, 0, 0), @@ -260,9 +268,9 @@ test('SourceCache#addTile', (t) => { test('SourceCache#removeTile', (t) => { t.test('removes tile', (t) => { const tileID = new OverscaledTileID(0, 0, 0, 0, 0); - const sourceCache = createSourceCache({}); + const {sourceCache, eventedParent} = createSourceCache({}); sourceCache._addTile(tileID); - sourceCache.on('data', () => { + eventedParent.on('data', () => { sourceCache._removeTile(tileID.key); t.notOk(sourceCache._tiles[tileID.key]); t.end(); @@ -271,7 +279,7 @@ test('SourceCache#removeTile', (t) => { t.test('caches (does not unload) loaded tile', (t) => { const tileID = new OverscaledTileID(0, 0, 0, 0, 0); - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile) { tile.state = 'loaded'; }, @@ -296,7 +304,7 @@ test('SourceCache#removeTile', (t) => { let abort = 0, unload = 0; - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ abortTile(tile) { t.deepEqual(tile.tileID, tileID); abort++; @@ -319,7 +327,7 @@ test('SourceCache#removeTile', (t) => { t.test('_tileLoaded after _removeTile skips tile.added', (t) => { const tileID = new OverscaledTileID(0, 0, 0, 0, 0); - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.added = t.notOk(); sourceCache._removeTile(tileID.key); @@ -338,59 +346,64 @@ test('SourceCache#removeTile', (t) => { test('SourceCache / Source lifecycle', (t) => { t.test('does not fire load or change before source load event', (t) => { - const sourceCache = createSourceCache({noLoad: true}) - .on('data', t.fail); + const {sourceCache, eventedParent} = createSourceCache({noLoad: true}); + eventedParent.on('data', t.fail); sourceCache.onAdd(); setTimeout(t.end, 1); }); t.test('forward load event', (t) => { - const sourceCache = createSourceCache({}).on('data', (e) => { + const {sourceCache, eventedParent} = createSourceCache({}); + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') t.end(); }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('forward change event', (t) => { - const sourceCache = createSourceCache().on('data', (e) => { + const {sourceCache, eventedParent} = createSourceCache(); + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') t.end(); }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); sourceCache.getSource().fire(new Event('data')); }); t.test('forward error event', (t) => { - const sourceCache = createSourceCache({error: 'Error loading source'}).on('error', (err) => { + const {sourceCache, eventedParent} = createSourceCache({error: 'Error loading source'}); + eventedParent.on('error', (err) => { t.equal(err.error, 'Error loading source'); t.end(); }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('suppress 404 errors', (t) => { - const sourceCache = createSourceCache({status: 404, message: 'Not found'}) - .on('error', t.fail); - sourceCache.onAdd(); + const {sourceCache, eventedParent} = createSourceCache({status: 404, message: 'Not found'}); + eventedParent.on('error', t.fail); + sourceCache.getSource().onAdd(); t.end(); }); t.test('loaded() true after source error', (t) => { - const sourceCache = createSourceCache({error: 'Error loading source'}).on('error', () => { + const {sourceCache, eventedParent} = createSourceCache({error: 'Error loading source'}); + eventedParent.on('error', () => { t.ok(sourceCache.loaded()); t.end(); }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('loaded() true after tile error', (t) => { const transform = new Transform(); transform.resize(511, 511); transform.zoom = 0; - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile (tile, callback) { callback("error"); } - }).on('data', (e) => { + }); + eventedParent.on('data', (e) => { if (e.dataType === 'source' && e.sourceDataType === 'metadata') { sourceCache.update(transform); } @@ -399,7 +412,7 @@ test('SourceCache / Source lifecycle', (t) => { t.end(); }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('reloads tiles after a data event where source is updated', (t) => { @@ -410,7 +423,7 @@ test('SourceCache / Source lifecycle', (t) => { const expected = [ new OverscaledTileID(0, 0, 0, 0, 0).key, new OverscaledTileID(0, 0, 0, 0, 0).key ]; t.plan(expected.length); - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile (tile, callback) { t.equal(tile.tileID.key, expected.shift()); tile.loaded = true; @@ -418,14 +431,14 @@ test('SourceCache / Source lifecycle', (t) => { } }); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.dataType === 'source' && e.sourceDataType === 'metadata') { sourceCache.update(transform); sourceCache.getSource().fire(new Event('data', {dataType: 'source', sourceDataType: 'content'})); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('does not reload errored tiles', (t) => { @@ -433,7 +446,7 @@ test('SourceCache / Source lifecycle', (t) => { transform.resize(511, 511); transform.zoom = 1; - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile (tile, callback) { // this transform will try to load the four tiles at z1 and a single z0 tile // we only expect _reloadTile to be called with the 'loaded' z0 tile @@ -443,13 +456,13 @@ test('SourceCache / Source lifecycle', (t) => { }); const reloadTileSpy = t.spy(sourceCache, '_reloadTile'); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.dataType === 'source' && e.sourceDataType === 'metadata') { sourceCache.update(transform); sourceCache.getSource().fire(new Event('data', {dataType: 'source', sourceDataType: 'content'})); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); // we expect the source cache to have five tiles, but only to have reloaded one t.equal(Object.keys(sourceCache._tiles).length, 5); t.ok(reloadTileSpy.calledOnce); @@ -466,15 +479,15 @@ test('SourceCache#update', (t) => { transform.resize(512, 512); transform.zoom = 0; - const sourceCache = createSourceCache({}, false); - sourceCache.on('data', (e) => { + const {sourceCache, eventedParent} = createSourceCache({}, false); + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { sourceCache.update(transform); t.deepEqual(sourceCache.getIds(), []); t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('loads covering tiles', (t) => { @@ -482,15 +495,15 @@ test('SourceCache#update', (t) => { transform.resize(511, 511); transform.zoom = 0; - const sourceCache = createSourceCache({}); - sourceCache.on('data', (e) => { + const {sourceCache, eventedParent} = createSourceCache({}); + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { sourceCache.update(transform); t.deepEqual(sourceCache.getIds(), [new OverscaledTileID(0, 0, 0, 0, 0).key]); t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('respects Source#hasTile method if it is present', (t) => { @@ -498,10 +511,10 @@ test('SourceCache#update', (t) => { transform.resize(511, 511); transform.zoom = 1; - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ hasTile: (coord) => (coord.canonical.x !== 0) }); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { sourceCache.update(transform); t.deepEqual(sourceCache.getIds().sort(), [ @@ -511,7 +524,7 @@ test('SourceCache#update', (t) => { t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('removes unused tiles', (t) => { @@ -519,14 +532,14 @@ test('SourceCache#update', (t) => { transform.resize(511, 511); transform.zoom = 0; - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile: (tile, callback) => { tile.state = 'loaded'; callback(null); } }); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { sourceCache.update(transform); t.deepEqual(sourceCache.getIds(), [new OverscaledTileID(0, 0, 0, 0, 0).key]); @@ -544,7 +557,7 @@ test('SourceCache#update', (t) => { } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('retains parent tiles for pending children', (t) => { @@ -553,14 +566,14 @@ test('SourceCache#update', (t) => { transform.resize(511, 511); transform.zoom = 0; - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.state = (tile.tileID.key === new OverscaledTileID(0, 0, 0, 0, 0).key) ? 'loaded' : 'loading'; callback(); } }); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { sourceCache.update(transform); t.deepEqual(sourceCache.getIds(), [new OverscaledTileID(0, 0, 0, 0, 0).key]); @@ -578,7 +591,7 @@ test('SourceCache#update', (t) => { t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('retains parent tiles for pending children (wrapped)', (t) => { @@ -587,14 +600,14 @@ test('SourceCache#update', (t) => { transform.zoom = 0; transform.center = new LngLat(360, 0); - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.state = (tile.tileID.key === new OverscaledTileID(0, 1, 0, 0, 0).key) ? 'loaded' : 'loading'; callback(); } }); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { sourceCache.update(transform); t.deepEqual(sourceCache.getIds(), [new OverscaledTileID(0, 1, 0, 0, 0).key]); @@ -612,7 +625,7 @@ test('SourceCache#update', (t) => { t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('retains covered child tiles while parent tile is fading in', (t) => { @@ -620,7 +633,7 @@ test('SourceCache#update', (t) => { transform.resize(511, 511); transform.zoom = 2; - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.timeAdded = Infinity; tile.state = 'loaded'; @@ -631,7 +644,7 @@ test('SourceCache#update', (t) => { sourceCache._source.type = 'raster'; - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { sourceCache.update(transform); t.deepEqual(sourceCache.getIds(), [ @@ -648,7 +661,54 @@ test('SourceCache#update', (t) => { t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); + }); + + t.test('retains covered child tiles while parent tile is fading at high pitch', (t) => { + const transform = new Transform(); + transform.resize(511, 511); + transform.zoom = 16; + transform.maxPitch = 85; + transform.pitch = 85; + transform.center = new LngLat(0, 0); + + const {sourceCache, eventedParent} = createSourceCache({ + loadTile(tile, callback) { + tile.timeAdded = Infinity; + tile.state = 'loaded'; + tile.registerFadeDuration(100); + callback(); + } + }); + + sourceCache._source.type = 'raster'; + + eventedParent.on('data', (e) => { + if (e.sourceDataType === 'metadata') { + sourceCache.update(transform); + t.deepEqual(sourceCache.getIds(), [ + new OverscaledTileID(11, 0, 11, 1024, 1022).key, + new OverscaledTileID(11, 0, 11, 1023, 1022).key, + new OverscaledTileID(12, 0, 12, 2048, 2046).key, + new OverscaledTileID(12, 0, 12, 2047, 2046).key, + new OverscaledTileID(13, 0, 13, 4096, 4094).key, + new OverscaledTileID(13, 0, 13, 4095, 4094).key, + new OverscaledTileID(14, 0, 14, 8192, 8192).key, + new OverscaledTileID(14, 0, 14, 8191, 8192).key, + new OverscaledTileID(14, 0, 14, 8192, 8191).key, + new OverscaledTileID(14, 0, 14, 8191, 8191).key, + new OverscaledTileID(14, 0, 14, 8192, 8190).key, + new OverscaledTileID(14, 0, 14, 8191, 8190).key + ]); + + transform.center = new LngLat(0, -0.005); + sourceCache.update(transform); + + t.deepEqual(sourceCache.getRenderableIds().length, 14); + t.end(); + } + }); + sourceCache.getSource().onAdd(); }); t.test('retains a parent tile for fading even if a tile is partially covered by children', (t) => { @@ -656,7 +716,7 @@ test('SourceCache#update', (t) => { transform.resize(511, 511); transform.zoom = 0; - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.timeAdded = Infinity; tile.state = 'loaded'; @@ -667,7 +727,7 @@ test('SourceCache#update', (t) => { sourceCache._source.type = 'raster'; - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { sourceCache.update(transform); @@ -681,7 +741,7 @@ test('SourceCache#update', (t) => { t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('retains children for fading when tile.fadeEndTime is not set', (t) => { @@ -689,7 +749,7 @@ test('SourceCache#update', (t) => { transform.resize(511, 511); transform.zoom = 1; - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.timeAdded = Date.now(); tile.state = 'loaded'; @@ -699,7 +759,7 @@ test('SourceCache#update', (t) => { sourceCache._source.type = 'raster'; - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { sourceCache.update(transform); @@ -710,7 +770,7 @@ test('SourceCache#update', (t) => { t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('retains children when tile.fadeEndTime is in the future', (t) => { @@ -724,7 +784,7 @@ test('SourceCache#update', (t) => { let time = start; t.stub(browser, 'now').callsFake(() => time); - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.timeAdded = browser.now(); tile.state = 'loaded'; @@ -735,7 +795,7 @@ test('SourceCache#update', (t) => { sourceCache._source.type = 'raster'; - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { // load children sourceCache.update(transform); @@ -756,7 +816,7 @@ test('SourceCache#update', (t) => { } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('retains overscaled loaded children', (t) => { @@ -767,7 +827,7 @@ test('SourceCache#update', (t) => { // use slightly offset center so that sort order is better defined transform.center = new LngLat(-0.001, 0.001); - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ reparseOverscaled: true, loadTile(tile, callback) { tile.state = tile.tileID.overscaledZ === 16 ? 'loaded' : 'loading'; @@ -775,7 +835,7 @@ test('SourceCache#update', (t) => { } }); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { sourceCache.update(transform); t.deepEqual(sourceCache.getRenderableIds(), [ @@ -797,7 +857,7 @@ test('SourceCache#update', (t) => { t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('reassigns tiles for large jumps in longitude', (t) => { @@ -806,8 +866,8 @@ test('SourceCache#update', (t) => { transform.resize(511, 511); transform.zoom = 0; - const sourceCache = createSourceCache({}); - sourceCache.on('data', (e) => { + const {sourceCache, eventedParent} = createSourceCache({}); + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { transform.center = new LngLat(360, 0); const tileID = new OverscaledTileID(0, 1, 0, 0, 0); @@ -823,7 +883,7 @@ test('SourceCache#update', (t) => { t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.end(); @@ -833,7 +893,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { t.test('loads ideal tiles if they exist', (t) => { const stateCache = {}; - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = stateCache[tile.tileID.key] || 'errored'; callback(); @@ -843,14 +903,14 @@ test('SourceCache#_updateRetainedTiles', (t) => { const getTileSpy = t.spy(sourceCache, 'getTile'); const idealTile = new OverscaledTileID(1, 0, 1, 1, 1); stateCache[idealTile.key] = 'loaded'; - sourceCache._updateRetainedTiles([idealTile], 1); + sourceCache._updateRetainedTiles([idealTile]); t.ok(getTileSpy.notCalled); t.deepEqual(sourceCache.getIds(), [idealTile.key]); t.end(); }); t.test('retains all loaded children ', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = 'errored'; callback(); @@ -876,7 +936,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { sourceCache._tiles[t.key].state = 'loaded'; } - const retained = sourceCache._updateRetainedTiles([idealTile], 3); + const retained = sourceCache._updateRetainedTiles([idealTile]); t.deepEqual(Object.keys(retained).sort(), [ // parents are requested because ideal ideal tile is not completely covered by // loaded child tiles @@ -889,9 +949,82 @@ test('SourceCache#_updateRetainedTiles', (t) => { t.end(); }); + t.test('retains children for LOD cover', (t) => { + const {sourceCache} = createSourceCache({ + minzoom: 2, + maxzoom: 5, + loadTile(tile, callback) { + tile.state = 'errored'; + callback(); + } + }); + + const idealTiles = [ + new OverscaledTileID(5, 1, 5, 7, 10), + new OverscaledTileID(4, 2, 4, 2, 4), + new OverscaledTileID(3, 0, 3, 1, 2) + ]; + for (const t of idealTiles) { + sourceCache._tiles[t.key] = new Tile(t); + sourceCache._tiles[t.key].state = 'errored'; + } + + const loadedChildren = [ + // Children of OverscaledTileID(3, 0, 3, 1, 2) + new OverscaledTileID(4, 0, 4, 2, 4), + new OverscaledTileID(4, 0, 4, 3, 4), + new OverscaledTileID(4, 0, 4, 2, 5), + new OverscaledTileID(5, 0, 5, 6, 10), + new OverscaledTileID(5, 0, 5, 7, 10), + new OverscaledTileID(5, 0, 5, 6, 11), + new OverscaledTileID(5, 0, 5, 7, 11), + + // Children of OverscaledTileID(4, 2, 4, 2, 4). Overscale (not canonical.z) over maxzoom. + new OverscaledTileID(5, 2, 5, 4, 8), + new OverscaledTileID(5, 2, 5, 5, 8), + new OverscaledTileID(6, 2, 5, 4, 9), + new OverscaledTileID(9, 2, 5, 5, 9), // over maxUnderzooming. + + // Children over maxzoom and parent of new OverscaledTileID(5, 1, 5, 7, 10) + new OverscaledTileID(6, 1, 6, 14, 20), + new OverscaledTileID(6, 1, 6, 15, 20), + new OverscaledTileID(6, 1, 6, 14, 21), + new OverscaledTileID(6, 1, 6, 15, 21), + new OverscaledTileID(4, 1, 4, 3, 5) + ]; + + for (const t of loadedChildren) { + sourceCache._tiles[t.key] = new Tile(t); + sourceCache._tiles[t.key].state = 'loaded'; + } + + const retained = sourceCache._updateRetainedTiles(idealTiles); + + // Filter out those that are not supposed to be retained: + const filteredChildren = loadedChildren.filter(t => { + return ![ + new OverscaledTileID(6, 1, 6, 14, 20), + new OverscaledTileID(6, 1, 6, 15, 20), + new OverscaledTileID(6, 1, 6, 14, 21), + new OverscaledTileID(6, 1, 6, 15, 21), + new OverscaledTileID(9, 2, 5, 5, 9) + ].map(t => t.key).includes(t.key); + }); + + t.deepEqual(Object.keys(retained).sort(), [ + // parents are requested up to minzoom because ideal tiles are not + // completely covered by loaded child tiles + new OverscaledTileID(2, 0, 2, 0, 1), + new OverscaledTileID(2, 2, 2, 0, 1), + new OverscaledTileID(3, 2, 3, 1, 2) + ].concat(idealTiles).concat(filteredChildren).map(t => t.key).sort()); + + t.end(); + }); + t.test('adds parent tile if ideal tile errors and no child tiles are loaded', (t) => { const stateCache = {}; - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = stateCache[tile.tileID.key] || 'errored'; callback(); @@ -903,7 +1036,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { const idealTiles = [new OverscaledTileID(1, 0, 1, 1, 1), new OverscaledTileID(1, 0, 1, 0, 1)]; stateCache[idealTiles[0].key] = 'loaded'; - const retained = sourceCache._updateRetainedTiles(idealTiles, 1); + const retained = sourceCache._updateRetainedTiles(idealTiles); t.deepEqual(getTileSpy.getCalls().map((c) => { return c.args[0]; }), [ // when child tiles aren't found, check and request parent tile new OverscaledTileID(0, 0, 0, 0, 0) @@ -913,11 +1046,11 @@ test('SourceCache#_updateRetainedTiles', (t) => { // non-existant tiles t.deepEqual(retained, { // 1/0/1 - '211': new OverscaledTileID(1, 0, 1, 0, 1), + '1040': new OverscaledTileID(1, 0, 1, 0, 1), // 1/1/1 - '311': new OverscaledTileID(1, 0, 1, 1, 1), + '1552': new OverscaledTileID(1, 0, 1, 1, 1), // parent - '000': new OverscaledTileID(0, 0, 0, 0, 0) + '0': new OverscaledTileID(0, 0, 0, 0, 0) }); addTileSpy.restore(); getTileSpy.restore(); @@ -925,7 +1058,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { }); t.test('don\'t use wrong parent tile', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = 'errored'; callback(); @@ -942,7 +1075,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { const addTileSpy = t.spy(sourceCache, '_addTile'); const getTileSpy = t.spy(sourceCache, 'getTile'); - sourceCache._updateRetainedTiles([idealTile], 2); + sourceCache._updateRetainedTiles([idealTile]); t.deepEqual(getTileSpy.getCalls().map((c) => { return c.args[0]; }), [ // parents new OverscaledTileID(1, 0, 1, 0, 0), // not found @@ -963,7 +1096,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { }); t.test('use parent tile when ideal tile is not loaded', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loading'; callback(); @@ -979,7 +1112,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { const addTileSpy = t.spy(sourceCache, '_addTile'); const getTileSpy = t.spy(sourceCache, 'getTile'); - const retained = sourceCache._updateRetainedTiles([idealTile], 1); + const retained = sourceCache._updateRetainedTiles([idealTile]); t.deepEqual(getTileSpy.getCalls().map((c) => { return c.args[0]; }), [ // parents @@ -988,9 +1121,9 @@ test('SourceCache#_updateRetainedTiles', (t) => { t.deepEqual(retained, { // parent of ideal tile 0/0/0 - '000' : new OverscaledTileID(0, 0, 0, 0, 0), + '0' : new OverscaledTileID(0, 0, 0, 0, 0), // ideal tile id 1/0/1 - '211' : new OverscaledTileID(1, 0, 1, 0, 1) + '1040' : new OverscaledTileID(1, 0, 1, 0, 1) }, 'retain ideal and parent tile when ideal tiles aren\'t loaded'); addTileSpy.resetHistory(); @@ -998,12 +1131,12 @@ test('SourceCache#_updateRetainedTiles', (t) => { // now make sure we don't retain the parent tile when the ideal tile is loaded sourceCache._tiles[idealTile.key].state = 'loaded'; - const retainedLoaded = sourceCache._updateRetainedTiles([idealTile], 1); + const retainedLoaded = sourceCache._updateRetainedTiles([idealTile]); t.ok(getTileSpy.notCalled); t.deepEqual(retainedLoaded, { // only ideal tile retained - '211' : new OverscaledTileID(1, 0, 1, 0, 1) + '1040' : new OverscaledTileID(1, 0, 1, 0, 1) }, 'only retain ideal tiles when they\'re all loaded'); addTileSpy.restore(); @@ -1013,7 +1146,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { }); t.test('don\'t load parent if all immediate children are loaded', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loading'; callback(); @@ -1028,7 +1161,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { }); const getTileSpy = t.spy(sourceCache, 'getTile'); - const retained = sourceCache._updateRetainedTiles([idealTile], 2); + const retained = sourceCache._updateRetainedTiles([idealTile]); // parent tile isn't requested because all covering children are loaded t.deepEqual(getTileSpy.getCalls(), []); t.deepEqual(Object.keys(retained), [idealTile.key].concat(loadedTiles.map(t => t.key))); @@ -1037,7 +1170,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { }); t.test('prefer loaded child tiles to parent tiles', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loading'; callback(); @@ -1051,7 +1184,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { }); const getTileSpy = t.spy(sourceCache, 'getTile'); - let retained = sourceCache._updateRetainedTiles([idealTile], 1); + let retained = sourceCache._updateRetainedTiles([idealTile]); t.deepEqual(getTileSpy.getCalls().map((c) => { return c.args[0]; }), [ // parent new OverscaledTileID(0, 0, 0, 0, 0) @@ -1060,31 +1193,31 @@ test('SourceCache#_updateRetainedTiles', (t) => { t.deepEqual(retained, { // parent of ideal tile (0, 0, 0) (only partially covered by loaded child // tiles, so we still need to load the parent) - '000' : new OverscaledTileID(0, 0, 0, 0, 0), + '0' : new OverscaledTileID(0, 0, 0, 0, 0), // ideal tile id (1, 0, 0) - '011' : new OverscaledTileID(1, 0, 1, 0, 0), + '16' : new OverscaledTileID(1, 0, 1, 0, 0), // loaded child tile (2, 0, 0) - '022': new OverscaledTileID(2, 0, 2, 0, 0) + '32': new OverscaledTileID(2, 0, 2, 0, 0) }, 'retains children and parent when ideal tile is partially covered by a loaded child tile'); getTileSpy.restore(); // remove child tile and check that it only uses parent tile - delete sourceCache._tiles['022']; - retained = sourceCache._updateRetainedTiles([idealTile], 1); + delete sourceCache._tiles['32']; + retained = sourceCache._updateRetainedTiles([idealTile]); t.deepEqual(retained, { // parent of ideal tile (0, 0, 0) (only partially covered by loaded child // tiles, so we still need to load the parent) - '000' : new OverscaledTileID(0, 0, 0, 0, 0), + '0' : new OverscaledTileID(0, 0, 0, 0, 0), // ideal tile id (1, 0, 0) - '011' : new OverscaledTileID(1, 0, 1, 0, 0) + '16' : new OverscaledTileID(1, 0, 1, 0, 0) }, 'only retains parent tile if no child tiles are loaded'); t.end(); }); t.test('don\'t use tiles below minzoom', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loading'; callback(); @@ -1099,13 +1232,13 @@ test('SourceCache#_updateRetainedTiles', (t) => { }); const getTileSpy = t.spy(sourceCache, 'getTile'); - const retained = sourceCache._updateRetainedTiles([idealTile], 2); + const retained = sourceCache._updateRetainedTiles([idealTile]); t.deepEqual(getTileSpy.getCalls().map((c) => { return c.args[0]; }), [], 'doesn\'t request parent tiles bc they are lower than minzoom'); t.deepEqual(retained, { // ideal tile id (2, 0, 0) - '022' : new OverscaledTileID(2, 0, 2, 0, 0) + '32' : new OverscaledTileID(2, 0, 2, 0, 0) }, 'doesn\'t retain parent tiles below minzoom'); getTileSpy.restore(); @@ -1113,7 +1246,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { }); t.test('use overzoomed tile above maxzoom', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loading'; callback(); @@ -1123,7 +1256,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { const idealTile = new OverscaledTileID(2, 0, 2, 0, 0); const getTileSpy = t.spy(sourceCache, 'getTile'); - const retained = sourceCache._updateRetainedTiles([idealTile], 2); + const retained = sourceCache._updateRetainedTiles([idealTile]); t.deepEqual(getTileSpy.getCalls().map((c) => { return c.args[0]; }), [ // overzoomed child @@ -1135,7 +1268,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { t.deepEqual(retained, { // ideal tile id (2, 0, 0) - '022' : new OverscaledTileID(2, 0, 2, 0, 0) + '32' : new OverscaledTileID(2, 0, 2, 0, 0) }, 'doesn\'t retain child tiles above maxzoom'); getTileSpy.restore(); @@ -1143,7 +1276,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { }); t.test('dont\'t ascend multiple times if a tile is not found', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loading'; callback(); @@ -1152,7 +1285,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { const idealTiles = [new OverscaledTileID(8, 0, 8, 0, 0), new OverscaledTileID(8, 0, 8, 1, 0)]; const getTileSpy = t.spy(sourceCache, 'getTile'); - sourceCache._updateRetainedTiles(idealTiles, 8); + sourceCache._updateRetainedTiles(idealTiles); t.deepEqual(getTileSpy.getCalls().map((c) => { return c.args[0]; }), [ // parent tile ascent new OverscaledTileID(7, 0, 7, 0, 0), @@ -1173,7 +1306,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { sourceCache._tiles[t.key].state = 'loaded'; }); - sourceCache._updateRetainedTiles(idealTiles, 8); + sourceCache._updateRetainedTiles(idealTiles); t.deepEqual(getTileSpy.getCalls().map((c) => { return c.args[0]; }), [ // parent tile ascent new OverscaledTileID(7, 0, 7, 0, 0), @@ -1187,7 +1320,7 @@ test('SourceCache#_updateRetainedTiles', (t) => { }); t.test('adds correct leaded parent tiles for overzoomed tiles', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loading'; callback(); @@ -1201,14 +1334,14 @@ test('SourceCache#_updateRetainedTiles', (t) => { }); const idealTiles = [new OverscaledTileID(8, 0, 7, 0, 0), new OverscaledTileID(8, 0, 7, 1, 0)]; - const retained = sourceCache._updateRetainedTiles(idealTiles, 8); + const retained = sourceCache._updateRetainedTiles(idealTiles); - t.deepEqual(Object.keys(retained), [ + t.deepEqual(Uint32Array.from(Object.keys(retained)).sort(), Uint32Array.from([ new OverscaledTileID(7, 0, 7, 1, 0).key, new OverscaledTileID(8, 0, 7, 1, 0).key, new OverscaledTileID(8, 0, 7, 0, 0).key, new OverscaledTileID(7, 0, 7, 0, 0).key - ]); + ]).sort()); t.end(); }); @@ -1222,7 +1355,7 @@ test('SourceCache#clearTiles', (t) => { let abort = 0, unload = 0; - const sourceCache = createSourceCache({ + const {sourceCache} = createSourceCache({ abortTile(tile) { t.deepEqual(tile.tileID, coord); abort++; @@ -1252,21 +1385,20 @@ test('SourceCache#tilesIn', (t) => { tr.width = 512; tr.height = 512; tr._calcMatrices(); - const sourceCache = createSourceCache({noLoad: true}); + const {sourceCache} = createSourceCache({noLoad: true}); sourceCache.transform = tr; sourceCache.onAdd(); - t.same(sourceCache.tilesIn([ - new Point(0, 0), - new Point(512, 256) - ], 10, tr), []); + const queryGeometry = QueryGeometry.createFromScreenPoints([new Point(0, 0), new Point(512, 256)], tr); + t.same(sourceCache.tilesIn(queryGeometry), []); t.end(); }); function round(queryGeometry) { - return queryGeometry.map((p) => { - return p.round(); - }); + return { + min: queryGeometry.min.round(), + max: queryGeometry.max.round() + }; } t.test('regular tiles', (t) => { @@ -1275,7 +1407,7 @@ test('SourceCache#tilesIn', (t) => { transform.zoom = 1; transform.center = new LngLat(0, 1); - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loaded'; tile.additionalRadius = 0; @@ -1283,7 +1415,7 @@ test('SourceCache#tilesIn', (t) => { } }); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { sourceCache.update(transform); @@ -1295,32 +1427,28 @@ test('SourceCache#tilesIn', (t) => { ]); transform._calcMatrices(); - const tiles = sourceCache.tilesIn([ - new Point(0, 0), - new Point(512, 256) - ], 1, transform); + const queryGeometry = QueryGeometry.createFromScreenPoints([new Point(0, 0), new Point(512, 256)], transform); + const tiles = sourceCache.tilesIn(queryGeometry, false, false); tiles.sort((a, b) => { return a.tile.tileID.canonical.x - b.tile.tileID.canonical.x; }); tiles.forEach((result) => { delete result.tile.uid; }); - t.equal(tiles[0].tile.tileID.key, "011"); + t.equal(tiles[0].tile.tileID.key, 16); t.equal(tiles[0].tile.tileSize, 512); - t.equal(tiles[0].scale, 1); - t.deepEqual(round(tiles[0].queryGeometry), [{x: 4096, y: 4050}, {x:12288, y: 8146}]); + t.deepEqual(round(tiles[0].bufferedTilespaceBounds), {min: {x: 4080, y: 4050}, max: {x:8192, y: 8162}}); - t.equal(tiles[1].tile.tileID.key, "111"); + t.equal(tiles[1].tile.tileID.key, 528); t.equal(tiles[1].tile.tileSize, 512); - t.equal(tiles[1].scale, 1); - t.deepEqual(round(tiles[1].queryGeometry), [{x: -4096, y: 4050}, {x: 4096, y: 8146}]); + t.deepEqual(round(tiles[1].bufferedTilespaceBounds), {min: {x: 0, y: 4050}, max: {x: 4112, y: 8162}}); t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('reparsed overscaled tiles', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loaded'; tile.additionalRadius = 0; @@ -1332,7 +1460,7 @@ test('SourceCache#tilesIn', (t) => { tileSize: 512 }); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { const transform = new Transform(); transform.resize(1024, 1024); @@ -1347,32 +1475,29 @@ test('SourceCache#tilesIn', (t) => { new OverscaledTileID(2, 0, 1, 0, 0).key ]); - const tiles = sourceCache.tilesIn([ - new Point(0, 0), - new Point(1024, 512) - ], 1, transform); + const queryGeometry = QueryGeometry.createFromScreenPoints([new Point(0, 0), new Point(1024, 512)], transform); + + const tiles = sourceCache.tilesIn(queryGeometry); tiles.sort((a, b) => { return a.tile.tileID.canonical.x - b.tile.tileID.canonical.x; }); tiles.forEach((result) => { delete result.tile.uid; }); - t.equal(tiles[0].tile.tileID.key, "012"); + t.equal(tiles[0].tile.tileID.key, 17); t.equal(tiles[0].tile.tileSize, 1024); - t.equal(tiles[0].scale, 1); - t.deepEqual(round(tiles[0].queryGeometry), [{x: 4096, y: 4050}, {x:12288, y: 8146}]); + t.deepEqual(round(tiles[0].bufferedTilespaceBounds), {min: {x: 4088, y: 4050}, max: {x:8192, y: 8154}}); - t.equal(tiles[1].tile.tileID.key, "112"); + t.equal(tiles[1].tile.tileID.key, 529); t.equal(tiles[1].tile.tileSize, 1024); - t.equal(tiles[1].scale, 1); - t.deepEqual(round(tiles[1].queryGeometry), [{x: -4096, y: 4050}, {x: 4096, y: 8146}]); + t.deepEqual(round(tiles[1].bufferedTilespaceBounds), {min: {x: 0, y: 4050}, max: {x: 4104, y: 8154}}); t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.test('overscaled tiles', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loaded'; callback(); }, reparseOverscaled: false, minzoom: 1, @@ -1380,7 +1505,7 @@ test('SourceCache#tilesIn', (t) => { tileSize: 512 }); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { const transform = new Transform(); transform.resize(512, 512); @@ -1390,21 +1515,21 @@ test('SourceCache#tilesIn', (t) => { t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); t.end(); }); test('SourceCache#loaded (no errors)', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile, callback) { tile.state = 'loaded'; callback(); } }); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { const coord = new OverscaledTileID(0, 0, 0, 0, 0); sourceCache._addTile(coord); @@ -1413,17 +1538,17 @@ test('SourceCache#loaded (no errors)', (t) => { t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); test('SourceCache#loaded (with errors)', (t) => { - const sourceCache = createSourceCache({ + const {sourceCache, eventedParent} = createSourceCache({ loadTile(tile) { tile.state = 'errored'; } }); - sourceCache.on('data', (e) => { + eventedParent.on('data', (e) => { if (e.sourceDataType === 'metadata') { const coord = new OverscaledTileID(0, 0, 0, 0, 0); sourceCache._addTile(coord); @@ -1432,7 +1557,7 @@ test('SourceCache#loaded (with errors)', (t) => { t.end(); } }); - sourceCache.onAdd(); + sourceCache.getSource().onAdd(); }); test('SourceCache#getIds (ascending order by zoom level)', (t) => { @@ -1443,7 +1568,7 @@ test('SourceCache#getIds (ascending order by zoom level)', (t) => { new OverscaledTileID(2, 0, 2, 0, 0) ]; - const sourceCache = createSourceCache({}); + const {sourceCache} = createSourceCache({}); sourceCache.transform = new Transform(); for (let i = 0; i < ids.length; i++) { sourceCache._tiles[ids[i].key] = {tileID: ids[i]}; @@ -1461,7 +1586,7 @@ test('SourceCache#getIds (ascending order by zoom level)', (t) => { test('SourceCache#findLoadedParent', (t) => { t.test('adds from previously used tiles (sourceCache._tiles)', (t) => { - const sourceCache = createSourceCache({}); + const {sourceCache} = createSourceCache({}); sourceCache.onAdd(); const tr = new Transform(); tr.width = 512; @@ -1481,7 +1606,7 @@ test('SourceCache#findLoadedParent', (t) => { }); t.test('retains parents', (t) => { - const sourceCache = createSourceCache({}); + const {sourceCache} = createSourceCache({}); sourceCache.onAdd(); const tr = new Transform(); tr.width = 512; @@ -1499,7 +1624,7 @@ test('SourceCache#findLoadedParent', (t) => { }); t.test('Search cache for loaded parent tiles', (t) => { - const sourceCache = createSourceCache({}); + const {sourceCache} = createSourceCache({}); sourceCache.onAdd(); const tr = new Transform(); tr.width = 512; @@ -1567,7 +1692,7 @@ test('SourceCache#findLoadedParent', (t) => { test('SourceCache#reload', (t) => { t.test('before loaded', (t) => { - const sourceCache = createSourceCache({noLoad: true}); + const {sourceCache} = createSourceCache({noLoad: true}); sourceCache.onAdd(); t.doesNotThrow(() => { @@ -1586,7 +1711,7 @@ test('SourceCache reloads expiring tiles', (t) => { const expiryDate = new Date(); expiryDate.setMilliseconds(expiryDate.getMilliseconds() + 50); - const sourceCache = createSourceCache({expires: expiryDate}); + const {sourceCache} = createSourceCache({expires: expiryDate}); sourceCache._reloadTile = (id, state) => { t.equal(state, 'expired'); @@ -1600,8 +1725,8 @@ test('SourceCache reloads expiring tiles', (t) => { }); test('SourceCache sets max cache size correctly', (t) => { - t.test('sets cache size based on 512 tiles', (t) => { - const sourceCache = createSourceCache({ + t.test('sets cache size based on 256 tiles', (t) => { + const {sourceCache} = createSourceCache({ tileSize: 256 }); @@ -1615,8 +1740,23 @@ test('SourceCache sets max cache size correctly', (t) => { t.end(); }); - t.test('sets cache size based on 256 tiles', (t) => { - const sourceCache = createSourceCache({ + t.test('sets cache size given optional tileSize', (t) => { + const {sourceCache} = createSourceCache({ + tileSize: 256 + }); + + const tr = new Transform(); + tr.width = 512; + tr.height = 512; + sourceCache.updateCacheSize(tr, 2048); + + // Expect max size to be ((512 / tileSize + 1) ^ 2) * 5 => 3 * 3 * 5 + t.equal(sourceCache._cache.max, 20); + t.end(); + }); + + t.test('sets cache size based on 512 tiles', (t) => { + const {sourceCache} = createSourceCache({ tileSize: 512 }); diff --git a/test/unit/source/tile_id.test.js b/test/unit/source/tile_id.test.js index 09255aa5fb5..a5e9cca74fc 100644 --- a/test/unit/source/tile_id.test.js +++ b/test/unit/source/tile_id.test.js @@ -23,10 +23,10 @@ test('CanonicalTileID', (t) => { }); t.test('.key', (t) => { - t.deepEqual(new CanonicalTileID(0, 0, 0).key, "000"); - t.deepEqual(new CanonicalTileID(1, 0, 0).key, "011"); - t.deepEqual(new CanonicalTileID(1, 1, 0).key, "111"); - t.deepEqual(new CanonicalTileID(1, 1, 1).key, "311"); + t.deepEqual(new CanonicalTileID(0, 0, 0).key, 0); + t.deepEqual(new CanonicalTileID(1, 0, 0).key, 16); + t.deepEqual(new CanonicalTileID(1, 1, 0).key, 528); + t.deepEqual(new CanonicalTileID(1, 1, 1).key, 1552); t.end(); }); @@ -77,11 +77,11 @@ test('OverscaledTileID', (t) => { }); t.test('.key', (t) => { - t.deepEqual(new OverscaledTileID(0, 0, 0, 0, 0).key, "000"); - t.deepEqual(new OverscaledTileID(1, 0, 1, 0, 0).key, "011"); - t.deepEqual(new OverscaledTileID(1, 0, 1, 1, 0).key, "111"); - t.deepEqual(new OverscaledTileID(1, 0, 1, 1, 1).key, "311"); - t.deepEqual(new OverscaledTileID(1, -1, 1, 1, 1).key, "711"); + t.deepEqual(new OverscaledTileID(0, 0, 0, 0, 0).key, 0); + t.deepEqual(new OverscaledTileID(1, 0, 1, 0, 0).key, 16); + t.deepEqual(new OverscaledTileID(1, 0, 1, 1, 0).key, 528); + t.deepEqual(new OverscaledTileID(1, 0, 1, 1, 1).key, 1552); + t.deepEqual(new OverscaledTileID(1, -1, 1, 1, 1).key, 3600); t.end(); }); diff --git a/test/unit/source/vector_tile_source.test.js b/test/unit/source/vector_tile_source.test.js index 664b0d28305..75dc9622097 100644 --- a/test/unit/source/vector_tile_source.test.js +++ b/test/unit/source/vector_tile_source.test.js @@ -9,7 +9,8 @@ const wrapDispatcher = (dispatcher) => { return { getActor() { return dispatcher; - } + }, + ready: true }; }; @@ -23,7 +24,11 @@ function createSource(options, transformCallback) { transform: {showCollisionBoxes: false}, _getMapId: () => 1, _requestManager: new RequestManager(transformCallback), - style: {sourceCaches: {id: {clearTiles: () => {}}}} + style: { + _getSourceCaches: () => { + return [{clearTiles: () => {}}]; + } + } }); source.on('error', (e) => { diff --git a/test/unit/source/vector_tile_worker_source.test.js b/test/unit/source/vector_tile_worker_source.test.js index df90a9e4632..4886729a7a3 100644 --- a/test/unit/source/vector_tile_worker_source.test.js +++ b/test/unit/source/vector_tile_worker_source.test.js @@ -34,6 +34,24 @@ test('VectorTileWorkerSource#abortTile aborts pending request', (t) => { t.end(); }); +test('VectorTileWorkerSource#abortTile aborts pending async request', (t) => { + const source = new VectorTileWorkerSource(actor, new StyleLayerIndex(), [], (params, cb) => { + setTimeout(() => { + cb(null, {}); + }, 0); + }); + + source.loadTile({ + uid: 0, + tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}} + }, (err, res) => { + t.false(err); + t.false(res); + t.end(); + }); + source.abortTile({uid: 0}, () => {}); +}); + test('VectorTileWorkerSource#removeTile removes loaded tile', (t) => { const source = new VectorTileWorkerSource(actor, new StyleLayerIndex(), []); @@ -253,13 +271,18 @@ test('VectorTileWorkerSource provides resource timing information (fallback meth }]); const source = new VectorTileWorkerSource(actor, layerIndex, [], loadVectorData); + const url = 'http://localhost:2900/faketile.pbf'; - const sampleMarks = [100, 350]; + const sampleMarks = {}; + sampleMarks[`${url}#start`] = 100; + sampleMarks[`${url}#end`] = 350; const marks = {}; const measures = {}; t.stub(perf, 'getEntriesByName').callsFake((name) => { return measures[name] || []; }); t.stub(perf, 'mark').callsFake((name) => { - marks[name] = sampleMarks.shift(); + if (sampleMarks[name]) { + marks[name] = sampleMarks[name]; + } return null; }); t.stub(perf, 'measure').callsFake((name, start, end) => { @@ -279,7 +302,7 @@ test('VectorTileWorkerSource provides resource timing information (fallback meth source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, - request: {url: 'http://localhost:2900/faketile.pbf', collectResourceTiming: true} + request: {url, collectResourceTiming: true} }, (err, res) => { t.false(err); t.deepEquals(res.resourceTiming[0], {"duration": 250, "entryType": "measure", "name": "http://localhost:2900/faketile.pbf", "startTime": 100}, 'resourceTiming resp is expected'); diff --git a/test/unit/style-spec/fixture/layers.output-api-supported.json b/test/unit/style-spec/fixture/layers.output-api-supported.json index 45cd08356e4..5531538f5e8 100644 --- a/test/unit/style-spec/fixture/layers.output-api-supported.json +++ b/test/unit/style-spec/fixture/layers.output-api-supported.json @@ -56,7 +56,7 @@ "line": 83 }, { - "message": "layers[12].type: expected one of [fill, line, symbol, circle, heatmap, fill-extrusion, raster, hillshade, background], \"invalid\" found", + "message": "layers[12].type: expected one of [fill, line, symbol, circle, heatmap, fill-extrusion, raster, hillshade, background, sky], \"invalid\" found", "line": 90 }, { diff --git a/test/unit/style-spec/fixture/layers.output.json b/test/unit/style-spec/fixture/layers.output.json index a68f6b0688f..cdd80df491a 100644 --- a/test/unit/style-spec/fixture/layers.output.json +++ b/test/unit/style-spec/fixture/layers.output.json @@ -56,7 +56,7 @@ "line": 83 }, { - "message": "layers[12].type: expected one of [fill, line, symbol, circle, heatmap, fill-extrusion, raster, hillshade, background], \"invalid\" found", + "message": "layers[12].type: expected one of [fill, line, symbol, circle, heatmap, fill-extrusion, raster, hillshade, background, sky], \"invalid\" found", "line": 90 }, { diff --git a/test/unit/style-spec/fixture/terrain-empty-source.input.json b/test/unit/style-spec/fixture/terrain-empty-source.input.json new file mode 100644 index 00000000000..6ae83269baf --- /dev/null +++ b/test/unit/style-spec/fixture/terrain-empty-source.input.json @@ -0,0 +1,11 @@ +{ + "version": 8, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "url": "mapbox://mapbox.terrain-rgb" + } + }, + "terrain": {}, + "layers": [] +} \ No newline at end of file diff --git a/test/unit/style-spec/fixture/terrain-empty-source.output-api-supported.json b/test/unit/style-spec/fixture/terrain-empty-source.output-api-supported.json new file mode 100644 index 00000000000..c18ce7f679e --- /dev/null +++ b/test/unit/style-spec/fixture/terrain-empty-source.output-api-supported.json @@ -0,0 +1,6 @@ +[ + { + "message": "terrain: terrain is missing required property \"source\"", + "line": 9 + } +] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/terrain-empty-source.output.json b/test/unit/style-spec/fixture/terrain-empty-source.output.json new file mode 100644 index 00000000000..c18ce7f679e --- /dev/null +++ b/test/unit/style-spec/fixture/terrain-empty-source.output.json @@ -0,0 +1,6 @@ +[ + { + "message": "terrain: terrain is missing required property \"source\"", + "line": 9 + } +] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/terrain-exaggeration.input.json b/test/unit/style-spec/fixture/terrain-exaggeration.input.json new file mode 100644 index 00000000000..17ed5a35783 --- /dev/null +++ b/test/unit/style-spec/fixture/terrain-exaggeration.input.json @@ -0,0 +1,14 @@ +{ + "version": 8, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "url": "mapbox://mapbox.terrain-rgb" + } + }, + "terrain": { + "source": "mapbox-dem", + "exaggeration": "blah" + }, + "layers": [] +} \ No newline at end of file diff --git a/test/unit/style-spec/fixture/terrain-exaggeration.output-api-supported.json b/test/unit/style-spec/fixture/terrain-exaggeration.output-api-supported.json new file mode 100644 index 00000000000..d9596f31613 --- /dev/null +++ b/test/unit/style-spec/fixture/terrain-exaggeration.output-api-supported.json @@ -0,0 +1,6 @@ +[ + { + "message": "exaggeration: number expected, string found", + "line": 11 + } +] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/terrain-exaggeration.output.json b/test/unit/style-spec/fixture/terrain-exaggeration.output.json new file mode 100644 index 00000000000..d9596f31613 --- /dev/null +++ b/test/unit/style-spec/fixture/terrain-exaggeration.output.json @@ -0,0 +1,6 @@ +[ + { + "message": "exaggeration: number expected, string found", + "line": 11 + } +] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/terrain-invalid-source.input.json b/test/unit/style-spec/fixture/terrain-invalid-source.input.json new file mode 100644 index 00000000000..5f25b1ffaa8 --- /dev/null +++ b/test/unit/style-spec/fixture/terrain-invalid-source.input.json @@ -0,0 +1,13 @@ +{ + "version": 8, + "sources": { + "mapbox-dem": { + "type": "vector", + "url": "mapbox://mapbox.streets-v11" + } + }, + "terrain": { + "source": "mapbox-dem" + }, + "layers": [] +} \ No newline at end of file diff --git a/test/unit/style-spec/fixture/terrain-invalid-source.output-api-supported.json b/test/unit/style-spec/fixture/terrain-invalid-source.output-api-supported.json new file mode 100644 index 00000000000..6ce18282414 --- /dev/null +++ b/test/unit/style-spec/fixture/terrain-invalid-source.output-api-supported.json @@ -0,0 +1,6 @@ +[ + { + "message": "terrain: terrain cannot be used with a source of type vector, it only be used with a \"raster-dem\" source type", + "line": 10 + } +] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/terrain-invalid-source.output.json b/test/unit/style-spec/fixture/terrain-invalid-source.output.json new file mode 100644 index 00000000000..6ce18282414 --- /dev/null +++ b/test/unit/style-spec/fixture/terrain-invalid-source.output.json @@ -0,0 +1,6 @@ +[ + { + "message": "terrain: terrain cannot be used with a source of type vector, it only be used with a \"raster-dem\" source type", + "line": 10 + } +] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/terrain.input.json b/test/unit/style-spec/fixture/terrain.input.json new file mode 100644 index 00000000000..b3dd430d501 --- /dev/null +++ b/test/unit/style-spec/fixture/terrain.input.json @@ -0,0 +1,13 @@ +{ + "version": 8, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "url": "mapbox://mapbox.terrain-rgb" + } + }, + "terrain": { + "source": "mapbox-dem" + }, + "layers": [] +} \ No newline at end of file diff --git a/test/unit/style-spec/fixture/terrain.output-api-supported.json b/test/unit/style-spec/fixture/terrain.output-api-supported.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/test/unit/style-spec/fixture/terrain.output-api-supported.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/terrain.output.json b/test/unit/style-spec/fixture/terrain.output.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/test/unit/style-spec/fixture/terrain.output.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/test/unit/style/light.test.js b/test/unit/style/light.test.js index 584d42cf6aa..f0dcef89f28 100644 --- a/test/unit/style/light.test.js +++ b/test/unit/style/light.test.js @@ -1,8 +1,7 @@ import {test} from '../../util/test'; -import Light from '../../../src/style/light'; +import Light, {sphericalToCartesian} from '../../../src/style/light'; import styleSpec from '../../../src/style-spec/reference/latest'; import Color from '../../../src/style-spec/util/color'; -import {sphericalToCartesian} from '../../../src/util/util'; const spec = styleSpec.light; diff --git a/test/unit/style/style.test.js b/test/unit/style/style.test.js index ffe0bc968a6..c52390fdf16 100644 --- a/test/unit/style/style.test.js +++ b/test/unit/style/style.test.js @@ -288,7 +288,7 @@ test('Style#loadJSON', (t) => { const style = new Style(new StubMap()); style.on('style.load', () => { - t.ok(style.sourceCaches['mapbox'] instanceof SourceCache); + t.ok(style._getSourceCache('mapbox') instanceof SourceCache); t.end(); }); @@ -413,7 +413,7 @@ test('Style#_remove', (t) => { })); style.on('style.load', () => { - const sourceCache = style.sourceCaches['source-id']; + const sourceCache = style._getSourceCache('source-id'); t.spy(sourceCache, 'clearTiles'); style._remove(); t.ok(sourceCache.clearTiles.calledOnce); @@ -576,7 +576,7 @@ test('Style#setState', (t) => { style.loadJSON(initialState); style.on('style.load', () => { - const geoJSONSource = style.sourceCaches['source-id'].getSource(); + const geoJSONSource = style.getSource('source-id'); t.spy(style, 'setGeoJSONSourceData'); t.spy(geoJSONSource, 'setData'); const didChange = style.setState(nextState); @@ -641,7 +641,7 @@ test('Style#addSource', (t) => { style.loadJSON(createStyleJSON()); style.on('style.load', () => { style.on('error', () => { - t.notOk(style.sourceCaches['source-id']); + t.notOk(style._getSourceCache('source-id')); t.end(); }); style.addSource('source-id', { @@ -679,8 +679,8 @@ test('Style#addSource', (t) => { }); style.addSource('source-id', source); // fires data twice - style.sourceCaches['source-id'].fire(new Event('error')); - style.sourceCaches['source-id'].fire(new Event('data')); + style.getSource('source-id').fire(new Event('error')); + style.getSource('source-id').fire(new Event('data')); }); }); @@ -713,7 +713,7 @@ test('Style#removeSource', (t) => { })); style.on('style.load', () => { - const sourceCache = style.sourceCaches['source-id']; + const sourceCache = style._getSourceCache('source-id'); t.spy(sourceCache, 'clearTiles'); style.removeSource('source-id'); t.ok(sourceCache.clearTiles.calledOnce); @@ -781,7 +781,7 @@ test('Style#removeSource', (t) => { style.on('style.load', () => { style.addSource('source-id', source); - source = style.sourceCaches['source-id']; + source = style.getSource('source-id'); style.removeSource('source-id'); @@ -936,7 +936,7 @@ test('Style#addLayer', (t) => { style.on('data', (e) => { if (e.dataType === 'source' && e.sourceDataType === 'content') { - style.sourceCaches['mapbox'].reload = t.end; + style._getSourceCache('mapbox').reload = t.end; style.addLayer(layer); style.update({}); } @@ -970,8 +970,8 @@ test('Style#addLayer', (t) => { style.on('data', (e) => { if (e.dataType === 'source' && e.sourceDataType === 'content') { - style.sourceCaches['mapbox'].reload = t.end; - style.sourceCaches['mapbox'].clearTiles = t.fail; + style._getSourceCache('mapbox').reload = t.end; + style._getSourceCache('mapbox').clearTiles = t.fail; style.removeLayer('my-layer'); style.addLayer(layer); style.update({}); @@ -1006,8 +1006,8 @@ test('Style#addLayer', (t) => { }; style.on('data', (e) => { if (e.dataType === 'source' && e.sourceDataType === 'content') { - style.sourceCaches['mapbox'].reload = t.fail; - style.sourceCaches['mapbox'].clearTiles = t.end; + style._getSourceCache('mapbox').reload = t.fail; + style._getSourceCache('mapbox').clearTiles = t.end; style.removeLayer('my-layer'); style.addLayer(layer); style.update({}); @@ -1334,7 +1334,7 @@ test('Style#setPaintProperty', (t) => { style.once('style.load', () => { style.update(tr.zoom, 0); - const sourceCache = style.sourceCaches['geojson']; + const sourceCache = style._getSourceCache('geojson'); const source = style.getSource('geojson'); let begun = false; @@ -1811,7 +1811,7 @@ test('Style#queryRenderedFeatures', (t) => { const transform = new Transform(); transform.resize(512, 512); - function queryMapboxFeatures(layers, serializedLayers, getFeatureState, queryGeom, cameraQueryGeom, scale, params) { + function queryMapboxFeatures(layers, serializedLayers, getFeatureState, queryGeom, params) { const features = { 'land': [{ type: 'Feature', @@ -1902,32 +1902,34 @@ test('Style#queryRenderedFeatures', (t) => { }); style.on('style.load', () => { - style.sourceCaches.mapbox.tilesIn = () => { + style._getSourceCache('mapbox').tilesIn = () => { return [{ - tile: {queryRenderedFeatures: queryMapboxFeatures}, - tileID: new OverscaledTileID(0, 0, 0, 0, 0), - queryGeometry: [], - scale: 1 + queryGeometry: {}, + tilespaceGeometry: {}, + bufferedTilespaceGeometry: {}, + bufferedTilespaceBounds: {}, + tile: {queryRenderedFeatures: queryMapboxFeatures, tileID: new OverscaledTileID(0, 0, 0, 0, 0)}, + tileID: new OverscaledTileID(0, 0, 0, 0, 0) }]; }; - style.sourceCaches.other.tilesIn = () => { + style._getSourceCache('other').tilesIn = () => { return []; }; - style.sourceCaches.mapbox.transform = transform; - style.sourceCaches.other.transform = transform; + style._getSourceCache('mapbox').transform = transform; + style._getSourceCache('other').transform = transform; style.update(0); style._updateSources(transform); t.test('returns feature type', (t) => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform); + const results = style.queryRenderedFeatures([0, 0], {}, transform); t.equal(results[0].geometry.type, 'Line'); t.end(); }); t.test('filters by `layers` option', (t) => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {layers: ['land']}, transform); + const results = style.queryRenderedFeatures([0, 0], {layers: ['land']}, transform); t.equal(results.length, 2); t.end(); }); @@ -1937,26 +1939,26 @@ test('Style#queryRenderedFeatures', (t) => { t.stub(style, 'fire').callsFake((event) => { if (event.error && event.error.message.includes('parameters.layers must be an Array.')) errors++; }); - style.queryRenderedFeatures([{x: 0, y: 0}], {layers:'string'}, transform); + style.queryRenderedFeatures([0, 0], {layers:'string'}, transform); t.equals(errors, 1); t.end(); }); t.test('includes layout properties', (t) => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform); + const results = style.queryRenderedFeatures([0, 0], {}, transform); const layout = results[0].layer.layout; t.deepEqual(layout['line-cap'], 'round'); t.end(); }); t.test('includes paint properties', (t) => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform); + const results = style.queryRenderedFeatures([0, 0], {}, transform); t.deepEqual(results[2].layer.paint['line-color'], 'red'); t.end(); }); t.test('includes metadata', (t) => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform); + const results = style.queryRenderedFeatures([0, 0], {}, transform); const layer = results[1].layer; t.equal(layer.metadata.something, 'else'); @@ -1965,14 +1967,14 @@ test('Style#queryRenderedFeatures', (t) => { }); t.test('include multiple layers', (t) => { - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {layers: ['land', 'landref']}, transform); + const results = style.queryRenderedFeatures([0, 0], {layers: ['land', 'landref']}, transform); t.equals(results.length, 3); t.end(); }); t.test('does not query sources not implicated by `layers` parameter', (t) => { - style.sourceCaches.mapbox.queryRenderedFeatures = function() { t.fail(); }; - style.queryRenderedFeatures([{x: 0, y: 0}], {layers: ['land--other']}, transform); + style._getSourceCache('mapbox').queryRenderedFeatures = function() { t.fail(); }; + style.queryRenderedFeatures([0, 0], {layers: ['land--other']}, transform); t.end(); }); @@ -1981,7 +1983,7 @@ test('Style#queryRenderedFeatures', (t) => { t.stub(style, 'fire').callsFake((event) => { if (event.error && event.error.message.includes('does not exist in the map\'s style and cannot be queried for features.')) errors++; }); - const results = style.queryRenderedFeatures([{x: 0, y: 0}], {layers:['merp']}, transform); + const results = style.queryRenderedFeatures([0, 0], {layers:['merp']}, transform); t.equals(errors, 1); t.equals(results.length, 0); t.end(); @@ -2075,7 +2077,7 @@ test('Style#query*Features', (t) => { }); t.test('queryRenderedFeatures emits an error on incorrect filter', (t) => { - t.deepEqual(style.queryRenderedFeatures([{x: 0, y: 0}], {filter: 7}, transform), []); + t.deepEqual(style.queryRenderedFeatures([0, 0], {filter: 7}, transform), []); t.match(onError.args[0][0].error.message, /queryRenderedFeatures\.filter/); t.end(); }); @@ -2088,7 +2090,7 @@ test('Style#query*Features', (t) => { errors++; } }); - style.queryRenderedFeatures([{x: 0, y: 0}], {filter: "invalidFilter", validate: false}, transform); + style.queryRenderedFeatures([0, 0], {filter: "invalidFilter", validate: false}, transform); t.equals(errors, 0); t.end(); }); @@ -2206,3 +2208,60 @@ test('Style#hasTransitions', (t) => { t.end(); }); + +test('Style#setTerrain', (t) => { + t.test('rolls up inline source into style', (t) => { + const style = new Style(new StubMap()); + style.loadJSON({ + "version": 8, + "sources": {}, + "layers": [{ + "id": "background", + "type": "background" + }] + }); + + style.on('style.load', () => { + style.setTerrain({ + "source": { + "type": "raster-dem", + "tiles": ['http://example.com/{z}/{x}/{y}.png'], + "tileSize": 256, + "maxzoom": 14 + } + }); + t.ok(style.getSource('terrain-dem-src')); + t.equal(style.getSource('terrain-dem-src').type, 'raster-dem'); + t.end(); + }); + }); + + t.test('setTerrain(undefined) removes terrain', (t) => { + const style = new Style(new StubMap()); + style.loadJSON({ + "version": 8, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "tiles": ['http://example.com/{z}/{x}/{y}.png'], + "tileSize": 256, + "maxzoom": 14 + } + }, + "terrain": {"source": "mapbox-dem"}, + "layers": [{ + "id": "background", + "type": "background" + }] + }); + + style.on('style.load', () => { + style.setTerrain(undefined); + t.ok(style.terrain == null); + const serialized = style.serialize(); + t.ok(serialized.terrain == null); + t.end(); + }); + }); + t.end(); +}); diff --git a/test/unit/symbol/collision_feature.js b/test/unit/symbol/collision_feature.js index b039c02f0b2..a898d13d618 100644 --- a/test/unit/symbol/collision_feature.js +++ b/test/unit/symbol/collision_feature.js @@ -1,6 +1,6 @@ import {test} from '../../util/test'; -import CollisionFeature from '../../../src/symbol/collision_feature'; import Anchor from '../../../src/symbol/anchor'; +import {evaluateCircleCollisionFeature, evaluateBoxCollisionFeature} from '../../../src/symbol/symbol_layout'; import Point from '@mapbox/point-geometry'; import {CollisionBoxArray} from '../../../src/data/array_types'; @@ -19,11 +19,10 @@ test('CollisionFeature', (t) => { const point = new Point(500, 0); const anchor = new Anchor(point.x, point.y, 0, undefined); - const cf = new CollisionFeature(collisionBoxArray, anchor, 0, 0, 0, shapedText, 1, 0, false); - t.notOk(cf.circleDiameter); - t.equal(cf.boxEndIndex - cf.boxStartIndex, 1); + const index = evaluateBoxCollisionFeature(collisionBoxArray, anchor, 0, 0, 0, shapedText, 1, 0); + t.equal(index, 0); - const box = collisionBoxArray.get(cf.boxStartIndex); + const box = collisionBoxArray.get(index); t.equal(box.x1, -50); t.equal(box.x2, 50); t.equal(box.y1, -10); @@ -32,11 +31,8 @@ test('CollisionFeature', (t) => { }); test('Compute line height for runtime collision circles (line label)', (t) => { - const anchor = new Anchor(505, 95, 0, 1); - const cf = new CollisionFeature(collisionBoxArray, anchor, 0, 0, 0, shapedText, 1, 0, true); - t.ok(cf.circleDiameter); - t.equal(cf.circleDiameter, shapedText.bottom - shapedText.top); - t.equal(cf.boxEndIndex - cf.boxStartIndex, 0); + const diameter = evaluateCircleCollisionFeature(shapedText); + t.equal(diameter, shapedText.bottom - shapedText.top); t.end(); }); @@ -48,10 +44,8 @@ test('CollisionFeature', (t) => { bottom: -10 }; - const anchor = new Anchor(505, 95, 0, 1); - const cf = new CollisionFeature(collisionBoxArray, anchor, 0, 0, 0, shapedText, 1, 0, true); - t.equal(cf.boxEndIndex - cf.boxStartIndex, 0); - t.notOk(cf.circleDiameter); + const diameter = evaluateCircleCollisionFeature(shapedText); + t.notOk(diameter); t.end(); }); @@ -63,10 +57,8 @@ test('CollisionFeature', (t) => { bottom: -10 }; - const anchor = new Anchor(505, 95, 0, 1); - const cf = new CollisionFeature(collisionBoxArray, anchor, 0, 0, 0, shapedText, 1, 0, true); - t.equal(cf.boxEndIndex - cf.boxStartIndex, 0); - t.notOk(cf.circleDiameter); + const diameter = evaluateCircleCollisionFeature(shapedText); + t.notOk(diameter); t.end(); }); @@ -78,10 +70,8 @@ test('CollisionFeature', (t) => { bottom: 10.00001 }; - const anchor = new Anchor(505, 95, 0, 1); - const cf = new CollisionFeature(collisionBoxArray, anchor, 0, 0, 0, shapedText, 1, 0, true); - t.equal(cf.boxEndIndex - cf.boxStartIndex, 0); - t.equal(cf.circleDiameter, 10); + const diameter = evaluateCircleCollisionFeature(shapedText); + t.equal(diameter, 10); t.end(); }); diff --git a/test/unit/terrain/terrain.test.js b/test/unit/terrain/terrain.test.js new file mode 100644 index 00000000000..381d9268b0a --- /dev/null +++ b/test/unit/terrain/terrain.test.js @@ -0,0 +1,926 @@ +import {test} from '../../util/test'; +import {extend} from '../../../src/util/util'; +import {createMap} from '../../util'; +import DEMData from '../../../src/data/dem_data'; +import {RGBAImage} from '../../../src/util/image'; +import MercatorCoordinate from '../../../src/geo/mercator_coordinate'; +import window from '../../../src/util/window'; +import {OverscaledTileID} from '../../../src/source/tile_id'; +import styleSpec from '../../../src/style-spec/reference/latest'; +import Terrain from '../../../src/style/terrain'; +import Tile from '../../../src/source/tile'; +import {VertexMorphing} from '../../../src/terrain/draw_terrain_raster'; +import {fixedLngLat, fixedCoord, fixedPoint} from '../../util/fixed'; +import Point from '@mapbox/point-geometry'; +import LngLat from '../../../src/geo/lng_lat'; +import Marker from '../../../src/ui/marker'; +import Popup from '../../../src/ui/popup'; +import simulate from '../../util/simulate_interaction'; +import {createConstElevationDEM, setMockElevationTerrain} from '../../util/dem_mock'; + +function createStyle() { + return { + version: 8, + center: [180, 0], + zoom: 14, + sources: {}, + layers: [] + }; +} + +const TILE_SIZE = 128; +const zeroDem = createConstElevationDEM(0, TILE_SIZE); + +const createGradientDEM = () => { + const pixels = new Uint8Array((TILE_SIZE + 2) * (TILE_SIZE + 2) * 4); + // 1, 134, 160 encodes 0m. + const word = [1, 134, 160]; + for (let j = 1; j < TILE_SIZE + 1; j++) { + for (let i = 1; i < TILE_SIZE + 1; i++) { + const index = (j * (TILE_SIZE + 2) + i) * 4; + pixels[index] = word[0]; + pixels[index + 1] = word[1]; + pixels[index + 2] = word[2]; + // Increment word for next pixel. + word[2] += 1; + if (word[2] === 256) { + word[2] = 0; + word[1] += 1; + } + if (word[1] === 256) { + word[1] = 0; + word[0] += 1; + } + } + } + return new DEMData(0, new RGBAImage({height: TILE_SIZE + 2, width: TILE_SIZE + 2}, pixels), "mapbox", false, true); +}; + +test('Elevation', (t) => { + const dem = createGradientDEM(); + + t.beforeEach((callback) => { + window.useFakeXMLHttpRequest(); + callback(); + }); + + t.afterEach((callback) => { + window.restore(); + callback(); + }); + + t.test('elevation sampling', t => { + const map = createMap(t); + map.on('style.load', () => { + setMockElevationTerrain(map, zeroDem, TILE_SIZE); + map.once('render', () => { + const elevationError = -1; + t.test('Sample', t => { + const elevation = map.painter.terrain.getAtPoint({x: 0.51, y: 0.49}, elevationError); + t.equal(elevation, 0); + t.end(); + }); + + t.test('Invalid sample position', t => { + const elevation1 = map.painter.terrain.getAtPoint({x: 0.5, y: 1.1}, elevationError); + const elevation2 = map.painter.terrain.getAtPoint({x: 1.15, y: -0.001}, elevationError); + t.equal(elevation1, elevationError); + t.equal(elevation2, elevationError); + t.end(); + }); + t.end(); + }); + }); + }); + + t.test('style diff / remove dem source cache', t => { + const map = createMap(t); + map.on('style.load', () => { + setMockElevationTerrain(map, zeroDem, TILE_SIZE); + map.once('render', () => { + t.test('Throws error if style update tries to remove terrain DEM source', t => { + t.test('remove source', t => { + const stub = t.stub(console, 'error'); + map.removeSource('mapbox-dem'); + t.ok(stub.calledOnce); + t.end(); + }); + t.end(); + }); + t.end(); + }); + }); + }); + + t.test('style diff=false removes dem source', t => { + const map = createMap(t); + map.once('style.load', () => { + setMockElevationTerrain(map, zeroDem, TILE_SIZE); + map.once('render', () => { + map._updateTerrain(); + const elevationError = -1; + const terrain = map.painter.terrain; + const elevation1 = map.painter.terrain.getAtPoint({x: 0.5, y: 0.5}, elevationError); + t.equal(elevation1, 0); + + map.setStyle(createStyle(), {diff: false}); + + const elevation2 = terrain.getAtPoint({x: 0.5, y: 0.5}, elevationError); + t.equal(elevation2, elevationError); + t.end(); + }); + }); + }); + + t.test('interpolation', t => { + const map = createMap(t, { + style: extend(createStyle(), { + sources: { + mapbox: { + type: 'vector', + minzoom: 1, + maxzoom: 10, + tiles: ['http://example.com/{z}/{x}/{y}.png'] + } + }, + layers: [{ + id: 'layerId1', + type: 'circle', + source: 'mapbox', + 'source-layer': 'sourceLayer' + }] + }) + }); + map.on('style.load', () => { + map.addSource('mapbox-dem', { + "type": "raster-dem", + "tiles": ['http://example.com/{z}/{x}/{y}.png'], + "tileSize": TILE_SIZE, + "maxzoom": 14 + }); + const cache = map.style._getSourceCache('mapbox-dem'); + cache.used = cache._sourceLoaded = true; + cache._loadTile = (tile, callback) => { + tile.dem = dem; + tile.needsHillshadePrepare = true; + tile.needsDEMTextureUpload = true; + tile.state = 'loaded'; + callback(null); + }; + map.setTerrain({"source": "mapbox-dem"}); + map.once('render', () => { + const cache = map.style._getSourceCache('mapbox-dem'); + + t.test('terrain tiles loaded wrap', t => { + const tile = cache.getTile(new OverscaledTileID(14, 1, 14, 0, 8192)); + t.assert(tile.dem); + t.end(); + }); + + t.test('terrain tiles loaded no wrap', t => { + const tile = cache.getTile(new OverscaledTileID(14, 0, 14, 16383, 8192)); + t.assert(tile.dem); + t.end(); + }); + + const tilesAtTileZoom = 1 << 14; + // Calculate offset to neighbor value in dem bitmap. + const dx = 1 / (tilesAtTileZoom * TILE_SIZE); + + const coord = MercatorCoordinate.fromLngLat({lng: 180, lat: 0}); + t.equal(map.painter.terrain.getAtPoint(coord), 0); + + t.test('dx', t => { + const elevationDx = map.painter.terrain.getAtPoint({x: coord.x + dx, y: coord.y}); + t.assert(Math.abs(elevationDx - 0.1) < 1e-12); + t.end(); + }); + + t.test('dy', t => { + const elevationDy = map.painter.terrain.getAtPoint({x: coord.x, y: coord.y + dx}); + const expectation = TILE_SIZE * 0.1; + t.assert(Math.abs(elevationDy - expectation) < 1e-12); + t.end(); + }); + + t.test('dx/3 dy/3', t => { + const elevation = map.painter.terrain.getAtPoint({x: coord.x + dx / 3, y: coord.y + dx / 3}); + const expectation = (2 * TILE_SIZE + 2) * 0.1 / 6; + t.assert(Math.abs(elevation - expectation) < 1e-9); + t.end(); + }); + + t.test('-dx -wrap', t => { + const elevation = map.painter.terrain.getAtPoint({x: coord.x - dx, y: coord.y}); + const expectation = (TILE_SIZE - 1) * 0.1; + t.assert(Math.abs(elevation - expectation) < 1e-12); + t.end(); + }); + + t.test('-1.5dx -wrap', t => { + const elevation = map.painter.terrain.getAtPoint({x: coord.x - 1.5 * dx, y: coord.y}); + const expectation = (TILE_SIZE - 1.5) * 0.1; + t.assert(Math.abs(elevation - expectation) < 1e-12); + t.end(); + }); + + t.test('disable terrain', t => { + t.ok(map.painter.terrain); + map.setTerrain(null); + map.once('render', () => { + t.notOk(map.painter.terrain); + t.end(); + }); + }); + + t.end(); + }); + }); + }); + + t.test('mapbox-gl-js-internal#91', t => { + const data = { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [] + } + }] + }; + const map = createMap(t, { + style: extend(createStyle(), { + sources: { + trace: { + type: 'geojson', + data + } + }, + layers: [{ + "id": "background", + "type": "background", + "paint": { + "background-color": "black" + } + }, { + 'id': 'trace', + 'type': 'line', + 'source': 'trace', + 'paint': { + 'line-color': "red", + 'line-width': 5, + 'line-opacity': 1 + } + }] + }) + }); + map.on('style.load', () => { + setMockElevationTerrain(map, zeroDem, TILE_SIZE); + const source = map.getSource('trace'); + data.features[0].geometry.coordinates = [ + [180, 0], + [180.1, 0], + [180.2, 0.1] + ]; + source.setData(data); + t.equal(source.loaded(), false); + const onLoaded = (e) => { + if (e.sourceDataType === 'visibility') return; + source.off('data', onLoaded); + t.equal(map.getSource('trace').loaded(), true); + let beganRenderingContent = false; + map.on('render', () => { + const gl = map.painter.context.gl; + const pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4); + gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + const centerOffset = map.getContainer().clientWidth / 2 * (map.getContainer().clientHeight + 1) * 4; + const isCenterRendered = pixels[centerOffset] === 255; + if (!beganRenderingContent) { + beganRenderingContent = isCenterRendered; + if (beganRenderingContent) { + data.features[0].geometry.coordinates.push([180.1, 0.1]); + source.setData(data); + t.equal(map.getSource('trace').loaded(), false); + } + } else { + // Previous trace data should be rendered while loading update. + t.ok(isCenterRendered); + setTimeout(() => map.remove(), 0); // avoids re-triggering render after t.end. Don't remove while in render(). + t.end(); + } + }); + }; + source.on('data', onLoaded); + }); + }); + + t.test('mapbox-gl-js-internal#281', t => { + const data = { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [] + } + }] + }; + const map = createMap(t, { + style: { + version: 8, + center: [85, 85], + zoom: 2.1, + sources: { + mapbox: { + type: 'vector', + minzoom: 1, + maxzoom: 10, + tiles: ['http://example.com/{z}/{x}/{y}.png'] + }, + 'mapbox-dem': { + type: "raster-dem", + tiles: ['http://example.com/{z}/{x}/{y}.png'], + tileSize: 512, + maxzoom: 14 + } + }, + layers: [{ + "id": "background", + "type": "background", + "paint": { + "background-color": "black" + } + }] + } + }); + map.on('style.load', () => { + map.addSource('trace', {type: 'geojson', data}); + map.addLayer({ + 'id': 'trace', + 'type': 'line', + 'source': 'trace', + 'paint': { + 'line-color': 'yellow', + 'line-opacity': 0.75, + 'line-width': 5 + } + }); + const cache = map.style._getSourceCache('mapbox-dem'); + cache._loadTile = (tile, callback) => { + const pixels = new Uint8Array((512 + 2) * (512 + 2) * 4); + tile.dem = new DEMData(0, new RGBAImage({height: 512 + 2, width: 512 + 2}, pixels)); + tile.needsHillshadePrepare = true; + tile.needsDEMTextureUpload = true; + tile.state = 'loaded'; + callback(null); + }; + cache.used = cache._sourceLoaded = true; + map.setTerrain({"source": "mapbox-dem"}); + map.once('render', () => { + map._updateTerrain(); + map.painter.style.on('data', (event) => { + if (event.sourceCacheId === 'other:trace') { + t.test('Source other:trace is cleared from cache', t => { + t.ok(map.painter.terrain._tilesDirty.hasOwnProperty('other:trace')); + t.true(map.painter.terrain._tilesDirty['other:trace']['0']); + t.end(); + }); + t.end(); + } + }); + t.test('Source other:mapbox-dem is cleared from cache', t => { + t.ok(map.painter.terrain._tilesDirty.hasOwnProperty('other:mapbox-dem')); + t.true(map.painter.terrain._tilesDirty['other:mapbox-dem']['0']); + t.end(); + }); + const cache = map.style._getSourceCache('trace'); + cache.transform = map.painter.transform; + cache._addTile(new OverscaledTileID(0, 0, 0, 0, 0)); + cache.onAdd(); + cache.reload(); + cache.used = cache._sourceLoaded = true; + }); + }); + }); + + t.test('mapbox-gl-js-internal#349', t => { + const map = createMap(t, { + style: { + version: 8, + center: [85, 85], + zoom: 2.1, + sources: { + 'mapbox-dem': { + type: "raster-dem", + tiles: ['http://example.com/{z}/{x}/{y}.png'], + tileSize: 512, + maxzoom: 14 + } + }, + layers: [{ + "id": "background", + "type": "background", + "paint": { + "background-color": "black" + } + }] + } + }); + map.on('style.load', () => { + const customLayer = { + id: 'custom', + type: 'custom', + onAdd: () => {}, + render: () => {} + }; + map.addLayer(customLayer, 'background'); + map.setTerrain({"source": "mapbox-dem"}); + map.once('render', () => { + map.painter.terrain.drapeFirst = true; + t.false(map.painter.terrain._shouldDisableRenderCache()); + t.end(); + }); + }); + }); + + t.test('mapbox-gl-js-internal#32', t => { + const map = createMap(t, { + style: { + version: 8, + center: [85, 85], + zoom: 2.1, + sources: { + mapbox: { + type: 'vector', + minzoom: 1, + maxzoom: 10, + tiles: ['http://example.com/{z}/{x}/{y}.png'] + }, + 'mapbox-dem': { + type: "raster-dem", + tiles: ['http://example.com/{z}/{x}/{y}.png'], + tileSize: 512, + maxzoom: 14 + } + }, + layers: [{ + id: 'layerId1', + type: 'circle', + source: 'mapbox', + 'source-layer': 'sourceLayer' + }] + } + }); + + map.on('style.load', () => { + const cache = map.style._getSourceCache('mapbox-dem'); + cache._loadTile = (tile, callback) => { + const pixels = new Uint8Array((512 + 2) * (512 + 2) * 4); + tile.dem = new DEMData(0, new RGBAImage({height: 512 + 2, width: 512 + 2}, pixels)); + tile.needsHillshadePrepare = true; + tile.needsDEMTextureUpload = true; + tile.state = 'loaded'; + callback(null); + }; + cache.used = cache._sourceLoaded = true; + const tr = map.painter.transform.clone(); + map.setTerrain({"source": "mapbox-dem"}); + map.once('render', () => { + map._updateTerrain(); + t.test('center is not further constrained', t => { + t.deepEqual(tr.center, map.painter.transform.center); + t.end(); + }); + t.end(); + }); + }); + }); + + t.end(); +}); + +const spec = styleSpec.terrain; + +test('Terrain style', (t) => { + test('Terrain defaults', (t) => { + const terrain = new Terrain({}); + terrain.recalculate({zoom: 0, zoomHistory: {}}); + + t.deepEqual(terrain.properties.get('source'), spec.source.default); + t.deepEqual(terrain.properties.get('exaggeration'), spec.exaggeration.default); + + t.end(); + }); + + test('Exaggeration with stops function', (t) => { + const terrain = new Terrain({ + source: "dem", + exaggeration: { + stops: [[15, 0.2], [17, 0.8]] + } + }); + terrain.recalculate({zoom: 16, zoomHistory: {}}); + + t.deepEqual(terrain.properties.get('exaggeration'), 0.5); + t.end(); + }); + + t.end(); +}); + +function nearlyEquals(a, b, eps = 0.000000001) { + return Object.keys(a).length >= 2 && Object.keys(a).every(key => Math.abs(a[key] - b[key]) < eps); +} + +test('Raycast projection 2D/3D', t => { + const map = createMap(t, { + style: { + version: 8, + center: [0, 0], + zoom: 14, + sources: {}, + layers: [{ + "id": "background", + "type": "background", + "paint": { + "background-color": "black" + } + }], + pitch: 80 + } + }); + + map.once('style.load', () => { + setMockElevationTerrain(map, zeroDem, TILE_SIZE); + map.once('render', () => { + map._updateTerrain(); + + const transform = map.transform; + const cx = transform.width / 2; + const cy = transform.height / 2; + t.deepEqual(fixedLngLat(transform.pointLocation(new Point(cx, cy))), {lng: 0, lat: 0}); + t.deepEqual(fixedCoord(transform.pointCoordinate(new Point(cx, cy))), {x: 0.5, y: 0.5, z: 0}); + t.ok(nearlyEquals(fixedPoint(transform.locationPoint(new LngLat(0, 0))), {x: cx, y: cy})); + // Lower precision as we are raycasting using GPU depth render. + t.ok(nearlyEquals(fixedLngLat(transform.pointLocation3D(new Point(cx, cy))), {lng: 0, lat: 0}, 0.00006)); + t.ok(nearlyEquals(fixedCoord(transform.pointCoordinate3D(new Point(cx, cy))), {x: 0.5, y: 0.5, z: 0}, 0.000001)); + t.ok(nearlyEquals(fixedPoint(transform.locationPoint3D(new LngLat(0, 0))), {x: cx, y: cy})); + + // above horizon: + // raycast implementation returns null as there is no point at the top. + t.equal(transform.elevation.pointCoordinate(new Point(cx, 0)), null); + + t.ok(transform.elevation.pointCoordinate(new Point(transform.width, transform.height))); + t.deepEqual(transform.elevation.pointCoordinate(new Point(transform.width, transform.height))[2].toFixed(10), 0); + + const latLng3D = transform.pointLocation3D(new Point(cx, 0)); + const latLng2D = transform.pointLocation(new Point(cx, 0)); + // Project and get horizon line. + + const horizonPoint3D = transform.locationPoint3D(latLng3D); + const horizonPoint2D = transform.locationPoint(latLng2D); + t.ok(Math.abs(horizonPoint3D.x - cx) < 0.0000001); + t.ok(Math.abs(transform.horizonLineFromTop() - 48.68884861327036) < 0.000000001); + t.ok(Math.abs(horizonPoint3D.y - 48.68884861327036) < 0.000000001); + t.deepEqual(horizonPoint2D, horizonPoint3D); // Using the same code path for horizon. + + // disable terrain. + map.setTerrain(null); + map.once('render', () => { + t.notOk(map.painter.terrain); + const latLng = transform.pointLocation3D(new Point(cx, 0)); + t.deepEqual(latLng, latLng2D); + + t.deepEqual(fixedLngLat(transform.pointLocation(new Point(cx, cy))), {lng: 0, lat: 0}); + t.deepEqual(fixedCoord(transform.pointCoordinate(new Point(cx, cy))), {x: 0.5, y: 0.5, z: 0}); + t.ok(nearlyEquals(fixedPoint(transform.locationPoint(new LngLat(0, 0))), {x: cx, y: cy})); + // Higher precision as we are using the same as for 2D, given there is no terrain. + t.ok(nearlyEquals(fixedLngLat(transform.pointLocation3D(new Point(cx, cy))), {lng: 0, lat: 0})); + t.ok(nearlyEquals(fixedCoord(transform.pointCoordinate3D(new Point(cx, cy))), {x: 0.5, y: 0.5, z: 0})); + t.ok(nearlyEquals(fixedPoint(transform.locationPoint3D(new LngLat(0, 0))), {x: cx, y: cy})); + + t.end(); + }); + }); + }); +}); + +test('Vertex morphing', (t) => { + const createTile = (id) => { + const tile = new Tile(id); + tile.demTexture = {}; + tile.state = 'loaded'; + return tile; + }; + + t.test('Morph single tile', (t) => { + const morphing = new VertexMorphing(); + const coord = new OverscaledTileID(2, 0, 2, 1, 1); + const src = createTile(new OverscaledTileID(4, 0, 4, 8, 15)); + const dst = createTile(new OverscaledTileID(5, 0, 5, 8, 15)); + + morphing.newMorphing(coord.key, src, dst, 0, 250); + let values = morphing.getMorphValuesForProxy(coord.key); + t.ok(values); + + // Initial state + t.deepEqual(values.from, src); + t.deepEqual(values.to, dst); + t.equal(values.phase, 0); + + morphing.update(125); + + // Half way through + values = morphing.getMorphValuesForProxy(coord.key); + t.ok(values); + t.deepEqual(values.from, src); + t.deepEqual(values.to, dst); + t.equal(values.phase, 0.5); + + // Done + values = morphing.getMorphValuesForProxy(250); + t.notOk(values); + + t.end(); + }); + + t.test('Queue dem tiles', (t) => { + const morphing = new VertexMorphing(); + const coord = new OverscaledTileID(2, 0, 2, 1, 1); + const src = createTile(new OverscaledTileID(4, 0, 4, 8, 15)); + const dst = createTile(new OverscaledTileID(5, 0, 5, 8, 15)); + const intermediate = createTile(new OverscaledTileID(5, 0, 5, 9, 16)); + const queued = createTile(new OverscaledTileID(6, 0, 5, 9, 16)); + + // Intermediate steps are expected to be discarded and only destination tile matters for queued morphing + morphing.newMorphing(coord.key, src, dst, 0, 500); + morphing.newMorphing(coord.key, dst, intermediate, 0, 500); + morphing.newMorphing(coord.key, src, queued, 0, 500); + let values = morphing.getMorphValuesForProxy(coord.key); + t.ok(values); + + morphing.update(250); + values = morphing.getMorphValuesForProxy(coord.key); + t.ok(values); + t.deepEqual(values.from, src); + t.deepEqual(values.to, dst); + t.equal(values.phase, 0.5); + + // Expect to find the `queued` tile. `intermediate` should have been discarded + morphing.update(500); + values = morphing.getMorphValuesForProxy(coord.key); + t.ok(values); + t.deepEqual(values.from, dst); + t.deepEqual(values.to, queued); + t.equal(values.phase, 0.0); + + morphing.update(750); + values = morphing.getMorphValuesForProxy(coord.key); + t.ok(values); + t.deepEqual(values.from, dst); + t.deepEqual(values.to, queued); + t.equal(values.phase, 0.5); + + morphing.update(1000); + values = morphing.getMorphValuesForProxy(coord.key); + t.notOk(values); + + t.end(); + }); + + t.test('Queue dem tiles multiple times', (t) => { + const morphing = new VertexMorphing(); + const coord = new OverscaledTileID(2, 0, 2, 1, 1); + const src = createTile(new OverscaledTileID(4, 0, 4, 8, 15)); + const dst = createTile(new OverscaledTileID(5, 0, 5, 8, 15)); + const duplicate0 = createTile(new OverscaledTileID(5, 0, 5, 8, 15)); + const duplicate1 = createTile(new OverscaledTileID(5, 0, 5, 8, 15)); + const duplicate2 = createTile(new OverscaledTileID(5, 0, 5, 8, 15)); + + morphing.newMorphing(coord.key, src, dst, 0, 100); + morphing.newMorphing(coord.key, src, duplicate0, 0, 100); + morphing.newMorphing(coord.key, src, duplicate1, 0, 100); + morphing.newMorphing(coord.key, src, duplicate2, 0, 100); + let values = morphing.getMorphValuesForProxy(coord.key); + t.ok(values); + + morphing.update(75); + values = morphing.getMorphValuesForProxy(coord.key); + t.ok(values); + t.deepEqual(values.from, src); + t.deepEqual(values.to, dst); + t.equal(values.phase, 0.75); + + morphing.update(110); + values = morphing.getMorphValuesForProxy(coord.key); + t.notOk(values); + + t.end(); + }); + + t.test('Expired data', (t) => { + const morphing = new VertexMorphing(); + const coord = new OverscaledTileID(2, 0, 2, 1, 1); + const src = createTile(new OverscaledTileID(4, 0, 4, 8, 15)); + const dst = createTile(new OverscaledTileID(5, 0, 5, 8, 15)); + const queued = createTile(new OverscaledTileID(6, 0, 5, 9, 16)); + + morphing.newMorphing(coord.key, src, dst, 0, 1000); + morphing.newMorphing(coord.key, dst, queued, 0, 1000); + + morphing.update(200); + let values = morphing.getMorphValuesForProxy(coord.key); + t.ok(values); + t.deepEqual(values.from, src); + t.deepEqual(values.to, dst); + t.equal(values.phase, 0.2); + + // source tile is expired + src.state = 'unloaded'; + morphing.update(300); + values = morphing.getMorphValuesForProxy(coord.key); + t.ok(values); + t.deepEqual(values.from, dst); + t.deepEqual(values.to, queued); + t.equal(values.phase, 0.0); + + const newQueued = createTile(new OverscaledTileID(7, 0, 7, 9, 16)); + morphing.newMorphing(coord.key, queued, newQueued, 1000); + + // The target tile is expired. The morphing operation should be cancelled + queued.state = 'unloaded'; + morphing.update(500); + values = morphing.getMorphValuesForProxy(coord.key); + t.notOk(values); + + t.end(); + }); + + t.end(); +}); + +test('Marker interaction and raycast', (t) => { + const map = createMap(t, { + style: extend(createStyle(), { + layers: [{ + "id": "background", + "type": "background", + "paint": { + "background-color": "black" + } + }] + }) + }); + map.setPitch(85); + map.setZoom(13); + + const tr = map.transform; + const marker = new Marker({draggable: true}) + .setLngLat(tr.center) + .addTo(map) + .setPopup(new Popup().setHTML(`a popup content`)) + .togglePopup(); + t.equal(map.project(marker.getLngLat()).y, tr.height / 2); + t.equal(tr.locationPoint3D(marker.getLngLat()).y, tr.height / 2); + t.deepEqual(marker.getPopup()._pos, new Point(tr.width / 2, tr.height / 2)); + + map.once('style.load', () => { + map.addSource('mapbox-dem', { + "type": "raster-dem", + "tiles": ['http://example.com/{z}/{x}/{y}.png'], + "tileSize": TILE_SIZE, + "maxzoom": 14 + }); + const cache = map.style._getSourceCache('mapbox-dem'); + cache.used = cache._sourceLoaded = true; + cache._loadTile = (tile, callback) => { + // Elevate tiles above center. + tile.dem = createConstElevationDEM(300 * (tr.zoom - tile.tileID.overscaledZ), TILE_SIZE); + tile.needsHillshadePrepare = true; + tile.needsDEMTextureUpload = true; + tile.state = 'loaded'; + callback(null); + }; + map.setTerrain({"source": "mapbox-dem"}); + map.once('render', () => { + map._updateTerrain(); + // expect no changes at center + t.equal(map.project(marker.getLngLat()).y, tr.height / 2); + t.equal(tr.locationPoint3D(marker.getLngLat()).y, tr.height / 2); + t.deepEqual(marker.getPopup()._pos, new Point(tr.width / 2, tr.height / 2)); + + const terrainTopLngLat = tr.pointLocation3D(new Point(tr.width / 2, 0)); // gets clamped at the top of terrain + const terrainTop = tr.locationPoint3D(terrainTopLngLat); + // With a bit of tweaking (given that const terrain planes are used), terrain is above horizon line. + t.ok(terrainTop.y < tr.horizonLineFromTop() - 3); + + t.test('Drag above clamps at horizon', (t) => { + // Offset marker down, 2 pixels under terrain top above horizon. + const startPos = new Point(0, 2)._add(terrainTop); + marker.setLngLat(tr.pointLocation3D(startPos)); + t.ok(Math.abs(tr.locationPoint3D(marker.getLngLat()).y - startPos.y) < 0.000001); + const el = marker.getElement(); + + simulate.mousedown(el); + simulate.mousemove(el, {clientX: 0, clientY: -40}); + simulate.mouseup(el); + + const endPos = tr.locationPoint3D(marker.getLngLat()); + t.true(Math.abs(endPos.x - startPos.x) < 0.00000000001); + t.equal(endPos.y, terrainTop.y); + t.deepEqual(marker.getPopup()._pos, endPos); + + t.end(); + }); + + t.test('Drag below / behind camera', (t) => { + const startPos = new Point(terrainTop.x, tr.height - 20); + marker.setLngLat(tr.pointLocation3D(startPos)); + t.ok(Math.abs(tr.locationPoint3D(marker.getLngLat()).y - startPos.y) < 0.000001); + const el = marker.getElement(); + + simulate.mousedown(el); + simulate.mousemove(el, {clientX: 0, clientY: 40}); + simulate.mouseup(el); + + const endPos = tr.locationPoint3D(marker.getLngLat()); + t.equal(Math.round(endPos.y), Math.round(startPos.y) + 40); + t.deepEqual(marker.getPopup()._pos, endPos); + t.end(); + }); + + t.test('Occluded', (t) => { + marker._occlusionTimer = null; + marker.setLngLat(terrainTopLngLat); + const bottomLngLat = tr.pointLocation3D(new Point(terrainTop.x, tr.height)); + // Raycast returns distance to closer point evaluates to occluded marker. + t.stub(tr, 'pointLocation3D').returns(bottomLngLat); + setTimeout(() => { + t.ok(marker.getElement().classList.contains('mapboxgl-marker-occluded')); + t.end(); + }, 100); + }); + + map.remove(); + t.end(); + }); + }); +}); + +test('terrain getBounds', (t) => { + const map = createMap(t, { + style: extend(createStyle(), { + layers: [{ + "id": "background", + "type": "background", + "paint": { + "background-color": "black" + } + }] + }) + }); + map.setPitch(85); + map.setZoom(13); + + const tr = map.transform; + map.once('style.load', () => { + map.addSource('mapbox-dem', { + "type": "raster-dem", + "tiles": ['http://example.com/{z}/{x}/{y}.png'], + "tileSize": TILE_SIZE, + "maxzoom": 14 + }); + const cache = map.style._getSourceCache('mapbox-dem'); + cache.used = cache._sourceLoaded = true; + cache._loadTile = (tile, callback) => { + // Elevate tiles above center. + tile.dem = createConstElevationDEM(300 * (tr.zoom - tile.tileID.overscaledZ), TILE_SIZE); + tile.needsHillshadePrepare = true; + tile.needsDEMTextureUpload = true; + tile.state = 'loaded'; + callback(null); + }; + + t.deepEqual(map.getBounds().getCenter().lng.toFixed(10), 0, 'horizon, no terrain getBounds'); + t.deepEqual(map.getBounds().getCenter().lat.toFixed(10), 0.4076172064, 'horizon, no terrain getBounds'); + + map.setTerrain({"source": "mapbox-dem"}); + map.once('render', () => { + map._updateTerrain(); + + // As tiles above center are elevated, center of bounds is closer to camera. + t.deepEqual(map.getBounds().getCenter().lng.toFixed(10), 0, 'horizon terrain getBounds'); + t.deepEqual(map.getBounds().getCenter().lat.toFixed(10), -1.2344797596, 'horizon terrain getBounds'); + + map.setPitch(0); + map.once('render', () => { + t.deepEqual(map.getBounds().getCenter().lng.toFixed(10), 0, 'terrain 0 getBounds'); + t.deepEqual(map.getBounds().getCenter().lat.toFixed(10), 0, 'terrain 0 getBounds'); + t.end(); + }); + }); + }); +}); diff --git a/test/unit/ui/camera.test.js b/test/unit/ui/camera.test.js index 1286f7c138a..3d24c2373f9 100644 --- a/test/unit/ui/camera.test.js +++ b/test/unit/ui/camera.test.js @@ -1,10 +1,14 @@ import {test} from '../../util/test'; import Camera from '../../../src/ui/camera'; +import {FreeCameraOptions} from '../../../src/ui/free_camera'; import Transform from '../../../src/geo/transform'; import TaskQueue from '../../../src/util/task_queue'; import browser from '../../../src/util/browser'; -import {fixedLngLat, fixedNum} from '../../util/fixed'; +import {fixedLngLat, fixedNum, fixedVec3} from '../../util/fixed'; import {equalWithPrecision} from '../../util'; +import MercatorCoordinate from '../../../src/geo/mercator_coordinate'; +import LngLat from '../../../src/geo/lng_lat'; +import {vec3, quat} from 'gl-matrix'; test('camera', (t) => { function attachSimulateFrame(camera) { @@ -18,7 +22,7 @@ test('camera', (t) => { function createCamera(options) { options = options || {}; - const transform = new Transform(0, 20, 0, 60, options.renderWorldCopies); + const transform = new Transform(0, 20, 0, 85, options.renderWorldCopies); transform.resize(512, 512); const camera = attachSimulateFrame(new Camera(transform, {})) @@ -1964,6 +1968,38 @@ test('camera', (t) => { t.deepEqual(fixedLngLat(camera.getCenter(), 4), {lng: -45, lat: 40.9799}, 'centers, rotates 225 degrees, and zooms based on screen coordinates'); t.equal(fixedNum(camera.getZoom(), 3), 1.5); t.equal(camera.getBearing(), -135); + t.equal(camera.getPitch(), 0); + t.end(); + }); + + t.test('bearing 225, pitch 30', (t) => { + const pitch = 30; + const camera = createCamera({pitch}); + const p0 = [200, 500]; + const p1 = [210, 510]; + const bearing = 225; + + camera.fitScreenCoordinates(p0, p1, bearing, {duration:0}); + t.deepEqual(fixedLngLat(camera.getCenter(), 4), {lng: -30.215, lat: -19.8767}, 'centers, rotates 225 degrees, pitch 30 degrees, and zooms based on screen coordinates'); + t.equal(fixedNum(camera.getZoom(), 3), 0.173); + t.equal(camera.getBearing(), -16.844931165335765); + t.end(); + }); + + t.test('bearing 225, pitch 80, over horizon', (t) => { + const pitch = 80; + const camera = createCamera({pitch}); + const p0 = [128, 0]; + const p1 = [256, 10]; + const bearing = 225; + + const zoom = camera.getZoom(); + const center = camera.getCenter(); + camera.fitScreenCoordinates(p0, p1, bearing, {duration:0}); + t.deepEqual(fixedLngLat(camera.getCenter(), 4), center, 'centers, rotates 225 degrees, pitch 80 degrees, and zooms based on screen coordinates'); + t.equal(fixedNum(camera.getZoom(), 3), zoom); + t.equal(camera.getBearing(), 0); + t.equal(camera.getPitch(), pitch); t.end(); }); @@ -1997,5 +2033,249 @@ test('camera', (t) => { t.end(); }); + test('FreeCameraOptions', (t) => { + + const camera = createCamera(); + + const rotatedFrame = (quaternion) => { + return { + up: vec3.transformQuat([], [0, -1, 0], quaternion), + forward: vec3.transformQuat([], [0, 0, -1], quaternion), + right: vec3.transformQuat([], [1, 0, 0], quaternion) + }; + }; + + test('lookAtPoint', (t) => { + const options = new FreeCameraOptions(); + const cosPi4 = fixedNum(1.0 / Math.sqrt(2.0)); + let frame = null; + + // Pitch: 45, bearing: 0 + options.position = new MercatorCoordinate(0.5, 0.5, 0.5); + options.lookAtPoint(new LngLat(0.0, 85.051128779806604)); + t.true(options.orientation); + frame = rotatedFrame(options.orientation); + + t.deepEqual(fixedVec3(frame.right), [1, 0, 0]); + t.deepEqual(fixedVec3(frame.up), [0, -cosPi4, cosPi4]); + t.deepEqual(fixedVec3(frame.forward), [0, -cosPi4, -cosPi4]); + + // Look directly to east + options.position = new MercatorCoordinate(0.5, 0.5, 0.0); + options.lookAtPoint(new LngLat(30, 0)); + t.true(options.orientation); + frame = rotatedFrame(options.orientation); + + t.deepEqual(fixedVec3(frame.right), [0, 1, 0]); + t.deepEqual(fixedVec3(frame.up), [0, 0, 1]); + t.deepEqual(fixedVec3(frame.forward), [1, 0, 0]); + + // Pitch: 0, bearing: 0 + options.position = MercatorCoordinate.fromLngLat(new LngLat(24.9384, 60.1699), 100.0); + options.lookAtPoint(new LngLat(24.9384, 60.1699), [0.0, -1.0, 0.0]); + t.true(options.orientation); + frame = rotatedFrame(options.orientation); + + t.deepEqual(fixedVec3(frame.right), [1.0, 0.0, 0.0]); + t.deepEqual(fixedVec3(frame.up), [0.0, -1.0, 0.0]); + t.deepEqual(fixedVec3(frame.forward), [0.0, 0.0, -1.0]); + + // Pitch: 0, bearing: 45 + options.position = MercatorCoordinate.fromLngLat(new LngLat(24.9384, 60.1699), 100.0); + options.lookAtPoint(new LngLat(24.9384, 60.1699), [-1.0, -1.0, 0.0]); + t.true(options.orientation); + frame = rotatedFrame(options.orientation); + + t.deepEqual(fixedVec3(frame.right), [cosPi4, -cosPi4, 0.0]); + t.deepEqual(fixedVec3(frame.up), [-cosPi4, -cosPi4, 0.0]); + t.deepEqual(fixedVec3(frame.forward), [0.0, 0.0, -1.0]); + + // Looking south, up vector almost same as forward vector + options.position = MercatorCoordinate.fromLngLat(new LngLat(122.4194, 37.7749)); + options.lookAtPoint(new LngLat(122.4194, 37.5), [0.0, 1.0, 0.00001]); + t.true(options.orientation); + frame = rotatedFrame(options.orientation); + + t.deepEqual(fixedVec3(frame.right), [-1.0, 0.0, 0.0]); + t.deepEqual(fixedVec3(frame.up), [0.0, 0.0, 1.0]); + t.deepEqual(fixedVec3(frame.forward), [0.0, 1.0, 0.0]); + + // Orientation with roll-component + options.position = MercatorCoordinate.fromLngLat(new LngLat(151.2093, -33.8688)); + options.lookAtPoint(new LngLat(160.0, -33.8688), [0.0, -1.0, 0.1]); + t.true(options.orientation); + frame = rotatedFrame(options.orientation); + + t.deepEqual(fixedVec3(frame.right), [0.0, 1.0, 0.0]); + t.deepEqual(fixedVec3(frame.up), [0.0, 0.0, 1.0]); + t.deepEqual(fixedVec3(frame.forward), [1.0, 0.0, 0.0]); + + // Up vector pointing downwards + options.position = new MercatorCoordinate(0.5, 0.5, 0.5); + options.lookAtPoint(new LngLat(0.0, 85.051128779806604), [0.0, 0.0, -0.5]); + t.true(options.orientation); + frame = rotatedFrame(options.orientation); + + t.deepEqual(fixedVec3(frame.right), [1.0, 0.0, 0.0]); + t.deepEqual(fixedVec3(frame.up), [0.0, -cosPi4, cosPi4]); + t.deepEqual(fixedVec3(frame.forward), [0.0, -cosPi4, -cosPi4]); + + test('invalid input', (t) => { + const options = new FreeCameraOptions(); + + // Position not set + options.orientation = [0, 0, 0, 0]; + options.lookAtPoint(new LngLat(0, 0)); + t.false(options.orientation); + + // Target same as position + options.orientation = [0, 0, 0, 0]; + options.position = new MercatorCoordinate(0.5, 0.5, 0.0); + options.lookAtPoint(new LngLat(0, 0)); + t.false(options.orientation); + + // Camera looking directly down without an explicit up vector + options.orientation = [0, 0, 0, 0]; + options.position = new MercatorCoordinate(0.5, 0.5, 0.5); + options.lookAtPoint(new LngLat(0, 0)); + t.false(options.orientation); + + // Zero length up vector + options.orientation = [0, 0, 0, 0]; + options.lookAtPoint(new LngLat(0, 0), [0, 0, 0]); + t.false(options.orientation); + + // Up vector same as direction + options.orientation = [0, 0, 0, 0]; + options.lookAtPoint(new LngLat(0, 0), [0, 0, -1]); + t.false(options.orientation); + + t.end(); + }); + + t.end(); + }); + + test('setPitchBearing', (t) => { + const options = new FreeCameraOptions(); + const cos60 = fixedNum(Math.cos(60 * Math.PI / 180.0)); + const sin60 = fixedNum(Math.sin(60 * Math.PI / 180.0)); + let frame = null; + + options.setPitchBearing(0, 0); + t.true(options.orientation); + frame = rotatedFrame(options.orientation); + t.deepEqual(fixedVec3(frame.right), [1, 0, 0]); + t.deepEqual(fixedVec3(frame.up), [0, -1, 0]); + t.deepEqual(fixedVec3(frame.forward), [0, 0, -1]); + + options.setPitchBearing(0, 180); + t.true(options.orientation); + frame = rotatedFrame(options.orientation); + t.deepEqual(fixedVec3(frame.right), [-1, 0, 0]); + t.deepEqual(fixedVec3(frame.up), [0, 1, 0]); + t.deepEqual(fixedVec3(frame.forward), [0, 0, -1]); + + options.setPitchBearing(60, 0); + t.true(options.orientation); + frame = rotatedFrame(options.orientation); + t.deepEqual(fixedVec3(frame.right), [1, 0, 0]); + t.deepEqual(fixedVec3(frame.up), [0, -cos60, sin60]); + t.deepEqual(fixedVec3(frame.forward), [0, -sin60, -cos60]); + + options.setPitchBearing(60, -450); + t.true(options.orientation); + frame = rotatedFrame(options.orientation); + t.deepEqual(fixedVec3(frame.right), [0, -1, 0]); + t.deepEqual(fixedVec3(frame.up), [-cos60, 0, sin60]); + t.deepEqual(fixedVec3(frame.forward), [-sin60, 0, -cos60]); + + t.end(); + }); + + t.test('emits move events', (t) => { + let started, moved, ended; + const eventData = {data: 'ok'}; + + camera + .on('movestart', (d) => { started = d.data; }) + .on('move', (d) => { moved = d.data; }) + .on('moveend', (d) => { ended = d.data; }); + + const options = camera.getFreeCameraOptions(); + options.position.x = 0.2; + options.position.y = 0.2; + camera.setFreeCameraOptions(options, eventData); + + t.equal(started, 'ok'); + t.equal(moved, 'ok'); + t.equal(ended, 'ok'); + t.end(); + }); + + t.test('changing orientation emits bearing events', (t) => { + let rotatestarted, rotated, rotateended, pitch; + const eventData = {data: 'ok'}; + + camera + .on('rotatestart', (d) => { rotatestarted = d.data; }) + .on('rotate', (d) => { rotated = d.data; }) + .on('rotateend', (d) => { rotateended = d.data; }) + .on('pitch', (d) => { pitch = d.data; }); + + const options = camera.getFreeCameraOptions(); + quat.rotateZ(options.orientation, options.orientation, 0.1); + camera.setFreeCameraOptions(options, eventData); + + t.equal(rotatestarted, 'ok'); + t.equal(rotated, 'ok'); + t.equal(rotateended, 'ok'); + t.equal(pitch, undefined); + t.end(); + }); + + t.test('changing orientation emits pitch events', (t) => { + let pitchstarted, pitch, pitchended, rotated; + const eventData = {data: 'ok'}; + + camera + .on('pitchstart', (d) => { pitchstarted = d.data; }) + .on('pitch', (d) => { pitch = d.data; }) + .on('pitchend', (d) => { pitchended = d.data; }) + .on('rotate', (d) => { rotated = d.data; }); + + const options = camera.getFreeCameraOptions(); + quat.rotateX(options.orientation, options.orientation, -0.1); + camera.setFreeCameraOptions(options, eventData); + + t.equal(pitchstarted, 'ok'); + t.equal(pitch, 'ok'); + t.equal(pitchended, 'ok'); + t.equal(rotated, undefined); + t.end(); + }); + + t.test('changing altitude emits zoom events', (t) => { + let zoomstarted, zoom, zoomended; + const eventData = {data: 'ok'}; + + camera + .on('zoomstart', (d) => { zoomstarted = d.data; }) + .on('zoom', (d) => { zoom = d.data; }) + .on('zoomend', (d) => { zoomended = d.data; }); + + const options = camera.getFreeCameraOptions(); + options.position.z *= 0.8; + camera.setFreeCameraOptions(options, eventData); + + t.equal(zoomstarted, 'ok'); + t.equal(zoom, 'ok'); + t.equal(zoomended, 'ok'); + t.end(); + }); + + t.end(); + }); + t.end(); }); diff --git a/test/unit/ui/handler/box_zoom.test.js b/test/unit/ui/handler/box_zoom.test.js index 3b44a40fb29..4f1671c9b71 100644 --- a/test/unit/ui/handler/box_zoom.test.js +++ b/test/unit/ui/handler/box_zoom.test.js @@ -6,6 +6,7 @@ import simulate from '../../../util/simulate_interaction'; function createMap(t, clickTolerance) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map({container: DOM.create('div', '', window.document.body), clickTolerance}); } diff --git a/test/unit/ui/handler/dblclick_zoom.test.js b/test/unit/ui/handler/dblclick_zoom.test.js index 737c7b86cf6..d89bc85163f 100644 --- a/test/unit/ui/handler/dblclick_zoom.test.js +++ b/test/unit/ui/handler/dblclick_zoom.test.js @@ -6,6 +6,7 @@ import simulate from '../../../util/simulate_interaction'; function createMap(t) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map({container: DOM.create('div', '', window.document.body)}); } diff --git a/test/unit/ui/handler/drag_pan.test.js b/test/unit/ui/handler/drag_pan.test.js index 726900e9e82..16c5aed89ef 100644 --- a/test/unit/ui/handler/drag_pan.test.js +++ b/test/unit/ui/handler/drag_pan.test.js @@ -6,6 +6,7 @@ import simulate from '../../../util/simulate_interaction'; function createMap(t, clickTolerance, dragPan) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map({ container: DOM.create('div', '', window.document.body), clickTolerance: clickTolerance || 0, diff --git a/test/unit/ui/handler/drag_rotate.test.js b/test/unit/ui/handler/drag_rotate.test.js index f5dd762e084..f5aa987089a 100644 --- a/test/unit/ui/handler/drag_rotate.test.js +++ b/test/unit/ui/handler/drag_rotate.test.js @@ -8,6 +8,7 @@ import browser from '../../../../src/util/browser'; function createMap(t, options) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map(extend({container: DOM.create('div', '', window.document.body)}, options)); } diff --git a/test/unit/ui/handler/keyboard.test.js b/test/unit/ui/handler/keyboard.test.js index 6a59f00d74c..edb35372e93 100644 --- a/test/unit/ui/handler/keyboard.test.js +++ b/test/unit/ui/handler/keyboard.test.js @@ -7,6 +7,7 @@ import {extend} from '../../../../src/util/util'; function createMap(t, options) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map(extend({ container: DOM.create('div', '', window.document.body), }, options)); diff --git a/test/unit/ui/handler/map_event.test.js b/test/unit/ui/handler/map_event.test.js index d99a496358b..d10bc82d538 100644 --- a/test/unit/ui/handler/map_event.test.js +++ b/test/unit/ui/handler/map_event.test.js @@ -6,6 +6,7 @@ import simulate from '../../../util/simulate_interaction'; function createMap(t) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map({interactive: false, container: DOM.create('div', '', window.document.body)}); } diff --git a/test/unit/ui/handler/mouse_rotate.test.js b/test/unit/ui/handler/mouse_rotate.test.js index 0a587b7a0dd..ba8c1f5a0ad 100644 --- a/test/unit/ui/handler/mouse_rotate.test.js +++ b/test/unit/ui/handler/mouse_rotate.test.js @@ -8,6 +8,7 @@ import browser from '../../../../src/util/browser'; function createMap(t, options) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map(extend({container: DOM.create('div', '', window.document.body)}, options)); } diff --git a/test/unit/ui/handler/scroll_zoom.test.js b/test/unit/ui/handler/scroll_zoom.test.js index 5bde7c568e5..8ca46b5140e 100644 --- a/test/unit/ui/handler/scroll_zoom.test.js +++ b/test/unit/ui/handler/scroll_zoom.test.js @@ -6,9 +6,13 @@ import DOM from '../../../../src/util/dom'; import simulate from '../../../util/simulate_interaction'; import {equalWithPrecision} from '../../../util'; import sinon from 'sinon'; +import {createConstElevationDEM, setMockElevationTerrain} from '../../../util/dem_mock'; +import {fixedNum} from '../../../util/fixed'; +import MercatorCoordinate from '../../../../src/geo/mercator_coordinate'; function createMap(t) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map({ container: DOM.create('div', '', window.document.body), style: { @@ -75,7 +79,6 @@ test('ScrollZoomHandler', (t) => { const end = now + 500; let lastWheelEvent = now; - // simulate the above sequence of wheel events, with render frames // interspersed every 20ms while (now++ < end) { @@ -88,13 +91,132 @@ test('ScrollZoomHandler', (t) => { map._renderTaskQueue.run(); } } - equalWithPrecision(t, map.getZoom() - startZoom, 1.944, 0.001); map.remove(); t.end(); }); + t.test('Terrain', (t) => { + const tileSize = 128; + const deltas = [100, 10, 123, 45, 1, -10, -100]; + const expected = [10, 9.41454, 8.78447, 8.49608, 8.48888, 8.55921, 9.10727]; + + const zeroElevationDem = createConstElevationDEM(0, tileSize); + const highElevationDem = createConstElevationDEM(1500, tileSize); + + const simulateWheel = (_map) => { + const actual = []; + for (const delta of deltas) { + simulate.wheel(_map.getCanvas(), {type: 'wheel', deltaY: delta}); + _map._renderTaskQueue.run(); + actual.push(_map.getZoom()); + } + return actual; + }; + + t.test('Scroll zoom on zero elevation', (t) => { + const map = createMap(t); + map.on('style.load', () => { + map.transform.zoom = 10; + setMockElevationTerrain(map, zeroElevationDem, tileSize); + map.once('render', () => { + t.equal(map.painter.terrain.getAtPoint(new MercatorCoordinate(0.5, 0.5)), 0); + t.deepEqual(simulateWheel(map).map(v => fixedNum(v, 5)), expected); + map.remove(); + t.end(); + }); + }); + }); + + t.test('Scroll zoom on high elevation', (t) => { + const map = createMap(t); + map.on('style.load', () => { + map.transform.zoom = 10; + setMockElevationTerrain(map, highElevationDem, tileSize); + map.once('render', () => { + t.equal(map.painter.terrain.getAtPoint(new MercatorCoordinate(0.5, 0.5)), 1500); + t.deepEqual(simulateWheel(map).map(v => fixedNum(v, 5)), expected); + map.remove(); + t.end(); + }); + }); + }); + + t.test('No movement when zoom is constrained', (t) => { + const map = createMap(t); + map.on('style.load', () => { + map.transform.zoom = 0; + setMockElevationTerrain(map, zeroElevationDem, tileSize); + map.once('render', () => { + // zoom out to reach min zoom. + for (let i = 0; i < 2; i++) { + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: 100}); + map._renderTaskQueue.run(); + } + const tr = map.transform.clone(); + // zooming out further should keep the map center stabile. + for (let i = 0; i < 5; i++) { + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: 0.0001}); + map._renderTaskQueue.run(); + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: 100}); + map._renderTaskQueue.run(); + } + t.equal(tr.center.lng.toFixed(10), map.transform.center.lng.toFixed(10)); + t.equal(tr.center.lat.toFixed(10), map.transform.center.lat.toFixed(10)); + map.remove(); + t.end(); + }); + }); + }); + + t.test('Consistent deltas if elevation changes', (t) => { + const map = createMap(t); + map.on('style.load', () => { + map.transform.zoom = 10; + + // Setup the map with high elevation dem data + setMockElevationTerrain(map, highElevationDem, tileSize); + + map.once('render', () => { + t.equal(map.painter.terrain.getAtPoint(new MercatorCoordinate(0.5, 0.5)), 1500); + + // Start the scroll gesture with high elevation data by performing few scroll events + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: simulate.magicWheelZoomDelta}); + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: 200}); + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: 200}); + map._renderTaskQueue.run(); + + // Simulate the switching of DEM tiles (due to LOD perhaps) to low elevation dems + const tiles = map.style._getSourceCache('mapbox-dem')._tiles; + for (const tile in tiles) + tiles[tile].dem = zeroElevationDem; + + // Let easing function to modify the zoom + let prevZoom = map.getZoom(); + let thisZoom; + const deltas = []; + + while (prevZoom !== thisZoom) { + now++; + prevZoom = map.getZoom(); + map._renderTaskQueue.run(); + thisZoom = map.getZoom(); + + deltas.push(thisZoom - prevZoom); + } + + // Direction of the zoom should not change, ie. sign of deltas is always negative + t.false(deltas.find(d => d > 0)); + + map.remove(); + t.end(); + }); + }); + }); + t.end(); + }); + t.test('Gracefully ignores wheel events with deltaY: 0', (t) => { const map = createMap(t); map._renderTaskQueue.run(); diff --git a/test/unit/ui/handler/touch_zoom_rotate.test.js b/test/unit/ui/handler/touch_zoom_rotate.test.js index 5028aed6c0c..d7c1a6f988f 100644 --- a/test/unit/ui/handler/touch_zoom_rotate.test.js +++ b/test/unit/ui/handler/touch_zoom_rotate.test.js @@ -7,6 +7,7 @@ import simulate from '../../../util/simulate_interaction'; function createMap(t) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map({container: DOM.create('div', '', window.document.body)}); } @@ -283,4 +284,3 @@ test('TouchZoomRotateHandler does not zoom when touching an element not on the m map.remove(); t.end(); }); - diff --git a/test/unit/ui/map.test.js b/test/unit/ui/map.test.js index 159597fcb52..88563c13b7b 100755 --- a/test/unit/ui/map.test.js +++ b/test/unit/ui/map.test.js @@ -76,15 +76,15 @@ test('Map', (t) => { t.test('initial bounds options in constructor options', (t) => { const bounds = [[-133, 16], [-68, 50]]; - const map = (fitBoundsOptions, skipCSSStub) => { + const map = (fitBoundsOptions, skipCSSStub, skipAuthenticateStub) => { const container = window.document.createElement('div'); Object.defineProperty(container, 'offsetWidth', {value: 512}); Object.defineProperty(container, 'offsetHeight', {value: 512}); - return createMap(t, {skipCSSStub, container, bounds, fitBoundsOptions}); + return createMap(t, {skipCSSStub, skipAuthenticateStub, container, bounds, fitBoundsOptions}); }; - const unpadded = map(undefined, false); - const padded = map({padding: 100}, true); + const unpadded = map(undefined, false, true); + const padded = map({padding: 100}, true, true); t.ok(unpadded.getZoom() > padded.getZoom()); @@ -132,6 +132,7 @@ test('Map', (t) => { t.test('emits load event after a style is set', (t) => { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); const map = new Map({container: window.document.createElement('div')}); map.on('load', fail); @@ -228,6 +229,7 @@ test('Map', (t) => { t.test('style transform overrides unmodified map transform', (t) => { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); const map = new Map({container: window.document.createElement('div')}); map.transform.lngRange = [-120, 140]; map.transform.latRange = [-60, 80]; @@ -246,6 +248,7 @@ test('Map', (t) => { t.test('style transform does not override map transform modified via options', (t) => { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); const map = new Map({container: window.document.createElement('div'), zoom: 10, center: [-77.0186, 38.8888]}); t.notOk(map.transform.unmodified, 'map transform is modified by options'); map.setStyle(createStyle()); @@ -260,6 +263,7 @@ test('Map', (t) => { t.test('style transform does not override map transform modified via setters', (t) => { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); const map = new Map({container: window.document.createElement('div')}); t.ok(map.transform.unmodified); map.setZoom(10); @@ -285,6 +289,31 @@ test('Map', (t) => { t.end(); }); + t.test('updating terrain triggers style diffing using setTerrain operation', (t) => { + const style = createStyle(); + style['sources']["mapbox-dem"] = { + "type": "raster-dem", + "tiles": ['http://example.com/{z}/{x}/{y}.png'], + "tileSize": 256, + "maxzoom": 14 + }; + style['terrain'] = { + "source": "mapbox-dem" + }; + const map = createMap(t, {style}); + const initStyleObj = map.style; + t.spy(initStyleObj, 'setTerrain'); + t.spy(initStyleObj, 'setState'); + map.on('style.load', () => { + map.setStyle(createStyle()); + t.equal(initStyleObj, map.style); + t.equal(initStyleObj.setState.callCount, 1); + t.equal(initStyleObj.setTerrain.callCount, 1); + t.ok(map.style.terrain == null); + t.end(); + }); + }); + t.end(); }); @@ -324,9 +353,9 @@ test('Map', (t) => { map.on('load', () => { const fakeTileId = new OverscaledTileID(0, 0, 0, 0, 0); map.addSource('geojson', createStyleSource()); - map.style.sourceCaches.geojson._tiles[fakeTileId.key] = new Tile(fakeTileId); + map.style._getSourceCache('geojson')._tiles[fakeTileId.key] = new Tile(fakeTileId); t.equal(map.areTilesLoaded(), false, 'returns false if tiles are loading'); - map.style.sourceCaches.geojson._tiles[fakeTileId.key].state = 'loaded'; + map.style._getSourceCache('geojson')._tiles[fakeTileId.key].state = 'loaded'; t.equal(map.areTilesLoaded(), true, 'returns true if tiles are loaded'); t.end(); }); @@ -358,6 +387,26 @@ test('Map', (t) => { }); }); + t.test('returns the style with added terrain', (t) => { + const style = createStyle(); + const map = createMap(t, {style}); + + map.on('load', () => { + const terrain = {source: "terrain-source-id", exaggeration: 2}; + map.addSource('terrain-source-id', { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrain.png" + ] + }); + map.setTerrain(terrain); + t.deepEqual(map.getStyle(), extend(createStyle(), { + terrain, 'sources': map.getStyle().sources + })); + t.end(); + }); + }); + t.test('fires an error on checking if non-existant source is loaded', (t) => { const style = createStyle(); const map = createMap(t, {style}); @@ -606,7 +655,7 @@ test('Map', (t) => { [ 70.31249999999977, 57.32652122521695 ] ])); t.test('rotated bounds', (t) => { - const map = createMap(t, {zoom: 1, bearing: 45, skipCSSStub: true}); + const map = createMap(t, {zoom: 1, bearing: 45, skipCSSStub: true, skipAuthenticateStub: true}); t.deepEqual( toFixed([[-49.718445552178764, -44.44541580601936], [49.7184455522, 44.445415806019355]]), toFixed(map.getBounds().toArray()) @@ -887,7 +936,7 @@ test('Map', (t) => { t.test('#getMaxPitch', (t) => { const map = createMap(t, {pitch: 0}); - t.equal(map.getMaxPitch(), 60, 'returns default value'); + t.equal(map.getMaxPitch(), 85, 'returns default value'); map.setMaxPitch(10); t.equal(map.getMaxPitch(), 10, 'returns custom value'); t.end(); @@ -920,7 +969,7 @@ test('Map', (t) => { t.test('throw on maxPitch greater than valid maxPitch at init', (t) => { t.throws(() => { createMap(t, {maxPitch: 90}); - }, new Error(`maxPitch must be less than or equal to 60`)); + }, new Error(`maxPitch must be less than or equal to 85`)); t.end(); }); @@ -1096,7 +1145,7 @@ test('Map', (t) => { const output = map.queryRenderedFeatures(map.project(new LngLat(0, 0))); const args = map.style.queryRenderedFeatures.getCall(0).args; - t.deepEqual(args[0], [{x: 100, y: 100}]); // query geometry + t.deepEqual(args[0], {x: 100, y: 100}); // query geometry t.deepEqual(args[1], {availableImages: []}); // params t.deepEqual(args[2], map.transform); // transform t.deepEqual(output, []); @@ -1144,7 +1193,7 @@ test('Map', (t) => { map.queryRenderedFeatures(map.project(new LngLat(360, 0))); - t.deepEqual(map.style.queryRenderedFeatures.getCall(0).args[0], [{x: 612, y: 100}]); + t.deepEqual(map.style.queryRenderedFeatures.getCall(0).args[0], {x: 612, y: 100}); t.end(); }); }); @@ -1230,7 +1279,7 @@ test('Map', (t) => { }); }); - t.test('fires a data event', (t) => { + t.test('fires a data event on background layer', (t) => { // background layers do not have a source const map = createMap(t, { style: { @@ -1257,6 +1306,37 @@ test('Map', (t) => { }); }); + t.test('fires a data event on sky layer', (t) => { + // sky layers do not have a source + const map = createMap(t, { + style: { + "version": 8, + "sources": {}, + "layers": [{ + "id": "sky", + "type": "sky", + "layout": { + "visibility": "none" + }, + "paint": { + "sky-type": "atmosphere", + "sky-atmosphere-sun": [0, 0] + } + }] + } + }); + + map.once('style.load', () => { + map.once('data', (e) => { + if (e.dataType === 'style') { + t.end(); + } + }); + + map.setLayoutProperty('sky', 'visibility', 'visible'); + }); + }); + t.test('sets visibility on background layer', (t) => { // background layers do not have a source const map = createMap(t, { diff --git a/test/unit/ui/map/isMoving.test.js b/test/unit/ui/map/isMoving.test.js index f215bb2ddc3..52226351ef2 100644 --- a/test/unit/ui/map/isMoving.test.js +++ b/test/unit/ui/map/isMoving.test.js @@ -7,6 +7,7 @@ import simulate from '../../../util/simulate_interaction'; function createMap(t) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map({container: DOM.create('div', '', window.document.body)}); } diff --git a/test/unit/ui/map/isRotating.test.js b/test/unit/ui/map/isRotating.test.js index 69a6dca56a4..b369681d37c 100644 --- a/test/unit/ui/map/isRotating.test.js +++ b/test/unit/ui/map/isRotating.test.js @@ -7,6 +7,7 @@ import browser from '../../../../src/util/browser'; function createMap(t) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map({container: DOM.create('div', '', window.document.body)}); } diff --git a/test/unit/ui/map/isZooming.test.js b/test/unit/ui/map/isZooming.test.js index 6cd6c76b41b..78844d77b57 100644 --- a/test/unit/ui/map/isZooming.test.js +++ b/test/unit/ui/map/isZooming.test.js @@ -7,6 +7,7 @@ import simulate from '../../../util/simulate_interaction'; function createMap(t) { t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); return new Map({container: DOM.create('div', '', window.document.body)}); } diff --git a/test/unit/ui/marker.test.js b/test/unit/ui/marker.test.js index fbfbc5b33dd..4d015f41b02 100644 --- a/test/unit/ui/marker.test.js +++ b/test/unit/ui/marker.test.js @@ -784,3 +784,47 @@ test('Marker pitchAlignment when set to auto defaults to rotationAlignment (sett map.remove(); t.end(); }); + +test('Drag above horizon clamps', (t) => { + const map = createMap(t); + map.setPitch(85); + const marker = new Marker({draggable: true}) + .setLngLat(map.unproject([map.transform.width / 2, map.transform.horizonLineFromTop() + 20])) + .addTo(map); + const el = marker.getElement(); + const startPos = map.project(marker.getLngLat()); + const atHorizon = map.project(map.unproject([map.transform.width / 2, map.transform.horizonLineFromTop()])); + t.true(atHorizon.y < startPos.y + 5); + + simulate.mousedown(el); + simulate.mousemove(el, {clientX: 0, clientY: -40}); + simulate.mouseup(el); + + const endPos = map.project(marker.getLngLat()); + t.true(Math.abs(endPos.x - startPos.x) < 0.00000000001); + t.equal(endPos.y, atHorizon.y); + + map.remove(); + t.end(); +}); + +test('Drag below / behind camera', (t) => { + const map = createMap(t); + map.setPitch(85); + const marker = new Marker({draggable: true}) + .setLngLat(map.unproject([map.transform.width / 2, map.transform.height - 20])) + .addTo(map); + const el = marker.getElement(); + const startPos = map.project(marker.getLngLat()); + + simulate.mousedown(el); + simulate.mousemove(el, {clientX: 0, clientY: 40}); + simulate.mouseup(el); + + const endPos = map.project(marker.getLngLat()); + t.true(Math.abs(endPos.x - startPos.x) < 0.00000000001); + t.equal(Math.round(endPos.y), Math.round(startPos.y) + 40); + + map.remove(); + t.end(); +}); diff --git a/test/unit/ui/popup.test.js b/test/unit/ui/popup.test.js index 0863c75bcfb..76e4b6c9bd1 100644 --- a/test/unit/ui/popup.test.js +++ b/test/unit/ui/popup.test.js @@ -323,6 +323,31 @@ test('Popup preserves object constancy of position after auto-wrapping center (r t.end(); }); +test('Popup preserves object constancy of position after auto-wrapping center with horizon', (t) => { + const map = createMap(t, {width: 1024}); + map.setCenter([-175, 0]); // longitude bounds: [-535, 185] + map.setPitch(69); + map.setBearing(90); + + const popup = new Popup() + .setLngLat([-720, 0]) + .setText('Test') + .addTo(map); + // invoke smart wrap multiple times. + map.setCenter([0, 0]); + map.setCenter([300, 0]); + map.setPitch(72); + map.setCenter([600, 0]); + map.setPitch(75); + map.setCenter([900, 0]); + map.setPitch(80); + map.setCenter([175, 0]); + + t.deepEqual(popup._pos, map.project([720, 0])); + + t.end(); +}); + test('Popup wraps position after map move if it would otherwise go offscreen (right)', (t) => { const map = createMap(t, {width: 1024}); // longitude bounds: [-360, 360] @@ -399,6 +424,7 @@ test('Popup anchors as specified by the anchor option', (t) => { Object.defineProperty(popup.getElement(), 'offsetHeight', {value: 100}); t.stub(map, 'project').returns(point); + t.stub(map.transform, 'locationPoint3D').returns(point); popup.setLngLat([0, 0]); t.ok(popup.getElement().classList.contains(`mapboxgl-popup-anchor-${anchor}`)); @@ -408,6 +434,7 @@ test('Popup anchors as specified by the anchor option', (t) => { test(`Popup translation reflects offset and ${anchor} anchor`, (t) => { const map = createMap(t); t.stub(map, 'project').returns(new Point(0, 0)); + t.stub(map.transform, 'locationPoint3D').returns(new Point(0, 0)); const popup = new Popup({anchor, offset: 10}) .setLngLat([0, 0]) @@ -444,6 +471,7 @@ test('Popup automatically anchors to top if its bottom offset would push it off- test('Popup is offset via a PointLike offset option', (t) => { const map = createMap(t); t.stub(map, 'project').returns(new Point(0, 0)); + t.stub(map.transform, 'locationPoint3D').returns(new Point(0, 0)); const popup = new Popup({anchor: 'top-left', offset: [5, 10]}) .setLngLat([0, 0]) @@ -457,6 +485,7 @@ test('Popup is offset via a PointLike offset option', (t) => { test('Popup is offset via an object offset option', (t) => { const map = createMap(t); t.stub(map, 'project').returns(new Point(0, 0)); + t.stub(map.transform, 'locationPoint3D').returns(new Point(0, 0)); const popup = new Popup({anchor: 'top-left', offset: {'top-left': [5, 10]}}) .setLngLat([0, 0]) @@ -470,6 +499,7 @@ test('Popup is offset via an object offset option', (t) => { test('Popup is offset via an incomplete object offset option', (t) => { const map = createMap(t); t.stub(map, 'project').returns(new Point(0, 0)); + t.stub(map.transform, 'locationPoint3D').returns(new Point(0, 0)); const popup = new Popup({anchor: 'top-right', offset: {'top-left': [5, 10]}}) .setLngLat([0, 0]) diff --git a/test/unit/util/browser.test.js b/test/unit/util/browser.test.js index 4a482d53191..90ddbd2729e 100644 --- a/test/unit/util/browser.test.js +++ b/test/unit/util/browser.test.js @@ -28,10 +28,5 @@ test('browser', (t) => { t.end(); }); - t.test('hardwareConcurrency', (t) => { - t.equal(typeof browser.hardwareConcurrency, 'number'); - t.end(); - }); - t.end(); }); diff --git a/test/unit/util/dispatcher.test.js b/test/unit/util/dispatcher.test.js index 0fc7db0e4a0..520f9860d1b 100644 --- a/test/unit/util/dispatcher.test.js +++ b/test/unit/util/dispatcher.test.js @@ -30,6 +30,7 @@ test('Dispatcher', (t) => { const ids = []; function Actor (target, parent, mapId) { ids.push(mapId); } t.stub(Dispatcher, 'Actor').callsFake(Actor); + t.stub(Dispatcher.prototype, 'broadcast').callsFake(() => {}); t.stub(WorkerPool, 'workerCount').value(1); const workerPool = new WorkerPool(); @@ -45,6 +46,7 @@ test('Dispatcher', (t) => { this.remove = function() { actorsRemoved.push(this); }; } t.stub(Dispatcher, 'Actor').callsFake(Actor); + t.stub(Dispatcher.prototype, 'broadcast').callsFake(() => {}); t.stub(WorkerPool, 'workerCount').value(4); const workerPool = new WorkerPool(); diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index aebba256595..93655c2b4b0 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -307,6 +307,8 @@ test("mapbox", (t) => { "mapbox://tiles/a.b/{z}/{x}/{y}.png"); t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png?access_token=key", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.png"); + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/raster/v1/a.b/{z}/{x}/{y}.png?access_token=key", tileJSONURL), + "mapbox://raster/a.b/{z}/{x}/{y}.png"); // We don't ever expect to see these inputs, but be safe anyway. t.equals(manager.canonicalizeTileURL("http://path"), "http://path"); @@ -326,25 +328,23 @@ test("mapbox", (t) => { t.end(); }); - t.test('.normalizeTileURL inserts @2x on 2x devices', (t) => { - window.devicePixelRatio = 2; + t.test('.normalizeTileURL inserts @2x if source requests it', (t) => { config.API_URL = 'http://path.png'; config.REQUIRE_ACCESS_TOKEN = false; - t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png'), `http://path.png/v4/tile@2x.png`); - t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png32'), `http://path.png/v4/tile@2x.png32`); - t.equal(manager.normalizeTileURL('mapbox://path.png/tile.jpg70'), `http://path.png/v4/tile@2x.jpg70`); - t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png?access_token=foo'), `http://path.png/v4/tile@2x.png?access_token=foo`); - window.devicePixelRatio = 1; + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png', true), `http://path.png/v4/tile@2x.png`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png32', true), `http://path.png/v4/tile@2x.png32`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.jpg70', true), `http://path.png/v4/tile@2x.jpg70`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png?access_token=foo', true), `http://path.png/v4/tile@2x.png?access_token=foo`); t.end(); }); - t.test('.normalizeTileURL inserts @2x when tileSize == 512', (t) => { + t.test('.normalizeTileURL inserts @2x for 512 raster tiles on v4 of the api', (t) => { config.API_URL = 'http://path.png'; config.REQUIRE_ACCESS_TOKEN = false; - t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png', 512), `http://path.png/v4/tile@2x.png`); - t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png32', 512), `http://path.png/v4/tile@2x.png32`); - t.equal(manager.normalizeTileURL('mapbox://path.png/tile.jpg70', 512), `http://path.png/v4/tile@2x.jpg70`); - t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png?access_token=foo', 512), `http://path.png/v4/tile@2x.png?access_token=foo`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png', false, 256), `http://path.png/v4/tile.png`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png', false, 512), `http://path.png/v4/tile@2x.png`); + t.equal(manager.normalizeTileURL("mapbox://raster/a.b/0/0/0.png", false, 256), `http://path.png/raster/v1/a.b/0/0/0.png`); + t.equal(manager.normalizeTileURL("mapbox://raster/a.b/0/0/0.png", false, 512), `http://path.png/raster/v1/a.b/0/0/0.png`); t.end(); }); @@ -402,6 +402,7 @@ test("mapbox", (t) => { t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.png"), `https://api.mapbox.com/v4/a.b/0/0/0.png?sku=${manager._skuToken}&access_token=key`); t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0@2x.png"), `https://api.mapbox.com/v4/a.b/0/0/0@2x.png?sku=${manager._skuToken}&access_token=key`); t.equal(manager.normalizeTileURL("mapbox://tiles/a.b,c.d/0/0/0.pbf"), `https://api.mapbox.com/v4/a.b,c.d/0/0/0.pbf?sku=${manager._skuToken}&access_token=key`); + t.equal(manager.normalizeTileURL("mapbox://raster/a.b/0/0/0.png"), `https://api.mapbox.com/raster/v1/a.b/0/0/0.png?sku=${manager._skuToken}&access_token=key`); config.API_URL = 'https://api.example.com/'; t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.png"), `https://api.example.com/v4/a.b/0/0/0.png?sku=${manager._skuToken}&access_token=key`); @@ -772,7 +773,7 @@ test("mapbox", (t) => { }); t.test('contains skuId and skuToken', (t) => { - event.postMapLoadEvent(mapboxTileURLs, 1, skuToken); + event.postMapLoadEvent(1, skuToken); const reqBody = window.server.requests[0].requestBody; // reqBody is a string of an array containing the event object so pick out the stringified event object and convert to an object const mapLoadEvent = JSON.parse(reqBody.slice(1, reqBody.length - 1)); @@ -785,14 +786,7 @@ test("mapbox", (t) => { t.test('does not POST when mapboxgl.ACCESS_TOKEN is not set', (t) => { config.ACCESS_TOKEN = null; - event.postMapLoadEvent(mapboxTileURLs, 1, skuToken); - t.equal(window.server.requests.length, 0); - t.end(); - }); - - t.test('does not POST when url does not point to mapbox.com', (t) => { - event.postMapLoadEvent(nonMapboxTileURLs, 1, skuToken); - + event.postMapLoadEvent(1, skuToken, null, () => {}); t.equal(window.server.requests.length, 0); t.end(); }); @@ -800,7 +794,7 @@ test("mapbox", (t) => { t.test('POSTs cn event when API_URL changes to cn endpoint', (t) => { config.API_URL = 'https://api.mapbox.cn'; - event.postMapLoadEvent(mapboxTileURLs, 1, skuToken); + event.postMapLoadEvent(1, skuToken); const req = window.server.requests[0]; req.respond(200); @@ -811,14 +805,14 @@ test("mapbox", (t) => { t.test('POSTs no event when API_URL unavailable', (t) => { config.API_URL = null; - event.postMapLoadEvent(mapboxTileURLs, 1, skuToken); + event.postMapLoadEvent(1, skuToken); t.equal(window.server.requests.length, 0, 'no events posted'); t.end(); }); t.test('POSTs no event when API_URL is non-standard', (t) => { config.API_URL = "https://api.example.com"; - event.postMapLoadEvent(mapboxTileURLs, 1, skuToken); + event.postMapLoadEvent(1, skuToken); t.equal(window.server.requests.length, 0, 'no events posted'); t.end(); }); @@ -852,7 +846,7 @@ test("mapbox", (t) => { anonId: 'anonymous' })); - event.postMapLoadEvent(mapboxTileURLs, 1, skuToken); + event.postMapLoadEvent(1, skuToken); const req = window.server.requests[0]; req.respond(200); @@ -863,12 +857,12 @@ test("mapbox", (t) => { t.test('does not POST map.load event second time within same calendar day', (t) => { let now = +Date.now(); - withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1, skuToken)); + withFixedDate(t, now, () => event.postMapLoadEvent(1, skuToken)); //Post second event const firstEvent = now; now += (60 * 1000); // A bit later - withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1, skuToken)); + withFixedDate(t, now, () => event.postMapLoadEvent(1, skuToken)); const req = window.server.requests[0]; req.respond(200); @@ -883,12 +877,12 @@ test("mapbox", (t) => { t.test('does not POST map.load event second time when clock goes backwards less than a day', (t) => { let now = +Date.now(); - withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1, skuToken)); + withFixedDate(t, now, () => event.postMapLoadEvent(1, skuToken)); //Post second event const firstEvent = now; now -= (60 * 1000); // A bit earlier - withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1, skuToken)); + withFixedDate(t, now, () => event.postMapLoadEvent(1, skuToken)); const req = window.server.requests[0]; req.respond(200); @@ -904,7 +898,7 @@ test("mapbox", (t) => { t.test('POSTs map.load event when access token changes', (t) => { config.ACCESS_TOKEN = 'pk.new.*'; - event.postMapLoadEvent(mapboxTileURLs, 1, skuToken); + event.postMapLoadEvent(1, skuToken); const req = window.server.requests[0]; req.respond(200); @@ -918,7 +912,7 @@ test("mapbox", (t) => { const anonId = uuid(); window.localStorage.setItem(`mapbox.eventData.uuid:${config.ACCESS_TOKEN}`, anonId); turnstileEvent.postTurnstileEvent(mapboxTileURLs); - event.postMapLoadEvent(mapboxTileURLs, 1, skuToken); + event.postMapLoadEvent(1, skuToken); const turnstileReq = window.server.requests[0]; turnstileReq.respond(200); @@ -939,7 +933,7 @@ test("mapbox", (t) => { t.test('when LocalStorage is not available', (t) => { t.test('POSTs map.load event', (t) => { - event.postMapLoadEvent(mapboxTileURLs, 1, skuToken); + event.postMapLoadEvent(1, skuToken); const req = window.server.requests[0]; req.respond(200); @@ -956,8 +950,8 @@ test("mapbox", (t) => { t.test('does not POST map.load multiple times for the same map instance', (t) => { const now = Date.now(); - withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1, skuToken)); - withFixedDate(t, now + 5, () => event.postMapLoadEvent(mapboxTileURLs, 1, skuToken)); + withFixedDate(t, now, () => event.postMapLoadEvent(1, skuToken)); + withFixedDate(t, now + 5, () => event.postMapLoadEvent(1, skuToken)); const req = window.server.requests[0]; req.respond(200); @@ -973,7 +967,7 @@ test("mapbox", (t) => { t.test('POSTs map.load event when access token changes', (t) => { config.ACCESS_TOKEN = 'pk.new.*'; - event.postMapLoadEvent(mapboxTileURLs, 1, skuToken); + event.postMapLoadEvent(1, skuToken); const req = window.server.requests[0]; req.respond(200); @@ -989,9 +983,9 @@ test("mapbox", (t) => { }); t.test('POSTs distinct map.load for multiple maps', (t) => { - event.postMapLoadEvent(mapboxTileURLs, 1, skuToken); + event.postMapLoadEvent(1, skuToken); const now = +Date.now(); - withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 2, skuToken)); + withFixedDate(t, now, () => event.postMapLoadEvent(2, skuToken)); let req = window.server.requests[0]; req.respond(200); @@ -1011,9 +1005,9 @@ test("mapbox", (t) => { t.test('Queues and POSTs map.load events when triggerred in quick succession by different maps', (t) => { const now = Date.now(); - withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1, skuToken)); - withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 2, skuToken)); - withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 3, skuToken)); + withFixedDate(t, now, () => event.postMapLoadEvent(1, skuToken)); + withFixedDate(t, now, () => event.postMapLoadEvent(2, skuToken)); + withFixedDate(t, now, () => event.postMapLoadEvent(3, skuToken)); const reqOne = window.server.requests[0]; reqOne.respond(200); @@ -1039,5 +1033,55 @@ test("mapbox", (t) => { t.end(); }); + t.test('MapSessionAPI', (t) => { + let sessionAPI; + const skuToken = '1234567890123'; + t.beforeEach((callback) => { + window.useFakeXMLHttpRequest(); + sessionAPI = new mapbox.MapSessionAPI(); + callback(); + }); + + t.afterEach((callback) => { + window.restore(); + callback(); + }); + + t.test('mapbox.getMapSessionAPI', (t) => { + t.ok(mapbox.getMapSessionAPI); + t.end(); + }); + + t.test('contains access token and skuToken', (t) => { + sessionAPI.getSession(1, skuToken); + const requestURL = new URL(window.server.requests[0].url); + const urlParam = new URLSearchParams(requestURL.search); + t.equals(urlParam.get('sku'), skuToken); + t.equals(urlParam.get('access_token'), config.ACCESS_TOKEN); + t.end(); + }); + + t.test('no API is sent when API_URL unavailable', (t) => { + config.API_URL = null; + sessionAPI.getSession(1, skuToken); + t.equal(window.server.requests.length, 0, 'no request'); + + t.end(); + }); + + t.test('send a new request when access token changes', (t) => { + config.ACCESS_TOKEN = 'pk.new.*'; + sessionAPI.getSession(1, skuToken); + + const req = window.server.requests[0]; + t.equal(req.url, `${config.API_URL + config.SESSION_PATH}?sku=${skuToken}&access_token=pk.new.*`); + t.equal(req.method, 'GET'); + + t.end(); + }); + + t.end(); + }); + t.end(); }); diff --git a/test/unit/util/primitives.test.js b/test/unit/util/primitives.test.js index 35b56d50cba..e09271bc5e2 100644 --- a/test/unit/util/primitives.test.js +++ b/test/unit/util/primitives.test.js @@ -1,5 +1,5 @@ import {test} from '../../util/test'; -import {Aabb, Frustum} from '../../../src/util/primitives'; +import {Aabb, Frustum, Ray} from '../../../src/util/primitives'; import {mat4, vec3} from 'gl-matrix'; test('primitives', (t) => { @@ -150,5 +150,52 @@ test('primitives', (t) => { }); t.end(); }); + + t.test('ray', (t) => { + t.test('intersectsPlane', (t) => { + t.test('parallel', (t) => { + const r = new Ray(vec3.fromValues(0, 0, 1), vec3.fromValues(1, 1, 0)); + t.notOk(r.intersectsPlane(vec3.fromValues(0, 0, 0), vec3.fromValues(0, 0, 1), vec3.create())); + t.end(); + }); + + t.test('orthogonal', (t) => { + const r = new Ray(vec3.fromValues(10, 20, 50), vec3.fromValues(0, 0, -1)); + const out = vec3.create(); + t.ok(r.intersectsPlane(vec3.fromValues(0, 0, 5), vec3.fromValues(0, 0, 1), out)); + assertAlmostEqual(t, out[0], 10); + assertAlmostEqual(t, out[1], 20); + assertAlmostEqual(t, out[2], 5); + t.end(); + }); + + t.test('angled down', (t) => { + const r = new Ray(vec3.fromValues(-10, -10, 20), vec3.fromValues(0.5773, 0.5773, -0.5773)); + const out = vec3.create(); + t.ok(r.intersectsPlane(vec3.fromValues(0, 0, 10), vec3.fromValues(0, 0, 1), out)); + assertAlmostEqual(t, out[0], 0); + assertAlmostEqual(t, out[1], 0); + assertAlmostEqual(t, out[2], 10); + t.end(); + }); + + t.test('angled up', (t) => { + const r = new Ray(vec3.fromValues(-10, -10, 20), vec3.fromValues(0.5773, 0.5773, 0.5773)); + const out = vec3.create(); + t.ok(r.intersectsPlane(vec3.fromValues(0, 0, 10), vec3.fromValues(0, 0, 1), out)); + assertAlmostEqual(t, out[0], -20); + assertAlmostEqual(t, out[1], -20); + assertAlmostEqual(t, out[2], 10); + t.end(); + }); + + t.end(); + }); + t.end(); + }); t.end(); }); + +function assertAlmostEqual(t, actual, expected, epsilon = 1e-6) { + t.ok(Math.abs(actual - expected) < epsilon); +} diff --git a/test/unit/util/util.test.js b/test/unit/util/util.test.js index bad799c54bb..4afda3d922e 100644 --- a/test/unit/util/util.test.js +++ b/test/unit/util/util.test.js @@ -2,9 +2,21 @@ import {test} from '../../util/test'; -import {easeCubicInOut, keysDifference, extend, pick, uniqueId, bindAll, asyncAll, clamp, wrap, bezier, endsWith, mapObject, filterObject, deepEqual, clone, arraysIntersect, isCounterClockwise, isClosedPolygon, parseCacheControl, uuid, validateUuid, nextPowerOfTwo, isPowerOfTwo} from '../../../src/util/util'; +import {degToRad, radToDeg, easeCubicInOut, keysDifference, extend, pick, uniqueId, bindAll, asyncAll, clamp, wrap, bezier, endsWith, mapObject, filterObject, deepEqual, clone, arraysIntersect, isCounterClockwise, isClosedPolygon, parseCacheControl, uuid, validateUuid, nextPowerOfTwo, isPowerOfTwo, bufferConvexPolygon, prevPowerOfTwo} from '../../../src/util/util'; import Point from '@mapbox/point-geometry'; +const EPSILON = 1e-8; + +function pointsetEqual(t, actual, expected) { + t.equal(actual.length, expected.length); + for (let i = 0; i < actual.length; i++) { + const p1 = actual[i]; + const p2 = expected[i]; + t.ok(Math.abs(p1.x - p2.x) < EPSILON); + t.ok(Math.abs(p1.y - p2.y) < EPSILON); + } +} + test('util', (t) => { t.equal(easeCubicInOut(0), 0, 'easeCubicInOut=0'); t.equal(easeCubicInOut(0.2), 0.03200000000000001); @@ -17,6 +29,11 @@ test('util', (t) => { t.deepEqual(pick({a:1, b:2, c:3}, ['a', 'c', 'd']), {a:1, c:3}, 'pick'); t.ok(typeof uniqueId() === 'number', 'uniqueId'); + t.equal(degToRad(radToDeg(Math.PI)), Math.PI); + t.equal(degToRad(radToDeg(-Math.PI)), -Math.PI); + t.equal(radToDeg(degToRad(65)), 65); + t.equal(radToDeg(degToRad(-34.2)), -34.2); + t.test('bindAll', (t) => { function MyClass() { bindAll(['ontimer'], this); @@ -96,6 +113,17 @@ test('util', (t) => { t.end(); }); + t.test('prevPowerOfTwo', (t) => { + t.equal(prevPowerOfTwo(1), 1); + t.equal(prevPowerOfTwo(2), 2); + t.equal(prevPowerOfTwo(256), 256); + t.equal(prevPowerOfTwo(258), 256); + t.equal(prevPowerOfTwo(514), 512); + t.equal(prevPowerOfTwo(512), 512); + t.equal(prevPowerOfTwo(98), 64); + t.end(); + }); + t.test('nextPowerOfTwo', (t) => { t.equal(isPowerOfTwo(nextPowerOfTwo(1)), true); t.equal(isPowerOfTwo(nextPowerOfTwo(2)), true); @@ -306,6 +334,33 @@ test('util', (t) => { t.end(); }); + t.test('bufferConvexPolygon', (t) => { + t.throws(() => { + bufferConvexPolygon([new Point(0, 0), new Point(1, 1)], 5); + }); + t.throws(() => { + bufferConvexPolygon([new Point(0, 0)], 5); + }); + t.ok(bufferConvexPolygon([new Point(0, 0), new Point(0, 1), new Point(1, 1)], 1)); + + t.test('numerical tests', (t) => { + t.test('box', (t) => { + const buffered = bufferConvexPolygon([new Point(0, 0), new Point(1, 0), new Point(1, 1), new Point(0, 1)], 1); + pointsetEqual(t, buffered, [new Point(-1, -1), new Point(2, -1), new Point(2, 2), new Point(-1, 2)]); + t.end(); + }); + t.test('triangle', (t) => { + const buffered = bufferConvexPolygon([new Point(0, 0), new Point(1, 0), new Point(0, 1)], 1); + pointsetEqual(t, buffered, [new Point(-1, -1), new Point(3.414213562373095, -1), new Point(-1, 3.414213562373095)]); + t.end(); + }); + + t.end(); + }); + + t.end(); + }); + t.test('parseCacheControl', (t) => { t.test('max-age', (t) => { t.deepEqual(parseCacheControl('max-age=123456789'), { diff --git a/test/util/browser_write_file.js b/test/util/browser_write_file.js index 81de4ca5876..c1297ef0f65 100644 --- a/test/util/browser_write_file.js +++ b/test/util/browser_write_file.js @@ -1,16 +1,20 @@ -/* eslint-disable import/no-commonjs */ /* eslint-env browser */ +/* eslint-disable import/no-commonjs */ +/* global self, WorkerGlobalScope */ // Tests running in the browser need to be able to persist files to disk in certain situations. // our test server (server.js) actually handles the file-io and listens for POST request to /write-file -// This is a helper method to send that POST request. +// This worker provides a helper method to send that POST request. // filepath: relative filepath from the root of the mapboxgl repo // data: base64 encoded string of the data to be persisted to disk -module.exports = function(filepath, data, callback) { +function isWorker() { + return typeof WorkerGlobalScope !== 'undefined' && typeof self !== 'undefined' && self instanceof WorkerGlobalScope; +} +const browserWriteFile = (filepath, data, cb) => { const xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (xhttp.readyState === 4 && xhttp.status === 200) { - callback(); + cb(); } }; xhttp.open("POST", "/write-file", true); @@ -22,3 +26,20 @@ module.exports = function(filepath, data, callback) { }; xhttp.send(JSON.stringify(postData)); }; + +if (isWorker()) { + onmessage = function(e) { + e.data.forEach((file) => { + browserWriteFile( + file.path, + file.data, + () => { + self.postMessage(true); + } + ); + }); + }; +} else { + module.exports = browserWriteFile; +} + diff --git a/test/util/dem_mock.js b/test/util/dem_mock.js new file mode 100644 index 00000000000..a1ffee6f622 --- /dev/null +++ b/test/util/dem_mock.js @@ -0,0 +1,35 @@ +import DEMData from '../../src/data/dem_data'; +import {RGBAImage} from '../../src/util/image'; + +export function createConstElevationDEM(elevation, tileSize) { + const pixelCount = (tileSize + 2) * (tileSize + 2); + const pixelData = new Uint8Array(pixelCount * 4); + const encoded = DEMData.pack(elevation, "mapbox"); + + for (let i = 0; i < pixelCount * 4; i += 4) { + pixelData[i + 0] = encoded[0]; + pixelData[i + 1] = encoded[1]; + pixelData[i + 2] = encoded[2]; + pixelData[i + 3] = encoded[3]; + } + return new DEMData(0, new RGBAImage({height: tileSize + 2, width: tileSize + 2}, pixelData, "mapbox", false, true)); +} + +export function setMockElevationTerrain(map, demData, tileSize) { + map.addSource('mapbox-dem', { + "type": "raster-dem", + "tiles": ['http://example.com/{z}/{x}/{y}.png'], + tileSize, + "maxzoom": 14 + }); + const cache = map.style._getSourceCache('mapbox-dem'); + cache.used = cache._sourceLoaded = true; + cache._loadTile = (tile, callback) => { + tile.dem = demData; + tile.needsHillshadePrepare = true; + tile.needsDEMTextureUpload = true; + tile.state = 'loaded'; + callback(null); + }; + map.setTerrain({"source": "mapbox-dem"}); +} diff --git a/test/util/fixed.js b/test/util/fixed.js index 0b300b1bfe8..3cd7d5add61 100644 --- a/test/util/fixed.js +++ b/test/util/fixed.js @@ -20,3 +20,30 @@ export function fixedCoord(coord, precision) { z: fixedNum(coord.z, precision) }; } + +export function fixedPoint(point, precision) { + if (precision === undefined) precision = 10; + return { + x: fixedNum(point.x, precision), + y: fixedNum(point.y, precision) + }; +} + +export function fixedVec3(vec, precision) { + if (precision === undefined) precision = 10; + return [ + fixedNum(vec[0], precision), + fixedNum(vec[1], precision), + fixedNum(vec[2], precision) + ]; +} + +export function fixedVec4(vec, precision) { + if (precision === undefined) precision = 10; + return [ + fixedNum(vec[0], precision), + fixedNum(vec[1], precision), + fixedNum(vec[2], precision), + fixedNum(vec[3], precision) + ]; +} diff --git a/test/util/html_generator.js b/test/util/html_generator.js new file mode 100644 index 00000000000..86b63af483d --- /dev/null +++ b/test/util/html_generator.js @@ -0,0 +1,131 @@ +/* eslint-env browser */ +import template from 'lodash.template'; + +const CI = process.env.CI; + +const generateResultHTML = template(` +
+ <% if (r.status === 'failed') { %> + + <% } else { %> + + <% } %> + +
+ <% if (r.status !== 'errored') { %> + + <% } %> + <% if (r.expected) { %> + + <% } %> + <% if (r.imgDiff) { %> + + <% } %> + <% if (r.error) { %>

Error: <%- r.error.message %>

<% } %> + <% if (r.jsonDiff) { %> +
<%- r.jsonDiff.trim() %>
+ <% } %> +
+
+`); + +const pageCss = ` +body { font: 18px/1.2 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; padding: 10px; background: #ecf0f1 } +h1 { font-size: 32px; margin-bottom: 0; } +img { margin: 0 10px 10px 0; border: 1px dotted #ccc; } +input { position: absolute; opacity: 0; z-index: -1;} +.tests { border-top: 1px dotted #bbb; margin-top: 10px; padding-top: 15px; overflow: hidden; } +.status-container { margin: 0; } +.status { text-transform: uppercase; } +.label { color: white; font-size: 18px; padding: 2px 6px 3px; border-radius: 3px; margin-right: 3px; vertical-align: bottom; display: inline-block; } +.tab { margin-bottom: 30px; width: 100%; overflow: hidden; } +.tab-label { display: flex; color: white; border-radius: 5px; justify-content: space-between; padding: 1em; font-weight: bold; cursor: pointer; } +.tab-label:hover { filter: brightness(85%); } +.tab-label::after { content: "\\276F"; width: 1em; height: 1em; text-align: center; transition: all .35s; } +.tab-content { max-height: 0; padding: 0 1em; background: white; transition: all .35s; } +.tab-content pre { font-size: 14px; margin: 0 0 10px; } +input:checked + .tab-label { filter: brightness(90%); }; +input:checked + .tab-label::after { transform: rotate(90deg); } +input:checked ~ .tab-content { max-height: 100vh; padding: 1em; border: 1px solid #eee; border-top: 0; border-radius: 5px; } +iframe { pointer-events: none; } +`; + +const stats = { + failed: 0, + passed: 0, + todo: 0 +}; +const colors = { + passed: 'green', + failed: 'red', + todo: '#e89b00' +}; + +const counterDom = { + passed: null, + failed: null, + todo: null, +}; + +let resultsContainer; + +export function setupHTML() { + // Add CSS to the page + const style = document.createElement('style'); + document.head.appendChild(style); + style.appendChild(document.createTextNode(pageCss)); + + //Create a container to hold test stats + const statsContainer = document.createElement('div'); + + const failedTestContainer = document.createElement('h1'); + failedTestContainer.style.color = 'red'; + counterDom.failed = document.createElement('span'); + counterDom.failed.innerHTML = '0'; + const failedTests = document.createElement('span'); + failedTests.innerHTML = ' tests failed.'; + failedTestContainer.appendChild(counterDom.failed); + failedTestContainer.appendChild(failedTests); + statsContainer.appendChild(failedTestContainer); + + const passedTestContainer = document.createElement('h1'); + passedTestContainer.style.color = 'green'; + counterDom.passed = document.createElement('span'); + counterDom.passed.innerHTML = '0'; + const passedTests = document.createElement('span'); + passedTests.innerHTML = ' tests passed.'; + passedTestContainer.appendChild(counterDom.passed); + passedTestContainer.appendChild(passedTests); + statsContainer.appendChild(passedTestContainer); + + const todoTestContainer = document.createElement('h1'); + todoTestContainer.style.color = '#e89b00'; + counterDom.todo = document.createElement('span'); + counterDom.todo.innerHTML = '0'; + const todoTests = document.createElement('span'); + todoTests.innerHTML = ' tests todo.'; + todoTestContainer.appendChild(counterDom.todo); + todoTestContainer.appendChild(todoTests); + statsContainer.appendChild(todoTestContainer); + + document.body.appendChild(statsContainer); + + //Create a container to hold test results + resultsContainer = document.createElement('div'); + resultsContainer.className = 'tests'; + document.body.appendChild(resultsContainer); +} + +export function updateHTML(testData) { + const status = testData.status; + stats[status]++; + + testData["color"] = colors[status]; + testData["id"] = `${status}Test-${stats[status]}`; + counterDom[status].innerHTML = stats[status]; + + // skip adding passing tests to report in CI mode + if (CI && status === 'passed') return; + const resultHTMLFrag = document.createRange().createContextualFragment(generateResultHTML({r: testData})); + resultsContainer.appendChild(resultHTMLFrag); +} diff --git a/test/util/index.js b/test/util/index.js index e136124f9ab..111ec840361 100644 --- a/test/util/index.js +++ b/test/util/index.js @@ -19,6 +19,7 @@ export function createMap(t, options, callback) { Object.defineProperty(container, 'clientWidth', {value: 200, configurable: true}); Object.defineProperty(container, 'clientHeight', {value: 200, configurable: true}); + if (!options || !options.skipAuthenticateStub) t.stub(Map.prototype, '_authenticate'); if (!options || !options.skipCSSStub) t.stub(Map.prototype, '_detectMissingCSS'); if (options && options.deleteStyle) delete defaultOptions.style; diff --git a/test/util/tap_html.js b/test/util/tap_html.js deleted file mode 100644 index 560a85406ee..00000000000 --- a/test/util/tap_html.js +++ /dev/null @@ -1,112 +0,0 @@ -/* eslint-disable import/no-commonjs */ -/* eslint-env browser */ -const Parser = require('tap-parser'); -const {template} = require('lodash'); - -const generateResultHTML = template(` -
-

<%- r.status %> <%- r.name %>

- <% if (r.status !== 'errored') { %> - - <% } %> - <% if (r.error) { %>

Error: <%- r.error.message %>

<% } %> - <% if (r.difference) { %> -
<%- r.difference.trim() %>
- <% } %> -
`); - -const generateStatsHTML = template(` -

-<%- failedTests %> tests failed. -

-

-<%- passedTests %> tests passed. -

-`); - -const pageCss = ` -body { font: 18px/1.2 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; padding: 10px; } -h1 { font-size: 32px; margin-bottom: 0; } -button { vertical-align: middle; } -h2 { font-size: 24px; font-weight: normal; margin: 10px 0 10px; line-height: 1; } -img { margin: 0 10px 10px 0; border: 1px dotted #ccc; } -.stats { margin-top: 10px; } -.test { border-bottom: 1px dotted #bbb; padding-bottom: 5px; } -.tests { border-top: 1px dotted #bbb; margin-top: 10px; } -.diff { color: #777; } -.test p, .test pre { margin: 0 0 10px; } -.test pre { font-size: 14px; } -.label { color: white; font-size: 18px; padding: 2px 6px 3px; border-radius: 3px; margin-right: 3px; vertical-align: bottom; display: inline-block; } -.hide { display: none; } -`; - -/** - * A class that can be used to incrementally generate prettified HTML output from tap output. - * - * @class TapHtmlGenerator - */ -class TapHtmlGenerator { - - constructor() { - // Add CSS to the page - const style = document.createElement('style'); - document.head.appendChild(style); - style.appendChild(document.createTextNode(pageCss)); - - //Create a container to hold test stats - this.statsContainer = document.createElement('div'); - document.body.appendChild(this.statsContainer); - - //Create a container to hold test results - this.resultsContainer = document.createElement('div'); - this.resultsContainer.className = 'tests'; - document.body.appendChild(this.resultsContainer); - - this.stats = { - failedTests: 0, - passedTests: 0 - }; - - this.tapParser = new Parser(); - this.tapParser.on('pass', this._onTestPassed.bind(this)); - this.tapParser.on('fail', this._onTestFailed.bind(this)); - } - - /** - * Pushes a line of tap output into the html generaor. - * - * @param {string} tapLine - * @memberof TapHtmlGenerator - */ - pushTapLine(tapLine) { - this.tapParser.write(`${tapLine}\n`); - } - - _onTestPassed(assert) { - this.stats.passedTests++; - - const metaData = JSON.parse(assert.name); - metaData["status"] = "passed"; - metaData["color"] = "green"; - - this.resultsContainer.innerHTML += generateResultHTML({r: metaData}); - this._updateStatsContainer(); - } - - _onTestFailed(assert) { - this.stats.failedTests++; - - const metaData = JSON.parse(assert.name); - metaData["status"] = "failed"; - metaData["color"] = "red"; - - this.resultsContainer.innerHTML += generateResultHTML({r: metaData}); - this._updateStatsContainer(); - } - - _updateStatsContainer() { - this.statsContainer.innerHTML = generateStatsHTML(this.stats); - } -} - -module.exports = TapHtmlGenerator; diff --git a/test/util/tape_config.js b/test/util/tape_config.js index ab8e99da491..d640801195d 100644 --- a/test/util/tape_config.js +++ b/test/util/tape_config.js @@ -4,8 +4,7 @@ // This file sets up tape with the add-ons we need, // this file also acts as the entrypoint for browserify. const tape = require('tape'); -const TapHtmlGenerator = require('./tap_html'); -const browserWriteFile = require('./browser_write_file'); +const browserWriteFile = require('../util/browser_write_file.js'); //Add test filtering ability const filter = getQueryVariable('filter') || '.*'; @@ -15,7 +14,8 @@ module.exports = test; // Helper method to extract query params from url function getQueryVariable(variable) { - const query = window.location.search.substring(1); + let query = window.location.search.substring(1); + query = decodeURIComponent(query); const vars = query.split("&"); for (let i = 0; i < vars.length; i++) { const pair = vars[i].split("="); @@ -24,26 +24,20 @@ function getQueryVariable(variable) { return (false); } -const tapHtmlGenerator = new TapHtmlGenerator(); // Testem object is available globally in the browser test page. // Tape outputs via `console.log` and is intercepted by testem using this function Testem.handleConsoleMessage = function(msg) { // Send output over ws to testem server Testem.emit('tap', msg); - // Pipe to html generator - tapHtmlGenerator.pushTapLine(msg); return false; }; // Persist the current html on the page as an artifact once tests finish Testem.afterTests((config, data, cb) => { browserWriteFile( - 'test/integration/query-tests/index.html', + `test/integration/${window._suiteName}/index.html`, window.btoa(document.documentElement.outerHTML), - () => { - cb(); - } + () => cb() ); }); - diff --git a/yarn.lock b/yarn.lock index eed1aa82d83..338c2f11ef1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -985,7 +985,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.8.4": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== @@ -1137,100 +1137,120 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== +"@octokit/auth-app@^2.4.7": + version "2.4.7" + resolved "https://registry.yarnpkg.com/@octokit/auth-app/-/auth-app-2.4.7.tgz#a036755c2f801d1414a50fad304050e8cbf2898f" + integrity sha512-qCHtVsrbk1SeokGWv6jeVkkpjqSBwAoor2VymBlETzrJgvM4I69sp8Zf8c/sAY8qf4/VLzK0/Ql1kJx0pb3CAg== + dependencies: + "@octokit/request" "^5.3.0" + "@octokit/request-error" "^2.0.0" + "@octokit/types" "^5.0.0" + "@types/lru-cache" "^5.1.0" + lru-cache "^5.1.1" + universal-github-app-jwt "^1.0.1" + universal-user-agent "^5.0.0" + "@octokit/auth-token@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.0.tgz#b64178975218b99e4dfe948253f0673cbbb59d9f" - integrity sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg== + version "2.4.2" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.2.tgz#10d0ae979b100fa6b72fa0e8e63e27e6d0dbff8a" + integrity sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ== dependencies: - "@octokit/types" "^2.0.0" + "@octokit/types" "^5.0.0" -"@octokit/endpoint@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.0.tgz#4c7acd79ab72df78732a7d63b09be53ec5a2230b" - integrity sha512-3nx+MEYoZeD0uJ+7F/gvELLvQJzLXhep2Az0bBSXagbApDvDW0LWwpnAIY/hb0Jwe17A0fJdz0O12dPh05cj7A== +"@octokit/core@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.0.0.tgz#4b7bf2a9e744a49abcbb3aca7b4dfc219513e4f9" + integrity sha512-FGUUqZbIwl5UPvuUTWq8ly2B12gJGWjYh1DviBzZLXp5LzHUgyzL+NDGsXeE4vszXoGsD/JfpZS+kjkLiD2T9w== + dependencies: + "@octokit/auth-token" "^2.4.0" + "@octokit/graphql" "^4.3.1" + "@octokit/request" "^5.4.0" + "@octokit/types" "^5.0.0" + before-after-hook "^2.1.0" + universal-user-agent "^5.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.3.tgz#dd09b599662d7e1b66374a177ab620d8cdf73487" + integrity sha512-Y900+r0gIz+cWp6ytnkibbD95ucEzDSKzlEnaWS52hbCDNcCJYO5mRmWW7HRAnDc7am+N/5Lnd8MppSaTYx1Yg== dependencies: - "@octokit/types" "^2.0.0" + "@octokit/types" "^5.0.0" is-plain-object "^3.0.0" universal-user-agent "^5.0.0" -"@octokit/plugin-paginate-rest@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc" - integrity sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q== +"@octokit/graphql@^4.3.1": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.5.1.tgz#162aed1490320b88ce34775b3f6b8de945529fa9" + integrity sha512-qgMsROG9K2KxDs12CO3bySJaYoUu2aic90qpFrv7A8sEBzZ7UFGvdgPKiLw5gOPYEYbS0Xf8Tvf84tJutHPulQ== dependencies: - "@octokit/types" "^2.0.1" + "@octokit/request" "^5.3.0" + "@octokit/types" "^5.0.0" + universal-user-agent "^5.0.0" + +"@octokit/plugin-paginate-rest@^2.2.0": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.2.tgz#d78f6ff2f188753ff217e51e3415a997cd6abce8" + integrity sha512-3OO/SjB5BChRTVRRQcZzpL0ZGcDGEB2dBzNhfqVqqMs6WDwo7cYW8cDwxqW8+VvA78mDK/abXgR/UrYg4HqrQg== + dependencies: + "@octokit/types" "^5.0.0" "@octokit/plugin-request-log@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e" integrity sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw== -"@octokit/plugin-rest-endpoint-methods@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e" - integrity sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ== +"@octokit/plugin-rest-endpoint-methods@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.0.0.tgz#b02a2006dda8e908c3f8ab381dd5475ef5a810a8" + integrity sha512-emS6gysz4E9BNi9IrCl7Pm4kR+Az3MmVB0/DoDCmF4U48NbYG3weKyDlgkrz6Jbl4Mu4nDx8YWZwC4HjoTdcCA== dependencies: - "@octokit/types" "^2.0.1" + "@octokit/types" "^5.0.0" deprecation "^2.3.1" -"@octokit/request-error@^1.0.2": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" - integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA== - dependencies: - "@octokit/types" "^2.0.0" - deprecation "^2.0.0" - once "^1.4.0" - "@octokit/request-error@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.0.tgz#94ca7293373654400fbb2995f377f9473e00834b" - integrity sha512-rtYicB4Absc60rUv74Rjpzek84UbVHGHJRu4fNVlZ1mCcyUPPuzFfG9Rn6sjHrd95DEsmjSt1Axlc699ZlbDkw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.1.tgz#49bd71e811daffd5bdd06ef514ca47b5039682d1" + integrity sha512-5lqBDJ9/TOehK82VvomQ6zFiZjPeSom8fLkFVLuYL3sKiIb5RB8iN/lenLkY7oBmyQcGP7FBMGiIZTO8jufaRQ== dependencies: - "@octokit/types" "^2.0.0" + "@octokit/types" "^4.0.1" deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.2.0": - version "5.3.4" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.3.4.tgz#fbc950bf785d59da3b0399fc6d042c8cf52e2905" - integrity sha512-qyj8G8BxQyXjt9Xu6NvfvOr1E0l35lsXtwm3SopsYg/JWXjlsnwqLc8rsD2OLguEL/JjLfBvrXr4az7z8Lch2A== +"@octokit/request@^5.3.0", "@octokit/request@^5.4.0": + version "5.4.5" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.5.tgz#8df65bd812047521f7e9db6ff118c06ba84ac10b" + integrity sha512-atAs5GAGbZedvJXXdjtKljin+e2SltEs48B3naJjqWupYl2IUBbB/CJisyjbNHcKpHzb3E+OYEZ46G8eakXgQg== dependencies: - "@octokit/endpoint" "^6.0.0" + "@octokit/endpoint" "^6.0.1" "@octokit/request-error" "^2.0.0" - "@octokit/types" "^2.0.0" + "@octokit/types" "^5.0.0" deprecation "^2.0.0" is-plain-object "^3.0.0" node-fetch "^2.3.0" once "^1.4.0" universal-user-agent "^5.0.0" -"@octokit/rest@^16.30.1": - version "16.43.1" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.43.1.tgz#3b11e7d1b1ac2bbeeb23b08a17df0b20947eda6b" - integrity sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw== +"@octokit/rest@^18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.0.tgz#7f401d9ce13530ad743dfd519ae62ce49bcc0358" + integrity sha512-4G/a42lry9NFGuuECnua1R1eoKkdBYJap97jYbWDNYBOUboWcM75GJ1VIcfvwDV/pW0lMPs7CEmhHoVrSV5shg== dependencies: - "@octokit/auth-token" "^2.4.0" - "@octokit/plugin-paginate-rest" "^1.1.1" + "@octokit/core" "^3.0.0" + "@octokit/plugin-paginate-rest" "^2.2.0" "@octokit/plugin-request-log" "^1.0.0" - "@octokit/plugin-rest-endpoint-methods" "2.4.0" - "@octokit/request" "^5.2.0" - "@octokit/request-error" "^1.0.2" - atob-lite "^2.0.0" - before-after-hook "^2.0.0" - btoa-lite "^1.0.0" - deprecation "^2.0.0" - lodash.get "^4.4.2" - lodash.set "^4.3.2" - lodash.uniq "^4.5.0" - octokit-pagination-methods "^1.1.0" - once "^1.4.0" - universal-user-agent "^4.0.0" + "@octokit/plugin-rest-endpoint-methods" "4.0.0" -"@octokit/types@^2.0.0", "@octokit/types@^2.0.1": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.5.0.tgz#f1bbd147e662ae2c79717d518aac686e58257773" - integrity sha512-KEnLwOfdXzxPNL34fj508bhi9Z9cStyN7qY1kOfVahmqtAfrWw6Oq3P4R+dtsg0lYtZdWBpUrS/Ixmd5YILSww== +"@octokit/types@^4.0.1": + version "4.1.10" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-4.1.10.tgz#e4029c11e2cc1335051775bc1600e7e740e4aca4" + integrity sha512-/wbFy1cUIE5eICcg0wTKGXMlKSbaAxEr00qaBXzscLXpqhcwgXeS6P8O0pkysBhRfyjkKjJaYrvR1ExMO5eOXQ== + dependencies: + "@types/node" ">= 8" + +"@octokit/types@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-5.0.0.tgz#cbdf3c060f6c0436c004ec402c5082c32de72511" + integrity sha512-3LVS+MbeqwSd5G4KS8123cZz+hWomsiGeMnQ/QJIBFDwL/YHX8kkr0FZXrgWEMO7Fgi2/VOrhbiFnk9sZ+s4qA== dependencies: "@types/node" ">= 8" @@ -1308,16 +1328,33 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/jsonwebtoken@^8.3.3": + version "8.5.0" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#2531d5e300803aa63279b232c014acf780c981c5" + integrity sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg== + dependencies: + "@types/node" "*" + +"@types/lru-cache@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" + integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/node@*", "@types/node@>= 8": +"@types/node@*": version "12.12.31" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.31.tgz#d6b4f9645fee17f11319b508fb1001797425da51" integrity sha512-T+wnJno8uh27G9c+1T+a1/WYCHzLeDqtsGJkoEdSp2X8RTh3oOCZQcUnjAx90CS8cmmADX51O0FI/tu9s0yssg== +"@types/node@>= 8": + version "14.0.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.13.tgz#ee1128e881b874c371374c1f72201893616417c9" + integrity sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA== + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" @@ -1382,11 +1419,6 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-dynamic-import@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" - integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== - acorn-globals@^4.3.0: version "4.3.4" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" @@ -1395,7 +1427,7 @@ acorn-globals@^4.3.0: acorn "^6.0.1" acorn-walk "^6.0.1" -acorn-jsx@^5.0.0, acorn-jsx@^5.0.1: +acorn-jsx@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== @@ -1726,11 +1758,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -atob-lite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" - integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY= - atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -1845,7 +1872,7 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -before-after-hook@^2.0.0: +before-after-hook@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== @@ -2145,25 +2172,6 @@ browserslist@^4.0.0, browserslist@^4.11.0, browserslist@^4.8.3, browserslist@^4. node-releases "^1.1.52" pkg-up "^3.1.0" -btoa-lite@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" - integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= - -buble@^0.19.8: - version "0.19.8" - resolved "https://registry.yarnpkg.com/buble/-/buble-0.19.8.tgz#d642f0081afab66dccd897d7b6360d94030b9d3d" - integrity sha512-IoGZzrUTY5fKXVkgGHw3QeXFMUNBFv+9l8a4QJKG1JhG3nCMHTdEX1DCOg8568E2Q9qvAQIiSokv6Jsgx8p2cA== - dependencies: - acorn "^6.1.1" - acorn-dynamic-import "^4.0.0" - acorn-jsx "^5.0.1" - chalk "^2.4.2" - magic-string "^0.25.3" - minimist "^1.2.0" - os-homedir "^2.0.0" - regexpu-core "^4.5.4" - buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -6168,7 +6176,7 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= -jsonwebtoken@^8.3.0: +jsonwebtoken@^8.3.0, jsonwebtoken@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== @@ -6463,11 +6471,6 @@ lodash.flattendeep@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -6523,11 +6526,6 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= -lodash.set@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -6620,6 +6618,13 @@ lru-cache@^4.0.0, lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + ls-archive@^1.2.3: version "1.3.4" resolved "https://registry.yarnpkg.com/ls-archive/-/ls-archive-1.3.4.tgz#52150919dab1acb094cdcef9dde9c66934a4650f" @@ -6637,7 +6642,7 @@ macos-release@^2.2.0: resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f" integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA== -magic-string@^0.25.2, magic-string@^0.25.3, magic-string@^0.25.5: +magic-string@^0.25.2, magic-string@^0.25.5: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== @@ -6986,13 +6991,6 @@ minipass@^2.2.0, minipass@^2.3.5, minipass@^2.6.0, minipass@^2.8.6, minipass@^2. safe-buffer "^5.1.2" yallist "^3.0.0" -minipass@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.1.tgz#7607ce778472a185ad6d89082aa2070f79cedcd5" - integrity sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w== - dependencies: - yallist "^4.0.0" - minizlib@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" @@ -7548,11 +7546,6 @@ object.values@^1.1.0, object.values@^1.1.1: function-bind "^1.1.1" has "^1.0.3" -octokit-pagination-methods@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" - integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== - on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -7620,11 +7613,6 @@ os-homedir@^1.0.0, os-homedir@^1.0.1, os-homedir@^1.0.2: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-homedir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-2.0.0.tgz#a0c76bb001a8392a503cbd46e7e650b3423a923c" - integrity sha512-saRNz0DSC5C/I++gFIaJTXoFJMRwiP5zHar5vV3xQ2TkgEw6hDCcU5F272JjUylpiVgBrZNQHnfjkLabTfb92Q== - os-locale@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" @@ -8988,7 +8976,7 @@ regexpp@^2.0.1: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== -regexpu-core@^4.5.4, regexpu-core@^4.7.0: +regexpu-core@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ== @@ -9369,14 +9357,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rollup-plugin-buble@^0.19.8: - version "0.19.8" - resolved "https://registry.yarnpkg.com/rollup-plugin-buble/-/rollup-plugin-buble-0.19.8.tgz#f9232e2bb62a7573d04f9705c1bd6f02c2a02c6a" - integrity sha512-8J4zPk2DQdk3rxeZvxgzhHh/rm5nJkjwgcsUYisCQg1QbT5yagW+hehYEW7ZNns/NVbDCTv4JQ7h4fC8qKGOKw== - dependencies: - buble "^0.19.8" - rollup-pluginutils "^2.3.3" - rollup-plugin-commonjs@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz#417af3b54503878e084d127adf4d1caf8beb86fb" @@ -9445,7 +9425,7 @@ rollup-plugin-unassert@^0.3.0: rollup-pluginutils "^2.5.0" unassert "^1.5.1" -rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.3.3, rollup-pluginutils@^2.5.0, rollup-pluginutils@^2.6.0, rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2: +rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.5.0, rollup-pluginutils@^2.6.0, rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2: version "2.8.2" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== @@ -9675,7 +9655,12 @@ side-channel@^1.0.2: es-abstract "^1.17.0-next.1" object-inspect "^1.7.0" -signal-exit@^3.0.0, signal-exit@^3.0.2: +signal-exit@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= @@ -10400,15 +10385,6 @@ tap-mocha-reporter@^3.0.7: optionalDependencies: readable-stream "^2.1.5" -tap-parser@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/tap-parser/-/tap-parser-10.0.1.tgz#b63c2500eeef2be8fbf09d512914196d1f12ebec" - integrity sha512-qdT15H0DoJIi7zOqVXDn9X0gSM68JjNy1w3VemwTJlDnETjbi6SutnqmBfjDJAwkFS79NJ97gZKqie00ZCGmzg== - dependencies: - events-to-array "^1.0.1" - minipass "^3.0.0" - tap-yaml "^1.0.0" - tap-parser@^5.1.0: version "5.4.0" resolved "https://registry.yarnpkg.com/tap-parser/-/tap-parser-5.4.0.tgz#6907e89725d7b7fa6ae41ee2c464c3db43188aec" @@ -10428,13 +10404,6 @@ tap-parser@^7.0.0: js-yaml "^3.2.7" minipass "^2.2.0" -tap-yaml@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/tap-yaml/-/tap-yaml-1.0.0.tgz#4e31443a5489e05ca8bbb3e36cef71b5dec69635" - integrity sha512-Rxbx4EnrWkYk0/ztcm5u3/VznbyFJpyXO12dDBHKWiDVxy7O2Qw6MRrwO5H6Ww0U5YhRY/4C/VzWmFPhBQc4qQ== - dependencies: - yaml "^1.5.0" - tap@~12.4.1: version "12.4.1" resolved "https://registry.yarnpkg.com/tap/-/tap-12.4.1.tgz#0c50480291c8bfffe889e448a847b66a8f2fd809" @@ -11077,12 +11046,13 @@ unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.3.0: dependencies: unist-util-visit-parents "^2.0.0" -universal-user-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" - integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg== +universal-github-app-jwt@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/universal-github-app-jwt/-/universal-github-app-jwt-1.0.2.tgz#9a7305e44b2a0eb565d83d11682eebe5be8bde8b" + integrity sha512-bJ3hVBdPREry3vob+JBOjXkO76QAQkYTIJvQ62Ja7XBSrKv6v6gHaRBWADddvS0HiLF0Q6lCK1kg4ZJrj/Kl9g== dependencies: - os-name "^3.1.0" + "@types/jsonwebtoken" "^8.3.3" + jsonwebtoken "^8.5.1" universal-user-agent@^5.0.0: version "5.0.0" @@ -11468,9 +11438,9 @@ wide-align@^1.1.0: string-width "^1.0.2 || 2" windows-release@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f" - integrity sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA== + version "3.3.1" + resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.1.tgz#cb4e80385f8550f709727287bf71035e209c4ace" + integrity sha512-Pngk/RDCaI/DkuHPlGTdIkDiTAnAkyMjoQMZqRsxydNl1qGXNIoZrB7RK8g53F2tEgQBMqQJHQdYZuQEEAu54A== dependencies: execa "^1.0.0" @@ -11584,23 +11554,11 @@ yallist@^2.1.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= -yallist@^3.0.0, yallist@^3.0.3: +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.5.0: - version "1.8.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.8.3.tgz#2f420fca58b68ce3a332d0ca64be1d191dd3f87a" - integrity sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw== - dependencies: - "@babel/runtime" "^7.8.7" - yapool@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yapool/-/yapool-1.0.0.tgz#f693f29a315b50d9a9da2646a7a6645c96985b6a"