diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 266858085f4..00000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "root": true, - "extends": [ - "mourner", - "plugin:import/recommended", - "plugin:import/typescript" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module", - "createDefaultProgram": true - }, - "plugins": [ - "@typescript-eslint", - "import", - "eslint-plugin-tsdoc", - "jest" - ], - "rules": { - "no-dupe-class-members": "off", - "@typescript-eslint/no-dupe-class-members": ["error"], - "@typescript-eslint/no-unused-vars": [ - "warn", - { "argsIgnorePattern": "^_" } - ], - "@typescript-eslint/member-delimiter-style": ["error"], - "no-useless-constructor": "off", - "@typescript-eslint/no-useless-constructor": ["error"], - // Disable no-undef. It's covered by @typescript-eslint - "no-undef": "off", - // temporarily disabled due to https://github.com/babel/babel-eslint/issues/485 - "no-use-before-define": "off", - - // no-duplicate-imports doesn't play well with Flow - // https://github.com/babel/eslint-plugin-babel/issues/59 - "no-duplicate-imports": "off", - "import/no-duplicates": "error", - - // temporarily disabled for easier upgrading of dependencies - "implicit-arrow-linebreak": "off", - "arrow-parens": "off", - "arrow-body-style": "off", - "no-confusing-arrow": "off", - "no-control-regex": "off", - "no-invalid-this": "off", - "no-buffer-constructor": "off", - - "array-bracket-spacing": "error", - "consistent-return": "off", - "global-require": "off", - "import/no-commonjs": "error", - // TSC throws an error on unresolved imports; eslint doesn't understand .js extension in import statement - "import/no-unresolved": "off", - "key-spacing": "error", - "no-eq-null": "off", - "no-lonely-if": "off", - "no-new": "off", - "no-restricted-properties": [2, { - "object": "Object", - "property": "assign" - }], - "no-unused-vars": "off", - "no-warning-comments": "error", - "object-curly-spacing": ["error", "never"], - "prefer-arrow-callback": "error", - "prefer-const": ["error", {"destructuring": "all"}], - "prefer-template": "error", - "prefer-spread": "off", - // @typescript-eslint/quotes requires standard quotes rule to be turned off - "quotes": "off", - "@typescript-eslint/quotes": ["error", "single"], - "no-redeclare": "off", - "@typescript-eslint/no-redeclare": ["error"], - "space-before-function-paren": "off", - "template-curly-spacing": "error", - "no-useless-escape": "off", - // @typescript-eslint/indent requires standard indent rule to be turned off - "indent": "off", - "@typescript-eslint/indent": ["error"], - "no-multiple-empty-lines": [ "error", { - "max": 1 - }], - "import/no-relative-packages": ["error"], - "tsdoc/syntax": "warn", - // Jest https://www.npmjs.com/package/eslint-plugin-jest - "jest/no-commented-out-tests": "error", - "jest/no-disabled-tests": "warn", - "jest/no-focused-tests": "error", - "jest/prefer-to-contain": "warn", - "jest/prefer-to-have-length": "warn", - "jest/valid-expect": "error", - "jest/prefer-to-be": "warn", - "jest/no-alias-methods": "warn", - "jest/no-interpolation-in-snapshots": "warn", - "jest/no-large-snapshots": ["warn", { "maxSize": 50, "inlineMaxSize": 20 }], - "jest/no-deprecated-functions": "warn" - }, - "reportUnusedDisableDirectives": true, - "ignorePatterns": ["build/*.js", "*.json"], - "globals": { - "performance": true - }, - "env": { - "es6": true, - "browser": false - } -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d41fffb4a6..0b36abc2cb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,28 @@ ## main ### ✨ Features and improvements +- ⚠️ Changed `geometry-type` to identify "Multi-" features ([#4877](https://github.com/maplibre/maplibre-gl-js/pull/4877)) +- Add support for pitch > 90 degrees ([#4717](https://github.com/maplibre/maplibre-gl-js/issues/4717)) - _...Add new stuff here..._ ### 🐞 Bug fixes +- ⚠️ Fix order of normalizeSpriteURL and transformRequest in loadSprite ([#3897](https://github.com/maplibre/maplibre-gl-js/issues/3897)) +- ⚠️ Remove unminified prod build ([#4906](https://github.com/maplibre/maplibre-gl-js/pull/4906)) +- Fix issue where raster tile source won't fetch updates following request error ([#4890](https://github.com/maplibre/maplibre-gl-js/pull/4890)) +- _...Add new stuff here..._ + +## v5.0.0-pre.3 + +### ✨ Features and improvements + +- Add support for camera roll angle ([#4717](https://github.com/maplibre/maplibre-gl-js/issues/4717)) + +### 🐞 Bug fixes + - Fix text not being hidden behind the globe when overlap mode was set to `always` ([#4802](https://github.com/maplibre/maplibre-gl-js/issues/4802)) +- Fix 3D models in custom layers not being properly occluded by the globe ([#4817](https://github.com/maplibre/maplibre-gl-js/issues/4817)) - Fix a single white frame being displayed when the map internally transitions from mercator to globe projection ([#4816](https://github.com/maplibre/maplibre-gl-js/issues/4816)) -- _...Add new stuff here..._ +- Fix loading of RTL plugin version 0.3.0 ([#4860](https://github.com/maplibre/maplibre-gl-js/pull/4860)) ## 5.0.0-pre.2 diff --git a/build/generate-doc-images.ts b/build/generate-doc-images.ts index fdf99c7da69..5ce312095c7 100644 --- a/build/generate-doc-images.ts +++ b/build/generate-doc-images.ts @@ -34,7 +34,7 @@ async function createImage(exampleName) { const waitTime = (exampleName.includes('3d-model') || exampleName.includes('globe')) ? 5000 : 1500; console.log(`waiting for ${waitTime} ms`); await new Promise(resolve => setTimeout(resolve, waitTime)); - } catch (err) { + } catch { // map.loaded() does not evaluate to true within 3 seconds, it's probably an animated example. // In this case we take the screenshot immediately. console.log(`Timed out waiting for map load on ${exampleName}.`); diff --git a/build/generate-struct-arrays.ts b/build/generate-struct-arrays.ts index 257e06bdeb8..009a12df79b 100644 --- a/build/generate-struct-arrays.ts +++ b/build/generate-struct-arrays.ts @@ -407,7 +407,7 @@ export class ${structArrayClass} extends ${structArrayLayoutClass} {`); output.push( ` /** * Return the ${structTypeClass} at the given location in the array. - * @param index The index of the element. + * @param index - The index of the element. */ get(index: number): ${structTypeClass} { return new ${structTypeClass}(this, index); diff --git a/build/rollup_plugins.ts b/build/rollup_plugins.ts index d5d525b79b3..d1b0a464ebb 100644 --- a/build/rollup_plugins.ts +++ b/build/rollup_plugins.ts @@ -16,7 +16,7 @@ export const nodeResolve = resolve({ preferBuiltins: false }); -export const plugins = (production: boolean, minified: boolean): Plugin[] => [ +export const plugins = (production: boolean): Plugin[] => [ json(), // https://github.com/zaach/jison/issues/351 replace({ @@ -31,9 +31,8 @@ export const plugins = (production: boolean, minified: boolean): Plugin[] => [ sourceMap: true, functions: ['PerformanceUtils.*'] }), - minified && terser({ + terser({ compress: { - // eslint-disable-next-line camelcase pure_getters: true, passes: 3 }, diff --git a/developer-guides/assets/center-point_high-pitch.png b/developer-guides/assets/center-point_high-pitch.png new file mode 100644 index 00000000000..f781f85e2a3 Binary files /dev/null and b/developer-guides/assets/center-point_high-pitch.png differ diff --git a/developer-guides/assets/center-point_nominal.png b/developer-guides/assets/center-point_nominal.png new file mode 100644 index 00000000000..14a0c55b33e Binary files /dev/null and b/developer-guides/assets/center-point_nominal.png differ diff --git a/developer-guides/assets/center-point_straight-up.png b/developer-guides/assets/center-point_straight-up.png new file mode 100644 index 00000000000..1e05797d471 Binary files /dev/null and b/developer-guides/assets/center-point_straight-up.png differ diff --git a/developer-guides/assets/center-point_underground.png b/developer-guides/assets/center-point_underground.png new file mode 100644 index 00000000000..f64e8de7c4b Binary files /dev/null and b/developer-guides/assets/center-point_underground.png differ diff --git a/developer-guides/center-point.md b/developer-guides/center-point.md new file mode 100644 index 00000000000..0bad09c8ead --- /dev/null +++ b/developer-guides/center-point.md @@ -0,0 +1,32 @@ +# How Camera position is calculated + +This guide describes how camera position is calculated from the center point, zoom, and camera rotation. +The `Transform` variables `center`, `elevation`, `zoom`, `pitch`, `bearing`, and `fov` control the location of the camera indirectly. + + `elevation` sets the height of the "center point" above sea level. In the typical use case (`centerClampedToGround = true`), the library modifies `elevation` in an attempt to keep the center point always on the terrain (or 0 MSL if no terrain is enabled). When `centerClampedToGround = false`, the user provides the elevation of the center point. + +`zoom` sets the distance from the center point to the camera (in conjunction with `fovInRadians`, which is currently hardcoded). + +Together, `zoom`, `elevation`, and `pitch` set the altitude of the camera: + +See `MercatorTransform::getCameraAltitude()`: +```typescript + getCameraAltitude(): number { + const altitude = Math.cos(this.pitchInRadians) * this._cameraToCenterDistance / this._helper._pixelPerMeter; + return altitude + this.elevation; + } +``` + +![image](assets/center-point_nominal.png) + +To allow pitch > 90, the "center point" must be placed off of the ground. This will allow the camera to stay above the ground when it pitches above 90. This requires setting `centerClampedToGround = false`. + +![image](assets/center-point_high-pitch.png) + +The same math applies whether the center point is on terrain or not, and whether the camera is above or below the ground: + +![image](assets/center-point_straight-up.png) +![image](assets/center-point_underground.png) + + +To help users position the camera, `Camera` exports the function `calculateCameraOptionsFromCameraLngLatAltRotation()`. \ No newline at end of file diff --git a/docs/assets/examples/add-3d-model-threejs-with-shadow.png b/docs/assets/examples/add-3d-model-threejs-with-shadow.png new file mode 100644 index 00000000000..13ab758a35e Binary files /dev/null and b/docs/assets/examples/add-3d-model-threejs-with-shadow.png differ diff --git a/docs/assets/examples/center-point.png b/docs/assets/examples/center-point.png new file mode 100644 index 00000000000..3a3b2b540be Binary files /dev/null and b/docs/assets/examples/center-point.png differ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000000..9590d35e19a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,157 @@ +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import stylisticTs from '@stylistic/eslint-plugin-ts' +import tsdoc from 'eslint-plugin-tsdoc'; +import jest from 'eslint-plugin-jest'; +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import react from 'eslint-plugin-react'; +import html from 'eslint-plugin-html'; + +export default [ + { + ignores: ['build/*.js', 'staging/**', 'coverage/**', 'node_modules/**', 'docs/**', 'dist/**'] + }, + { + ignores: ['test/bench/**'], + files: ['**/*.ts', '**/*.js'], + plugins: { + '@typescript-eslint': typescriptEslint, + '@stylistic': stylisticTs, + tsdoc, + jest, + }, + + linterOptions: { + reportUnusedDisableDirectives: true, + }, + + languageOptions: { + globals: { + ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, 'off'])), + performance: true, + }, + + parser: tsParser, + ecmaVersion: 5, + sourceType: 'module', + + parserOptions: { + createDefaultProgram: true, + }, + }, + + rules: { + 'no-dupe-class-members': 'off', + '@typescript-eslint/no-dupe-class-members': ['error'], + + '@typescript-eslint/no-unused-vars': ['warn', { + argsIgnorePattern: '^_', + }], + + '@stylistic/member-delimiter-style': ['error'], + 'no-useless-constructor': 'off', + '@typescript-eslint/no-useless-constructor': ['error'], + 'no-undef': 'off', + 'no-use-before-define': 'off', + 'no-duplicate-imports': 'off', + 'implicit-arrow-linebreak': 'off', + 'arrow-parens': 'off', + 'arrow-body-style': 'off', + 'no-confusing-arrow': 'off', + 'no-control-regex': 'off', + 'no-invalid-this': 'off', + 'no-buffer-constructor': 'off', + 'array-bracket-spacing': 'error', + 'consistent-return': 'off', + 'global-require': 'off', + 'key-spacing': 'error', + 'no-eq-null': 'off', + 'no-lonely-if': 'off', + 'no-new': 'off', + + 'no-restricted-properties': [2, { + object: 'Object', + property: 'assign', + }], + + 'no-unused-vars': 'off', + 'no-warning-comments': 'error', + 'object-curly-spacing': ['error', 'never'], + 'prefer-arrow-callback': 'error', + + 'prefer-const': ['error', { + destructuring: 'all', + }], + + 'prefer-template': 'error', + 'prefer-spread': 'off', + quotes: 'off', + '@stylistic/quotes': ['error', 'single'], + 'no-redeclare': 'off', + '@typescript-eslint/no-redeclare': ['error'], + 'space-before-function-paren': 'off', + 'template-curly-spacing': 'error', + 'no-useless-escape': 'off', + indent: 'off', + '@stylistic/indent': ['error'], + + 'no-multiple-empty-lines': ['error', { + max: 1, + }], + + 'tsdoc/syntax': 'warn', + 'jest/no-commented-out-tests': 'error', + 'jest/no-disabled-tests': 'warn', + 'jest/no-focused-tests': 'error', + 'jest/prefer-to-contain': 'warn', + 'jest/prefer-to-have-length': 'warn', + 'jest/valid-expect': 'error', + 'jest/prefer-to-be': 'warn', + 'jest/no-alias-methods': 'warn', + 'jest/no-interpolation-in-snapshots': 'warn', + + 'jest/no-large-snapshots': ['warn', { + maxSize: 50, + inlineMaxSize: 20, + }], + + 'jest/no-deprecated-functions': 'warn', + }, + }, + { + files: ['**/*.html'], + plugins: { + html + }, + rules: { + 'no-restricted-properties': 'off', + 'new-cap': 'off', + '@typescript-eslint/no-unused-vars': 'off' + } + }, + { + files: ['test/bench/**/*.jsx', 'test/bench/**/*.js', 'test/bench/**/*.ts'], + plugins: { + react + }, + rules: { + 'react/jsx-uses-vars': [2], + 'no-restricted-properties': 'off' + }, + languageOptions: { + globals: { + ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, 'off'])), + performance: true, + }, + + parser: tsParser, + ecmaVersion: 5, + sourceType: 'module', + + parserOptions: { + createDefaultProgram: true, + }, + }, + }, + +]; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4e6e941ddca..f55e54d28ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "maplibre-gl", - "version": "5.0.0-pre.2", + "version": "5.0.0-pre.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "maplibre-gl", - "version": "5.0.0-pre.2", + "version": "5.0.0-pre.3", "license": "BSD-3-Clause", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", @@ -16,7 +16,7 @@ "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^20.4.0", + "@maplibre/maplibre-gl-style-spec": "^21.0.0", "@types/geojson": "^7946.0.14", "@types/geojson-vt": "3.2.5", "@types/mapbox__point-geometry": "^0.1.4", @@ -46,50 +46,51 @@ "@rollup/plugin-strip": "^3.0.4", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.1", + "@stylistic/eslint-plugin-ts": "^2.9.0", "@types/benchmark": "^2.1.5", - "@types/cssnano": "^5.0.0", + "@types/cssnano": "^5.1.3", "@types/d3": "^7.4.3", "@types/diff": "^5.2.3", "@types/earcut": "^2.1.4", - "@types/eslint": "^8.56.7", + "@types/eslint": "^9.6.1", "@types/gl": "^6.0.5", "@types/glob": "^8.1.0", - "@types/jest": "^29.5.13", + "@types/jest": "^29.5.14", "@types/jsdom": "^21.1.7", "@types/minimist": "^1.2.5", "@types/murmurhash-js": "^1.0.6", "@types/nise": "^1.4.5", - "@types/node": "^22.7.6", + "@types/node": "^22.8.1", "@types/offscreencanvas": "^2019.7.3", "@types/pixelmatch": "^5.2.6", "@types/pngjs": "^6.0.5", - "@types/react": "^18.3.11", + "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/request": "^2.48.12", "@types/shuffle-seed": "^1.1.3", "@types/window-or-global": "^1.0.6", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/eslint-plugin": "^8.11.0", + "@typescript-eslint/parser": "^8.11.0", "address": "^2.0.3", "autoprefixer": "^10.4.20", "benchmark": "^2.1.4", "canvas": "^2.11.2", - "cspell": "^8.15.3", + "cspell": "^8.15.4", "cssnano": "^7.0.6", "d3": "^7.9.0", "d3-queue": "^3.0.7", - "devtools-protocol": "^0.0.1368592", + "devtools-protocol": "^0.0.1373723", "diff": "^7.0.0", "dts-bundle-generator": "^9.5.1", - "eslint": "^8.57.0", - "eslint-config-mourner": "^3.0.0", + "eslint": "^9.13.0", "eslint-plugin-html": "^8.1.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.8.3", - "eslint-plugin-react": "^7.37.1", + "eslint-plugin-react": "^7.37.2", "eslint-plugin-tsdoc": "0.3.0", "expect": "^29.7.0", "glob": "^11.0.0", + "globals": "^15.11.0", "is-builtin-module": "^4.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -114,13 +115,13 @@ "puppeteer": "^23.6.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "rollup": "^4.24.0", + "rollup": "^4.24.2", "rollup-plugin-sourcemaps2": "^0.4.2", "rw": "^1.3.3", "semver": "^7.6.3", "shuffle-seed": "^1.1.6", "source-map-explorer": "^2.5.3", - "st": "^3.0.0", + "st": "^3.0.1", "stylelint": "^16.10.0", "stylelint-config-standard": "^36.0.1", "ts-jest": "^29.2.5", @@ -700,6 +701,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", @@ -721,9 +731,9 @@ "dev": true }, "node_modules/@cspell/cspell-bundled-dicts": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.15.3.tgz", - "integrity": "sha512-wIuJomJEJn60w+ts4dFBYEo3kkwdPe1R4qVn52hDq5CUzrNniSywCpeBQO8Sgy5ljk73ojENbMBCE8+Jrukk0Q==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.15.4.tgz", + "integrity": "sha512-t5b2JwGeUmzmjl319mCuaeKGxTvmzLLRmrpdHr+ZZGRO4nf7L48Lbe9A6uwNUvsZe0cXohiNXsrrsuzRVXswVA==", "dev": true, "dependencies": { "@cspell/dict-ada": "^4.0.5", @@ -736,7 +746,7 @@ "@cspell/dict-css": "^4.0.16", "@cspell/dict-dart": "^2.2.4", "@cspell/dict-django": "^4.1.3", - "@cspell/dict-docker": "^1.1.10", + "@cspell/dict-docker": "^1.1.11", "@cspell/dict-dotnet": "^5.0.8", "@cspell/dict-elixir": "^4.0.6", "@cspell/dict-en_us": "^4.3.26", @@ -767,17 +777,17 @@ "@cspell/dict-php": "^4.0.13", "@cspell/dict-powershell": "^5.0.13", "@cspell/dict-public-licenses": "^2.0.11", - "@cspell/dict-python": "^4.2.11", + "@cspell/dict-python": "^4.2.12", "@cspell/dict-r": "^2.0.4", "@cspell/dict-ruby": "^5.0.7", "@cspell/dict-rust": "^4.0.9", "@cspell/dict-scala": "^5.0.6", - "@cspell/dict-software-terms": "^4.1.10", + "@cspell/dict-software-terms": "^4.1.11", "@cspell/dict-sql": "^2.1.8", "@cspell/dict-svelte": "^1.0.5", "@cspell/dict-swift": "^2.0.4", "@cspell/dict-terraform": "^1.0.5", - "@cspell/dict-typescript": "^3.1.9", + "@cspell/dict-typescript": "^3.1.10", "@cspell/dict-vue": "^3.0.3" }, "engines": { @@ -785,30 +795,30 @@ } }, "node_modules/@cspell/cspell-json-reporter": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.15.3.tgz", - "integrity": "sha512-348UsYDdPTaGUvzdRY+Tj8rnN/1yLFkbEc6FkTejkBPwL0rpaJLp27N3Y8uZfrFGyMem/fYS+aHAvB3kTIzieQ==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.15.4.tgz", + "integrity": "sha512-solraYhZG4l++NeVCOtpc8DMvwHc46TmJt8DQbgvKtk6wOjTEcFrwKfA6Ei9YKbvyebJlpWMenO3goOll0loYg==", "dev": true, "dependencies": { - "@cspell/cspell-types": "8.15.3" + "@cspell/cspell-types": "8.15.4" }, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-pipe": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.15.3.tgz", - "integrity": "sha512-Xodpkm1HJjGtmlL+V4B06PbeEsfhZtNwvPLTtaMExP4ED78VimBYlSz3lR+8jZgkHvZOhbQuHw7zwBqQd4u4Mg==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.15.4.tgz", + "integrity": "sha512-WfCmZVFC6mX6vYlf02hWwelcSBTbDQgc5YqY+1miuMk+OHSUAHSACjZId6/a4IAID94xScvFfj7jgrdejUVvIQ==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-resolver": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.15.3.tgz", - "integrity": "sha512-KoSv9iGbItS1uGlXspTg9XQmbBnjR7wkW5Du9Q3pLYAjSwcmArOVqQnumNAPfTsIldn9WsBalwGSm/uwawxAPg==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.15.4.tgz", + "integrity": "sha512-Zr428o+uUTqywrdKyjluJVnDPVAJEqZ1chQLKIrHggUah1cgs5aQ7rZ+0Rv5euYMlC2idZnP7IL6TDaIib80oA==", "dev": true, "dependencies": { "global-directory": "^4.0.1" @@ -818,18 +828,18 @@ } }, "node_modules/@cspell/cspell-service-bus": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.15.3.tgz", - "integrity": "sha512-BLAaAc9fWn/sdNo/Z7bPwHtQ+z7snUnjXoLHYY9Vg8N0K2nMYkuJqAm7xbeKDy64sLykpu+pubdMR3DqEQJo/g==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.15.4.tgz", + "integrity": "sha512-pXYofnV/V9Y3LZdfFGbmhdxPX/ABjiD3wFjGHt5YhIU9hjVVuvjFlgY7pH2AvRjs4F8xKXv1ReWl44BJOL9gLA==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-types": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.15.3.tgz", - "integrity": "sha512-05xy2eeIQIHk2X6hUfBPBNbCnWcuSjE6D/F0XFTxLBl4ecUurSthJqvR3PrMjluETeZ71/cRIZMBnW+v7+yBgw==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.15.4.tgz", + "integrity": "sha512-1hDtgYDQVW11zgtrr12EmGW45Deoi7IjZOhzPFLb+3WkhZ46ggWdbrRalWwBolQPDDo6+B2Q6WXz5hdND+Tpwg==", "dev": true, "engines": { "node": ">=18" @@ -1163,9 +1173,9 @@ "dev": true }, "node_modules/@cspell/dynamic-import": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.15.3.tgz", - "integrity": "sha512-RG35KnoLSFRj9BwmDW3AmkEbh6NxkZrhRxCQ8s4ZfMl5QEkoYKdVpWYSPUBMlPqkq0U+SVggMvNbdMSqyITxxQ==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.15.4.tgz", + "integrity": "sha512-tr0F6EYN6qtniNvt1Uib+PgYQHeo4dQHXE2Optap+hYTOoQ2VoQ+SwBVjZ+Q2bmSAB0fmOyf0AvgsUtnWIpavw==", "dev": true, "dependencies": { "import-meta-resolve": "^4.1.0" @@ -1175,27 +1185,27 @@ } }, "node_modules/@cspell/filetypes": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/@cspell/filetypes/-/filetypes-8.15.3.tgz", - "integrity": "sha512-2B22/c2/pVqS2p3latOj3zCHk7vUWsxwkhCKhOKMA2tKt2cc7MHKUKMfsX4XpfY/571S/TTy1YYeGXlAxUtF3g==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@cspell/filetypes/-/filetypes-8.15.4.tgz", + "integrity": "sha512-sNl6jr3ym/4151EY76qlI/00HHsiLZBqW7Vb1tqCzsgSg3EpL30ddjr74So6Sg2PN26Yf09hvxGTJzXn1R4aYw==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/strong-weak-map": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.15.3.tgz", - "integrity": "sha512-IhH+Reh2P+QXj8i5qGYsFI3Z01IWYMqUuN6CLnFXx5W0R8tWtxvmwWyT7j8lchV5foHSs8+mWaijKzwS6FSFVQ==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.15.4.tgz", + "integrity": "sha512-m5DeQksbhJFqcSYF8Q0Af/WXmXCMAJocCUShkzOXK+uZNXnvhBZN7VyQ9hL+GRzX8JTPEPdVcz2lFyVE5p+LzQ==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/url": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/@cspell/url/-/url-8.15.3.tgz", - "integrity": "sha512-JuQdGHj+W5anXpc+2pYggoUd+LsbNac4Rc7PdUUTnzMxV1EvlQZs28jEK4y27i4RI5pNQmsUWGZTHeMuwdlgSQ==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@cspell/url/-/url-8.15.4.tgz", + "integrity": "sha512-K2oZu/oLQPs5suRpLS8uu04O3YMUioSlEU1D66fRoOxzI5NzLt7i7yMg3HQHjChGa09N5bzqmrVdhmQrRZXwGg==", "dev": true, "engines": { "node": ">=18.0" @@ -1333,24 +1343,47 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -1358,7 +1391,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1371,15 +1404,12 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1397,39 +1427,56 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/@eslint/js": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", "dev": true, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz", + "integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==", + "dev": true, + "dependencies": { + "levn": "^0.4.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=18.18.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -1444,11 +1491,18 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -2035,9 +2089,9 @@ } }, "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", - "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-21.0.0.tgz", + "integrity": "sha512-uV762lmulFyBHqAD3eVqIYgwwi/p+cz+LkjmHgHfRWvNKMik+0vBaL6Bxio4/YdheKidv3+bm25kabOBf+/8EA==", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", @@ -2374,9 +2428,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.2.tgz", + "integrity": "sha512-ufoveNTKDg9t/b7nqI3lwbCG/9IJMhADBNjjz/Jn6LxIZxD7T5L8l2uO/wD99945F1Oo8FvgbbZJRguyk/BdzA==", "cpu": [ "arm" ], @@ -2387,9 +2441,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.2.tgz", + "integrity": "sha512-iZoYCiJz3Uek4NI0J06/ZxUgwAfNzqltK0MptPDO4OR0a88R4h0DSELMsflS6ibMCJ4PnLvq8f7O1d7WexUvIA==", "cpu": [ "arm64" ], @@ -2400,9 +2454,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.2.tgz", + "integrity": "sha512-/UhrIxobHYCBfhi5paTkUDQ0w+jckjRZDZ1kcBL132WeHZQ6+S5v9jQPVGLVrLbNUebdIRpIt00lQ+4Z7ys4Rg==", "cpu": [ "arm64" ], @@ -2413,9 +2467,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.2.tgz", + "integrity": "sha512-1F/jrfhxJtWILusgx63WeTvGTwE4vmsT9+e/z7cZLKU8sBMddwqw3UV5ERfOV+H1FuRK3YREZ46J4Gy0aP3qDA==", "cpu": [ "x64" ], @@ -2425,10 +2479,36 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.2.tgz", + "integrity": "sha512-1YWOpFcGuC6iGAS4EI+o3BV2/6S0H+m9kFOIlyFtp4xIX5rjSnL3AwbTBxROX0c8yWtiWM7ZI6mEPTI7VkSpZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.2.tgz", + "integrity": "sha512-3qAqTewYrCdnOD9Gl9yvPoAoFAVmPJsBvleabvx4bnu1Kt6DrB2OALeRVag7BdWGWLhP1yooeMLEi6r2nYSOjg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.2.tgz", + "integrity": "sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig==", "cpu": [ "arm" ], @@ -2439,9 +2519,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.2.tgz", + "integrity": "sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw==", "cpu": [ "arm" ], @@ -2452,9 +2532,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.2.tgz", + "integrity": "sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ==", "cpu": [ "arm64" ], @@ -2465,9 +2545,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.2.tgz", + "integrity": "sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA==", "cpu": [ "arm64" ], @@ -2478,9 +2558,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.2.tgz", + "integrity": "sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw==", "cpu": [ "ppc64" ], @@ -2491,9 +2571,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.2.tgz", + "integrity": "sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg==", "cpu": [ "riscv64" ], @@ -2504,9 +2584,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.2.tgz", + "integrity": "sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q==", "cpu": [ "s390x" ], @@ -2517,9 +2597,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.2.tgz", + "integrity": "sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ==", "cpu": [ "x64" ], @@ -2530,9 +2610,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.2.tgz", + "integrity": "sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q==", "cpu": [ "x64" ], @@ -2543,9 +2623,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.2.tgz", + "integrity": "sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw==", "cpu": [ "arm64" ], @@ -2556,9 +2636,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.2.tgz", + "integrity": "sha512-ZhcrakbqA1SCiJRMKSU64AZcYzlZ/9M5LaYil9QWxx9vLnkQ9Vnkve17Qn4SjlipqIIBFKjBES6Zxhnvh0EAEw==", "cpu": [ "ia32" ], @@ -2569,9 +2649,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.2.tgz", + "integrity": "sha512-2mLH46K1u3r6uwc95hU+OR9q/ggYMpnS7pSp83Ece1HUQgF9Nh/QwTK5rcgbFnV9j+08yBrU5sA/P0RK2MSBNA==", "cpu": [ "x64" ], @@ -2680,6 +2760,35 @@ "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", "dev": true }, + "node_modules/@stylistic/eslint-plugin-ts": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.9.0.tgz", + "integrity": "sha512-CxB73paAKlmaIDtOfKoIHlhNJVlyRMVobuBqdOc4wbVSqfhbgpCWuJYpBkV3ydGDKRfVWNJ9yg5b99lzZtrjhg==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^8.8.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "dev": true, @@ -2776,9 +2885,11 @@ "license": "MIT" }, "node_modules/@types/cssnano": { - "version": "5.1.0", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/cssnano/-/cssnano-5.1.3.tgz", + "integrity": "sha512-BahAZSSvuFXyhgJiwQgsfsNlStE9K/ULGL+YEzK4mmL2Vf02Pjl2yZs+KmbkAg3MxkC9WwMuFwuwnwvrg7CqvQ==", + "deprecated": "This is a stub types definition. cssnano provides its own type definitions, so you do not need this installed.", "dev": true, - "license": "MIT", "dependencies": { "cssnano": "*" } @@ -3019,9 +3130,9 @@ "dev": true }, "node_modules/@types/eslint": { - "version": "8.56.7", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz", - "integrity": "sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "dependencies": { "@types/estree": "*", @@ -3103,9 +3214,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.13", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", - "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -3184,12 +3295,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.7.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", - "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", + "version": "22.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", + "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", "dev": true, "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.19.8" } }, "node_modules/@types/offscreencanvas": { @@ -3227,9 +3338,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", - "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -3325,31 +3436,31 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", + "integrity": "sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/type-utils": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -3358,26 +3469,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.11.0.tgz", + "integrity": "sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -3386,16 +3497,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz", + "integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -3403,26 +3514,23 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.11.0.tgz", + "integrity": "sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/utils": "8.11.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^8.56.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -3430,12 +3538,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz", + "integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==", "dev": true, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -3443,22 +3551,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz", + "integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -3495,38 +3603,38 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz", + "integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.11.0.tgz", + "integrity": "sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/types": "8.11.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -4901,24 +5009,24 @@ } }, "node_modules/cspell": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.15.3.tgz", - "integrity": "sha512-diJiVBMunG3m7dYJpelSTsO4biLxVZ3glzLJF9jVs8n86dph04A9NXf29DKeAR0py62CFBKOrB1R4VGhxDxStg==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.15.4.tgz", + "integrity": "sha512-hUOxcwmNWuHzVeGHyN5v/T9MkyCE5gi0mvatxsM794B2wOuR1ZORgjZH62P2HY1uBkXe/x5C6ITWrSyh0WgAcg==", "dev": true, "dependencies": { - "@cspell/cspell-json-reporter": "8.15.3", - "@cspell/cspell-pipe": "8.15.3", - "@cspell/cspell-types": "8.15.3", - "@cspell/dynamic-import": "8.15.3", - "@cspell/url": "8.15.3", + "@cspell/cspell-json-reporter": "8.15.4", + "@cspell/cspell-pipe": "8.15.4", + "@cspell/cspell-types": "8.15.4", + "@cspell/dynamic-import": "8.15.4", + "@cspell/url": "8.15.4", "chalk": "^5.3.0", "chalk-template": "^1.1.0", "commander": "^12.1.0", - "cspell-dictionary": "8.15.3", - "cspell-gitignore": "8.15.3", - "cspell-glob": "8.15.3", - "cspell-io": "8.15.3", - "cspell-lib": "8.15.3", + "cspell-dictionary": "8.15.4", + "cspell-gitignore": "8.15.4", + "cspell-glob": "8.15.4", + "cspell-io": "8.15.4", + "cspell-lib": "8.15.4", "fast-json-stable-stringify": "^2.1.0", "file-entry-cache": "^9.1.0", "get-stdin": "^9.0.0", @@ -4937,12 +5045,12 @@ } }, "node_modules/cspell-config-lib": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.15.3.tgz", - "integrity": "sha512-IMIvZ/2fzl628obiFhcgNsorcS4pimAgDi9M0k9GDA/zbLeweWZqjmSEN9tgUPvkRznQvJd0TZXJ0B5RkM5+2Q==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.15.4.tgz", + "integrity": "sha512-vUgikQTRkRMTdkZqSs7F2cTdPpX61cTjr/9L/VCkXkbW38ObCr4650ioiF1Wq3zDF3Gy2bc4ECTpD2PZUXX5SA==", "dev": true, "dependencies": { - "@cspell/cspell-types": "8.15.3", + "@cspell/cspell-types": "8.15.4", "comment-json": "^4.2.5", "yaml": "^2.6.0" }, @@ -4951,14 +5059,14 @@ } }, "node_modules/cspell-dictionary": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.15.3.tgz", - "integrity": "sha512-FwYpDou0oyHmfjz70juVvIskZo1/+Xzq4s6eX2ZjUNQSp/jaykWNOiqIw5eVx0Z3sq3cWzCJ9zUuHcXxvFi7EQ==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.15.4.tgz", + "integrity": "sha512-8+p/l9Saac7qyCbqtELneDoT7CwHu9gYmnI8uXMu34/lPGjhVhy10ZeI0+t1djaO2YyASK400YFKq5uP/5KulA==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.15.3", - "@cspell/cspell-types": "8.15.3", - "cspell-trie-lib": "8.15.3", + "@cspell/cspell-pipe": "8.15.4", + "@cspell/cspell-types": "8.15.4", + "cspell-trie-lib": "8.15.4", "fast-equals": "^5.0.1" }, "engines": { @@ -4966,14 +5074,14 @@ } }, "node_modules/cspell-gitignore": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.15.3.tgz", - "integrity": "sha512-h1O9y3F81e8RyDE87Bv7m6Faz4FjkUhr72QqakbGuhRsjembST5YEw1B5Okc4BSUQhRER+dEh4xLp90EmNzZuw==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.15.4.tgz", + "integrity": "sha512-9n5PpQ8gEf8YcvEtoZGZ2Ma6wnqSFkD2GrmyjISy39DfIX/jNLN7GX2wJm6OD2P4FjXer95ypmIb/JWTlfmbTw==", "dev": true, "dependencies": { - "@cspell/url": "8.15.3", - "cspell-glob": "8.15.3", - "cspell-io": "8.15.3", + "@cspell/url": "8.15.4", + "cspell-glob": "8.15.4", + "cspell-io": "8.15.4", "find-up-simple": "^1.0.0" }, "bin": { @@ -4984,12 +5092,12 @@ } }, "node_modules/cspell-glob": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.15.3.tgz", - "integrity": "sha512-nsxe1PCFZyOHxVeo3Bqi2MyVy2JASF9p1xSCZAFjiVjeRmeqDrS098UcoucXRDFScJ2RP8A62niC6P3m6qg5IA==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.15.4.tgz", + "integrity": "sha512-TTfRRHRAN+PN9drIz4MAEgKKYnPThBOlPMdFddyuisvU33Do1sPAnqkkOjTEFdi3jAA5KwnSva68SVH6IzzMBQ==", "dev": true, "dependencies": { - "@cspell/url": "8.15.3", + "@cspell/url": "8.15.4", "micromatch": "^4.0.8" }, "engines": { @@ -4997,13 +5105,13 @@ } }, "node_modules/cspell-grammar": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.15.3.tgz", - "integrity": "sha512-HCtGzym6JsdrYjwGySxKFsLjvASAgftv7nEOTBFp/u3Y2zVPmoQaFmmlSWBbJRsNQa9elL0DyQOSC7WC6GbGSQ==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.15.4.tgz", + "integrity": "sha512-MKiKyYi05mRtXOxPoTv3Ksi0GwYLiK84Uq0C+5PaMrnIjXeed0bsddSFXCT+7ywFJc7PdjhTtz0M/9WWK3UgbA==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.15.3", - "@cspell/cspell-types": "8.15.3" + "@cspell/cspell-pipe": "8.15.4", + "@cspell/cspell-types": "8.15.4" }, "bin": { "cspell-grammar": "bin.mjs" @@ -5013,40 +5121,40 @@ } }, "node_modules/cspell-io": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.15.3.tgz", - "integrity": "sha512-ycKrfTSurfleQNR5x7QRmQ/qtMJ6JkBiqaq5qtCHNYUlOjrmQBNVXFpbmPJ3+qG+ObW+eQKEvZH5xr17F2BMjw==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.15.4.tgz", + "integrity": "sha512-rXIEREPTFV9dwwg4EKfvzqlCNOvT6910AYED5YrSt8Y68usRJ9lbqdx0BrDndVCd33bp1o+9JBfHuRiFIQC81g==", "dev": true, "dependencies": { - "@cspell/cspell-service-bus": "8.15.3", - "@cspell/url": "8.15.3" + "@cspell/cspell-service-bus": "8.15.4", + "@cspell/url": "8.15.4" }, "engines": { "node": ">=18" } }, "node_modules/cspell-lib": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.15.3.tgz", - "integrity": "sha512-LJIEZ3E8ZCOAqcMwkonXH3NEa6ITTlp3nZ9nhmxHE0GbMUYP5/CmTCWSnJUjrINmhjl5dMj6Z7xzDTgEAu0K9Q==", - "dev": true, - "dependencies": { - "@cspell/cspell-bundled-dicts": "8.15.3", - "@cspell/cspell-pipe": "8.15.3", - "@cspell/cspell-resolver": "8.15.3", - "@cspell/cspell-types": "8.15.3", - "@cspell/dynamic-import": "8.15.3", - "@cspell/filetypes": "8.15.3", - "@cspell/strong-weak-map": "8.15.3", - "@cspell/url": "8.15.3", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.15.4.tgz", + "integrity": "sha512-iLp/625fvCyFFxSyZYLMgqHIKcrhN4hT7Hw5+ySa38Bp/OfA81ANqWHpsDQ0bGsALTRn/DHBpQYj4xCW/aN9tw==", + "dev": true, + "dependencies": { + "@cspell/cspell-bundled-dicts": "8.15.4", + "@cspell/cspell-pipe": "8.15.4", + "@cspell/cspell-resolver": "8.15.4", + "@cspell/cspell-types": "8.15.4", + "@cspell/dynamic-import": "8.15.4", + "@cspell/filetypes": "8.15.4", + "@cspell/strong-weak-map": "8.15.4", + "@cspell/url": "8.15.4", "clear-module": "^4.1.2", "comment-json": "^4.2.5", - "cspell-config-lib": "8.15.3", - "cspell-dictionary": "8.15.3", - "cspell-glob": "8.15.3", - "cspell-grammar": "8.15.3", - "cspell-io": "8.15.3", - "cspell-trie-lib": "8.15.3", + "cspell-config-lib": "8.15.4", + "cspell-dictionary": "8.15.4", + "cspell-glob": "8.15.4", + "cspell-grammar": "8.15.4", + "cspell-io": "8.15.4", + "cspell-trie-lib": "8.15.4", "env-paths": "^3.0.0", "fast-equals": "^5.0.1", "gensequence": "^7.0.0", @@ -5073,13 +5181,13 @@ } }, "node_modules/cspell-trie-lib": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.15.3.tgz", - "integrity": "sha512-sJwGFE3ymkL6UsnZbMOGcD+iDOdYo7gyVafMDUJvb4rnKqAhLJumiCPT4bPLQ7oWAti7swHBQrOJ/Wp3phQ+LQ==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.15.4.tgz", + "integrity": "sha512-sg9klsNHyrfos0Boiio+qy5d6fI9cCNjBqFYrNxvpKpwZ4gEzDzjgEKdZY1C76RD2KoBQ8I1NF5YcGc0+hhhCw==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.15.3", - "@cspell/cspell-types": "8.15.3", + "@cspell/cspell-pipe": "8.15.4", + "@cspell/cspell-types": "8.15.4", "gensequence": "^7.0.0" }, "engines": { @@ -5974,9 +6082,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1368592", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1368592.tgz", - "integrity": "sha512-62kGmMknGlLv/sYrYIWQm2chYC3wkeA62Pinll0QJDzgcFV0TBOVMgmJchTRP+8gyJNCbp36Lwnbyc745VvKSQ==", + "version": "0.0.1373723", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1373723.tgz", + "integrity": "sha512-MvgeYHFSB3/x8AcH1ibnp4XVfxef3RYrsKJGtA/h4cwIAiNEi1wj3+5Z88W7PE1tbwXrYdPFwcOJhs3a8IglKg==", "dev": true }, "node_modules/diff": { @@ -6008,17 +6116,6 @@ "node": ">=8" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -6280,9 +6377,9 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", - "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz", + "integrity": "sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==", "dev": true, "dependencies": { "call-bind": "^1.0.7", @@ -6292,12 +6389,12 @@ "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", + "globalthis": "^1.0.4", "has-property-descriptors": "^1.0.2", "has-proto": "^1.0.3", "has-symbols": "^1.0.3", "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.2", + "iterator.prototype": "^1.1.3", "safe-array-concat": "^1.1.2" }, "engines": { @@ -6408,65 +6505,65 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", + "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.13.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/eslint-config-mourner": { - "version": "3.0.0", - "dev": true, - "license": "ISC" - }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -6632,9 +6729,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", - "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", + "version": "7.37.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", + "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", "dev": true, "dependencies": { "array-includes": "^3.1.8", @@ -6642,7 +6739,7 @@ "array.prototype.flatmap": "^1.3.2", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.19", + "es-iterator-helpers": "^1.1.0", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", @@ -6710,6 +6807,22 @@ "@microsoft/tsdoc-config": "0.17.0" } }, + "node_modules/eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -6722,11 +6835,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, "node_modules/eslint/node_modules/cross-spawn": { "version": "7.0.3", "dev": true, @@ -6751,17 +6859,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6793,31 +6897,6 @@ "node": ">=10.13.0" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "dev": true, @@ -6874,17 +6953,29 @@ } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", "dev": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6916,8 +7007,9 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -7174,14 +7266,15 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, - "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/filelist": { @@ -7248,17 +7341,16 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { @@ -7775,20 +7867,25 @@ } }, "node_modules/globals": { - "version": "11.12.0", + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", "dev": true, - "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -8516,14 +8613,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -8803,9 +8892,9 @@ } }, "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", + "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", "dev": true, "dependencies": { "define-properties": "^1.2.1", @@ -8813,6 +8902,9 @@ "has-symbols": "^1.0.3", "reflect.getprototypeof": "^1.0.4", "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/jackspeak": { @@ -9037,18 +9129,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-config/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -12735,9 +12815,9 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.2.tgz", + "integrity": "sha512-do/DFGq5g6rdDhdpPq5qb2ecoczeK6y+2UAjdJ5trjQJj5f1AiVdLRWRc9A9/fFukfvJRgM0UXzxBIYMovm5ww==", "dev": true, "dependencies": { "@types/estree": "1.0.6" @@ -12750,22 +12830,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", + "@rollup/rollup-android-arm-eabi": "4.24.2", + "@rollup/rollup-android-arm64": "4.24.2", + "@rollup/rollup-darwin-arm64": "4.24.2", + "@rollup/rollup-darwin-x64": "4.24.2", + "@rollup/rollup-freebsd-arm64": "4.24.2", + "@rollup/rollup-freebsd-x64": "4.24.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.2", + "@rollup/rollup-linux-arm-musleabihf": "4.24.2", + "@rollup/rollup-linux-arm64-gnu": "4.24.2", + "@rollup/rollup-linux-arm64-musl": "4.24.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.2", + "@rollup/rollup-linux-riscv64-gnu": "4.24.2", + "@rollup/rollup-linux-s390x-gnu": "4.24.2", + "@rollup/rollup-linux-x64-gnu": "4.24.2", + "@rollup/rollup-linux-x64-musl": "4.24.2", + "@rollup/rollup-win32-arm64-msvc": "4.24.2", + "@rollup/rollup-win32-ia32-msvc": "4.24.2", + "@rollup/rollup-win32-x64-msvc": "4.24.2", "fsevents": "~2.3.2" } }, @@ -13385,9 +13467,10 @@ "license": "BSD-3-Clause" }, "node_modules/st": { - "version": "3.0.0", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/st/-/st-3.0.1.tgz", + "integrity": "sha512-dy3Ly4csMnzbQgYhxiYqENUNdjylIPl2aeEf7n+dI3eXnV2mvGWiX97zegVXa9qVAHvY8M9HWUXqpXcWwJ2fSA==", "dev": true, - "license": "ISC", "dependencies": { "async-cache": "^1.1.0", "bl": "^5.0.0", @@ -13728,6 +13811,18 @@ "node": ">=6" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stylehacks": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.4.tgz", @@ -14509,17 +14604,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -14710,9 +14794,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz", - "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, "node_modules/unicorn-magic": { @@ -15045,13 +15129,13 @@ } }, "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", "dev": true, "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.0.5", "is-finalizationregistry": "^1.0.2", @@ -15060,8 +15144,8 @@ "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index 4f08fe7cfa5..daa194defc0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "maplibre-gl", "description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library", - "version": "5.0.0-pre.2", + "version": "5.0.0-pre.3", "main": "dist/maplibre-gl.js", "style": "dist/maplibre-gl.css", "license": "BSD-3-Clause", @@ -24,7 +24,7 @@ "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^20.4.0", + "@maplibre/maplibre-gl-style-spec": "^21.0.0", "@types/geojson": "^7946.0.14", "@types/geojson-vt": "3.2.5", "@types/mapbox__point-geometry": "^0.1.4", @@ -54,50 +54,51 @@ "@rollup/plugin-strip": "^3.0.4", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.1", + "@stylistic/eslint-plugin-ts": "^2.9.0", "@types/benchmark": "^2.1.5", - "@types/cssnano": "^5.0.0", + "@types/cssnano": "^5.1.3", "@types/d3": "^7.4.3", "@types/diff": "^5.2.3", "@types/earcut": "^2.1.4", - "@types/eslint": "^8.56.7", + "@types/eslint": "^9.6.1", "@types/gl": "^6.0.5", "@types/glob": "^8.1.0", - "@types/jest": "^29.5.13", + "@types/jest": "^29.5.14", "@types/jsdom": "^21.1.7", "@types/minimist": "^1.2.5", "@types/murmurhash-js": "^1.0.6", "@types/nise": "^1.4.5", - "@types/node": "^22.7.6", + "@types/node": "^22.8.1", "@types/offscreencanvas": "^2019.7.3", "@types/pixelmatch": "^5.2.6", "@types/pngjs": "^6.0.5", - "@types/react": "^18.3.11", + "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/request": "^2.48.12", "@types/shuffle-seed": "^1.1.3", "@types/window-or-global": "^1.0.6", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/eslint-plugin": "^8.11.0", + "@typescript-eslint/parser": "^8.11.0", "address": "^2.0.3", "autoprefixer": "^10.4.20", "benchmark": "^2.1.4", "canvas": "^2.11.2", - "cspell": "^8.15.3", + "cspell": "^8.15.4", "cssnano": "^7.0.6", "d3": "^7.9.0", "d3-queue": "^3.0.7", - "devtools-protocol": "^0.0.1368592", + "devtools-protocol": "^0.0.1373723", "diff": "^7.0.0", "dts-bundle-generator": "^9.5.1", - "eslint": "^8.57.0", - "eslint-config-mourner": "^3.0.0", + "eslint": "^9.13.0", "eslint-plugin-html": "^8.1.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.8.3", - "eslint-plugin-react": "^7.37.1", + "eslint-plugin-react": "^7.37.2", "eslint-plugin-tsdoc": "0.3.0", "expect": "^29.7.0", "glob": "^11.0.0", + "globals": "^15.11.0", "is-builtin-module": "^4.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -122,13 +123,13 @@ "puppeteer": "^23.6.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "rollup": "^4.24.0", + "rollup": "^4.24.2", "rollup-plugin-sourcemaps2": "^0.4.2", "rw": "^1.3.3", "semver": "^7.6.3", "shuffle-seed": "^1.1.6", "source-map-explorer": "^2.5.3", - "st": "^3.0.0", + "st": "^3.0.1", "stylelint": "^16.10.0", "stylelint-config-standard": "^36.0.1", "ts-jest": "^29.2.5", @@ -147,12 +148,11 @@ "generate-typings": "dts-bundle-generator --export-referenced-types --umd-module-name=maplibregl -o ./dist/maplibre-gl.d.ts ./src/index.ts", "generate-docs": "typedoc && node --no-warnings --loader ts-node/esm build/generate-docs.ts", "generate-images": "node --no-warnings --loader ts-node/esm build/generate-doc-images.ts", - "build-dist": "npm run build-css && npm run generate-typings && npm run build-dev && npm run build-csp-dev && npm run build-prod && npm run build-prod-unminified && npm run build-csp", + "build-dist": "npm run build-css && npm run generate-typings && npm run build-dev && npm run build-csp-dev && npm run build-prod && npm run build-csp", "build-dev": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:dev", "watch-dev": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:dev --watch", - "build-prod": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:production,MINIFY:true", - "build-prod-unminified": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:production", - "build-csp": "rollup --configPlugin @rollup/plugin-typescript -c rollup.config.csp.ts --environment BUILD:production,MINIFY:true", + "build-prod": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:production", + "build-csp": "rollup --configPlugin @rollup/plugin-typescript -c rollup.config.csp.ts --environment BUILD:production", "build-csp-dev": "rollup --configPlugin @rollup/plugin-typescript -c rollup.config.csp.ts --environment BUILD:dev", "build-css": "postcss -o dist/maplibre-gl.css src/css/maplibre-gl.css", "watch-css": "postcss --watch -o dist/maplibre-gl.css src/css/maplibre-gl.css", @@ -163,7 +163,7 @@ "start-docs": "docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material", "start": "run-p watch-css watch-dev start-server", "start-bench": "run-p watch-css watch-benchmarks start-server", - "lint": "eslint --cache --ext .ts,.tsx,.js,.html --ignore-path .gitignore .", + "lint": "eslint", "lint-css": "stylelint **/*.css --fix -f verbose", "test": "run-p lint lint-css test-render jest", "jest": "jest", diff --git a/rollup.config.csp.ts b/rollup.config.csp.ts index ada6543350f..a63c13841e2 100644 --- a/rollup.config.csp.ts +++ b/rollup.config.csp.ts @@ -5,8 +5,7 @@ import {InputOption, ModuleFormat, RollupOptions} from 'rollup'; // a config for generating a special GL JS bundle with static web worker code (in a separate file) // https://github.com/mapbox/mapbox-gl-js/issues/6058 -const {BUILD, MINIFY} = process.env; -const minified = MINIFY === 'true'; +const {BUILD} = process.env; const production: boolean = (BUILD !== 'dev'); const outputPostfix: string = production ? '' : '-dev'; @@ -22,7 +21,7 @@ const config = (input: InputOption, file: string, format: ModuleFormat): RollupO banner }, treeshake: production, - plugins: plugins(production, minified) + plugins: plugins(production) }); const configs = [ diff --git a/rollup.config.ts b/rollup.config.ts index e09e175473b..b13fa799b1c 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -4,11 +4,10 @@ import {plugins, watchStagingPlugin} from './build/rollup_plugins'; import banner from './build/banner'; import {RollupOptions} from 'rollup'; -const {BUILD, MINIFY} = process.env; -const minified = MINIFY === 'true'; +const {BUILD} = process.env; const production = BUILD === 'production'; -const outputFile = production ? (minified ? 'dist/maplibre-gl.js' : 'dist/maplibre-gl-unminified.js') : 'dist/maplibre-gl-dev.js'; +const outputFile = production ? 'dist/maplibre-gl.js' : 'dist/maplibre-gl-dev.js'; const config: RollupOptions[] = [{ // Before rollup you should run build-tsc to transpile from typescript to javascript (except when running rollup in watch mode) @@ -35,7 +34,7 @@ const config: RollupOptions[] = [{ throw message; }, treeshake: production, - plugins: plugins(production, minified) + plugins: plugins(production) }, { // 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/geo/lng_lat.test.ts b/src/geo/lng_lat.test.ts index 2833a7f10b0..79039d35eaf 100644 --- a/src/geo/lng_lat.test.ts +++ b/src/geo/lng_lat.test.ts @@ -5,12 +5,10 @@ describe('LngLat', () => { expect(new LngLat(0, 0) instanceof LngLat).toBeTruthy(); expect(() => { - /*eslint no-new: 0*/ new LngLat(0, -91); }).toThrow('Invalid LngLat latitude value: must be between -90 and 90'); expect(() => { - /*eslint no-new: 0*/ new LngLat(0, 91); }).toThrow('Invalid LngLat latitude value: must be between -90 and 90'); }); diff --git a/src/geo/projection/camera_helper.ts b/src/geo/projection/camera_helper.ts index 93937271fb7..a34ea06bcec 100644 --- a/src/geo/projection/camera_helper.ts +++ b/src/geo/projection/camera_helper.ts @@ -4,13 +4,15 @@ import {LngLat, LngLatLike} from '../lng_lat'; import {CameraForBoundsOptions, PointLike} from '../../ui/camera'; import {PaddingOptions} from '../edge_insets'; import {LngLatBounds} from '../lng_lat_bounds'; -import {warnOnce} from '../../util/util'; +import {getRollPitchBearing, RollPitchBearing, warnOnce} from '../../util/util'; +import {quat} from 'gl-matrix'; export type MapControlsDeltas = { panDelta: Point; zoomDelta: number; bearingDelta: number; pitchDelta: number; + rollDelta: number; around: Point; } @@ -23,6 +25,7 @@ export type CameraForBoxAndBearingHandlerResult = { export type EaseToHandlerOptions = { bearing: number; pitch: number; + roll: number; padding: PaddingOptions; offsetAsPoint: Point; around?: LngLat; @@ -41,6 +44,7 @@ export type EaseToHandlerResult = { export type FlyToHandlerOptions = { bearing: number; pitch: number; + roll: number; padding: PaddingOptions; offsetAsPoint: Point; center?: LngLatLike; @@ -78,7 +82,7 @@ export interface ICameraHelper { easingOffset: Point; }; - handleMapControlsPitchBearingZoom(deltas: MapControlsDeltas, tr: ITransform): void; + handleMapControlsRollPitchBearingZoom(deltas: MapControlsDeltas, tr: ITransform): void; handleMapControlsPan(deltas: MapControlsDeltas, tr: ITransform, preZoomAroundLoc: LngLat): void; @@ -90,3 +94,29 @@ export interface ICameraHelper { handleFlyTo(tr: ITransform, options: FlyToHandlerOptions): FlyToHandlerResult; } + +/** + * @internal + * Set a transform's rotation to a value interpolated between startRotation and endRotation + * @param startRotation - the starting rotation (rotation when k = 0) + * @param endRotation - the end rotation (rotation when k = 1) + * @param endEulerAngles - the end Euler angles. This is needed in case `endRotation` has an ambiguous Euler angle representation. + * @param tr - the transform to be updated + * @param k - the interpolation fraction, between 0 and 1. + */ +export function updateRotation(startRotation: quat, endRotation: quat, endEulerAngles: RollPitchBearing, tr: ITransform, k: number) { + // At pitch ==0, the Euler angle representation is ambiguous. In this case, set the Euler angles + // to the representation requested by the caller + if (k < 1) { + const rotation: quat = new Float64Array(4) as any; + quat.slerp(rotation, startRotation, endRotation, k); + const eulerAngles = getRollPitchBearing(rotation); + tr.setRoll(eulerAngles.roll); + tr.setPitch(eulerAngles.pitch); + tr.setBearing(eulerAngles.bearing); + } else { + tr.setRoll(endEulerAngles.roll); + tr.setPitch(endEulerAngles.pitch); + tr.setBearing(endEulerAngles.bearing); + } +} diff --git a/src/geo/projection/globe_camera_helper.ts b/src/geo/projection/globe_camera_helper.ts index dd67b8fa88b..89e50ee6fe8 100644 --- a/src/geo/projection/globe_camera_helper.ts +++ b/src/geo/projection/globe_camera_helper.ts @@ -1,12 +1,12 @@ import Point from '@mapbox/point-geometry'; import {IReadonlyTransform, ITransform} from '../transform_interface'; -import {cameraBoundsWarning, CameraForBoxAndBearingHandlerResult, EaseToHandlerResult, EaseToHandlerOptions, FlyToHandlerResult, FlyToHandlerOptions, ICameraHelper, MapControlsDeltas} from './camera_helper'; +import {cameraBoundsWarning, CameraForBoxAndBearingHandlerResult, EaseToHandlerResult, EaseToHandlerOptions, FlyToHandlerResult, FlyToHandlerOptions, ICameraHelper, MapControlsDeltas, updateRotation} from './camera_helper'; import {GlobeProjection} from './globe'; import {LngLat, LngLatLike} from '../lng_lat'; import {MercatorCameraHelper} from './mercator_camera_helper'; import {angularCoordinatesToSurfaceVector, computeGlobePanCenter, getGlobeRadiusPixels, getZoomAdjustment, globeDistanceOfLocationsPixels, interpolateLngLatForGlobe} from './globe_utils'; -import {clamp, createVec3f64, differenceOfAnglesDegrees, remapSaturate, warnOnce} from '../../util/util'; -import {mat4, vec3} from 'gl-matrix'; +import {clamp, createVec3f64, differenceOfAnglesDegrees, remapSaturate, rollPitchBearingToQuat, warnOnce} from '../../util/util'; +import {mat4, quat, vec3} from 'gl-matrix'; import {MAX_VALID_LATITUDE, normalizeCenter, scaleZoom, zoomScale} from '../transform_helper'; import {CameraForBoundsOptions} from '../../ui/camera'; import {LngLatBounds} from '../lng_lat_bounds'; @@ -48,9 +48,9 @@ export class GlobeCameraHelper implements ICameraHelper { }; } - handleMapControlsPitchBearingZoom(deltas: MapControlsDeltas, tr: ITransform): void { + handleMapControlsRollPitchBearingZoom(deltas: MapControlsDeltas, tr: ITransform): void { if (!this.useGlobeControls) { - this._mercatorCameraHelper.handleMapControlsPitchBearingZoom(deltas, tr); + this._mercatorCameraHelper.handleMapControlsRollPitchBearingZoom(deltas, tr); return; } @@ -59,6 +59,7 @@ export class GlobeCameraHelper implements ICameraHelper { if (deltas.bearingDelta) tr.setBearing(tr.bearing + deltas.bearingDelta); if (deltas.pitchDelta) tr.setPitch(tr.pitch + deltas.pitchDelta); + if (deltas.rollDelta) tr.setRoll(tr.roll + deltas.rollDelta); const oldZoomPreZoomDelta = tr.zoom; if (deltas.zoomDelta) tr.setZoom(tr.zoom + deltas.zoomDelta); const actualZoomDelta = tr.zoom - oldZoomPreZoomDelta; @@ -191,6 +192,7 @@ export class GlobeCameraHelper implements ICameraHelper { clonedTr.setCenter(result.center); clonedTr.setBearing(result.bearing); clonedTr.setPitch(0); + clonedTr.setRoll(0); clonedTr.setZoom(result.zoom); const matrix = clonedTr.modelViewProjectionMatrix; @@ -259,9 +261,12 @@ export class GlobeCameraHelper implements ICameraHelper { } const startZoom = tr.zoom; - const startBearing = tr.bearing; - const startPitch = tr.pitch; const startCenter = tr.center; + const startRotation = rollPitchBearingToQuat(tr.roll, tr.pitch, tr.bearing); + const endRoll = options.roll === undefined ? tr.roll : options.roll; + const endPitch = options.pitch === undefined ? tr.pitch : options.pitch; + const endBearing = options.bearing === undefined ? tr.bearing : options.bearing; + const endRotation = rollPitchBearingToQuat(endRoll, endPitch, endBearing); const optionsZoom = typeof options.zoom !== 'undefined'; @@ -312,11 +317,8 @@ export class GlobeCameraHelper implements ICameraHelper { isZooming = (endZoomWithShift !== startZoom); const easeFunc = (k: number) => { - if (startBearing !== options.bearing) { - tr.setBearing(interpolates.number(startBearing, options.bearing, k)); - } - if (startPitch !== options.pitch) { - tr.setPitch(interpolates.number(startPitch, options.pitch, k)); + if (!quat.equals(startRotation, endRotation)) { + updateRotation(startRotation, endRotation, {roll: endRoll, pitch: endPitch, bearing: endBearing}, tr, k); } if (options.around) { diff --git a/src/geo/projection/globe_transform.test.ts b/src/geo/projection/globe_transform.test.ts index 9fe47491890..151057ac8e4 100644 --- a/src/geo/projection/globe_transform.test.ts +++ b/src/geo/projection/globe_transform.test.ts @@ -130,6 +130,16 @@ describe('GlobeTransform', () => { globeTransform.setBearing(70); expectToBeCloseToArray(globeTransform.cameraPosition as Array, [-0.7098603286961542, 2.002400604307631, 0.6154310261827212], precisionDigits); + globeTransform.setPitch(35); + globeTransform.setBearing(70); + globeTransform.setRoll(40); + expectToBeCloseToArray(globeTransform.cameraPosition as Array, [-0.7098603286961542, 2.002400604307631, 0.6154310261827212], precisionDigits); + + globeTransform.setPitch(35); + globeTransform.setBearing(70); + globeTransform.setRoll(180); + expectToBeCloseToArray(globeTransform.cameraPosition as Array, [-0.7098603286961542, 2.002400604307631, 0.6154310261827212], precisionDigits); + globeTransform.setCenter(new LngLat(-10, 42)); expectToBeCloseToArray(globeTransform.cameraPosition as Array, [-3.8450970996236364, 2.9368285470351516, 4.311953269048194], precisionDigits); }); diff --git a/src/geo/projection/globe_transform.ts b/src/geo/projection/globe_transform.ts index 34146a6a9ab..c1c1b6e991f 100644 --- a/src/geo/projection/globe_transform.ts +++ b/src/geo/projection/globe_transform.ts @@ -93,6 +93,9 @@ export class GlobeTransform implements ITransform { setPitch(pitch: number): void { this._helper.setPitch(pitch); } + setRoll(roll: number): void { + this._helper.setRoll(roll); + } setFov(fov: number): void { this._helper.setFov(fov); } @@ -151,9 +154,6 @@ export class GlobeTransform implements ITransform { get height(): number { return this._helper.height; } - get angle(): number { - return this._helper.angle; - } get lngRange(): [number, number] { return this._helper.lngRange; } @@ -181,12 +181,27 @@ export class GlobeTransform implements ITransform { get pitch(): number { return this._helper.pitch; } + get pitchInRadians(): number { + return this._helper.pitchInRadians; + } + get roll(): number { + return this._helper.roll; + } + get rollInRadians(): number { + return this._helper.rollInRadians; + } get bearing(): number { return this._helper.bearing; } + get bearingInRadians(): number { + return this._helper.bearingInRadians; + } get fov(): number { return this._helper.fov; } + get fovInRadians(): number { + return this._helper.fovInRadians; + } get elevation(): number { return this._helper.elevation; } @@ -472,13 +487,13 @@ export class GlobeTransform implements ITransform { // - "cam" is camera origin // - "C" is globe center // - "B" is the point on "top" of the globe - camera is looking at B - "B" is the intersection between the camera center ray and the globe - // - this._pitch is the angle at B between points cam,B,A + // - this._pitchInRadians is the angle at B between points cam,B,A // - this.cameraToCenterDistance is the distance from camera to "B" // - globe radius is (0.5 * this.worldSize) // - "T" is any point where a tangent line from "cam" touches the globe surface // - elevation is assumed to be zero - globe rendering must be separate from terrain rendering anyway - const pitch = this.pitch * Math.PI / 180.0; + const pitch = this.pitchInRadians; // scale things so that the globe radius is 1 const distanceCameraToB = this.cameraToCenterDistance / globeRadiusPixels; const radius = 1; @@ -504,7 +519,7 @@ export class GlobeTransform implements ITransform { // Note the swizzled components const planeVector: vec3 = [0, vectorCtoCamX, vectorCtoCamY]; // Apply transforms - lat, lng and angle (NOT pitch - already accounted for, as it affects the tangent plane) - vec3.rotateZ(planeVector, planeVector, [0, 0, 0], this.angle); + vec3.rotateZ(planeVector, planeVector, [0, 0, 0], -this.bearingInRadians); vec3.rotateX(planeVector, planeVector, [0, 0, 0], -1 * this.center.lat * Math.PI / 180.0); vec3.rotateY(planeVector, planeVector, [0, 0, 0], this.center.lng * Math.PI / 180.0); // Scale the plane vector up @@ -609,7 +624,7 @@ export class GlobeTransform implements ITransform { const globeMatrixUncorrected = createMat4f64(); this._nearZ = 0.5; this._farZ = this.cameraToCenterDistance + globeRadiusPixels * 2.0; // just set the far plane far enough - we will calculate our own z in the vertex shader anyway - mat4.perspective(globeMatrix, this.fov * Math.PI / 180, this.width / this.height, this._nearZ, this._farZ); + mat4.perspective(globeMatrix, this.fovInRadians, this.width / this.height, this._nearZ, this._farZ); // Apply center of perspective offset const offset = this.centerOffset; @@ -620,8 +635,9 @@ export class GlobeTransform implements ITransform { this._globeProjMatrixInverted = createMat4f64(); mat4.invert(this._globeProjMatrixInverted, globeMatrix); mat4.translate(globeMatrix, globeMatrix, [0, 0, -this.cameraToCenterDistance]); - mat4.rotateX(globeMatrix, globeMatrix, -this.pitch * Math.PI / 180); - mat4.rotateZ(globeMatrix, globeMatrix, -this.angle); + mat4.rotateZ(globeMatrix, globeMatrix, this.rollInRadians); + mat4.rotateX(globeMatrix, globeMatrix, -this.pitchInRadians); + mat4.rotateZ(globeMatrix, globeMatrix, this.bearingInRadians); mat4.translate(globeMatrix, globeMatrix, [0.0, 0, -globeRadiusPixels]); // Rotate the sphere to center it on viewed coordinates @@ -647,8 +663,9 @@ export class GlobeTransform implements ITransform { const zero = createVec3f64(); this._cameraPosition = createVec3f64(); this._cameraPosition[2] = this.cameraToCenterDistance / globeRadiusPixels; - vec3.rotateX(this._cameraPosition, this._cameraPosition, zero, this.pitch * Math.PI / 180); - vec3.rotateZ(this._cameraPosition, this._cameraPosition, zero, this.angle); + vec3.rotateZ(this._cameraPosition, this._cameraPosition, zero, -this.rollInRadians); + vec3.rotateX(this._cameraPosition, this._cameraPosition, zero, this.pitchInRadians); + vec3.rotateZ(this._cameraPosition, this._cameraPosition, zero, -this.bearingInRadians); vec3.add(this._cameraPosition, this._cameraPosition, [0, 0, 1]); vec3.rotateX(this._cameraPosition, this._cameraPosition, zero, -this.center.lat * Math.PI / 180.0); vec3.rotateY(this._cameraPosition, this._cameraPosition, zero, this.center.lng * Math.PI / 180.0); @@ -684,8 +701,8 @@ export class GlobeTransform implements ITransform { return globeCoveringTiles(this._cachedFrustum, this._cachedClippingPlane, cameraCoord, centerCoord, coveringZ, options); } - recalculateZoom(terrain: Terrain): void { - this._mercatorTransform.recalculateZoom(terrain); + recalculateZoomAndCenter(terrain?: Terrain): void { + this._mercatorTransform.recalculateZoomAndCenter(terrain); this.apply(this._mercatorTransform); } @@ -702,6 +719,10 @@ export class GlobeTransform implements ITransform { return this._mercatorTransform.getCameraAltitude(); } + getCameraLngLat(): LngLat { + return this._mercatorTransform.getCameraLngLat(); + } + lngLatToCameraDepth(lngLat: LngLat, elevation: number): number { if (!this.isGlobeRendering) { return this._mercatorTransform.lngLatToCameraDepth(lngLat, elevation); @@ -810,6 +831,10 @@ export class GlobeTransform implements ITransform { }; } + calculateCenterFromCameraLngLatAlt(ll: LngLat, alt: number, bearing?: number, pitch?: number): {center: LngLat; elevation: number; zoom: number} { + return this._mercatorTransform.calculateCenterFromCameraLngLatAlt(ll, alt, bearing, pitch); + } + /** * Note: automatically adjusts zoom to keep planet size consistent * (same size before and after a {@link setLocationAtPoint} call). diff --git a/src/geo/projection/globe_utils.ts b/src/geo/projection/globe_utils.ts index c14c3da5ad1..5e32459a471 100644 --- a/src/geo/projection/globe_utils.ts +++ b/src/geo/projection/globe_utils.ts @@ -137,13 +137,13 @@ export function getDegreesPerPixel(worldSize: number, lat: number): number { * @returns New center location to set to the map's transform to apply the specified panning. */ export function computeGlobePanCenter(panDelta: Point, tr: { - readonly angle: number; + readonly bearingInRadians: number; readonly worldSize: number; readonly center: LngLat; readonly zoom: number; }): LngLat { // Apply map bearing to the panning vector - const rotatedPanDelta = panDelta.rotate(-tr.angle); + const rotatedPanDelta = panDelta.rotate(tr.bearingInRadians); // Compute what the current zoom would be if the transform center would be moved to latitude 0. const normalizedGlobeZoom = tr.zoom + getZoomAdjustment(tr.center.lat, 0); // Note: we divide longitude speed by planet width at the given latitude. But we diminish this effect when the globe is zoomed out a lot. diff --git a/src/geo/projection/mercator_camera_helper.ts b/src/geo/projection/mercator_camera_helper.ts index 0d9a105aa9d..91be1b7db21 100644 --- a/src/geo/projection/mercator_camera_helper.ts +++ b/src/geo/projection/mercator_camera_helper.ts @@ -1,14 +1,15 @@ import Point from '@mapbox/point-geometry'; import {LngLat, LngLatLike} from '../lng_lat'; import {IReadonlyTransform, ITransform} from '../transform_interface'; -import {cameraBoundsWarning, CameraForBoxAndBearingHandlerResult, EaseToHandlerResult, EaseToHandlerOptions, FlyToHandlerResult, FlyToHandlerOptions, ICameraHelper, MapControlsDeltas} from './camera_helper'; +import {cameraBoundsWarning, CameraForBoxAndBearingHandlerResult, EaseToHandlerResult, EaseToHandlerOptions, FlyToHandlerResult, FlyToHandlerOptions, ICameraHelper, MapControlsDeltas, updateRotation} from './camera_helper'; import {CameraForBoundsOptions} from '../../ui/camera'; import {PaddingOptions} from '../edge_insets'; import {LngLatBounds} from '../lng_lat_bounds'; import {normalizeCenter, scaleZoom, zoomScale} from '../transform_helper'; -import {degreesToRadians} from '../../util/util'; +import {degreesToRadians, rollPitchBearingToQuat} from '../../util/util'; import {projectToWorldCoordinates, unprojectFromWorldCoordinates} from './mercator_utils'; import {interpolates} from '@maplibre/maplibre-gl-style-spec'; +import {quat} from 'gl-matrix'; /** * @internal @@ -26,13 +27,20 @@ export class MercatorCameraHelper implements ICameraHelper { }; } - handleMapControlsPitchBearingZoom(deltas: MapControlsDeltas, tr: ITransform): void { + handleMapControlsRollPitchBearingZoom(deltas: MapControlsDeltas, tr: ITransform): void { if (deltas.bearingDelta) tr.setBearing(tr.bearing + deltas.bearingDelta); if (deltas.pitchDelta) tr.setPitch(tr.pitch + deltas.pitchDelta); + if (deltas.rollDelta) tr.setRoll(tr.roll + deltas.rollDelta); if (deltas.zoomDelta) tr.setZoom(tr.zoom + deltas.zoomDelta); } handleMapControlsPan(deltas: MapControlsDeltas, tr: ITransform, preZoomAroundLoc: LngLat): void { + // If we are rotating about the center point, there is no need to update the transform center. Doing so causes + // a small amount of drift of the center point, especially when pitch is close to 90 degrees. + // In this case, return early. + if (deltas.around.distSqr(tr.centerPoint) < 1.0e-2) { + return; + } tr.setLocationAtPoint(preZoomAroundLoc, deltas.around); } @@ -119,9 +127,12 @@ export class MercatorCameraHelper implements ICameraHelper { handleEaseTo(tr: ITransform, options: EaseToHandlerOptions): EaseToHandlerResult { const startZoom = tr.zoom; - const startBearing = tr.bearing; - const startPitch = tr.pitch; const startPadding = tr.padding; + const startRotation = rollPitchBearingToQuat(tr.roll, tr.pitch, tr.bearing); + const endRoll = options.roll === undefined ? tr.roll : options.roll; + const endPitch = options.pitch === undefined ? tr.pitch : options.pitch; + const endBearing = options.bearing === undefined ? tr.bearing : options.bearing; + const endRotation = rollPitchBearingToQuat(endRoll, endPitch, endBearing); const optionsZoom = typeof options.zoom !== 'undefined'; @@ -149,11 +160,8 @@ export class MercatorCameraHelper implements ICameraHelper { if (isZooming) { tr.setZoom(interpolates.number(startZoom, endZoom, k)); } - if (startBearing !== options.bearing) { - tr.setBearing(interpolates.number(startBearing, options.bearing, k)); - } - if (startPitch !== options.pitch) { - tr.setPitch(interpolates.number(startPitch, options.pitch, k)); + if (!quat.equals(startRotation, endRotation)) { + updateRotation(startRotation, endRotation, {roll: endRoll, pitch: endPitch, bearing: endBearing}, tr, k); } if (doPadding) { tr.interpolatePadding(startPadding, options.padding, k); diff --git a/src/geo/projection/mercator_transform.test.ts b/src/geo/projection/mercator_transform.test.ts index 6a86c022ae8..10487f4c41e 100644 --- a/src/geo/projection/mercator_transform.test.ts +++ b/src/geo/projection/mercator_transform.test.ts @@ -50,9 +50,10 @@ describe('transform', () => { 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 6); - expect([...transform.modelViewProjectionMatrix.values()]).toEqual([3, 0, 0, 0, 0, -2.954423259036624, -0.1780177690666898, -0.17364817766693033, 0, 0.006822967915294533, -0.013222891287479163, -0.012898324631281611, -786432, 774484.3308168967, 47414.91102496082, 46270.827886319785]); + expect([...transform.modelViewProjectionMatrix.values()]).toEqual([3, 0, 0, 0, 0, -2.954423259036624, -0.1780177690666898, -0.17364817766693033, -0, 0.006822967915294533, -0.013222891287479163, -0.012898324631281611, -786432, 774484.3308168967, 47414.91102496082, 46270.827886319785]); expect(fixedLngLat(transform.screenPointToLocation(new Point(250, 250)))).toEqual({lng: 0, lat: 0}); expect(fixedCoord(transform.screenPointToMercatorCoordinate(new Point(250, 250)))).toEqual({x: 0.5, y: 0.5, z: 0}); + expect(fixedCoord(transform.screenPointToMercatorCoordinateAtZ(new Point(250, 250), 1))).toEqual({x: 0.5, y: 0.5000000044, z: 1}); expect(transform.locationToScreenPoint(new LngLat(0, 0))).toEqual({x: 250, y: 250}); }); @@ -83,6 +84,17 @@ describe('transform', () => { expect(fixedLngLat(transform.screenPointToLocation(new Point(15, 45)))).toEqual({lng: 13, lat: 10}); }); + test('setLocationAt tilted rolled', () => { + const transform = new MercatorTransform(0, 22, 0, 60, true); + transform.resize(500, 500); + transform.setZoom(4); + transform.setPitch(50); + transform.setRoll(50); + expect(transform.center).toEqual({lng: 0, lat: 0}); + transform.setLocationAtPoint(new LngLat(13, 10), new Point(15, 45)); + expect(fixedLngLat(transform.screenPointToLocation(new Point(15, 45)))).toEqual({lng: 13, lat: 10}); + }); + test('has a default zoom', () => { const transform = new MercatorTransform(0, 22, 0, 60, true); transform.resize(500, 500); @@ -425,7 +437,7 @@ describe('transform', () => { expect(transform.maxPitchScaleFactor()).toBeCloseTo(2.366025418080343, 5); }); - test('recalculateZoom', () => { + test('recalculateZoomAndCenter: no change', () => { const transform = new MercatorTransform(0, 22, 0, 60, true); transform.setElevation(200); transform.setCenter(new LngLat(10.0, 50.0)); @@ -438,31 +450,108 @@ describe('transform', () => { // but that shouldn't change the camera's position in world space if that wasn't requested. const expectedAltitude = 1865.7579397718; expect(transform.getCameraAltitude()).toBeCloseTo(expectedAltitude, 10); + const expectedCamLngLat = transform.getCameraLngLat(); + expect(expectedCamLngLat.lng).toBeCloseTo(10, 10); + expect(expectedCamLngLat.lat).toBeCloseTo(49.9850171656428, 10); // expect same values because of no elevation change const terrain = { getElevationForLngLatZoom: () => 200, pointCoordinate: () => null }; - transform.recalculateZoom(terrain as any); + transform.recalculateZoomAndCenter(terrain as any); expect(transform.getCameraAltitude()).toBeCloseTo(expectedAltitude, 10); expect(transform.zoom).toBe(14); + }); + + test('recalculateZoomAndCenter: elevation increase', () => { + const transform = new MercatorTransform(0, 22, 0, 60, true); + transform.setElevation(200); + transform.setCenter(new LngLat(10.0, 50.0)); + transform.setZoom(14); + transform.setPitch(45); + transform.resize(512, 512); - // expect new zoom because of elevation change - terrain.getElevationForLngLatZoom = () => 400; - transform.recalculateZoom(terrain as any); + // This should be an invariant throughout - the zoom is greater when the camera is + // closer to the terrain (and therefore also when the terrain is closer to the camera), + // but that shouldn't change the camera's position in world space if that wasn't requested. + const expectedAltitude = 1865.7579397718; + expect(transform.getCameraAltitude()).toBeCloseTo(expectedAltitude, 10); + const expectedCamLngLat = transform.getCameraLngLat(); + expect(expectedCamLngLat.lng).toBeCloseTo(10, 10); + expect(expectedCamLngLat.lat).toBeCloseTo(49.9850171656428, 10); + + // expect new zoom and center because of elevation change + const terrain = { + getElevationForLngLatZoom: () => 400, + pointCoordinate: () => null + }; + transform.recalculateZoomAndCenter(terrain as any); expect(transform.elevation).toBe(400); expect(transform.center.lng).toBeCloseTo(10, 10); - expect(transform.center.lat).toBeCloseTo(50, 10); + expect(transform.center.lat).toBeCloseTo(49.99820083233254, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(expectedCamLngLat.lng, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(expectedCamLngLat.lat, 10); expect(transform.getCameraAltitude()).toBeCloseTo(expectedAltitude, 10); - expect(transform.zoom).toBeCloseTo(14.1845318986, 10); + expect(transform.zoom).toBeCloseTo(14.184585886440686, 10); + }); + + test('recalculateZoomAndCenter: elevation decrease', () => { + const transform = new MercatorTransform(0, 22, 0, 60, true); + transform.setElevation(200); + transform.setCenter(new LngLat(10.0, 50.0)); + transform.setZoom(14); + transform.setPitch(45); + transform.resize(512, 512); + + // This should be an invariant throughout - the zoom is greater when the camera is + // closer to the terrain (and therefore also when the terrain is closer to the camera), + // but that shouldn't change the camera's position in world space if that wasn't requested. + const expectedAltitude = 1865.7579397718; + expect(transform.getCameraAltitude()).toBeCloseTo(expectedAltitude, 10); + const expectedCamLngLat = transform.getCameraLngLat(); + expect(expectedCamLngLat.lng).toBeCloseTo(10, 10); + expect(expectedCamLngLat.lat).toBeCloseTo(49.9850171656428, 10); // expect new zoom because of elevation change to point below sea level - terrain.getElevationForLngLatZoom = () => -200; - transform.recalculateZoom(terrain as any); + const terrain = { + getElevationForLngLatZoom: () => -200, + pointCoordinate: () => null + }; + transform.recalculateZoomAndCenter(terrain as any); expect(transform.elevation).toBe(-200); + expect(transform.getCameraLngLat().lng).toBeCloseTo(expectedCamLngLat.lng, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(expectedCamLngLat.lat, 10); expect(transform.getCameraAltitude()).toBeCloseTo(expectedAltitude, 10); - expect(transform.zoom).toBeCloseTo(13.6895075574, 10); + expect(transform.zoom).toBeCloseTo(13.689399565250616, 10); + }); + + test('recalculateZoomAndCenterNoTerrain', () => { + const transform = new MercatorTransform(0, 22, 0, 60, true); + transform.setElevation(200); + transform.setCenter(new LngLat(10.0, 50.0)); + transform.setZoom(14); + transform.setPitch(45); + transform.resize(512, 512); + + // This should be an invariant throughout - the zoom is greater when the camera is + // closer to the terrain (and therefore also when the terrain is closer to the camera), + // but that shouldn't change the camera's position in world space if that wasn't requested. + const expectedAltitude = 1865.7579397718; + expect(transform.getCameraAltitude()).toBeCloseTo(expectedAltitude, 10); + const expectedCamLngLat = transform.getCameraLngLat(); + expect(expectedCamLngLat.lng).toBeCloseTo(10, 10); + expect(expectedCamLngLat.lat).toBeCloseTo(49.9850171656428, 10); + + // expect same values because of no elevation change + transform.recalculateZoomAndCenter(); + expect(transform.elevation).toBeCloseTo(0, 10); + expect(transform.center.lng).toBeCloseTo(10, 10); + expect(transform.center.lat).toBeCloseTo(50.00179923503546, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(expectedCamLngLat.lng, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(expectedCamLngLat.lat, 10); + expect(transform.getCameraAltitude()).toBeCloseTo(expectedAltitude, 10); + expect(transform.zoom).toBeCloseTo(13.836362951286565, 10); }); test('pointCoordinate with terrain when returning null should fall back to 2D', () => { @@ -517,4 +606,194 @@ describe('transform', () => { expect(projection.signedDistanceFromCamera).toBeCloseTo(787.6699126802941, precisionDigits); expect(projection.isOccluded).toBe(false); }); + + test('getCameraLngLat', () => { + const transform = new MercatorTransform(0, 22, 0, 180, true); + transform.setElevation(200); + transform.setCenter(new LngLat(15.0, 55.0)); + transform.setZoom(14); + transform.setPitch(55); + transform.setBearing(75); + transform.resize(512, 512); + + expect(transform.getCameraAltitude()).toBeCloseTo(1405.7075926414002, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(14.973921529405033, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(54.99599181678275, 10); + + transform.setRoll(31); + + expect(transform.getCameraAltitude()).toBeCloseTo(1405.7075926414002, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(14.973921529405033, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(54.99599181678275, 10); + }); + + test('calculateCenterFromCameraLngLatAlt no pitch no bearing', () => { + const transform = new MercatorTransform(0, 22, 0, 180, true); + transform.setPitch(55); + transform.setBearing(75); + transform.resize(512, 512); + + const camLngLat = new LngLat(15, 55); + const camAlt = 400; + const centerInfo = transform.calculateCenterFromCameraLngLatAlt(camLngLat, camAlt); + transform.setZoom(centerInfo.zoom); + transform.setCenter(centerInfo.center); + transform.setElevation(centerInfo.elevation); + expect(transform.zoom).toBeGreaterThan(0); + expect(transform.getCameraAltitude()).toBeCloseTo(camAlt, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(camLngLat.lng, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(camLngLat.lat, 10); + }); + + test('calculateCenterFromCameraLngLatAlt no pitch', () => { + const transform = new MercatorTransform(0, 22, 0, 180, true); + transform.setPitch(55); + transform.setBearing(75); + transform.resize(512, 512); + + const camLngLat = new LngLat(15, 55); + const camAlt = 400; + const bearing = 20; + const centerInfo = transform.calculateCenterFromCameraLngLatAlt(camLngLat, camAlt, bearing); + transform.setZoom(centerInfo.zoom); + transform.setCenter(centerInfo.center); + transform.setElevation(centerInfo.elevation); + transform.setBearing(bearing); + expect(transform.zoom).toBeGreaterThan(0); + expect(transform.getCameraAltitude()).toBeCloseTo(camAlt, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(camLngLat.lng, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(camLngLat.lat, 10); + }); + + test('calculateCenterFromCameraLngLatAlt', () => { + const transform = new MercatorTransform(0, 22, 0, 180, true); + transform.setPitch(55); + transform.setBearing(75); + transform.resize(512, 512); + + const camLngLat = new LngLat(15, 55); + const camAlt = 400; + const bearing = 40; + const pitch = 30; + const centerInfo = transform.calculateCenterFromCameraLngLatAlt(camLngLat, camAlt, bearing, pitch); + transform.setZoom(centerInfo.zoom); + transform.setCenter(centerInfo.center); + transform.setElevation(centerInfo.elevation); + transform.setBearing(bearing); + transform.setPitch(pitch); + expect(transform.zoom).toBeGreaterThan(0); + expect(transform.getCameraAltitude()).toBeCloseTo(camAlt, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(camLngLat.lng, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(camLngLat.lat, 10); + }); + + test('calculateCenterFromCameraLngLatAlt 89 degrees pitch', () => { + const transform = new MercatorTransform(0, 22, 0, 180, true); + transform.setPitch(55); + transform.setBearing(75); + transform.resize(512, 512); + + const camLngLat = new LngLat(15, 55); + const camAlt = 400; + const bearing = 40; + const pitch = 88; + const centerInfo = transform.calculateCenterFromCameraLngLatAlt(camLngLat, camAlt, bearing, pitch); + transform.setZoom(centerInfo.zoom); + transform.setCenter(centerInfo.center); + transform.setElevation(centerInfo.elevation); + transform.setBearing(bearing); + transform.setPitch(pitch); + expect(transform.zoom).toBeGreaterThan(0); + expect(transform.getCameraAltitude()).toBeCloseTo(camAlt, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(camLngLat.lng, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(camLngLat.lat, 10); + }); + + test('calculateCenterFromCameraLngLatAlt 89.99 degrees pitch', () => { + const transform = new MercatorTransform(0, 22, 0, 180, true); + transform.setPitch(55); + transform.setBearing(75); + transform.resize(512, 512); + + const camLngLat = new LngLat(15, 55); + const camAlt = 400; + const bearing = 40; + const pitch = 89.99; + const centerInfo = transform.calculateCenterFromCameraLngLatAlt(camLngLat, camAlt, bearing, pitch); + transform.setZoom(centerInfo.zoom); + transform.setCenter(centerInfo.center); + transform.setElevation(centerInfo.elevation); + transform.setBearing(bearing); + transform.setPitch(pitch); + expect(transform.zoom).toBeGreaterThan(0); + expect(transform.getCameraAltitude()).toBeCloseTo(camAlt, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(camLngLat.lng, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(camLngLat.lat, 10); + }); + + test('calculateCenterFromCameraLngLatAlt 90 degrees pitch', () => { + const transform = new MercatorTransform(0, 22, 0, 180, true); + transform.setPitch(55); + transform.setBearing(75); + transform.resize(512, 512); + + const camLngLat = new LngLat(15, 55); + const camAlt = 400; + const bearing = 40; + const pitch = 90; + const centerInfo = transform.calculateCenterFromCameraLngLatAlt(camLngLat, camAlt, bearing, pitch); + transform.setZoom(centerInfo.zoom); + transform.setCenter(centerInfo.center); + transform.setElevation(centerInfo.elevation); + transform.setBearing(bearing); + transform.setPitch(pitch); + expect(transform.zoom).toBeGreaterThan(0); + expect(transform.getCameraAltitude()).toBeCloseTo(camAlt, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(camLngLat.lng, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(camLngLat.lat, 10); + }); + + test('calculateCenterFromCameraLngLatAlt 95 degrees pitch', () => { + const transform = new MercatorTransform(0, 22, 0, 180, true); + transform.setPitch(55); + transform.setBearing(75); + transform.resize(512, 512); + + const camLngLat = new LngLat(15, 55); + const camAlt = 400; + const bearing = 40; + const pitch = 95; + const centerInfo = transform.calculateCenterFromCameraLngLatAlt(camLngLat, camAlt, bearing, pitch); + transform.setZoom(centerInfo.zoom); + transform.setCenter(centerInfo.center); + transform.setElevation(centerInfo.elevation); + transform.setBearing(bearing); + transform.setPitch(pitch); + expect(transform.zoom).toBeGreaterThan(0); + expect(transform.getCameraAltitude()).toBeCloseTo(camAlt, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(camLngLat.lng, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(camLngLat.lat, 10); + }); + + test('calculateCenterFromCameraLngLatAlt 180 degrees pitch', () => { + const transform = new MercatorTransform(0, 22, 0, 180, true); + transform.setPitch(55); + transform.setBearing(75); + transform.resize(512, 512); + + const camLngLat = new LngLat(15, 55); + const camAlt = 400; + const bearing = 40; + const pitch = 180; + const centerInfo = transform.calculateCenterFromCameraLngLatAlt(camLngLat, camAlt, bearing, pitch); + transform.setZoom(centerInfo.zoom); + transform.setCenter(centerInfo.center); + transform.setElevation(centerInfo.elevation); + transform.setBearing(bearing); + transform.setPitch(pitch); + expect(transform.zoom).toBeGreaterThan(0); + expect(transform.getCameraAltitude()).toBeCloseTo(camAlt, 10); + expect(transform.getCameraLngLat().lng).toBeCloseTo(camLngLat.lng, 10); + expect(transform.getCameraLngLat().lat).toBeCloseTo(camLngLat.lat, 10); + }); }); diff --git a/src/geo/projection/mercator_transform.ts b/src/geo/projection/mercator_transform.ts index 8866a700337..d75df8c0c53 100644 --- a/src/geo/projection/mercator_transform.ts +++ b/src/geo/projection/mercator_transform.ts @@ -1,7 +1,7 @@ import {LngLat, LngLatLike} from '../lng_lat'; -import {MercatorCoordinate, mercatorXfromLng, mercatorYfromLat, mercatorZfromAltitude} from '../mercator_coordinate'; +import {altitudeFromMercatorZ, MercatorCoordinate, mercatorXfromLng, mercatorYfromLat, mercatorZfromAltitude} from '../mercator_coordinate'; import Point from '@mapbox/point-geometry'; -import {wrap, clamp, createIdentityMat4f64, createMat4f64} from '../../util/util'; +import {wrap, clamp, createIdentityMat4f64, createMat4f64, degreesToRadians} from '../../util/util'; import {mat2, mat4, vec3, vec4} from 'gl-matrix'; import {UnwrappedTileID, OverscaledTileID, CanonicalTileID, calculateTileKey} from '../../source/tile_id'; import {Terrain} from '../../render/terrain'; @@ -10,7 +10,7 @@ import {PointProjection, xyTransformMat4} from '../../symbol/projection'; import {LngLatBounds} from '../lng_lat_bounds'; import {CoveringTilesOptions, CoveringZoomOptions, IReadonlyTransform, ITransform, TransformUpdateResult} from '../transform_interface'; import {PaddingOptions} from '../edge_insets'; -import {mercatorCoordinateToLocation, getBasicProjectionData, getMercatorHorizon, locationToMercatorCoordinate, projectToWorldCoordinates, unprojectFromWorldCoordinates, calculateTileMatrix} from './mercator_utils'; +import {mercatorCoordinateToLocation, getBasicProjectionData, getMercatorHorizon, locationToMercatorCoordinate, projectToWorldCoordinates, unprojectFromWorldCoordinates, calculateTileMatrix, maxMercatorHorizonAngle, cameraMercatorCoordinateFromCenterAndRotation} from './mercator_utils'; import {EXTENT} from '../../data/extent'; import type {ProjectionData} from './projection_data'; import {scaleZoom, TransformHelper, zoomScale} from '../transform_helper'; @@ -68,6 +68,9 @@ export class MercatorTransform implements ITransform { setPitch(pitch: number): void { this._helper.setPitch(pitch); } + setRoll(roll: number): void { + this._helper.setRoll(roll); + } setFov(fov: number): void { this._helper.setFov(fov); } @@ -126,9 +129,6 @@ export class MercatorTransform implements ITransform { get height(): number { return this._helper.height; } - get angle(): number { - return this._helper.angle; - } get lngRange(): [number, number] { return this._helper.lngRange; } @@ -156,12 +156,27 @@ export class MercatorTransform implements ITransform { get pitch(): number { return this._helper.pitch; } + get pitchInRadians(): number { + return this._helper.pitchInRadians; + } + get roll(): number { + return this._helper.roll; + } + get rollInRadians(): number { + return this._helper.rollInRadians; + } get bearing(): number { return this._helper.bearing; } + get bearingInRadians(): number { + return this._helper.bearingInRadians; + } get fov(): number { return this._helper.fov; } + get fovInRadians(): number { + return this._helper.fovInRadians; + } get elevation(): number { return this._helper.elevation; } @@ -258,36 +273,33 @@ export class MercatorTransform implements ITransform { return mercatorCoveringTiles(this, options, this._invViewProjMatrix); } - recalculateZoom(terrain: Terrain): void { - const origElevation = this.elevation; - const origAltitude = Math.cos(this._helper._pitch) * this._cameraToCenterDistance / this._helper._pixelPerMeter; - + recalculateZoomAndCenter(terrain?: Terrain): void { // find position the camera is looking on const center = this.screenPointToLocation(this.centerPoint, terrain); - const elevation = terrain.getElevationForLngLatZoom(center, this._helper._tileZoom); + const elevation = terrain ? terrain.getElevationForLngLatZoom(center, this._helper._tileZoom) : 0; const deltaElevation = this.elevation - elevation; if (!deltaElevation) return; - // The camera's altitude off the ground + the ground's elevation = a constant: - // this means the camera stays at the same total height. - const requiredAltitude = origAltitude + origElevation - elevation; - // Since altitude = Math.cos(this._pitch) * this.cameraToCenterDistance / pixelPerMeter: - const requiredPixelPerMeter = Math.cos(this._helper._pitch) * this._cameraToCenterDistance / requiredAltitude; - // Since pixelPerMeter = mercatorZfromAltitude(1, center.lat) * worldSize: - const requiredWorldSize = requiredPixelPerMeter / mercatorZfromAltitude(1, center.lat); - // Since worldSize = this.tileSize * scale: - const requiredScale = requiredWorldSize / this.tileSize; - const zoom = scaleZoom(requiredScale); + // Find the current camera position + const originalPixelPerMeter = mercatorZfromAltitude(1, this.center.lat) * this.worldSize; + const cameraToCenterDistanceMeters = this._cameraToCenterDistance / originalPixelPerMeter; + const origCenterMercator = MercatorCoordinate.fromLngLat(this.center, this.elevation); + const cameraMercator = cameraMercatorCoordinateFromCenterAndRotation(this.center, this.elevation, this.pitch, this.bearing, cameraToCenterDistanceMeters); - // update matrices + // update elevation to the new terrain intercept elevation and recalculate the center point this._helper._elevation = elevation; - this._helper._center = center; - this.setZoom(zoom); + const centerInfo = this.calculateCenterFromCameraLngLatAlt(cameraMercator.toLngLat(), altitudeFromMercatorZ(cameraMercator.z, origCenterMercator.y), this.bearing, this.pitch); + + // update matrices + this._helper._elevation = centerInfo.elevation; + this._helper._center = centerInfo.center; + this.setZoom(centerInfo.zoom); } setLocationAtPoint(lnglat: LngLat, point: Point) { - const a = this.screenPointToMercatorCoordinate(point); - const b = this.screenPointToMercatorCoordinate(this.centerPoint); + const z = mercatorZfromAltitude(this.elevation, this.center.lat); + const a = this.screenPointToMercatorCoordinateAtZ(point, z); + const b = this.screenPointToMercatorCoordinateAtZ(this.centerPoint, z); const loc = locationToMercatorCoordinate(lnglat); const newCenter = new MercatorCoordinate( loc.x - (a.x - b.x), @@ -316,9 +328,13 @@ export class MercatorTransform implements ITransform { return coordinate; } } + return this.screenPointToMercatorCoordinateAtZ(p); + } + + screenPointToMercatorCoordinateAtZ(p: Point, mercatorZ?: number): MercatorCoordinate { // calculate point-coordinate on flat earth - const targetZ = 0; + const targetZ = mercatorZ ? mercatorZ : 0; // 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 @@ -342,7 +358,8 @@ export class MercatorTransform implements ITransform { return new MercatorCoordinate( interpolates.number(x0, x1, t) / this.worldSize, - interpolates.number(y0, y1, t) / this.worldSize); + interpolates.number(y0, y1, t) / this.worldSize, + targetZ); } /** @@ -508,10 +525,58 @@ export class MercatorTransform implements ITransform { return result; } + calculateCenterFromCameraLngLatAlt(lnglat: LngLat, alt: number, bearing?: number, pitch?: number): {center: LngLat; elevation: number; zoom: number} { + const cameraBearing = bearing !== undefined ? bearing : this.bearing; + const cameraPitch = pitch = pitch !== undefined ? pitch : this.pitch; + + const camMercator = MercatorCoordinate.fromLngLat(lnglat, alt); + const dzNormalized = -Math.cos(degreesToRadians(cameraPitch)); + const dhNormalized = Math.sin(degreesToRadians(cameraPitch)); + const dxNormalized = dhNormalized * Math.sin(degreesToRadians(cameraBearing)); + const dyNormalized = -dhNormalized * Math.cos(degreesToRadians(cameraBearing)); + + let elevation = this.elevation; + const altitudeAGL = alt - elevation; + let distanceToCenterMeters; + if (dzNormalized * altitudeAGL >= 0.0 || Math.abs(dzNormalized) < 0.1) { + distanceToCenterMeters = 10000; + elevation = alt + distanceToCenterMeters * dzNormalized; + } else { + distanceToCenterMeters = -altitudeAGL / dzNormalized; + } + + // The mercator transform scale changes with latitude. At high latitudes, there are more "Merc units" per meter + // than at the equator. We treat the center point as our fundamental quantity. This means we want to convert + // elevation to Mercator Z using the scale factor at the center point (not the camera point). Since the center point is + // initially unknown, we compute it using the scale factor at the camera point. This gives us a better estimate of the + // center point scale factor, which we use to recompute the center point. We repeat until the error is very small. + // This typically takes about 5 iterations. + let metersPerMercUnit = altitudeFromMercatorZ(1, camMercator.y); + let centerMercator: MercatorCoordinate; + let dMercator: number; + let iter = 0; + const maxIter = 10; + do { + iter += 1; + if (iter > maxIter) { + break; + } + dMercator = distanceToCenterMeters / metersPerMercUnit; + const dx = dxNormalized * dMercator; + const dy = dyNormalized * dMercator; + centerMercator = new MercatorCoordinate(camMercator.x + dx, camMercator.y + dy); + metersPerMercUnit = 1 / centerMercator.meterInMercatorCoordinateUnits(); + } while (Math.abs(distanceToCenterMeters - dMercator * metersPerMercUnit) > 1.0e-12); + + const center = centerMercator.toLngLat(); + const zoom = scaleZoom(this.height / 2 / Math.tan(this.fovInRadians / 2) / dMercator / this.tileSize); + return {center, elevation, zoom}; + } + _calcMatrices(): void { if (!this._helper._height) return; - const halfFov = this._helper._fov / 2; + const halfFov = this.fovInRadians / 2; const offset = this.centerOffset; const point = projectToWorldCoordinates(this.worldSize, this.center); const x = point.x, y = point.y; @@ -519,30 +584,34 @@ export class MercatorTransform implements ITransform { this._helper._pixelPerMeter = mercatorZfromAltitude(1, this.center.lat) * this.worldSize; // Calculate the camera to sea-level distance in pixel in respect of terrain - const cameraToSeaLevelDistance = this._cameraToCenterDistance + this._helper._elevation * this._helper._pixelPerMeter / Math.cos(this._helper._pitch); + const limitedPitchRadians = degreesToRadians(Math.min(this.pitch, maxMercatorHorizonAngle)); + const cameraToSeaLevelDistance = Math.max(this._cameraToCenterDistance / 2, this._cameraToCenterDistance + this._helper._elevation * this._helper._pixelPerMeter / Math.cos(limitedPitchRadians)); // In case of negative minimum elevation (e.g. the dead see, under the sea maps) use a lower plane for calculation - const minElevation = Math.min(this.elevation, this.minElevationForCurrentTile); - const cameraToLowestPointDistance = cameraToSeaLevelDistance - minElevation * this._helper._pixelPerMeter / Math.cos(this._helper._pitch); + const minRenderDistanceBelowCameraInMeters = 100; + const minElevation = Math.min(this.elevation, this.minElevationForCurrentTile, this.getCameraAltitude() - minRenderDistanceBelowCameraInMeters); + const cameraToLowestPointDistance = cameraToSeaLevelDistance - minElevation * this._helper._pixelPerMeter / Math.cos(limitedPitchRadians); const lowestPlane = minElevation < 0 ? cameraToLowestPointDistance : cameraToSeaLevelDistance; // 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._helper._pitch; - const fovAboveCenter = this._helper._fov * (0.5 + offset.y / this._helper._height); + const groundAngle = Math.PI / 2 + this.pitchInRadians; + const zfov = degreesToRadians(this.fov) * (Math.abs(Math.cos(degreesToRadians(this.roll))) * this.height + Math.abs(Math.sin(degreesToRadians(this.roll))) * this.width) / this.height; + const fovAboveCenter = zfov * (0.5 + offset.y / this.height); const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * lowestPlane / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, 0.01, Math.PI - 0.01)); // Find the distance from the center point to the horizon const horizon = getMercatorHorizon(this); const horizonAngle = Math.atan(horizon / this._cameraToCenterDistance); - const fovCenterToHorizon = 2 * horizonAngle * (0.5 + offset.y / (horizon * 2)); + const minFovCenterToHorizonRadians = degreesToRadians(90 - maxMercatorHorizonAngle); + const fovCenterToHorizon = horizonAngle > minFovCenterToHorizonRadians ? 2 * horizonAngle * (0.5 + offset.y / (horizon * 2)) : minFovCenterToHorizonRadians; const topHalfSurfaceDistanceHorizon = Math.sin(fovCenterToHorizon) * lowestPlane / Math.sin(clamp(Math.PI - groundAngle - fovCenterToHorizon, 0.01, Math.PI - 0.01)); // Calculate z distance of the farthest fragment that should be rendered. // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` const topHalfMinDistance = Math.min(topHalfSurfaceDistance, topHalfSurfaceDistanceHorizon); - this._farZ = (Math.cos(Math.PI / 2 - this._helper._pitch) * topHalfMinDistance + lowestPlane) * 1.01; + this._farZ = (Math.cos(Math.PI / 2 - limitedPitchRadians) * topHalfMinDistance + lowestPlane) * 1.01; // The larger the value of nearZ is // - the more depth precision is available for features (good) @@ -556,7 +625,7 @@ export class MercatorTransform implements ITransform { // matrix for conversion from location to clip space(-1 .. 1) let m: mat4; m = new Float64Array(16) as any; - mat4.perspective(m, this._helper._fov, this._helper._width / this._helper._height, this._nearZ, this._farZ); + mat4.perspective(m, this.fovInRadians, this._helper._width / this._helper._height, this._nearZ, this._farZ); this._invProjMatrix = new Float64Array(16) as any as mat4; mat4.invert(this._invProjMatrix, m); @@ -567,8 +636,9 @@ export class MercatorTransform implements ITransform { mat4.scale(m, m, [1, -1, 1]); mat4.translate(m, m, [0, 0, -this._cameraToCenterDistance]); - mat4.rotateX(m, m, this._helper._pitch); - mat4.rotateZ(m, m, this._helper._angle); + mat4.rotateZ(m, m, -this.rollInRadians); + mat4.rotateX(m, m, this.pitchInRadians); + mat4.rotateZ(m, m, -this.bearingInRadians); mat4.translate(m, m, [-x, -y, 0]); // The mercatorMatrix can be used to transform points from mercator coordinates @@ -597,13 +667,14 @@ export class MercatorTransform implements ITransform { // create a fog matrix, same es proj-matrix but with near clipping-plane in mapcenter // needed to calculate a correct z-value for fog calculation, because projMatrix z value is not this._fogMatrix = new Float64Array(16) as any; - mat4.perspective(this._fogMatrix, this._helper._fov, this.width / this.height, cameraToSeaLevelDistance, this._farZ); + mat4.perspective(this._fogMatrix, this.fovInRadians, this.width / this.height, cameraToSeaLevelDistance, this._farZ); this._fogMatrix[8] = -offset.x * 2 / this.width; this._fogMatrix[9] = offset.y * 2 / this.height; mat4.scale(this._fogMatrix, this._fogMatrix, [1, -1, 1]); mat4.translate(this._fogMatrix, this._fogMatrix, [0, 0, -this.cameraToCenterDistance]); - mat4.rotateX(this._fogMatrix, this._fogMatrix, this._helper._pitch); - mat4.rotateZ(this._fogMatrix, this._fogMatrix, this.angle); + mat4.rotateZ(this._fogMatrix, this._fogMatrix, -this.rollInRadians); + mat4.rotateX(this._fogMatrix, this._fogMatrix, this.pitchInRadians); + mat4.rotateZ(this._fogMatrix, this._fogMatrix, -this.bearingInRadians); mat4.translate(this._fogMatrix, this._fogMatrix, [-x, -y, 0]); mat4.scale(this._fogMatrix, this._fogMatrix, [1, 1, this._helper._pixelPerMeter]); mat4.translate(this._fogMatrix, this._fogMatrix, [0, 0, -this.elevation]); // elevate camera over terrain @@ -618,7 +689,7 @@ export class MercatorTransform implements ITransform { // of the transformation so that 0°, 90°, 180°, and 270° rasters are crisp, and adjust the shift so that // it is always <= 0.5 pixels. const xShift = (this._helper._width % 2) / 2, yShift = (this._helper._height % 2) / 2, - angleCos = Math.cos(this._helper._angle), angleSin = Math.sin(this._helper._angle), + angleCos = Math.cos(this.bearingInRadians), angleSin = Math.sin(-this.bearingInRadians), dx = x - Math.round(x) + angleCos * xShift + angleSin * yShift, dy = y - Math.round(y) + angleCos * yShift + angleSin * xShift; const alignedM = new Float64Array(m) as any as mat4; @@ -650,16 +721,24 @@ export class MercatorTransform implements ITransform { } getCameraPoint(): Point { - const pitch = this._helper._pitch; + const pitch = this.pitchInRadians; const yOffset = Math.tan(pitch) * (this._cameraToCenterDistance || 1); return this.centerPoint.add(new Point(0, yOffset)); } getCameraAltitude(): number { - const altitude = Math.cos(this._helper._pitch) * this._cameraToCenterDistance / this._helper._pixelPerMeter; + const altitude = Math.cos(this.pitchInRadians) * this._cameraToCenterDistance / this._helper._pixelPerMeter; return altitude + this.elevation; } + getCameraLngLat(): LngLat { + const cameraToCenterDistancePixels = 0.5 / Math.tan(this.fovInRadians / 2) * this.height; + const pixelPerMeter = mercatorZfromAltitude(1, this.center.lat) * this.worldSize; + const cameraToCenterDistanceMeters = cameraToCenterDistancePixels / pixelPerMeter; + const camMercator = cameraMercatorCoordinateFromCenterAndRotation(this.center, this.elevation, this.pitch, this.bearing, cameraToCenterDistanceMeters); + return camMercator.toLngLat(); + } + lngLatToCameraDepth(lngLat: LngLat, elevation: number) { const coord = locationToMercatorCoordinate(lngLat); const p = [coord.x * this.worldSize, coord.y * this.worldSize, elevation, 1] as vec4; diff --git a/src/geo/projection/mercator_utils.test.ts b/src/geo/projection/mercator_utils.test.ts index b5853d167ad..44b302e3ced 100644 --- a/src/geo/projection/mercator_utils.test.ts +++ b/src/geo/projection/mercator_utils.test.ts @@ -36,6 +36,24 @@ describe('mercator utils', () => { expect(horizon).toBeCloseTo(170.8176101748407, 10); }); + test('getMercatorHorizon90', () => { + const transform = new MercatorTransform(0, 22, 0, 180, true); + transform.resize(500, 500); + transform.setPitch(90); + const horizon = getMercatorHorizon(transform); + + expect(horizon).toBeCloseTo(-9.818037813626313, 10); + }); + + test('getMercatorHorizon95', () => { + const transform = new MercatorTransform(0, 22, 0, 180, true); + transform.resize(500, 500); + transform.setPitch(95); + const horizon = getMercatorHorizon(transform); + + expect(horizon).toBeCloseTo(-75.52102888757743, 10); + }); + describe('getBasicProjectionData', () => { test('posMatrix is set', () => { const mat = mat4.create(); diff --git a/src/geo/projection/mercator_utils.ts b/src/geo/projection/mercator_utils.ts index dee0e77feba..de2a6ab8781 100644 --- a/src/geo/projection/mercator_utils.ts +++ b/src/geo/projection/mercator_utils.ts @@ -1,13 +1,21 @@ import {mat4} from 'gl-matrix'; import {EXTENT} from '../../data/extent'; import {OverscaledTileID} from '../../source/tile_id'; -import {clamp} from '../../util/util'; +import {clamp, degreesToRadians} from '../../util/util'; import {MAX_VALID_LATITUDE, UnwrappedTileIDType, zoomScale} from '../transform_helper'; import {LngLat} from '../lng_lat'; -import {MercatorCoordinate, mercatorXfromLng, mercatorYfromLat} from '../mercator_coordinate'; +import {MercatorCoordinate, mercatorXfromLng, mercatorYfromLat, mercatorZfromAltitude} from '../mercator_coordinate'; import Point from '@mapbox/point-geometry'; import type {ProjectionData} from './projection_data'; +/* +* The maximum angle to use for the Mercator horizon. This must be less than 90 +* to prevent errors in `MercatorTransform::_calcMatrices()`. It shouldn't be too close +* to 90, or the distance to the horizon will become very large, unnecessarily increasing +* the number of tiles needed to render the map. +*/ +export const maxMercatorHorizonAngle = 89.25; + /** * Returns mercator coordinates in range 0..1 for given coordinates inside a specified tile. * @param inTileX - X coordinate in tile units - range [0..EXTENT]. @@ -82,7 +90,8 @@ export function unprojectFromWorldCoordinates(worldSize: number, point: Point): * @returns Horizon above center in pixels. */ export function getMercatorHorizon(transform: {pitch: number; cameraToCenterDistance: number}): number { - return Math.tan(Math.PI / 2 - transform.pitch * Math.PI / 180.0) * transform.cameraToCenterDistance * 0.85; + return transform.cameraToCenterDistance * Math.min(Math.tan(degreesToRadians(90 - transform.pitch)) * 0.85, + Math.tan(degreesToRadians(maxMercatorHorizonAngle - transform.pitch))); } export function getBasicProjectionData(overscaledTileID: OverscaledTileID, tilePosMatrix?: mat4, ignoreTerrainMatrix?: boolean): ProjectionData { @@ -130,3 +139,14 @@ export function calculateTileMatrix(unwrappedTileID: UnwrappedTileIDType, worldS mat4.scale(worldMatrix, worldMatrix, [scale / EXTENT, scale / EXTENT, 1]); return worldMatrix; } + +export function cameraMercatorCoordinateFromCenterAndRotation(center: LngLat, elevation: number, pitch: number, bearing: number, distance: number): MercatorCoordinate { + const centerMercator = MercatorCoordinate.fromLngLat(center, elevation); + const mercUnitsPerMeter = mercatorZfromAltitude(1, center.lat); + const dMercator = distance * mercUnitsPerMeter; + const dzMercator = dMercator * Math.cos(degreesToRadians(pitch)); + const dhMercator = Math.sqrt(dMercator * dMercator - dzMercator * dzMercator); + const dxMercator = dhMercator * Math.sin(degreesToRadians(-bearing)); + const dyMercator = dhMercator * Math.cos(degreesToRadians(-bearing)); + return new MercatorCoordinate(centerMercator.x + dxMercator, centerMercator.y + dyMercator, centerMercator.z + dzMercator); +} diff --git a/src/geo/transform_helper.test.ts b/src/geo/transform_helper.test.ts index 066eb81bf79..5a1fdcbbec8 100644 --- a/src/geo/transform_helper.test.ts +++ b/src/geo/transform_helper.test.ts @@ -27,6 +27,7 @@ describe('TransformHelper', () => { left: 3, }); original.setPitch(3); + original.setRoll(7); original.setRenderWorldCopies(false); original.setZoom(2.3); @@ -40,7 +41,7 @@ describe('TransformHelper', () => { expect(cloned.worldSize).toEqual(original.worldSize); expect(cloned.width).toEqual(original.width); expect(cloned.height).toEqual(original.height); - expect(cloned.angle).toEqual(original.angle); + expect(cloned.bearingInRadians).toEqual(original.bearingInRadians); expect(cloned.lngRange).toEqual(original.lngRange); expect(cloned.latRange).toEqual(original.latRange); expect(cloned.minZoom).toEqual(original.minZoom); @@ -50,6 +51,7 @@ describe('TransformHelper', () => { expect(cloned.minPitch).toEqual(original.minPitch); expect(cloned.maxPitch).toEqual(original.maxPitch); expect(cloned.pitch).toEqual(original.pitch); + expect(cloned.roll).toEqual(original.roll); expect(cloned.bearing).toEqual(original.bearing); expect(cloned.fov).toEqual(original.fov); expect(cloned.elevation).toEqual(original.elevation); diff --git a/src/geo/transform_helper.ts b/src/geo/transform_helper.ts index 7a845ea4567..bf5d0df7549 100644 --- a/src/geo/transform_helper.ts +++ b/src/geo/transform_helper.ts @@ -93,16 +93,19 @@ export class TransformHelper implements ITransformGetters { /** * Vertical field of view in radians. */ - _fov: number; + _fovInRadians: number; /** * This transform's bearing in radians. - * Note that the sign of this variable is *opposite* to the sign of {@link bearing} */ - _angle: number; + _bearingInRadians: number; /** * Pitch in radians. */ - _pitch: number; + _pitchInRadians: number; + /** + * Roll in radians. + */ + _rollInRadians: number; _zoom: number; _renderWorldCopies: boolean; _minZoom: number; @@ -142,9 +145,10 @@ export class TransformHelper implements ITransformGetters { this._zoom = 0; this._tileZoom = getTileZoom(this._zoom); this._scale = zoomScale(this._zoom); - this._angle = 0; - this._fov = 0.6435011087932844; - this._pitch = 0; + this._bearingInRadians = 0; + this._fovInRadians = 0.6435011087932844; + this._pitchInRadians = 0; + this._rollInRadians = 0; this._unmodified = true; this._edgeInsets = new EdgeInsets(); this._minElevationForCurrentTile = 0; @@ -161,9 +165,10 @@ export class TransformHelper implements ITransformGetters { this._zoom = thatI.zoom; this._tileZoom = getTileZoom(this._zoom); this._scale = zoomScale(this._zoom); - this._angle = -thatI.bearing * Math.PI / 180; - this._fov = thatI.fov * Math.PI / 180; - this._pitch = thatI.pitch * Math.PI / 180; + this._bearingInRadians = thatI.bearingInRadians; + this._fovInRadians = thatI.fovInRadians; + this._pitchInRadians = thatI.pitchInRadians; + this._rollInRadians = thatI.rollInRadians; this._unmodified = thatI.unmodified; this._edgeInsets = new EdgeInsets(thatI.padding.top, thatI.padding.bottom, thatI.padding.left, thatI.padding.right); this._minZoom = thatI.minZoom; @@ -202,7 +207,7 @@ export class TransformHelper implements ITransformGetters { /** * Gets the transform's bearing in radians. */ - get angle(): number { return this._angle; } + get bearingInRadians(): number { return this._bearingInRadians; } get lngRange(): [number, number] { return this._lngRange; } get latRange(): [number, number] { return this._latRange; } @@ -264,41 +269,61 @@ export class TransformHelper implements ITransformGetters { } get bearing(): number { - return -this._angle / Math.PI * 180; + return this._bearingInRadians / Math.PI * 180; } setBearing(bearing: number) { - const b = -wrap(bearing, -180, 180) * Math.PI / 180; - if (this._angle === b) return; + const b = wrap(bearing, -180, 180) * Math.PI / 180; + if (this._bearingInRadians === b) return; this._unmodified = false; - this._angle = b; + this._bearingInRadians = b; this._calcMatrices(); // 2x2 matrix for rotating points this._rotationMatrix = mat2.create(); - mat2.rotate(this._rotationMatrix, this._rotationMatrix, this._angle); + mat2.rotate(this._rotationMatrix, this._rotationMatrix, -this._bearingInRadians); } get rotationMatrix(): mat2 { return this._rotationMatrix; } + get pitchInRadians(): number { + return this._pitchInRadians; + } get pitch(): number { - return this._pitch / Math.PI * 180; + return this._pitchInRadians / Math.PI * 180; } setPitch(pitch: number) { const p = clamp(pitch, this.minPitch, this.maxPitch) / 180 * Math.PI; - if (this._pitch === p) return; + if (this._pitchInRadians === p) return; + this._unmodified = false; + this._pitchInRadians = p; + this._calcMatrices(); + } + + get rollInRadians(): number { + return this._rollInRadians; + } + get roll(): number { + return this._rollInRadians / Math.PI * 180; + } + setRoll(roll: number) { + const r = roll / 180 * Math.PI; + if (this._rollInRadians === r) return; this._unmodified = false; - this._pitch = p; + this._rollInRadians = r; this._calcMatrices(); } + get fovInRadians(): number { + return this._fovInRadians; + } get fov(): number { - return this._fov / Math.PI * 180; + return this._fovInRadians / Math.PI * 180; } setFov(fov: number) { fov = Math.max(0.01, Math.min(60, fov)); - if (this._fov === fov) return; + if (this._fovInRadians === fov) return; this._unmodified = false; - this._fov = fov / 180 * Math.PI; + this._fovInRadians = fov / 180 * Math.PI; this._calcMatrices(); } diff --git a/src/geo/transform_interface.ts b/src/geo/transform_interface.ts index 755d80a5b9c..5fdf3cdcfb5 100644 --- a/src/geo/transform_interface.ts +++ b/src/geo/transform_interface.ts @@ -75,11 +75,6 @@ export interface ITransformGetters { */ get height(): number; - /** - * Gets the transform's bearing in radians. - */ - get angle(): number; - get lngRange(): [number, number]; get latRange(): [number, number]; @@ -90,18 +85,26 @@ export interface ITransformGetters { get minPitch(): number; get maxPitch(): number; + /** + * Roll in degrees. + */ + get roll(): number; + get rollInRadians(): number; /** * Pitch in degrees. */ get pitch(): number; + get pitchInRadians(): number; /** * Bearing in degrees. */ get bearing(): number; + get bearingInRadians(): number; /** * Vertical field of view in degrees. */ get fov(): number; + get fovInRadians(): number; get elevation(): number; get minElevationForCurrentTile(): number; @@ -152,6 +155,11 @@ interface ITransformMutators { * Recomputes internal matrices if needed. */ setPitch(pitch: number): void; + /** + * Sets the transform's roll, in degrees. + * Recomputes internal matrices if needed. + */ + setRoll(roll: number): void; /** * Sets the transform's vertical field of view, in degrees. * Recomputes internal matrices if needed. @@ -187,10 +195,10 @@ interface ITransformMutators { /** * This method works in combination with freezeElevation activated. * freezeElevation is enabled during map-panning because during this the camera should sit in constant height. - * After panning finished, call this method to recalculate the zoom level for the current camera-height in current terrain. + * After panning finished, call this method to recalculate the zoom level and center point for the current camera-height in current terrain. * @param terrain - the terrain */ - recalculateZoom(terrain: Terrain): void; + recalculateZoomAndCenter(terrain?: Terrain): void; /** * Set's the transform's center so that the given point on screen is at the given world coordinates. @@ -369,10 +377,24 @@ export interface IReadonlyTransform extends ITransformGetters { getCameraPoint(): Point; /** - * The altitude of the camera above the center of the map in meters. + * The altitude of the camera above the sea level in meters. */ getCameraAltitude(): number; + /** + * The longitude and latitude of the camera. + */ + getCameraLngLat(): LngLat; + + /** + * Given the camera position (lng, lat, alt), calculate the center point and zoom level + * @param lngLat - lng, lat of the camera + * @param alt - altitude of the camera above sea level, in meters + * @param bearing - bearing of the camera, in degrees + * @param pitch - pitch angle of the camera, in degrees + */ + calculateCenterFromCameraLngLatAlt(lngLat: LngLat, alt: number, bearing?: number, pitch?: number): {center: LngLat; elevation: number; zoom: number}; + getRayDirectionFromPixel(p: Point): vec3; /** diff --git a/src/gl/context.ts b/src/gl/context.ts index aa3749ae89d..2e2d4cae6ea 100644 --- a/src/gl/context.ts +++ b/src/gl/context.ts @@ -65,7 +65,6 @@ export class Context { pixelStoreUnpackPremultiplyAlpha: PixelStoreUnpackPremultiplyAlpha; pixelStoreUnpackFlipY: PixelStoreUnpackFlipY; - // eslint-disable-next-line camelcase extTextureFilterAnisotropic: EXT_texture_filter_anisotropic | null; extTextureFilterAnisotropicMax?: GLfloat; HALF_FLOAT?: GLenum; diff --git a/src/index.ts b/src/index.ts index 56cf375388f..a27ba961729 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,7 +60,7 @@ export type * from '@maplibre/maplibre-gl-style-spec'; * rtl text will then be rendered only after the plugin finishes loading. * @example * ```ts - * setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.js', false); + * setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.3.0/dist/mapbox-gl-rtl-text.js', false); * ``` * @see [Add support for right-to-left scripts](https://maplibre.org/maplibre-gl-js/docs/examples/mapbox-gl-rtl-text/) */ diff --git a/src/render/draw_background.ts b/src/render/draw_background.ts index 1b0182b1afc..1db26599eb1 100644 --- a/src/render/draw_background.ts +++ b/src/render/draw_background.ts @@ -29,7 +29,7 @@ export function drawBackground(painter: Painter, sourceCache: SourceCache, layer if (painter.renderPass !== pass) return; const stencilMode = StencilMode.disabled; - const depthMode = painter.depthModeForSublayer(0, pass === 'opaque' ? DepthMode.ReadWrite : DepthMode.ReadOnly); + const depthMode = painter.getDepthModeForSublayer(0, pass === 'opaque' ? DepthMode.ReadWrite : DepthMode.ReadOnly); const colorMode = painter.colorModeForRenderPass(); const program = painter.useProgram(image ? 'backgroundPattern' : 'background'); const tileIDs = coords ? coords : transform.coveringTiles({tileSize, terrain: painter.style.map.terrain}); diff --git a/src/render/draw_circle.ts b/src/render/draw_circle.ts index 02b91d11290..7960ff99f74 100644 --- a/src/render/draw_circle.ts +++ b/src/render/draw_circle.ts @@ -51,7 +51,7 @@ export function drawCircles(painter: Painter, sourceCache: SourceCache, layer: C const gl = context.gl; const transform = painter.transform; - const depthMode = painter.depthModeForSublayer(0, DepthMode.ReadOnly); + const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); // Turn off stencil testing to allow circles to be drawn across boundaries, // so that large circles are not clipped to tiles const stencilMode = StencilMode.disabled; diff --git a/src/render/draw_custom.test.ts b/src/render/draw_custom.test.ts index 9e5868ca4c3..47f7217e707 100644 --- a/src/render/draw_custom.test.ts +++ b/src/render/draw_custom.test.ts @@ -62,7 +62,7 @@ describe('drawCustom', () => { }); drawCustom(mockPainter, sourceCacheMock, mockLayer); expect(result.gl).toBeDefined(); - expect(result.args.farZ).toBe(804.8028169246645); + expect(result.args.farZ).toBeCloseTo(804.8028169246645, 6); expect(result.args.farZ).toBe(mockPainter.transform.farZ); expect(result.args.nearZ).toBe(mockPainter.transform.nearZ); expect(result.args.fov).toBe(mockPainter.transform.fov * Math.PI / 180); diff --git a/src/render/draw_custom.ts b/src/render/draw_custom.ts index 4cd102a1cb8..c34ec6b5aa7 100644 --- a/src/render/draw_custom.ts +++ b/src/render/draw_custom.ts @@ -28,8 +28,9 @@ export function drawCustom(painter: Painter, sourceCache: SourceCache, layer: Cu defaultProjectionData: projectionData, }; - if (painter.renderPass === 'offscreen') { + const renderingMode = implementation.renderingMode ? implementation.renderingMode : '2d'; + if (painter.renderPass === 'offscreen') { const prerender = implementation.prerender; if (prerender) { painter.setCustomLayerDefaults(); @@ -40,7 +41,6 @@ export function drawCustom(painter: Painter, sourceCache: SourceCache, layer: Cu context.setDirty(); painter.setBaseState(); } - } else if (painter.renderPass === 'translucent') { painter.setCustomLayerDefaults(); @@ -48,9 +48,9 @@ export function drawCustom(painter: Painter, sourceCache: SourceCache, layer: Cu context.setColorMode(painter.colorModeForRenderPass()); context.setStencilMode(StencilMode.disabled); - const depthMode = implementation.renderingMode === '3d' ? - new DepthMode(painter.context.gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D) : - painter.depthModeForSublayer(0, DepthMode.ReadOnly); + const depthMode = renderingMode === '3d' ? + painter.getDepthModeFor3D() : + painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); context.setDepthMode(depthMode); diff --git a/src/render/draw_fill.ts b/src/render/draw_fill.ts index 05edc7506de..0b23b5b5723 100644 --- a/src/render/draw_fill.ts +++ b/src/render/draw_fill.ts @@ -36,7 +36,7 @@ export function drawFill(painter: Painter, sourceCache: SourceCache, layer: Fill // Draw fill if (painter.renderPass === pass) { - const depthMode = painter.depthModeForSublayer( + const depthMode = painter.getDepthModeForSublayer( 1, painter.renderPass === 'opaque' ? DepthMode.ReadWrite : DepthMode.ReadOnly); drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode, false); } @@ -52,7 +52,7 @@ export function drawFill(painter: Painter, sourceCache: SourceCache, layer: Fill // or stroke color is translucent. If we wouldn't clip to outside // the current shape, some pixels from the outline stroke overlapped // the (non-antialiased) fill. - const depthMode = painter.depthModeForSublayer( + const depthMode = painter.getDepthModeForSublayer( layer.getPaintProperty('fill-outline-color') ? 2 : 0, DepthMode.ReadOnly); drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode, true); } diff --git a/src/render/draw_fill_extrusion.ts b/src/render/draw_fill_extrusion.ts index 0b2a4b8448b..02c2d8f457d 100644 --- a/src/render/draw_fill_extrusion.ts +++ b/src/render/draw_fill_extrusion.ts @@ -63,7 +63,6 @@ function drawExtrusionTiles( const opacity = layer.paint.get('fill-extrusion-opacity'); const constantPattern = patternProperty.constantOr(null); const transform = painter.transform; - const globeCameraPosition = transform.cameraPosition; for (const coord of coords) { const tile = source.getTile(coord); @@ -92,8 +91,8 @@ function drawExtrusionTiles( const shouldUseVerticalGradient = layer.paint.get('fill-extrusion-vertical-gradient'); const uniformValues = image ? - fillExtrusionPatternUniformValues(painter, shouldUseVerticalGradient, opacity, translate, globeCameraPosition, coord, crossfade, tile) : - fillExtrusionUniformValues(painter, shouldUseVerticalGradient, opacity, translate, globeCameraPosition); + fillExtrusionPatternUniformValues(painter, shouldUseVerticalGradient, opacity, translate, coord, crossfade, tile) : + fillExtrusionUniformValues(painter, shouldUseVerticalGradient, opacity, translate); program.draw(context, context.gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer, diff --git a/src/render/draw_hillshade.ts b/src/render/draw_hillshade.ts index 1b6412ca03a..6469ea153ce 100644 --- a/src/render/draw_hillshade.ts +++ b/src/render/draw_hillshade.ts @@ -20,7 +20,7 @@ export function drawHillshade(painter: Painter, sourceCache: SourceCache, layer: const projection = painter.style.projection; const useSubdivision = projection.useSubdivision; - const depthMode = painter.depthModeForSublayer(0, DepthMode.ReadOnly); + const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); const colorMode = painter.colorModeForRenderPass(); if (painter.renderPass === 'offscreen') { diff --git a/src/render/draw_line.ts b/src/render/draw_line.ts index 407812e3a53..0a8b0b1d9d3 100644 --- a/src/render/draw_line.ts +++ b/src/render/draw_line.ts @@ -24,7 +24,7 @@ export function drawLine(painter: Painter, sourceCache: SourceCache, layer: Line const width = layer.paint.get('line-width'); if (opacity.constantOr(1) === 0 || width.constantOr(1) === 0) return; - const depthMode = painter.depthModeForSublayer(0, DepthMode.ReadOnly); + const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); const colorMode = painter.colorModeForRenderPass(); const dasharray = layer.paint.get('line-dasharray'); diff --git a/src/render/draw_raster.ts b/src/render/draw_raster.ts index d505935b1e5..ccff8b98fff 100644 --- a/src/render/draw_raster.ts +++ b/src/render/draw_raster.ts @@ -82,7 +82,7 @@ function drawTiles( 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 = painter.getDepthModeForSublayer(coord.overscaledZ - minTileZ, layer.paint.get('raster-opacity') === 1 ? DepthMode.ReadWrite : DepthMode.ReadOnly, gl.LESS); const tile = sourceCache.getTile(coord); diff --git a/src/render/draw_sky.ts b/src/render/draw_sky.ts index 590484ce070..27b4d0c357a 100644 --- a/src/render/draw_sky.ts +++ b/src/render/draw_sky.ts @@ -64,8 +64,9 @@ function getSunPos(light: Light, transform: IReadonlyTransform): vec3 { const lightMat = mat4.identity(new Float64Array(16) as any); if (light.properties.get('anchor') === 'map') { - mat4.rotateX(lightMat, lightMat, -transform.pitch * Math.PI / 180); - mat4.rotateZ(lightMat, lightMat, -transform.angle); + mat4.rotateZ(lightMat, lightMat, transform.rollInRadians); + mat4.rotateX(lightMat, lightMat, -transform.pitchInRadians); + mat4.rotateZ(lightMat, lightMat, transform.bearingInRadians); mat4.rotateX(lightMat, lightMat, transform.center.lat * Math.PI / 180.0); mat4.rotateY(lightMat, lightMat, -transform.center.lng * Math.PI / 180.0); } diff --git a/src/render/draw_symbol.ts b/src/render/draw_symbol.ts index 0aa9db876fe..550ec1b77ff 100644 --- a/src/render/draw_symbol.ts +++ b/src/render/draw_symbol.ts @@ -245,7 +245,7 @@ function updateVariableAnchorsForBucket( const shift = calculateVariableRenderShift(anchor, width, height, textOffset, textBoxScale, renderTextSize); const pitchedTextCorrection = transform.getPitchedTextCorrection(tileAnchor.x + translation[0], tileAnchor.y + translation[1], unwrappedTileID); - const shiftedAnchor = getShiftedAnchor(projectedAnchor.point, projectionContext, rotateWithMap, shift, transform.angle, pitchedTextCorrection); + const shiftedAnchor = getShiftedAnchor(projectedAnchor.point, projectionContext, rotateWithMap, shift, -transform.bearingInRadians, pitchedTextCorrection); const angle = (bucket.allowVerticalPlacement && symbol.placedOrientation === WritingMode.vertical) ? Math.PI / 2 : 0; for (let g = 0; g < symbol.numGlyphs; g++) { @@ -320,7 +320,7 @@ function drawLayerSymbols( const hasSortKey = !layer.layout.get('symbol-sort-key').isConstant(); let sortFeaturesByKey = false; - const depthMode = painter.depthModeForSublayer(0, DepthMode.ReadOnly); + const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly); const hasVariablePlacement = layer._unevaluatedLayout.hasValue('text-variable-anchor') || layer._unevaluatedLayout.hasValue('text-variable-anchor-offset'); diff --git a/src/render/draw_terrain.ts b/src/render/draw_terrain.ts index 90badfaec83..ce5bcd5e027 100644 --- a/src/render/draw_terrain.ts +++ b/src/render/draw_terrain.ts @@ -74,7 +74,7 @@ function drawTerrain(painter: Painter, terrain: Terrain, tiles: Array) { const gl = context.gl; const tr = painter.transform; const colorMode = painter.colorModeForRenderPass(); - const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D); + const depthMode = painter.getDepthModeFor3D(); const program = painter.useProgram('terrain'); const mesh = terrain.getTerrainMesh(); diff --git a/src/render/painter.ts b/src/render/painter.ts index 96bad928749..741fb535f1b 100644 --- a/src/render/painter.ts +++ b/src/render/painter.ts @@ -308,6 +308,34 @@ export class Painter { } } + /** + * Fills the depth buffer with the geometry of all supplied tiles. + * Does not change the color buffer or the stencil buffer. + */ + _renderTilesDepthBuffer() { + const context = this.context; + const gl = context.gl; + const projection = this.style.projection; + const transform = this.transform; + + const program = this.useProgram('depth'); + const depthMode = this.getDepthModeFor3D(); + const tileIDs = transform.coveringTiles({tileSize: transform.tileSize}); + + // tiles are usually supplied in ascending order of z, then y, then x + for (const tileID of tileIDs) { + const terrainData = this.style.map.terrain && this.style.map.terrain.getTerrainData(tileID); + const mesh = projection.getMeshFromTileID(this.context, tileID.canonical, true, true, 'raster'); + + const projectionData = transform.getProjectionData(tileID); + + program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, + ColorMode.disabled, CullFaceMode.backCCW, null, + terrainData, projectionData, '$clipping', mesh.vertexBuffer, + mesh.indexBuffer, mesh.segments); + } + } + stencilModeFor3D(): StencilMode { this.currentStencilSource = undefined; @@ -406,12 +434,16 @@ export class Painter { } } - depthModeForSublayer(n: number, mask: DepthMaskType, func?: DepthFuncType | null): Readonly { + getDepthModeForSublayer(n: number, mask: DepthMaskType, func?: DepthFuncType | null): Readonly { if (!this.opaquePassEnabledForLayer()) return DepthMode.disabled; const depth = 1 - ((1 + this.currentLayer) * this.numSublayers + n) * this.depthEpsilon; return new DepthMode(func || this.context.gl.LEQUAL, mask, [depth, depth]); } + getDepthModeFor3D(): Readonly { + return new DepthMode(this.context.gl.LEQUAL, DepthMode.ReadWrite, this.depthRangeFor3D); + } + /* * The opaque pass and 3D layers both use the depth buffer. * Layers drawn above 3D layers need to be drawn using the @@ -525,12 +557,23 @@ export class Painter { // Draw all other layers bottom-to-top. this.renderPass = 'translucent'; + let globeDepthRendered = false; + for (this.currentLayer = 0; this.currentLayer < layerIds.length; this.currentLayer++) { const layer = this.style._layers[layerIds[this.currentLayer]]; const sourceCache = sourceCaches[layer.source]; if (this.renderToTexture && this.renderToTexture.renderLayer(layer)) continue; + if (!this.opaquePassEnabledForLayer() && !globeDepthRendered) { + globeDepthRendered = true; + // Render the globe sphere into the depth buffer - but only if globe is enabled and terrain is disabled. + // There should be no need for explicitly writing tile depths when terrain is enabled. + if (this.style.projection.name === 'globe' && !this.style.map.terrain) { + this._renderTilesDepthBuffer(); + } + } + // 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 diff --git a/src/render/program/fill_extrusion_program.ts b/src/render/program/fill_extrusion_program.ts index d600bca5a02..7b9c79a51f8 100644 --- a/src/render/program/fill_extrusion_program.ts +++ b/src/render/program/fill_extrusion_program.ts @@ -24,7 +24,6 @@ export type FillExtrusionUniformsType = { 'u_vertical_gradient': Uniform1f; 'u_opacity': Uniform1f; 'u_fill_translate': Uniform2f; - 'u_camera_pos_globe': Uniform3f; }; export type FillExtrusionPatternUniformsType = { @@ -36,7 +35,6 @@ export type FillExtrusionPatternUniformsType = { 'u_vertical_gradient': Uniform1f; 'u_opacity': Uniform1f; 'u_fill_translate': Uniform2f; - 'u_camera_pos_globe': Uniform3f; // pattern uniforms: 'u_texsize': Uniform2f; 'u_image': Uniform1i; @@ -54,7 +52,6 @@ const fillExtrusionUniforms = (context: Context, locations: UniformLocations): F 'u_vertical_gradient': new Uniform1f(context, locations.u_vertical_gradient), 'u_opacity': new Uniform1f(context, locations.u_opacity), 'u_fill_translate': new Uniform2f(context, locations.u_fill_translate), - 'u_camera_pos_globe': new Uniform3f(context, locations.u_camera_pos_globe) }); const fillExtrusionPatternUniforms = (context: Context, locations: UniformLocations): FillExtrusionPatternUniformsType => ({ @@ -66,7 +63,6 @@ const fillExtrusionPatternUniforms = (context: Context, locations: UniformLocati 'u_height_factor': new Uniform1f(context, locations.u_height_factor), 'u_opacity': new Uniform1f(context, locations.u_opacity), 'u_fill_translate': new Uniform2f(context, locations.u_fill_translate), - 'u_camera_pos_globe': new Uniform3f(context, locations.u_camera_pos_globe), // pattern uniforms 'u_image': new Uniform1i(context, locations.u_image), 'u_texsize': new Uniform2f(context, locations.u_texsize), @@ -81,14 +77,13 @@ const fillExtrusionUniformValues = ( shouldUseVerticalGradient: boolean, opacity: number, translate: [number, number], - cameraPosGlobe: vec3 ): UniformValues => { const light = painter.style.light; const _lp = light.properties.get('position'); const lightPos = [_lp.x, _lp.y, _lp.z] as vec3; const lightMat = mat3.create(); if (light.properties.get('anchor') === 'viewport') { - mat3.fromRotation(lightMat, -painter.transform.angle); + mat3.fromRotation(lightMat, painter.transform.bearingInRadians); } vec3.transformMat3(lightPos, lightPos, lightMat); const transformedLightPos = painter.transform.transformLightDirection(lightPos); @@ -103,7 +98,6 @@ const fillExtrusionUniformValues = ( 'u_vertical_gradient': +shouldUseVerticalGradient, 'u_opacity': opacity, 'u_fill_translate': translate, - 'u_camera_pos_globe': cameraPosGlobe, }; }; @@ -112,12 +106,11 @@ const fillExtrusionPatternUniformValues = ( shouldUseVerticalGradient: boolean, opacity: number, translate: [number, number], - cameraPosGlobe: vec3, coord: OverscaledTileID, crossfade: CrossfadeParameters, tile: Tile ): UniformValues => { - return extend(fillExtrusionUniformValues(painter, shouldUseVerticalGradient, opacity, translate, cameraPosGlobe), + return extend(fillExtrusionUniformValues(painter, shouldUseVerticalGradient, opacity, translate), patternUniformValues(crossfade, painter, tile), { 'u_height_factor': -Math.pow(2, coord.overscaledZ) / tile.tileSize / 8 diff --git a/src/render/program/hillshade_program.ts b/src/render/program/hillshade_program.ts index 32ea9b4d6c7..7dcdcf65a74 100644 --- a/src/render/program/hillshade_program.ts +++ b/src/render/program/hillshade_program.ts @@ -65,7 +65,7 @@ const hillshadeUniformValues = ( let azimuthal = layer.paint.get('hillshade-illumination-direction') * (Math.PI / 180); // modify azimuthal angle by map rotation if light is anchored at the viewport if (layer.paint.get('hillshade-illumination-anchor') === 'viewport') { - azimuthal -= painter.transform.angle; + azimuthal += painter.transform.bearingInRadians; } return { 'u_image': 0, diff --git a/src/render/program/program_uniforms.ts b/src/render/program/program_uniforms.ts index 69ee5580c45..d84722ebb4c 100644 --- a/src/render/program/program_uniforms.ts +++ b/src/render/program/program_uniforms.ts @@ -27,6 +27,7 @@ export const programUniforms = { collisionBox: collisionUniforms, collisionCircle: collisionCircleUniforms, debug: debugUniforms, + depth: emptyUniforms, clippingMask: emptyUniforms, heatmap: heatmapUniforms, heatmapTexture: heatmapTextureUniforms, diff --git a/src/render/program/sky_program.ts b/src/render/program/sky_program.ts index 524b67fc485..ce5a4d8014f 100644 --- a/src/render/program/sky_program.ts +++ b/src/render/program/sky_program.ts @@ -1,4 +1,4 @@ -import {UniformColor, Uniform1f} from '../uniform_binding'; +import {UniformColor, Uniform1f, Uniform2f} from '../uniform_binding'; import type {Context} from '../../gl/context'; import type {UniformValues, UniformLocations} from '../uniform_binding'; import {IReadonlyTransform} from '../../geo/transform_interface'; @@ -8,22 +8,31 @@ import {getMercatorHorizon} from '../../geo/projection/mercator_utils'; export type SkyUniformsType = { 'u_sky_color': UniformColor; 'u_horizon_color': UniformColor; - 'u_horizon': Uniform1f; + 'u_horizon': Uniform2f; + 'u_horizon_normal': Uniform2f; 'u_sky_horizon_blend': Uniform1f; }; const skyUniforms = (context: Context, locations: UniformLocations): SkyUniformsType => ({ 'u_sky_color': new UniformColor(context, locations.u_sky_color), 'u_horizon_color': new UniformColor(context, locations.u_horizon_color), - 'u_horizon': new Uniform1f(context, locations.u_horizon), + 'u_horizon': new Uniform2f(context, locations.u_horizon), + 'u_horizon_normal': new Uniform2f(context, locations.u_horizon_normal), 'u_sky_horizon_blend': new Uniform1f(context, locations.u_sky_horizon_blend), }); -const skyUniformValues = (sky: Sky, transform: IReadonlyTransform, pixelRatio: number): UniformValues => ({ - 'u_sky_color': sky.properties.get('sky-color'), - 'u_horizon_color': sky.properties.get('horizon-color'), - 'u_horizon': (transform.height / 2 + getMercatorHorizon(transform)) * pixelRatio, - 'u_sky_horizon_blend': (sky.properties.get('sky-horizon-blend') * transform.height / 2) * pixelRatio, -}); +const skyUniformValues = (sky: Sky, transform: IReadonlyTransform, pixelRatio: number): UniformValues => { + const cosRoll = Math.cos(transform.rollInRadians); + const sinRoll = Math.sin(transform.rollInRadians); + const mercatorHorizon = getMercatorHorizon(transform); + return { + 'u_sky_color': sky.properties.get('sky-color'), + 'u_horizon_color': sky.properties.get('horizon-color'), + 'u_horizon': [(transform.width / 2 - mercatorHorizon * sinRoll) * pixelRatio, + (transform.height / 2 + mercatorHorizon * cosRoll) * pixelRatio], + 'u_horizon_normal': [-sinRoll, cosRoll], + 'u_sky_horizon_blend': (sky.properties.get('sky-horizon-blend') * transform.height / 2) * pixelRatio, + }; +}; export {skyUniforms, skyUniformValues}; diff --git a/src/render/render_to_texture.test.ts b/src/render/render_to_texture.test.ts index 19dfce33100..e8e00421f47 100644 --- a/src/render/render_to_texture.test.ts +++ b/src/render/render_to_texture.test.ts @@ -15,6 +15,7 @@ import {FillStyleLayer} from '../style/style_layer/fill_style_layer'; import {RasterStyleLayer} from '../style/style_layer/raster_style_layer'; import {HillshadeStyleLayer} from '../style/style_layer/hillshade_style_layer'; import {BackgroundStyleLayer} from '../style/style_layer/background_style_layer'; +import {DepthMode} from '../gl/depth_mode'; describe('render to texture', () => { const gl = document.createElement('canvas').getContext('webgl'); @@ -66,6 +67,7 @@ describe('render to texture', () => { context: new Context(gl), transform: {zoom: 10, calculatePosMatrix: () => {}, getProjectionData(_a) {}, calculateFogMatrix: () => {}}, colorModeForRenderPass: () => ColorMode.alphaBlended, + getDepthModeFor3D: () => DepthMode.disabled, useProgram: () => { return {draw: () => { layersDrawn++; }}; }, _renderTileClippingMasks: () => {}, renderLayer: () => {} diff --git a/src/render/terrain.test.ts b/src/render/terrain.test.ts index 7b6cc893680..5d0c3f5e528 100644 --- a/src/render/terrain.test.ts +++ b/src/render/terrain.test.ts @@ -259,7 +259,6 @@ describe('Terrain', () => { getDEMElevation: Terrain.prototype.getDEMElevation, getTerrainData() { return { - // eslint-disable-next-line camelcase u_terrain_matrix: mat4.create(), tile: { dem: { diff --git a/src/shaders/depth.vertex.glsl b/src/shaders/depth.vertex.glsl new file mode 100644 index 00000000000..c5d4fb27363 --- /dev/null +++ b/src/shaders/depth.vertex.glsl @@ -0,0 +1,9 @@ +in vec2 a_pos; + +void main() { + #ifdef GLOBE + gl_Position = projectTileFor3D(a_pos, 0.0); + #else + gl_Position = u_projection_matrix * vec4(a_pos, 0.0, 1.0); + #endif +} diff --git a/src/shaders/fill_extrusion.fragment.glsl b/src/shaders/fill_extrusion.fragment.glsl index d9a40ec6ca3..496edebfe60 100644 --- a/src/shaders/fill_extrusion.fragment.glsl +++ b/src/shaders/fill_extrusion.fragment.glsl @@ -1,50 +1,9 @@ in vec4 v_color; -#ifdef GLOBE - in vec3 v_sphere_pos; - uniform vec3 u_camera_pos_globe; - uniform highp float u_projection_transition; -#endif - void main() { fragColor = v_color; #ifdef OVERDRAW_INSPECTOR fragColor = vec4(1.0); #endif - - #ifdef GLOBE - // We want extruded geometry to be occluded by the planet. - // This would be trivial in any traditional 3D renderer with Z-buffer, - // but not in MapLibre, since Z-buffer is used to mask certain layers - // and optimize overdraw. - // One solution would be to draw the planet into Z-buffer just before - // rendering fill-extrusion layers, but what if another layer - // is drawn after that which makes use of this Z-buffer mask? - // We can't just trash the mask with out own Z values. - // So instead, the "Z-test" against the planet is done here, - // in the pixel shader. - // Luckily the planet is (assumed to be) a perfect sphere, - // so the ray-planet intersection test is quite simple. - // We discard any fragments that are occluded by the planet. - - // Get nearest point along the ray from fragment to camera. - // Remember that planet center is at 0,0,0. - // Also clamp t to not consider intersections that happened behind the ray origin. - vec3 toPlanetCenter = -v_sphere_pos; - vec3 toCameraNormalized = normalize(u_camera_pos_globe - v_sphere_pos); - float t = dot(toPlanetCenter, toCameraNormalized); - vec3 nearest = v_sphere_pos + toCameraNormalized * max(t, 0.0); - - // We want to remove planet occlusion during the animated transition out of globe view. - // Thus we animate the "radius" of the planet sphere used in ray-sphere collision. - // Radius of 1.0 is equal to full size planet (since we raycast against a unit sphere). - // Note that unsquared globeness is intentionally compared to squared distance from planet center, - // (because `dot(nearest, nearest)` returns the squared length of the vector `nearest`) - // effectively using sqrt(globeness) as the planet radius. This is done to make the animation look better. - float distance_to_planet_center_squared = dot(nearest, nearest); - if (distance_to_planet_center_squared < u_projection_transition) { - discard; // Ray intersected the planet. - } - #endif } diff --git a/src/shaders/fill_extrusion.vertex.glsl b/src/shaders/fill_extrusion.vertex.glsl index c87e09d239e..8726b5dfb38 100644 --- a/src/shaders/fill_extrusion.vertex.glsl +++ b/src/shaders/fill_extrusion.vertex.glsl @@ -16,10 +16,6 @@ in vec4 a_normal_ed; out vec4 v_color; -#ifdef GLOBE - out vec3 v_sphere_pos; -#endif - #pragma mapbox: define highp float base #pragma mapbox: define highp float height @@ -54,8 +50,6 @@ void main() { #ifdef GLOBE vec3 spherePos = projectToSphere(posInTile); - vec3 elevatedPos = spherePos * (1.0 + elevation / GLOBE_RADIUS); - v_sphere_pos = elevatedPos; gl_Position = interpolateProjectionFor3D(posInTile, spherePos, elevation); #else gl_Position = u_projection_matrix * vec4(posInTile, elevation, 1.0); diff --git a/src/shaders/fill_extrusion_pattern.fragment.glsl b/src/shaders/fill_extrusion_pattern.fragment.glsl index 74f3a954458..2bddd558fd0 100644 --- a/src/shaders/fill_extrusion_pattern.fragment.glsl +++ b/src/shaders/fill_extrusion_pattern.fragment.glsl @@ -1,11 +1,6 @@ uniform vec2 u_texsize; uniform float u_fade; -#ifdef GLOBE - in vec3 v_sphere_pos; - uniform vec3 u_camera_pos_globe; -#endif - uniform sampler2D u_image; in vec2 v_pos_a; @@ -47,17 +42,4 @@ void main() { #ifdef OVERDRAW_INSPECTOR fragColor = vec4(1.0); #endif - - #ifdef GLOBE - // Discard fragments that are occluded by the planet - // See comment in fill_extrusion.fragment.glsl - vec3 toPlanetCenter = -v_sphere_pos; - vec3 toCameraNormalized = normalize(u_camera_pos_globe - v_sphere_pos); - float t = dot(toPlanetCenter, toCameraNormalized); - vec3 nearest = v_sphere_pos + toCameraNormalized * max(t, 0.0); - float distance_to_planet_center_squared = dot(nearest, nearest); - if (distance_to_planet_center_squared < u_projection_transition) { - discard; - } - #endif } diff --git a/src/shaders/shaders.ts b/src/shaders/shaders.ts index b110b43850c..bc7b88f2ecc 100644 --- a/src/shaders/shaders.ts +++ b/src/shaders/shaders.ts @@ -21,6 +21,7 @@ import collisionCircleFrag from './collision_circle.fragment.glsl.g'; import collisionCircleVert from './collision_circle.vertex.glsl.g'; import debugFrag from './debug.fragment.glsl.g'; import debugVert from './debug.vertex.glsl.g'; +import depthVert from './depth.vertex.glsl.g'; import fillFrag from './fill.fragment.glsl.g'; import fillVert from './fill.vertex.glsl.g'; import fillOutlineFrag from './fill_outline.fragment.glsl.g'; @@ -88,6 +89,7 @@ export const shaders = { collisionBox: compile(collisionBoxFrag, collisionBoxVert), collisionCircle: compile(collisionCircleFrag, collisionCircleVert), debug: compile(debugFrag, debugVert), + depth: compile(clippingMaskFrag, depthVert), fill: compile(fillFrag, fillVert), fillOutline: compile(fillOutlineFrag, fillOutlineVert), fillOutlinePattern: compile(fillOutlinePatternFrag, fillOutlinePatternVert), @@ -109,7 +111,7 @@ export const shaders = { terrainCoords: compile(terrainCoordsFrag, terrainVertCoords), projectionErrorMeasurement: compile(projectionErrorMeasurementFrag, projectionErrorMeasurementVert), atmosphere: compile(atmosphereFrag, atmosphereVert), - sky: compile(skyFrag, skyVert) + sky: compile(skyFrag, skyVert), }; // Expand #pragmas to #ifdefs. diff --git a/src/shaders/sky.fragment.glsl b/src/shaders/sky.fragment.glsl index e65b97c0302..3317a860282 100644 --- a/src/shaders/sky.fragment.glsl +++ b/src/shaders/sky.fragment.glsl @@ -1,13 +1,15 @@ uniform vec4 u_sky_color; uniform vec4 u_horizon_color; -uniform float u_horizon; +uniform vec2 u_horizon; +uniform vec2 u_horizon_normal; uniform float u_sky_horizon_blend; void main() { + float x = gl_FragCoord.x; float y = gl_FragCoord.y; - if (y > u_horizon) { - float blend = y - u_horizon; + float blend = (y - u_horizon.y) * u_horizon_normal.y + (x - u_horizon.x) * u_horizon_normal.x; + if (blend > 0.0) { if (blend < u_sky_horizon_blend) { gl_FragColor = mix(u_sky_color, u_horizon_color, pow(1.0 - blend / u_sky_horizon_blend, 2.0)); } else { diff --git a/src/source/geojson_source.test.ts b/src/source/geojson_source.test.ts index d205f920651..1ad4b279589 100644 --- a/src/source/geojson_source.test.ts +++ b/src/source/geojson_source.test.ts @@ -302,6 +302,7 @@ describe('GeoJSONSource#update', () => { })); test('forwards Supercluster options with worker request, ignore max zoom of source', () => new Promise(done => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); const mockDispatcher = wrapDispatcher({ sendAsync(message) { expect(message.type).toBe(MessageType.loadData); @@ -382,7 +383,7 @@ describe('GeoJSONSource#update', () => { test('fires "error"', () => new Promise(done => { const mockDispatcher = wrapDispatcher({ sendAsync(_message) { - return Promise.reject('error'); // eslint-disable-line prefer-promise-reject-errors + return Promise.reject('error'); } }); diff --git a/src/source/geojson_worker_source.ts b/src/source/geojson_worker_source.ts index 8f6689071ca..2a43c67f7df 100644 --- a/src/source/geojson_worker_source.ts +++ b/src/source/geojson_worker_source.ts @@ -226,7 +226,7 @@ export class GeoJSONWorkerSource extends VectorTileWorkerSource { const parsed = JSON.parse(params.data); this._dataUpdateable = isUpdateableGeoJSON(parsed, promoteId) ? toUpdateable(parsed, promoteId) : undefined; return parsed; - } catch (e) { + } catch { throw new Error(`Input data given to '${params.source}' is not a valid GeoJSON object.`); } } diff --git a/src/source/raster_dem_tile_source.ts b/src/source/raster_dem_tile_source.ts index de1a13b7300..ba04d92c505 100644 --- a/src/source/raster_dem_tile_source.ts +++ b/src/source/raster_dem_tile_source.ts @@ -86,13 +86,11 @@ export class RasterDEMTileSource extends RasterTileSource implements Source { if (!tile.actor || tile.state === 'expired') { tile.actor = this.dispatcher.getActor(); - /* eslint-disable require-atomic-updates */ const data = await tile.actor.sendAsync({type: MessageType.loadDEMTile, data: params}); tile.dem = data; tile.needsHillshadePrepare = true; tile.needsTerrainPrepare = true; tile.state = 'loaded'; - /* eslint-enable require-atomic-updates */ } } } catch (err) { @@ -112,7 +110,7 @@ export class RasterDEMTileSource extends RasterTileSource implements Source { const height = img.height + 2; try { return new RGBAImage({width, height}, await readImageUsingVideoFrame(img, -1, -1, width, height)); - } catch (e) { + } catch { // fall-back to browser canvas decoding } } diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index 7ad46cb0bdf..7e4193e4d5e 100644 --- a/src/source/raster_tile_source.ts +++ b/src/source/raster_tile_source.ts @@ -85,7 +85,7 @@ export class RasterTileSource extends Evented implements Source { extend(this, pick(options, ['url', 'scheme', 'tileSize'])); } - async load() { + async load(sourceDataChanged: boolean = false) { this._loaded = false; this.fire(new Event('dataloading', {dataType: 'source'})); this._tileJSONRequest = new AbortController(); @@ -101,7 +101,7 @@ export class RasterTileSource extends Evented implements Source { // before the TileJSON arrives. this makes sure the tiles needed are loaded once TileJSON arrives // ref: https://github.com/mapbox/mapbox-gl-js/pull/4347#discussion_r104418088 this.fire(new Event('data', {dataType: 'source', sourceDataType: 'metadata'})); - this.fire(new Event('data', {dataType: 'source', sourceDataType: 'content'})); + this.fire(new Event('data', {dataType: 'source', sourceDataType: 'content', sourceDataChanged})); } } catch (err) { this._tileJSONRequest = null; @@ -133,7 +133,7 @@ export class RasterTileSource extends Evented implements Source { callback(); - this.load(); + this.load(true); } /** diff --git a/src/source/rtl_text_plugin_worker.test.ts b/src/source/rtl_text_plugin_worker.test.ts new file mode 100644 index 00000000000..39993aed4ed --- /dev/null +++ b/src/source/rtl_text_plugin_worker.test.ts @@ -0,0 +1,118 @@ +import {PluginState} from './rtl_text_plugin_status'; +import {rtlWorkerPlugin} from './rtl_text_plugin_worker'; + +describe('RTLWorkerPlugin', () => { + beforeEach(() => { + // This is a static class, so we need to reset the properties before each test + rtlWorkerPlugin.processStyledBidirectionalText = null; + rtlWorkerPlugin.processBidirectionalText = null; + rtlWorkerPlugin.applyArabicShaping = null; + }); + + test('should throw if already parsed', () => { + const rtlTextPlugin = { + applyArabicShaping: jest.fn(), + processBidirectionalText: jest.fn(), + processStyledBidirectionalText: jest.fn(), + }; + + rtlWorkerPlugin.setMethods(rtlTextPlugin); + expect(() => { + rtlWorkerPlugin.setMethods(rtlTextPlugin); + }).toThrow('RTL text plugin already registered.'); + }); + + test('should move RTL plugin from unavailable to deferred', async () => { + rtlWorkerPlugin.pluginURL = ''; + rtlWorkerPlugin.pluginStatus = 'unavailable'; + + const mockMessage: PluginState = { + pluginURL: 'https://somehost/somescript', + pluginStatus: 'deferred' + }; + + await rtlWorkerPlugin.syncState(mockMessage, jest.fn()); + + expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('deferred'); + }); + + test('should not change RTL plugin status if already parsed', async () => { + const originalUrl = 'https://somehost/somescript1'; + rtlWorkerPlugin.pluginURL = originalUrl; + rtlWorkerPlugin.pluginStatus = 'loaded'; + rtlWorkerPlugin.setMethods({ + applyArabicShaping: jest.fn(), + processBidirectionalText: jest.fn(), + processStyledBidirectionalText: jest.fn(), + }); + const mockMessage: PluginState = { + pluginURL: 'https://somehost/somescript2', + pluginStatus: 'loading' + }; + + const workerResult: PluginState = await await rtlWorkerPlugin.syncState(mockMessage, jest.fn()); + + expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('loaded'); + expect(rtlWorkerPlugin.pluginURL).toBe(originalUrl); + + expect(workerResult.pluginStatus).toBe('loaded'); + expect(workerResult.pluginURL).toBe(originalUrl); + }); + + test('should do a full cycle of rtl loading synchronously', async () => { + const originalUrl = 'https://somehost/somescript1'; + const loadScriptsMock = jest.fn().mockImplementation((_) => { + rtlWorkerPlugin.setMethods({ + applyArabicShaping: jest.fn(), + processBidirectionalText: jest.fn(), + processStyledBidirectionalText: jest.fn(), + }); + }); + + const workerResult: PluginState = await rtlWorkerPlugin.syncState({ + pluginURL: originalUrl, + pluginStatus: 'loading' + }, loadScriptsMock); + + expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('loaded'); + expect(rtlWorkerPlugin.pluginURL).toBe(originalUrl); + expect(workerResult.pluginStatus).toBe('loaded'); + expect(workerResult.pluginURL).toBe(originalUrl); + }); + + test('should do a full cycle of rtl loading asynchronously', async () => { + const originalUrl = 'https://somehost/somescript1'; + const loadScriptsMock = jest.fn().mockImplementation((_) => { + setTimeout(() => { + rtlWorkerPlugin.setMethods({ + applyArabicShaping: jest.fn(), + processBidirectionalText: jest.fn(), + processStyledBidirectionalText: jest.fn(), + }); + }, 10); + }); + + const workerResult: PluginState = await rtlWorkerPlugin.syncState({ + pluginURL: originalUrl, + pluginStatus: 'loading' + }, loadScriptsMock); + + expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('loaded'); + expect(rtlWorkerPlugin.pluginURL).toBe(originalUrl); + expect(workerResult.pluginStatus).toBe('loaded'); + expect(workerResult.pluginURL).toBe(originalUrl); + }); + + test('should fail loading on timeout', async () => { + const originalUrl = 'https://somehost/somescript1'; + const loadScriptsMock = jest.fn().mockImplementation(() => {}); + + (rtlWorkerPlugin as any).TIMEOUT = 1; + + await expect(rtlWorkerPlugin.syncState({ + pluginURL: originalUrl, + pluginStatus: 'loading' + }, loadScriptsMock) + ).rejects.toThrow('RTL Text Plugin failed to import scripts from https://somehost/somescript1'); + }); +}); diff --git a/src/source/rtl_text_plugin_worker.ts b/src/source/rtl_text_plugin_worker.ts index 88f8d2187ef..b01c1efffbb 100644 --- a/src/source/rtl_text_plugin_worker.ts +++ b/src/source/rtl_text_plugin_worker.ts @@ -7,42 +7,81 @@ export interface RTLTextPlugin { } class RTLWorkerPlugin implements RTLTextPlugin { + readonly TIMEOUT = 5000; + applyArabicShaping: (a: string) => string = null; processBidirectionalText: ((b: string, a: Array) => Array) = null; processStyledBidirectionalText: ((c: string, b: Array, a: Array) => Array<[string, Array]>) = null; pluginStatus: RTLPluginStatus = 'unavailable'; pluginURL: string = null; + loadScriptResolve: () => void = () => {}; - setState(state: PluginState) { + private setState(state: PluginState) { this.pluginStatus = state.pluginStatus; this.pluginURL = state.pluginURL; } - getState(): PluginState { + private getState(): PluginState { return { pluginStatus: this.pluginStatus, pluginURL: this.pluginURL }; } - setMethods(rtlTextPlugin: RTLTextPlugin) { + public setMethods(rtlTextPlugin: RTLTextPlugin) { + if (rtlWorkerPlugin.isParsed()) { + throw new Error('RTL text plugin already registered.'); + } this.applyArabicShaping = rtlTextPlugin.applyArabicShaping; this.processBidirectionalText = rtlTextPlugin.processBidirectionalText; this.processStyledBidirectionalText = rtlTextPlugin.processStyledBidirectionalText; + this.loadScriptResolve(); } - isParsed(): boolean { + public isParsed(): boolean { return this.applyArabicShaping != null && this.processBidirectionalText != null && this.processStyledBidirectionalText != null; } - getPluginURL(): string { - return this.pluginURL; + public getRTLTextPluginStatus() { + return this.pluginStatus; } - getRTLTextPluginStatus() { - return this.pluginStatus; + public async syncState(incomingState: PluginState, importScripts: (url: string) => void): Promise { + // Parsed plugin cannot be changed, so just return its current state. + if (this.isParsed()) { + return this.getState(); + } + + if (incomingState.pluginStatus !== 'loading') { + // simply sync and done + this.setState(incomingState); + return incomingState; + } + const urlToLoad = incomingState.pluginURL; + const loadScriptPromise = new Promise((resolve) => { + this.loadScriptResolve = resolve; + }); + importScripts(urlToLoad); + const dontWaitForeverTimeoutPromise = new Promise((resolve) => setTimeout(() => resolve(), this.TIMEOUT)); + await Promise.race([loadScriptPromise, dontWaitForeverTimeoutPromise]); + const complete = this.isParsed(); + if (complete) { + const loadedState: PluginState = { + pluginStatus: 'loaded', + pluginURL: urlToLoad + }; + this.setState(loadedState); + return loadedState; + } + + // error case + this.setState({ + pluginStatus: 'error', + pluginURL: '' + }); + throw new Error(`RTL Text Plugin failed to import scripts from ${urlToLoad}`); } } diff --git a/src/source/source_cache.test.ts b/src/source/source_cache.test.ts index 02ba196d8d2..2810efbd692 100644 --- a/src/source/source_cache.test.ts +++ b/src/source/source_cache.test.ts @@ -527,6 +527,33 @@ describe('SourceCache / Source lifecycle', () => { }); + test('does reload errored tiles, if event is source data change', () => { + const transform = new MercatorTransform(); + transform.resize(511, 511); + transform.setZoom(1); + + const sourceCache = createSourceCache(); + sourceCache._source.loadTile = async (tile) => { + // 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 + tile.state = tile.tileID.canonical.z === 1 ? 'errored' : 'loaded'; + }; + + const reloadTileSpy = jest.spyOn(sourceCache, '_reloadTile'); + sourceCache.on('data', (e) => { + if (e.dataType === 'source' && e.sourceDataType === 'metadata') { + sourceCache.update(transform); + sourceCache.getSource().fire(new Event('data', {dataType: 'source', sourceDataType: 'content', sourceDataChanged: true})); + } + }); + sourceCache.onAdd(undefined); + // We expect the source cache to have five tiles, and for all of them + // to be reloaded + expect(Object.keys(sourceCache._tiles)).toHaveLength(5); + expect(reloadTileSpy).toHaveBeenCalledTimes(5); + + }); + }); describe('SourceCache#update', () => { diff --git a/src/source/source_cache.ts b/src/source/source_cache.ts index d3ede7a3685..edeb4580210 100644 --- a/src/source/source_cache.ts +++ b/src/source/source_cache.ts @@ -232,8 +232,8 @@ export class SourceCache extends Evented { return renderables.sort((a_: Tile, b_: Tile) => { const a = a_.tileID; const b = b_.tileID; - const rotatedA = (new Point(a.canonical.x, a.canonical.y))._rotate(this.transform.angle); - const rotatedB = (new Point(b.canonical.x, b.canonical.y))._rotate(this.transform.angle); + const rotatedA = (new Point(a.canonical.x, a.canonical.y))._rotate(-this.transform.bearingInRadians); + const rotatedB = (new Point(b.canonical.x, b.canonical.y))._rotate(-this.transform.bearingInRadians); return a.overscaledZ - b.overscaledZ || rotatedB.y - rotatedA.y || rotatedB.x - rotatedA.x; }).map(tile => tile.tileID.key); } @@ -253,7 +253,7 @@ export class SourceCache extends Evented { !this._coveredTiles[id] && (symbolLayer || !this._tiles[id].holdingForFade()); } - reload() { + reload(sourceDataChanged?: boolean) { if (this._paused) { this._shouldReloadOnResume = true; return; @@ -262,7 +262,9 @@ export class SourceCache extends Evented { this._cache.reset(); for (const i in this._tiles) { - if (this._tiles[i].state !== 'errored') this._reloadTile(i, 'reloading'); + if (sourceDataChanged || this._tiles[i].state !== 'errored') { + this._reloadTile(i, 'reloading'); + } } } @@ -928,7 +930,7 @@ export class SourceCache extends Evented { // for sources with mutable data, this event fires when the underlying data // to a source is changed. (i.e. GeoJSONSource#setData and ImageSource#serCoordinates) if (this._sourceLoaded && !this._paused && e.dataType === 'source' && eventSourceDataType === 'content') { - this.reload(); + this.reload(e.sourceDataChanged); if (this.transform) { this.update(this.transform, this.terrain); } diff --git a/src/source/tile_id.test.ts b/src/source/tile_id.test.ts index 56c71479037..bfbb31a6897 100644 --- a/src/source/tile_id.test.ts +++ b/src/source/tile_id.test.ts @@ -4,19 +4,15 @@ import {MAX_TILE_ZOOM, MIN_TILE_ZOOM} from '../util/util'; describe('CanonicalTileID', () => { test('#constructor', () => { expect(() => { - /*eslint no-new: 0*/ new CanonicalTileID(MIN_TILE_ZOOM - 1, 0, 0); }).toThrow(); expect(() => { - /*eslint no-new: 0*/ new CanonicalTileID(MAX_TILE_ZOOM + 1, 0, 0); }).toThrow(); expect(() => { - /*eslint no-new: 0*/ new CanonicalTileID(2, 4, 0); }).toThrow(); expect(() => { - /*eslint no-new: 0*/ new CanonicalTileID(2, 0, 4); }).toThrow(); }); diff --git a/src/source/worker.test.ts b/src/source/worker.test.ts index 7e850b72e5a..a973ef797db 100644 --- a/src/source/worker.test.ts +++ b/src/source/worker.test.ts @@ -6,7 +6,6 @@ import {CanonicalTileID, OverscaledTileID} from './tile_id'; import {WorkerSource, WorkerTileParameters, WorkerTileResult} from './worker_source'; import {rtlWorkerPlugin} from './rtl_text_plugin_worker'; import {ActorTarget, IActor} from '../util/actor'; -import {PluginState} from './rtl_text_plugin_status'; import {MessageType} from '../util/actor_messages'; class WorkerSourceMock implements WorkerSource { @@ -37,108 +36,22 @@ describe('Worker RTLTextPlugin', () => { } as any; worker = new Worker(_self); global.fetch = null; - rtlWorkerPlugin.setMethods({ - applyArabicShaping: null, - processBidirectionalText: null, - processStyledBidirectionalText: null - }); - jest.spyOn(rtlWorkerPlugin, 'isParsed').mockImplementation(() => { - return false; - }); - }); - - test('should not throw and set values in plugin', () => { - const rtlTextPlugin = { - applyArabicShaping: 'test', - processBidirectionalText: 'test', - processStyledBidirectionalText: 'test', - }; - - _self.registerRTLTextPlugin(rtlTextPlugin); - expect(rtlWorkerPlugin.applyArabicShaping).toBe('test'); - expect(rtlWorkerPlugin.processBidirectionalText).toBe('test'); - expect(rtlWorkerPlugin.processStyledBidirectionalText).toBe('test'); - }); - - test('should throw if already parsed', () => { - jest.spyOn(rtlWorkerPlugin, 'isParsed').mockImplementation(() => { - return true; - }); - - const rtlTextPlugin = { - applyArabicShaping: jest.fn(), - processBidirectionalText: jest.fn(), - processStyledBidirectionalText: jest.fn(), - }; - - expect(() => { - _self.registerRTLTextPlugin(rtlTextPlugin); - }).toThrow('RTL text plugin already registered.'); }); - test('should move RTL plugin from unavailable to deferred', async () => { - rtlWorkerPlugin.setState({ - pluginURL: '', - pluginStatus: 'unavailable' - } - ); - const mockMessage: PluginState = { - pluginURL: 'https://somehost/somescript', - pluginStatus: 'deferred' - }; - - await worker.actor.messageHandlers[MessageType.syncRTLPluginState]('', mockMessage); - expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('deferred'); - }); + test('should call setMethods in plugin', () => { + const spy = jest.spyOn(rtlWorkerPlugin, 'setMethods').mockImplementation(() => {}); - test('should download RTL plugin when "loading" message is received', async () => { - rtlWorkerPlugin.setState({ - pluginURL: '', - pluginStatus: 'deferred' - }); + _self.registerRTLTextPlugin({} as any); - const mockURL = 'https://somehost/somescript'; - const mockMessage: PluginState = { - pluginURL: mockURL, - pluginStatus: 'loading' - }; - - const importSpy = jest.spyOn(worker.self, 'importScripts').mockImplementation(() => { - // after importing isParse() to return true - jest.spyOn(rtlWorkerPlugin, 'isParsed').mockImplementation(() => { - return true; - }); - }); - - const syncResult: PluginState = await worker.actor.messageHandlers[MessageType.syncRTLPluginState]('', mockMessage) as any; - expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('loaded'); - expect(importSpy).toHaveBeenCalledWith(mockURL); - - expect(syncResult.pluginURL).toBe(mockURL); - expect(syncResult.pluginStatus).toBe('loaded'); + expect(spy).toHaveBeenCalled(); }); - test('should not change RTL plugin status if already parsed', async () => { - const originalUrl = 'https://somehost/somescript1'; - rtlWorkerPlugin.setState({ - pluginURL: originalUrl, - pluginStatus: 'loaded' - }); - - jest.spyOn(rtlWorkerPlugin, 'isParsed').mockImplementation(() => { - return true; - }); - const mockMessage: PluginState = { - pluginURL: 'https://somehost/somescript2', - pluginStatus: 'loading' - }; + test('should call syncState when rtl message is received', async () => { + const syncStateSpy = jest.spyOn(rtlWorkerPlugin, 'syncState').mockImplementation((_, __) => Promise.resolve({} as any)); - const workerResult: PluginState = await worker.actor.messageHandlers[MessageType.syncRTLPluginState]('', mockMessage) as any; - expect(rtlWorkerPlugin.getRTLTextPluginStatus()).toBe('loaded'); - expect(rtlWorkerPlugin.getPluginURL()).toBe(originalUrl); + await worker.actor.messageHandlers[MessageType.syncRTLPluginState]('', {} as any) as any; - expect(workerResult.pluginStatus).toBe('loaded'); - expect(workerResult.pluginURL).toBe(originalUrl); + expect(syncStateSpy).toHaveBeenCalled(); }); }); diff --git a/src/source/worker.ts b/src/source/worker.ts index 5aa69e8722f..f0bb8114421 100644 --- a/src/source/worker.ts +++ b/src/source/worker.ts @@ -83,9 +83,7 @@ export default class Worker { // This is invoked by the RTL text plugin when the download via the `importScripts` call has finished, and the code has been parsed. this.self.registerRTLTextPlugin = (rtlTextPlugin: RTLTextPlugin) => { - if (rtlWorkerPlugin.isParsed()) { - throw new Error('RTL text plugin already registered.'); - } + rtlWorkerPlugin.setMethods(rtlTextPlugin); }; @@ -191,35 +189,8 @@ export default class Worker { } private async _syncRTLPluginState(mapId: string, incomingState: PluginState): Promise { - - // Parsed plugin cannot be changed, so just return its current state. - if (rtlWorkerPlugin.isParsed()) { - return rtlWorkerPlugin.getState(); - } - - if (incomingState.pluginStatus !== 'loading') { - // simply sync and done - rtlWorkerPlugin.setState(incomingState); - return incomingState; - } - const urlToLoad = incomingState.pluginURL; - this.self.importScripts(urlToLoad); - const complete = rtlWorkerPlugin.isParsed(); - if (complete) { - const loadedState: PluginState = { - pluginStatus: 'loaded', - pluginURL: urlToLoad - }; - rtlWorkerPlugin.setState(loadedState); - return loadedState; - } - - // error case - rtlWorkerPlugin.setState({ - pluginStatus: 'error', - pluginURL: '' - }); - throw new Error(`RTL Text Plugin failed to import scripts from ${urlToLoad}`); + const state = await rtlWorkerPlugin.syncState(incomingState, this.self.importScripts); + return state; } private _getAvailableImages(mapId: string) { diff --git a/src/style/load_sprite.test.ts b/src/style/load_sprite.test.ts index 4dc379a458e..b2ff73fda13 100644 --- a/src/style/load_sprite.test.ts +++ b/src/style/load_sprite.test.ts @@ -26,10 +26,10 @@ describe('normalizeSpriteURL', () => { ).toBe('http://www.foo.com/bar@2x.png?fresh=true'); }); - test('test relative URL', () => { + test('No Path', () => { expect( - normalizeSpriteURL('/bar?fresh=true', '@2x', '.png') - ).toBe('/bar@2x.png?fresh=true'); + normalizeSpriteURL('http://www.foo.com?fresh=true', '@2x', '.json') + ).toBe('http://www.foo.com/@2x.json?fresh=true'); }); }); @@ -66,9 +66,70 @@ describe('loadSprite', () => { const result = await promise; - expect(transform).toHaveBeenCalledTimes(2); - expect(transform).toHaveBeenNthCalledWith(1, 'http://localhost:9966/test/unit/assets/sprite1.json', 'SpriteJSON'); - expect(transform).toHaveBeenNthCalledWith(2, 'http://localhost:9966/test/unit/assets/sprite1.png', 'SpriteImage'); + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenNthCalledWith(1, 'http://localhost:9966/test/unit/assets/sprite1', 'Sprite'); + + expect(Object.keys(result)).toHaveLength(1); + expect(Object.keys(result)[0]).toBe('default'); + + Object.values(result['default']).forEach(styleImage => { + expect(styleImage.spriteData).toBeTruthy(); + expect(styleImage.spriteData.context).toBeInstanceOf(CanvasRenderingContext2D); + }); + + expect(server.requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1.json'); + expect(server.requests[1].url).toBe('http://localhost:9966/test/unit/assets/sprite1.png'); + }); + + test('transform of relative url', async () => { + const transform = jest.fn().mockImplementation((url, type) => { + return {url: `http://localhost:9966${url}`, type}; + }); + + const manager = new RequestManager(transform); + + server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1.json', fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.json')).toString()); + server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1.png', bufferToArrayBuffer(fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.png')))); + + const promise = loadSprite('/test/unit/assets/sprite1', manager, 1, new AbortController()); + + server.respond(); + + const result = await promise; + + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenNthCalledWith(1, '/test/unit/assets/sprite1', 'Sprite'); + + expect(Object.keys(result)).toHaveLength(1); + expect(Object.keys(result)[0]).toBe('default'); + + Object.values(result['default']).forEach(styleImage => { + expect(styleImage.spriteData).toBeTruthy(); + expect(styleImage.spriteData.context).toBeInstanceOf(CanvasRenderingContext2D); + }); + + expect(server.requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1.json'); + expect(server.requests[1].url).toBe('http://localhost:9966/test/unit/assets/sprite1.png'); + }); + + test('transform of random Sprite String', async () => { + const transform = jest.fn().mockImplementation((url, type) => { + return {url: 'http://localhost:9966/test/unit/assets/sprite1', type}; + }); + + const manager = new RequestManager(transform); + + server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1.json', fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.json')).toString()); + server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1.png', bufferToArrayBuffer(fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.png')))); + + const promise = loadSprite('foobar', manager, 1, new AbortController()); + + server.respond(); + + const result = await promise; + + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenNthCalledWith(1, 'foobar', 'Sprite'); expect(Object.keys(result)).toHaveLength(1); expect(Object.keys(result)[0]).toBe('default'); @@ -98,9 +159,8 @@ describe('loadSprite', () => { const result = await promise; - expect(transform).toHaveBeenCalledTimes(2); - expect(transform).toHaveBeenNthCalledWith(1, '/test/unit/assets/sprite1.json', 'SpriteJSON'); - expect(transform).toHaveBeenNthCalledWith(2, '/test/unit/assets/sprite1.png', 'SpriteImage'); + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenNthCalledWith(1, '/test/unit/assets/sprite1', 'Sprite'); expect(Object.keys(result)).toHaveLength(1); expect(Object.keys(result)[0]).toBe('default'); @@ -131,11 +191,9 @@ describe('loadSprite', () => { server.respond(); const result = await promise; - expect(transform).toHaveBeenCalledTimes(4); - expect(transform).toHaveBeenNthCalledWith(1, 'http://localhost:9966/test/unit/assets/sprite1.json', 'SpriteJSON'); - expect(transform).toHaveBeenNthCalledWith(2, 'http://localhost:9966/test/unit/assets/sprite1.png', 'SpriteImage'); - expect(transform).toHaveBeenNthCalledWith(3, 'http://localhost:9966/test/unit/assets/sprite2.json', 'SpriteJSON'); - expect(transform).toHaveBeenNthCalledWith(4, 'http://localhost:9966/test/unit/assets/sprite2.png', 'SpriteImage'); + expect(transform).toHaveBeenCalledTimes(2); + expect(transform).toHaveBeenNthCalledWith(1, 'http://localhost:9966/test/unit/assets/sprite1', 'Sprite'); + expect(transform).toHaveBeenNthCalledWith(2, 'http://localhost:9966/test/unit/assets/sprite2', 'Sprite'); expect(Object.keys(result)).toHaveLength(2); expect(Object.keys(result)[0]).toBe('sprite1'); @@ -209,9 +267,8 @@ describe('loadSprite', () => { server.respond(); const result = await promise; - expect(transform).toHaveBeenCalledTimes(2); - expect(transform).toHaveBeenNthCalledWith(1, 'http://localhost:9966/test/unit/assets/sprite1@2x.json', 'SpriteJSON'); - expect(transform).toHaveBeenNthCalledWith(2, 'http://localhost:9966/test/unit/assets/sprite1@2x.png', 'SpriteImage'); + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenNthCalledWith(1, 'http://localhost:9966/test/unit/assets/sprite1', 'Sprite'); expect(Object.keys(result)).toHaveLength(1); expect(Object.keys(result)[0]).toBe('default'); diff --git a/src/style/load_sprite.ts b/src/style/load_sprite.ts index 12b270c3427..391317442b1 100644 --- a/src/style/load_sprite.ts +++ b/src/style/load_sprite.ts @@ -16,9 +16,9 @@ export type LoadSpriteResult = { } export function normalizeSpriteURL(url: string, format: string, extension: string): string { - const split = url.split('?'); - split[0] += `${format}${extension}`; - return split.join('?'); + const parsed = new URL(url); + parsed.pathname += `${format}${extension}`; + return parsed.toString(); } export async function loadSprite( @@ -34,11 +34,17 @@ export async function loadSprite( const imagesMap: {[id: string]: Promise>} = {}; for (const {id, url} of spriteArray) { - const jsonRequestParameters = requestManager.transformRequest(normalizeSpriteURL(url, format, '.json'), ResourceType.SpriteJSON); - jsonsMap[id] = getJSON(jsonRequestParameters, abortController); + const requestParameters = requestManager.transformRequest(url, ResourceType.Sprite); - const imageRequestParameters = requestManager.transformRequest(normalizeSpriteURL(url, format, '.png'), ResourceType.SpriteImage); - imagesMap[id] = ImageRequest.getImage(imageRequestParameters, abortController); + jsonsMap[id] = getJSON({ + ...requestParameters, + url: normalizeSpriteURL(requestParameters.url, format, '.json') + }, abortController); + + imagesMap[id] = ImageRequest.getImage({ + ...requestParameters, + url: normalizeSpriteURL(requestParameters.url, format, '.png') + }, abortController); } await Promise.all([...Object.values(jsonsMap), ...Object.values(imagesMap)]); diff --git a/src/style/style.test.ts b/src/style/style.test.ts index 6060cfc086a..81be8743647 100644 --- a/src/style/style.test.ts +++ b/src/style/style.test.ts @@ -340,11 +340,9 @@ describe('Style#loadJSON', () => { await style.once('style.load'); - expect(transformSpy).toHaveBeenCalledTimes(2); - expect(transformSpy.mock.calls[0][0]).toBe('http://example.com/sprites/bright-v8.json'); - expect(transformSpy.mock.calls[0][1]).toBe('SpriteJSON'); - expect(transformSpy.mock.calls[1][0]).toBe('http://example.com/sprites/bright-v8.png'); - expect(transformSpy.mock.calls[1][1]).toBe('SpriteImage'); + expect(transformSpy).toHaveBeenCalledTimes(1); + expect(transformSpy.mock.calls[0][0]).toBe('http://example.com/sprites/bright-v8'); + expect(transformSpy.mock.calls[0][1]).toBe('Sprite'); }); test('emits an error on non-existant vector source layer', () => new Promise(done => { diff --git a/src/style/style.ts b/src/style/style.ts index 9fd2f7ad48c..5514da3630c 100644 --- a/src/style/style.ts +++ b/src/style/style.ts @@ -747,6 +747,7 @@ export class Style extends Evented { case 'setZoom': case 'setBearing': case 'setPitch': + case 'setRoll': continue; case 'addLayer': operations.push(() => this.addLayer.apply(this, op.args)); diff --git a/src/style/style_layer/circle_style_layer.ts b/src/style/style_layer/circle_style_layer.ts index fb11dfa680d..d5755e34c2b 100644 --- a/src/style/style_layer/circle_style_layer.ts +++ b/src/style/style_layer/circle_style_layer.ts @@ -52,7 +52,7 @@ export class CircleStyleLayer extends StyleLayer { const translatedPolygon = translate(queryGeometry, this.paint.get('circle-translate'), this.paint.get('circle-translate-anchor'), - transform.angle, pixelsToTileUnits); + -transform.bearingInRadians, pixelsToTileUnits); 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; diff --git a/src/style/style_layer/fill_extrusion_style_layer.ts b/src/style/style_layer/fill_extrusion_style_layer.ts index aaad2553317..4a5086ee98b 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.ts +++ b/src/style/style_layer/fill_extrusion_style_layer.ts @@ -52,7 +52,7 @@ export class FillExtrusionStyleLayer extends StyleLayer { const translatedPolygon = translate(queryGeometry, this.paint.get('fill-extrusion-translate'), this.paint.get('fill-extrusion-translate-anchor'), - transform.angle, pixelsToTileUnits); + -transform.bearingInRadians, pixelsToTileUnits); const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState); const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState); diff --git a/src/style/style_layer/fill_style_layer.ts b/src/style/style_layer/fill_style_layer.ts index b07cc7011d5..5b7cff86ae7 100644 --- a/src/style/style_layer/fill_style_layer.ts +++ b/src/style/style_layer/fill_style_layer.ts @@ -55,7 +55,7 @@ export class FillStyleLayer extends StyleLayer { const translatedPolygon = translate(queryGeometry, this.paint.get('fill-translate'), this.paint.get('fill-translate-anchor'), - transform.angle, pixelsToTileUnits); + -transform.bearingInRadians, pixelsToTileUnits); return polygonIntersectsMultiPolygon(translatedPolygon, geometry); } diff --git a/src/style/style_layer/line_style_layer.ts b/src/style/style_layer/line_style_layer.ts index a993b964a92..b462a56651c 100644 --- a/src/style/style_layer/line_style_layer.ts +++ b/src/style/style_layer/line_style_layer.ts @@ -105,7 +105,7 @@ export class LineStyleLayer extends StyleLayer { const translatedPolygon = translate(queryGeometry, this.paint.get('line-translate'), this.paint.get('line-translate-anchor'), - transform.angle, pixelsToTileUnits); + -transform.bearingInRadians, pixelsToTileUnits); const halfWidth = pixelsToTileUnits / 2 * getLineWidth( this.paint.get('line-width').evaluate(feature, featureState), this.paint.get('line-gap-width').evaluate(feature, featureState)); diff --git a/src/symbol/collision_index.ts b/src/symbol/collision_index.ts index 683b0c345cc..072c795b03b 100644 --- a/src/symbol/collision_index.ts +++ b/src/symbol/collision_index.ts @@ -15,7 +15,7 @@ import type { } from '../data/array_types.g'; import type {OverlapMode} from '../style/style_layer/overlap_mode'; import {UnwrappedTileID} from '../source/tile_id'; -import {type PointProjection, SymbolProjectionContext, pathSlicedToLongestUnoccluded, placeFirstAndLastGlyph, projectPathSpecialProjection, xyTransformMat4} from '../symbol/projection'; +import {type PointProjection, SymbolProjectionContext, getTileSkewVectors, pathSlicedToLongestUnoccluded, placeFirstAndLastGlyph, projectPathSpecialProjection, xyTransformMat4} from '../symbol/projection'; import {clamp, getAABB} from '../util/util'; // When a symbol crosses the edge that causes it to be included in @@ -542,13 +542,11 @@ export class CollisionIndex { vecSouthY = cos; } else if (!rotateWithMap && pitchWithMap) { // Handles pitch-align: map texts that are always aligned with the viewport's X axis. - const angle = -this.transform.angle; - const sin = Math.sin(angle); - const cos = Math.cos(angle); - vecEastX = cos; - vecEastY = sin; - vecSouthX = -sin; - vecSouthY = cos; + const skew = getTileSkewVectors(this.transform); + vecEastX = skew.vecEast[0]; + vecEastY = skew.vecEast[1]; + vecSouthX = skew.vecSouth[0]; + vecSouthY = skew.vecSouth[1]; } // Configuration for screen space offsets diff --git a/src/symbol/placement.ts b/src/symbol/placement.ts index f0baee82e29..ee68d873a9d 100644 --- a/src/symbol/placement.ts +++ b/src/symbol/placement.ts @@ -818,7 +818,7 @@ export class Placement { if (zOrderByViewportY) { if (bucketPart.symbolInstanceStart !== 0) throw new Error('bucket.bucketInstanceId should be 0'); - const symbolIndexes = bucket.getSortedSymbolIndexes(this.transform.angle); + const symbolIndexes = bucket.getSortedSymbolIndexes(-this.transform.bearingInRadians); for (let i = symbolIndexes.length - 1; i >= 0; --i) { const symbolIndex = symbolIndexes[i]; placeSymbol(bucket.symbolInstances.get(symbolIndex), bucket.collisionArrays[symbolIndex], symbolIndex); @@ -1174,7 +1174,7 @@ export class Placement { variableOffset.textOffset, variableOffset.textBoxScale); if (rotateWithMap) { - shift._rotate(pitchWithMap ? this.transform.angle : -this.transform.angle); + shift._rotate(pitchWithMap ? -this.transform.bearingInRadians : this.transform.bearingInRadians); } } else { // No offset -> this symbol hasn't been placed since coming on-screen @@ -1213,7 +1213,7 @@ export class Placement { } } - bucket.sortFeatures(this.transform.angle); + bucket.sortFeatures(-this.transform.bearingInRadians); if (this.retainedQueryData[bucket.bucketInstanceId]) { this.retainedQueryData[bucket.bucketInstanceId].featureSortOrder = bucket.featureSortOrder; } diff --git a/src/symbol/projection.test.ts b/src/symbol/projection.test.ts index c0f16748193..8f9fcb325b4 100644 --- a/src/symbol/projection.test.ts +++ b/src/symbol/projection.test.ts @@ -1,9 +1,10 @@ -import {SymbolProjectionContext, ProjectionSyntheticVertexArgs, findOffsetIntersectionPoint, projectWithMatrix, transformToOffsetNormal, projectLineVertexToLabelPlane} from './projection'; +import {SymbolProjectionContext, ProjectionSyntheticVertexArgs, findOffsetIntersectionPoint, projectWithMatrix, transformToOffsetNormal, projectLineVertexToLabelPlane, getPitchedLabelPlaneMatrix, getGlCoordMatrix, getTileSkewVectors} from './projection'; import Point from '@mapbox/point-geometry'; import {mat4} from 'gl-matrix'; import {SymbolLineVertexArray} from '../data/array_types.g'; import {MercatorTransform} from '../geo/projection/mercator_transform'; +import {expectToBeCloseToArray} from '../util/test/util'; describe('Projection', () => { test('matrix float precision', () => { @@ -171,4 +172,149 @@ describe('Find offset line intersections', () => { expect(intersectionPoint.y).toBeCloseTo(1); }); + test('getPitchedLabelPlaneMatrix: bearing and roll', () => { + const transform = new MercatorTransform(); + transform.setBearing(0); + transform.setPitch(45); + transform.setRoll(45); + + expectToBeCloseToArray([...getPitchedLabelPlaneMatrix(false, transform, 2).values()], + [0.4330127239227295, -0.4330127239227295, 0, 0, 0.3061862289905548, 0.3061862289905548, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 9); + expectToBeCloseToArray([...getPitchedLabelPlaneMatrix(true, transform, 2).values()], + [0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 9); + }); + + test('getPitchedLabelPlaneMatrix: bearing and pitch', () => { + const transform = new MercatorTransform(); + transform.setBearing(45); + transform.setPitch(45); + transform.setRoll(0); + + expectToBeCloseToArray([...getPitchedLabelPlaneMatrix(false, transform, 2).values()], + [0.3535533845424652, -0.3535533845424652, 0, 0, 0.3535533845424652, 0.3535533845424652, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 9); + expectToBeCloseToArray([...getPitchedLabelPlaneMatrix(true, transform, 2).values()], + [0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 9); + }); + + test('getPitchedLabelPlaneMatrix: bearing, pitch, and roll', () => { + const transform = new MercatorTransform(); + transform.setBearing(45); + transform.setPitch(45); + transform.setRoll(45); + + expectToBeCloseToArray([...getPitchedLabelPlaneMatrix(false, transform, 2).values()], + [0.08967986702919006, -0.5226925611495972, 0, 0, 0.5226925611495972, -0.08967986702919006, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 9); + expectToBeCloseToArray([...getPitchedLabelPlaneMatrix(true, transform, 2).values()], + [0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 9); + }); + + test('getGlCoordMatrix: bearing, pitch, and roll', () => { + const transform = new MercatorTransform(); + transform.resize(128, 128); + transform.setBearing(45); + transform.setPitch(45); + transform.setRoll(45); + + expectToBeCloseToArray([...getGlCoordMatrix(false, false, transform, 2).values()], + [...transform.pixelsToClipSpaceMatrix.values()], 9); + expectToBeCloseToArray([...getGlCoordMatrix(false, true, transform, 2).values()], + [...transform.pixelsToClipSpaceMatrix.values()], 9); + expectToBeCloseToArray([...getGlCoordMatrix(true, false, transform, 2).values()], + [-0.33820396661758423, 1.9711971282958984, 0, 0, -1.9711971282958984, 0.33820396661758423, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 9); + expectToBeCloseToArray([...getGlCoordMatrix(true, true, transform, 2).values()], + [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 9); + }); + + test('getTileSkewVectors: bearing', () => { + const transform = new MercatorTransform(); + transform.setBearing(45); + transform.setPitch(0); + transform.setRoll(0); + + expectToBeCloseToArray([...getTileSkewVectors(transform).vecEast.values()], + [0.7071067690849304, 0.7071067690849304]); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecSouth.values()], + [-0.7071067690849304, 0.7071067690849304], 9); + }); + + test('getTileSkewVectors: roll', () => { + const transform = new MercatorTransform(); + transform.setBearing(0); + transform.setPitch(0); + transform.setRoll(45); + + expectToBeCloseToArray([...getTileSkewVectors(transform).vecEast.values()], + [0.7071067690849304, 0.7071067690849304]); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecSouth.values()], + [-0.7071067690849304, 0.7071067690849304], 9); + }); + + test('getTileSkewVectors: pitch', () => { + const transform = new MercatorTransform(); + transform.setBearing(0); + transform.setPitch(45); + transform.setRoll(0); + + expectToBeCloseToArray([...getTileSkewVectors(transform).vecEast.values()], + [1.0, 0.0]); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecSouth.values()], + [0.0, 1.0], 9); + }); + + test('getTileSkewVectors: roll pitch bearing', () => { + const transform = new MercatorTransform(); + transform.setBearing(45); + transform.setPitch(45); + transform.setRoll(45); + + expectToBeCloseToArray([...getTileSkewVectors(transform).vecEast.values()], + [-0.16910198330879211, 0.9855985641479492]); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecSouth.values()], + [-0.9855985641479492, 0.16910198330879211], 9); + }); + + test('getTileSkewVectors: pitch 90 degrees', () => { + const transform = new MercatorTransform(); + transform.setMaxPitch(180); + transform.setBearing(0); + transform.setPitch(89); + transform.setRoll(0); + + expectToBeCloseToArray([...getTileSkewVectors(transform).vecEast.values()], + [1, 0]); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecSouth.values()], + [0, 1], 9); + + transform.setPitch(90); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecEast.values()], + [0, 0]); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecSouth.values()], + [0, 1], 9); + + transform.setBearing(90); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecEast.values()], + [0, 0]); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecSouth.values()], + [-1, 0], 9); + }); + + test('getTileSkewVectors: pitch 90 degrees with roll and bearing', () => { + const transform = new MercatorTransform(); + transform.setMaxPitch(180); + transform.setBearing(45); + transform.setPitch(89); + transform.setRoll(45); + + expectToBeCloseToArray([...getTileSkewVectors(transform).vecEast.values()], + [-0.6946603059768677, 0.7193379402160645]); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecSouth.values()], + [-0.7193379402160645, 0.6946603059768677], 9); + + transform.setPitch(90); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecEast.values()], + [-0.7071067690849304, 0.7071067690849304]); + expectToBeCloseToArray([...getTileSkewVectors(transform).vecSouth.values()], + [-0.7071067690849304, 0.7071067690849304], 9); + }); + }); diff --git a/src/symbol/projection.ts b/src/symbol/projection.ts index 54a24b1d05d..bc843ef7859 100644 --- a/src/symbol/projection.ts +++ b/src/symbol/projection.ts @@ -1,6 +1,6 @@ import Point from '@mapbox/point-geometry'; -import {mat4, vec4} from 'gl-matrix'; +import {mat2, mat4, vec2, vec4} from 'gl-matrix'; import * as symbolSize from './symbol_size'; import {addDynamicAttributes} from '../data/bucket/symbol_bucket'; @@ -97,10 +97,20 @@ export function getPitchedLabelPlaneMatrix( transform: IReadonlyTransform, pixelsToTileUnits: number) { const m = mat4.create(); - mat4.scale(m, m, [1 / pixelsToTileUnits, 1 / pixelsToTileUnits, 1]); if (!rotateWithMap) { - mat4.rotateZ(m, m, transform.angle); + const {vecSouth, vecEast} = getTileSkewVectors(transform); + const skew = mat2.create(); + skew[0] = vecEast[0]; + skew[1] = vecEast[1]; + skew[2] = vecSouth[0]; + skew[3] = vecSouth[1]; + mat2.invert(skew, skew); + m[0] = skew[0]; + m[1] = skew[1]; + m[4] = skew[2]; + m[5] = skew[3]; } + mat4.scale(m, m, [1 / pixelsToTileUnits, 1 / pixelsToTileUnits, 1]); return m; } @@ -115,16 +125,48 @@ export function getGlCoordMatrix( pixelsToTileUnits: number) { if (pitchWithMap) { const m = mat4.create(); - mat4.scale(m, m, [pixelsToTileUnits, pixelsToTileUnits, 1]); if (!rotateWithMap) { - mat4.rotateZ(m, m, -transform.angle); + const {vecSouth, vecEast} = getTileSkewVectors(transform); + m[0] = vecEast[0]; + m[1] = vecEast[1]; + m[4] = vecSouth[0]; + m[5] = vecSouth[1]; } + mat4.scale(m, m, [pixelsToTileUnits, pixelsToTileUnits, 1]); return m; } else { return transform.pixelsToClipSpaceMatrix; } } +export function getTileSkewVectors(transform: IReadonlyTransform): {vecEast: vec2; vecSouth: vec2} { + const cosRoll = Math.cos(transform.rollInRadians); + const sinRoll = Math.sin(transform.rollInRadians); + const cosPitch = Math.cos(transform.pitchInRadians); + const cosBearing = Math.cos(transform.bearingInRadians); + const sinBearing = Math.sin(transform.bearingInRadians); + const vecSouth = vec2.create(); + vecSouth[0] = -cosBearing * cosPitch * sinRoll - sinBearing * cosRoll; + vecSouth[1] = -sinBearing * cosPitch * sinRoll + cosBearing * cosRoll; + const vecSouthLen = vec2.length(vecSouth); + if (vecSouthLen < 1.0e-9) { + vec2.zero(vecSouth); + } else { + vec2.scale(vecSouth, vecSouth, 1 / vecSouthLen); + } + const vecEast = vec2.create(); + vecEast[0] = cosBearing * cosPitch * cosRoll - sinBearing * sinRoll; + vecEast[1] = sinBearing * cosPitch * cosRoll + cosBearing * sinRoll; + const vecEastLen = vec2.length(vecEast); + if (vecEastLen < 1.0e-9) { + vec2.zero(vecEast); + } else { + vec2.scale(vecEast, vecEast, 1 / vecEastLen); + } + + return {vecEast, vecSouth}; +} + /** * Projects a point using a specified matrix, including the perspective divide. * Uses a fast path if `getElevation` is undefined. diff --git a/src/ui/camera.test.ts b/src/ui/camera.test.ts index 9aeb51a6a00..d288ff01897 100644 --- a/src/ui/camera.test.ts +++ b/src/ui/camera.test.ts @@ -95,12 +95,14 @@ describe('#calculateCameraOptionsFromTo', () => { expect(cameraOptions).toBeDefined(); expect(cameraOptions.center).toBeDefined(); expect(cameraOptions.bearing).toBeCloseTo(0); + expect(cameraOptions.roll).toBeUndefined(); }); test('look at west', () => { const cameraOptions = camera.calculateCameraOptionsFromTo({lng: 1, lat: 0}, 0, {lng: 0, lat: 0}); expect(cameraOptions).toBeDefined(); expect(cameraOptions.bearing).toBeCloseTo(-90); + expect(cameraOptions.roll).toBeUndefined(); }); test('pitch 45', () => { @@ -109,12 +111,14 @@ describe('#calculateCameraOptionsFromTo', () => { const cameraOptions: CameraOptions = camera.calculateCameraOptionsFromTo({lng: 1, lat: 0}, 111200, {lng: 0, lat: 0}); expect(cameraOptions).toBeDefined(); expect(cameraOptions.pitch).toBeCloseTo(45); + expect(cameraOptions.roll).toBeUndefined(); }); test('pitch 90', () => { const cameraOptions = camera.calculateCameraOptionsFromTo({lng: 1, lat: 0}, 0, {lng: 0, lat: 0}); expect(cameraOptions).toBeDefined(); expect(cameraOptions.pitch).toBeCloseTo(90); + expect(cameraOptions.roll).toBeUndefined(); }); test('pitch 153.435', () => { @@ -125,6 +129,7 @@ describe('#calculateCameraOptionsFromTo', () => { const cameraOptions: CameraOptions = camera.calculateCameraOptionsFromTo({lng: 1, lat: 0}, 111200, {lng: 0, lat: 0}, 111200 * 3); expect(cameraOptions).toBeDefined(); expect(cameraOptions.pitch).toBeCloseTo(153.435); + expect(cameraOptions.roll).toBeUndefined(); }); test('zoom distance 1000', () => { @@ -133,6 +138,7 @@ describe('#calculateCameraOptionsFromTo', () => { expect(cameraOptions).toBeDefined(); expect(cameraOptions.zoom).toBeCloseTo(expectedZoom); + expect(cameraOptions.roll).toBeUndefined(); }); test('zoom distance 1 lng (111.2km), 111.2km altitude away', () => { @@ -141,6 +147,7 @@ describe('#calculateCameraOptionsFromTo', () => { expect(cameraOptions).toBeDefined(); expect(cameraOptions.zoom).toBeCloseTo(expectedZoom); + expect(cameraOptions.roll).toBeUndefined(); }); test('same To as From error', () => { @@ -148,6 +155,65 @@ describe('#calculateCameraOptionsFromTo', () => { }); }); +describe('#calculateCameraOptionsFromCameraLngLatAltRotation', () => { + // Choose initial zoom to avoid center being constrained by mercator latitude limits. + const camera = createCamera({zoom: 1, maxPitch: 180}); + + test('look straight down', () => { + const cameraOptions: CameraOptions = camera.calculateCameraOptionsFromCameraLngLatAltRotation({lng: 1, lat: 0}, 0, 0, 0); + expect(cameraOptions).toBeDefined(); + expect(cameraOptions.center).toBeDefined(); + const center = cameraOptions.center as LngLat; + expect(center.lng).toBeCloseTo(1); + expect(center.lat).toBeCloseTo(0); + expect(cameraOptions.elevation).toBeDefined(); + expect(cameraOptions.elevation).toBeLessThan(0); + expect(cameraOptions.zoom).toBeGreaterThan(0); + expect(cameraOptions.bearing).toBeCloseTo(0); + expect(cameraOptions.pitch).toBeCloseTo(0); + expect(cameraOptions.roll).toBeUndefined(); + }); + + test('look straight up', () => { + const cameraOptions: CameraOptions = camera.calculateCameraOptionsFromCameraLngLatAltRotation({lng: 1, lat: 0}, 0, 0, 180); + expect(cameraOptions).toBeDefined(); + expect(cameraOptions.center).toBeDefined(); + const center = cameraOptions.center as LngLat; + expect(center.lng).toBeCloseTo(1); + expect(center.lat).toBeCloseTo(0); + expect(cameraOptions.elevation).toBeDefined(); + expect(cameraOptions.elevation).toBeGreaterThan(0); + expect(cameraOptions.zoom).toBeGreaterThan(0); + expect(cameraOptions.bearing).toBeCloseTo(0); + expect(cameraOptions.pitch).toBeCloseTo(180); + expect(cameraOptions.roll).toBeUndefined(); + }); + + test('look level', () => { + const cameraOptions: CameraOptions = camera.calculateCameraOptionsFromCameraLngLatAltRotation({lng: 1, lat: 0}, 0, 0, 90); + expect(cameraOptions).toBeDefined(); + expect(cameraOptions.center).toBeDefined(); + expect(cameraOptions.elevation).toBeDefined(); + expect(cameraOptions.elevation).toBeCloseTo(0); + expect(cameraOptions.zoom).toBeGreaterThan(0); + expect(cameraOptions.bearing).toBeCloseTo(0); + expect(cameraOptions.pitch).toBeCloseTo(90); + expect(cameraOptions.roll).toBeUndefined(); + }); + + test('roll passthru', () => { + const cameraOptions: CameraOptions = camera.calculateCameraOptionsFromCameraLngLatAltRotation({lng: 1, lat: 55}, 0, 34, 45, 123.4); + expect(cameraOptions).toBeDefined(); + expect(cameraOptions.center).toBeDefined(); + expect(cameraOptions.elevation).toBeDefined(); + expect(cameraOptions.elevation).toBeLessThan(0); + expect(cameraOptions.zoom).toBeGreaterThan(0); + expect(cameraOptions.bearing).toBeCloseTo(34); + expect(cameraOptions.pitch).toBeCloseTo(45); + expect(cameraOptions.roll).toBeCloseTo(123.4); + }); +}); + describe('#jumpTo', () => { // Choose initial zoom to avoid center being constrained by mercator latitude limits. const camera = createCamera({zoom: 1}); @@ -198,6 +264,17 @@ describe('#jumpTo', () => { expect(camera.getPitch()).toBe(45); }); + test('sets roll', () => { + camera.jumpTo({pitch: 0, roll: 45}); + expect(camera.getRoll()).toBe(45); + expect(camera.getPitch()).toBe(0); + }); + + test('keeps current roll if not specified', () => { + camera.jumpTo({}); + expect(camera.getRoll()).toBe(45); + }); + test('sets multiple properties', () => { camera.jumpTo({ center: [10, 20], @@ -211,6 +288,21 @@ describe('#jumpTo', () => { expect(camera.getPitch()).toBe(60); }); + test('sets more properties', () => { + camera.jumpTo({ + center: [1, 2], + zoom: 9, + bearing: 120, + pitch: 40, + roll: 20 + }); + expect(camera.getCenter()).toEqual({lng: 1, lat: 2}); + expect(camera.getZoom()).toBe(9); + expect(camera.getBearing()).toBe(120); + expect(camera.getPitch()).toBe(40); + expect(camera.getRoll()).toBe(20); + }); + test('emits move events, preserving eventData', () => { let started, moved, ended; const eventData = {data: 'ok'}; @@ -271,6 +363,21 @@ describe('#jumpTo', () => { expect(ended).toBe('ok'); }); + test('emits roll events, preserving eventData', () => { + let started, rolled, ended; + const eventData = {data: 'ok'}; + + camera + .on('rollstart', (d) => { started = d.data; }) + .on('roll', (d) => { rolled = d.data; }) + .on('rollend', (d) => { ended = d.data; }); + + camera.jumpTo({roll: 10}, eventData); + expect(started).toBe('ok'); + expect(rolled).toBe('ok'); + expect(ended).toBe('ok'); + }); + test('cancels in-progress easing', () => { camera.panTo([3, 4]); expect(camera.isEasing()).toBeTruthy(); @@ -383,13 +490,50 @@ describe('#setBearing', () => { }); test('cancels in-progress easing', () => { - camera.panTo([3, 4]); + camera.panTo([4, 3]); expect(camera.isEasing()).toBeTruthy(); camera.setBearing(6); expect(!camera.isEasing()).toBeTruthy(); }); }); +describe('#setRoll', () => { + const camera = createCamera(); + + test('sets roll', () => { + camera.setRoll(4); + expect(camera.getRoll()).toBe(4); + }); + + test('emits move and roll events, preserving eventData', () => { + let movestarted, moved, moveended, rollstarted, rolled, rollended; + const eventData = {data: 'ok'}; + + camera + .on('movestart', (d) => { movestarted = d.data; }) + .on('move', (d) => { moved = d.data; }) + .on('moveend', (d) => { moveended = d.data; }) + .on('rollstart', (d) => { rollstarted = d.data; }) + .on('roll', (d) => { rolled = d.data; }) + .on('rollend', (d) => { rollended = d.data; }); + + camera.setRoll(5, eventData); + expect(movestarted).toBe('ok'); + expect(moved).toBe('ok'); + expect(moveended).toBe('ok'); + expect(rollstarted).toBe('ok'); + expect(rolled).toBe('ok'); + expect(rollended).toBe('ok'); + }); + + test('cancels in-progress easing', () => { + camera.panTo([3, 4]); + expect(camera.isEasing()).toBeTruthy(); + camera.setRoll(6); + expect(!camera.isEasing()).toBeTruthy(); + }); +}); + describe('#setPadding', () => { test('sets padding', () => { const camera = createCamera(); @@ -685,7 +829,29 @@ describe('#easeTo', () => { test('pitches to specified pitch', () => { const camera = createCamera(); camera.easeTo({pitch: 45, duration: 0}); - expect(camera.getPitch()).toBe(45); + expect(camera.getPitch()).toBeCloseTo(45, 6); + }); + + test('rolls to specified roll', () => { + const camera = createCamera(); + camera.easeTo({pitch: 1, roll: 45, duration: 0}); + expect(camera.getRoll()).toBeCloseTo(45, 6); + }); + + test('roll behavior at Euler angle singularity', () => { + const camera = createCamera(); + camera.easeTo({bearing: 0, pitch: 0, roll: 45, duration: 0}); + expect(camera.getRoll()).toBeCloseTo(45, 6); + expect(camera.getPitch()).toBeCloseTo(0, 6); + expect(camera.getBearing()).toBeCloseTo(0, 6); + }); + + test('bearing behavior at Euler angle singularity', () => { + const camera = createCamera(); + camera.easeTo({bearing: 45, pitch: 0, roll: 0, duration: 0}); + expect(camera.getRoll()).toBeCloseTo(0, 6); + expect(camera.getPitch()).toBeCloseTo(0, 6); + expect(camera.getBearing()).toBeCloseTo(45, 6); }); test('pans and zooms', () => { @@ -780,12 +946,12 @@ describe('#easeTo', () => { expect(fixedLngLat(camera.getCenter())).toEqual(fixedLngLat({lng: -70.3125, lat: 0.000002552471840999715})); }); - test('emits move, zoom, rotate, and pitch events, preserving eventData', () => { + test('emits move, zoom, rotate, pitch, and roll events, preserving eventData', () => { const camera = createCamera(); - let movestarted, moved, zoomstarted, zoomed, rotatestarted, rotated, pitchstarted, pitched; + let movestarted, moved, zoomstarted, zoomed, rotatestarted, rotated, pitchstarted, pitched, rollstarted, rolled; const eventData = {data: 'ok'}; - expect.assertions(18); + expect.assertions(23); camera .on('movestart', (d) => { movestarted = d.data; }) @@ -794,11 +960,13 @@ describe('#easeTo', () => { expect(camera._zooming).toBeFalsy(); expect(camera._panning).toBeFalsy(); expect(camera._rotating).toBeFalsy(); + expect(camera._rolling).toBeFalsy(); expect(movestarted).toBe('ok'); expect(moved).toBe('ok'); expect(zoomed).toBe('ok'); expect(rotated).toBe('ok'); + expect(rolled).toBe('ok'); expect(pitched).toBe('ok'); expect(d.data).toBe('ok'); }); @@ -830,8 +998,17 @@ describe('#easeTo', () => { expect(d.data).toBe('ok'); }); + camera + .on('rollstart', (d) => { rollstarted = d.data; }) + .on('roll', (d) => { rolled = d.data; }) + .on('rollend', (d) => { + expect(rollstarted).toBe('ok'); + expect(rolled).toBe('ok'); + expect(d.data).toBe('ok'); + }); + camera.easeTo( - {center: [100, 0], zoom: 3.2, bearing: 90, duration: 0, pitch: 45}, + {center: [100, 0], zoom: 3.2, bearing: 90, duration: 0, pitch: 45, roll: 30}, eventData); }); @@ -1163,7 +1340,29 @@ describe('#flyTo', () => { test('tilts to specified pitch', () => { const camera = createCamera(); camera.flyTo({pitch: 45, animate: false}); - expect(camera.getPitch()).toBe(45); + expect(camera.getPitch()).toBeCloseTo(45, 6); + }); + + test('rolls to specified roll', () => { + const camera = createCamera(); + camera.flyTo({pitch: 1, roll: 45, animate: false}); + expect(camera.getRoll()).toBeCloseTo(45, 6); + }); + + test('roll behavior at Euler angle singularity', () => { + const camera = createCamera(); + camera.flyTo({bearing: 0, pitch: 0, roll: 45, animate: false}); + expect(camera.getRoll()).toBeCloseTo(45, 6); + expect(camera.getPitch()).toBeCloseTo(0, 6); + expect(camera.getBearing()).toBeCloseTo(0, 6); + }); + + test('bearing behavior at Euler angle singularity', () => { + const camera = createCamera(); + camera.flyTo({bearing: 45, pitch: 0, roll: 0, animate: false}); + expect(camera.getRoll()).toBeCloseTo(0, 6); + expect(camera.getPitch()).toBeCloseTo(0, 6); + expect(camera.getBearing()).toBeCloseTo(45, 6); }); test('pans and zooms', () => { @@ -1223,11 +1422,11 @@ describe('#flyTo', () => { expect(fixedLngLat(camera.getCenter())).toEqual({lng: 170.3125, lat: 0}); }); - test('emits move, zoom, rotate, and pitch events, preserving eventData', () => { - expect.assertions(18); + test('emits move, zoom, rotate, pitch, and roll events, preserving eventData', () => { + expect.assertions(22); const camera = createCamera(); - let movestarted, moved, zoomstarted, zoomed, rotatestarted, rotated, pitchstarted, pitched; + let movestarted, moved, zoomstarted, zoomed, rotatestarted, rotated, pitchstarted, pitched, rollstarted, rolled; const eventData = {data: 'ok'}; camera @@ -1235,6 +1434,7 @@ describe('#flyTo', () => { .on('move', (d) => { moved = d.data; }) .on('rotate', (d) => { rotated = d.data; }) .on('pitch', (d) => { pitched = d.data; }) + .on('roll', (d) => { rolled = d.data; }) .on('moveend', (d) => { expect(camera._zooming).toBeFalsy(); expect(camera._panning).toBeFalsy(); @@ -1245,6 +1445,7 @@ describe('#flyTo', () => { expect(zoomed).toBe('ok'); expect(rotated).toBe('ok'); expect(pitched).toBe('ok'); + expect(rolled).toBe('ok'); expect(d.data).toBe('ok'); }); @@ -1275,8 +1476,17 @@ describe('#flyTo', () => { expect(d.data).toBe('ok'); }); + camera + .on('rollstart', (d) => { rollstarted = d.data; }) + .on('roll', (d) => { rolled = d.data; }) + .on('rollend', (d) => { + expect(rollstarted).toBe('ok'); + expect(rolled).toBe('ok'); + expect(d.data).toBe('ok'); + }); + camera.flyTo( - {center: [100, 0], zoom: 3.2, bearing: 90, duration: 0, pitch: 45, animate: false}, + {center: [100, 0], zoom: 3.2, bearing: 90, duration: 0, pitch: 45, roll: 20, animate: false}, eventData); }); @@ -1798,7 +2008,7 @@ describe('#flyTo', () => { }; camera.transform = { elevation: 0, - recalculateZoom: () => true, + recalculateZoomAndCenter: () => true, setMinElevationForCurrentTile: (_a) => true, setElevation: (e) => { camera.transform.elevation = e; } }; @@ -2403,6 +2613,16 @@ describe('#jumpTo globe projection', () => { expect(camera.getPitch()).toBe(45); }); + test('sets roll', () => { + camera.jumpTo({roll: 45}); + expect(camera.getRoll()).toBe(45); + }); + + test('keeps current roll if not specified', () => { + camera.jumpTo({}); + expect(camera.getRoll()).toBe(45); + }); + test('sets multiple properties', () => { camera.jumpTo({ center: [10, 20], @@ -2531,6 +2751,29 @@ describe('#easeTo globe projection', () => { expect(camera.getPitch()).toBe(45); }); + test('rolls to specified roll', () => { + const camera = createCameraGlobe(); + camera.easeTo({pitch: 1, roll: 45, duration: 0}); + expect(camera.getPitch()).toBeCloseTo(1, 6); + expect(camera.getRoll()).toBeCloseTo(45, 6); + }); + + test('roll behavior at Euler angle singularity', () => { + const camera = createCameraGlobe(); + camera.easeTo({bearing: 0, pitch: 0, roll: 45, duration: 0}); + expect(camera.getRoll()).toBeCloseTo(45, 6); + expect(camera.getPitch()).toBeCloseTo(0, 6); + expect(camera.getBearing()).toBeCloseTo(0, 6); + }); + + test('bearing behavior at Euler angle singularity', () => { + const camera = createCameraGlobe(); + camera.easeTo({bearing: 45, pitch: 0, roll: 0, duration: 0}); + expect(camera.getRoll()).toBeCloseTo(0, 6); + expect(camera.getPitch()).toBeCloseTo(0, 6); + expect(camera.getBearing()).toBeCloseTo(45, 6); + }); + test('pans and zooms', () => { const camera = createCameraGlobe(); camera.easeTo({center: [100, 0], zoom: 3.2, duration: 0}); @@ -2830,6 +3073,29 @@ describe('#flyTo globe projection', () => { expect(camera.getPitch()).toBe(45); }); + test('rolls to specified roll', () => { + const camera = createCameraGlobe(); + camera.flyTo({pitch: 1, roll: 45, animate: false}); + expect(camera.getPitch()).toBeCloseTo(1, 6); + expect(camera.getRoll()).toBeCloseTo(45, 6); + }); + + test('roll behavior at Euler angle singularity', () => { + const camera = createCameraGlobe(); + camera.flyTo({bearing: 0, pitch: 0, roll: 45, animate: false}); + expect(camera.getRoll()).toBeCloseTo(45, 6); + expect(camera.getPitch()).toBeCloseTo(0, 6); + expect(camera.getBearing()).toBeCloseTo(0, 6); + }); + + test('bearing behavior at Euler angle singularity', () => { + const camera = createCameraGlobe(); + camera.flyTo({bearing: 45, pitch: 0, roll: 0, animate: false}); + expect(camera.getRoll()).toBeCloseTo(0, 6); + expect(camera.getPitch()).toBeCloseTo(0, 6); + expect(camera.getBearing()).toBeCloseTo(45, 6); + }); + test('pans and zooms', () => { const camera = createCameraGlobe(); camera.flyTo({center: [100, 0], zoom: 3.2, animate: false}); @@ -2890,11 +3156,11 @@ describe('#flyTo globe projection', () => { expect(fixedLngLat(camera.getCenter())).toEqual({lng: -174.079717746, lat: 0}); }); - test('emits move, zoom, rotate, and pitch events, preserving eventData', () => { - expect.assertions(18); + test('emits move, zoom, rotate, pitch, and roll events, preserving eventData', () => { + expect.assertions(24); const camera = createCameraGlobe(); - let movestarted, moved, zoomstarted, zoomed, rotatestarted, rotated, pitchstarted, pitched; + let movestarted, moved, zoomstarted, zoomed, rotatestarted, rotated, pitchstarted, pitched, rollstarted, rolled; const eventData = {data: 'ok'}; camera @@ -2902,16 +3168,20 @@ describe('#flyTo globe projection', () => { .on('move', (d) => { moved = d.data; }) .on('rotate', (d) => { rotated = d.data; }) .on('pitch', (d) => { pitched = d.data; }) + .on('roll', (d) => { rolled = d.data; }) .on('moveend', (d) => { expect(camera._zooming).toBeFalsy(); expect(camera._panning).toBeFalsy(); expect(camera._rotating).toBeFalsy(); + expect(camera._pitching).toBeFalsy(); + expect(camera._rolling).toBeFalsy(); expect(movestarted).toBe('ok'); expect(moved).toBe('ok'); expect(zoomed).toBe('ok'); expect(rotated).toBe('ok'); expect(pitched).toBe('ok'); + expect(rolled).toBe('ok'); expect(d.data).toBe('ok'); }); @@ -2942,8 +3212,17 @@ describe('#flyTo globe projection', () => { expect(d.data).toBe('ok'); }); + camera + .on('rollstart', (d) => { rollstarted = d.data; }) + .on('roll', (d) => { rolled = d.data; }) + .on('rollend', (d) => { + expect(rollstarted).toBe('ok'); + expect(rolled).toBe('ok'); + expect(d.data).toBe('ok'); + }); + camera.flyTo( - {center: [100, 0], zoom: 3.2, bearing: 90, duration: 0, pitch: 45, animate: false}, + {center: [100, 0], zoom: 3.2, bearing: 90, duration: 0, pitch: 45, roll: 10, animate: false}, eventData); }); @@ -3282,7 +3561,6 @@ describe('#flyTo globe projection', () => { }); camera.on('moveend', () => { - console.log(leastZoom); expect(zoomed).toBeTruthy(); done(); }); diff --git a/src/ui/camera.ts b/src/ui/camera.ts index 0a202bdffe8..ec60a28444c 100644 --- a/src/ui/camera.ts +++ b/src/ui/camera.ts @@ -37,7 +37,7 @@ export type RequireAtLeastOne = { [K in keyof T]-?: Required> & Pa /** * Options common to {@link Map#jumpTo}, {@link Map#easeTo}, and {@link Map#flyTo}, controlling the desired location, - * zoom, bearing, and pitch of the camera. All properties are optional, and when a property is omitted, the current + * zoom, bearing, pitch, and roll of the camera. All properties are optional, and when a property is omitted, the current * camera value for that property will remain unchanged. * * @example @@ -65,6 +65,14 @@ export type CameraOptions = CenterZoomBearing & { * Increasing the pitch value is often used to display 3D objects. */ pitch?: number; + /** + * The desired roll in degrees. The roll is the angle about the camera boresight. + */ + roll?: number; + /** + * The elevation of the center point in meters above sea level. + */ + elevation?: number; }; /** @@ -232,12 +240,14 @@ export type AnimationOptions = { export type CameraUpdateTransformFunction = (next: { center: LngLat; zoom: number; + roll: number; pitch: number; bearing: number; elevation: number; }) => { center?: LngLat; zoom?: number; + roll?: number; pitch?: number; bearing?: number; elevation?: number; @@ -253,6 +263,7 @@ export abstract class Camera extends Evented { _zooming: boolean; _rotating: boolean; _pitching: boolean; + _rolling: boolean; _padding: boolean; _bearingSnap: number; @@ -301,6 +312,15 @@ export abstract class Camera extends Evented { */ transformCameraUpdate: CameraUpdateTransformFunction | null; + /** + * @internal + * If true, the elevation of the center point will automatically be set to the terrain elevation + * (or zero if terrain is not enabled). If false, the elevation of the center point will default + * to sea level and will not automatically update. Defaults to true. Needs to be set to false to + * keep the camera above ground when pitch \> 90 degrees. + */ + _centerClampedToGround: boolean; + abstract _requestRenderFrame(a: () => void): TaskID; abstract _cancelRenderFrame(_: TaskID): void; @@ -361,6 +381,48 @@ export abstract class Camera extends Evented { return this.jumpTo({center}, eventData); } + /** + * Returns the elevation of the map's center point. + * + * @returns The elevation of the map's center point, in meters above sea level. + */ + getCenterElevation(): number { return this.transform.elevation; } + + /** + * Sets the elevation of the map's center point, in meters above sea level. Equivalent to `jumpTo({elevation: elevation})`. + * + * Triggers the following events: `movestart` and `moveend`. + * + * @param elevation - The elevation to set, in meters above sea level. + * @param eventData - Additional properties to be added to event objects of events triggered by this method. + */ + setCenterElevation(elevation: number, eventData?: any): this { + this.jumpTo({elevation}, eventData); + return this; + } + + /** + * Returns the value of `centerClampedToGround`. + * + * If true, the elevation of the center point will automatically be set to the terrain elevation + * (or zero if terrain is not enabled). If false, the elevation of the center point will default + * to sea level and will not automatically update. Defaults to true. Needs to be set to false to + * keep the camera above ground when pitch \> 90 degrees. + */ + getCenterClampedToGround(): boolean { return this._centerClampedToGround; } + + /** + * Sets the value of `centerClampedToGround`. + * + * If true, the elevation of the center point will automatically be set to the terrain elevation + * (or zero if terrain is not enabled). If false, the elevation of the center point will default + * to sea level and will not automatically update. Defaults to true. Needs to be set to false to + * keep the camera above ground when pitch \> 90 degrees. + */ + setCenterClampedToGround(centerClampedToGround: boolean): void { + this._centerClampedToGround = centerClampedToGround; + } + /** * Pans the map by the specified offset. * @@ -575,9 +637,9 @@ export abstract class Camera extends Evented { } /** - * Rotates and pitches the map so that north is up (0° bearing) and pitch is 0°, with an animated transition. + * Rotates and pitches the map so that north is up (0° bearing) and pitch and roll are 0°, with an animated transition. * - * Triggers the following events: `movestart`, `move`, `moveend`, `pitchstart`, `pitch`, `pitchend`, and `rotate`. + * Triggers the following events: `movestart`, `move`, `moveend`, `pitchstart`, `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`. * * @param options - Options object * @param eventData - Additional properties to be added to event objects of events triggered by this method. @@ -586,6 +648,7 @@ export abstract class Camera extends Evented { this.easeTo(extend({ bearing: 0, pitch: 0, + roll: 0, duration: 1000 }, options), eventData); return this; @@ -627,6 +690,26 @@ export abstract class Camera extends Evented { return this; } + /** + * Returns the map's current roll angle. + * + * @returns The map's current roll, measured in degrees about the camera boresight. + */ + getRoll(): number { return this.transform.roll; } + + /** + * Sets the map's roll angle. Equivalent to `jumpTo({roll: roll})`. + * + * Triggers the following events: `movestart`, `moveend`, `rollstart`, and `rollend`. + * + * @param roll - The roll to set, measured in degrees about the camera boresight + * @param eventData - Additional properties to be added to event objects of events triggered by this method. + */ + setRoll(roll: number, eventData?: any): this { + this.jumpTo({roll}, eventData); + return this; + } + /** * @param bounds - Calculate the center for these bounds in the viewport and use * the highest zoom level up to and including `Map#getMaxZoom()` that fits @@ -776,12 +859,12 @@ export abstract class Camera extends Evented { } /** - * Changes any combination of center, zoom, bearing, and pitch, without + * Changes any combination of center, zoom, bearing, pitch, and roll, without * an animated transition. The map will retain its current values for any * details not specified in `options`. * * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`, - * `pitch`, `pitchend`, and `rotate`. + * `pitch`, `pitchend`, `rollstart`, `roll`, `rollend` and `rotate`. * * @param options - Options object * @param eventData - Additional properties to be added to event objects of events triggered by this method. @@ -806,6 +889,7 @@ export abstract class Camera extends Evented { const tr = this._getTransformForUpdate(); let bearingChanged = false, pitchChanged = false; + let rollChanged = false; const oldZoom = tr.zoom; @@ -813,6 +897,10 @@ export abstract class Camera extends Evented { const zoomChanged = tr.zoom !== oldZoom; + if ('elevation' in options && tr.elevation !== +options.elevation) { + tr.setElevation(+options.elevation); + } + if ('bearing' in options && tr.bearing !== +options.bearing) { bearingChanged = true; tr.setBearing(+options.bearing); @@ -823,6 +911,11 @@ export abstract class Camera extends Evented { tr.setPitch(+options.pitch); } + if ('roll' in options && tr.roll !== +options.roll) { + rollChanged = true; + tr.setRoll(+options.roll); + } + if (options.padding != null && !tr.isPaddingEqual(options.padding)) { tr.setPadding(options.padding); } @@ -849,24 +942,40 @@ export abstract class Camera extends Evented { .fire(new Event('pitchend', eventData)); } + if (rollChanged) { + this.fire(new Event('rollstart', eventData)) + .fire(new Event('roll', eventData)) + .fire(new Event('rollend', eventData)); + } + return this.fire(new Event('moveend', eventData)); } /** - * Calculates pitch, zoom and bearing for looking at `newCenter` with the camera position being `newCenter` - * and returns them as {@link CameraOptions}. + * Given a camera 'from' position and a position to look at (`to`), calculates zoom and camera rotation and returns them as {@link CameraOptions}. * @param from - The camera to look from * @param altitudeFrom - The altitude of the camera to look from * @param to - The center to look at * @param altitudeTo - Optional altitude of the center to look at. If none given the ground height will be used. * @returns the calculated camera options + * @example + * ```ts + * // Calculate options to look from (1°, 0°, 1000m) to (1°, 1°, 0m) + * const cameraLngLat = new LngLat(1, 0); + * const cameraAltitude = 1000; + * const targetLngLat = new LngLat(1, 1); + * const targetAltitude = 0; + * const cameraOptions = map.calculateCameraOptionsFromTo(cameraLngLat, cameraAltitude, targetLngLat, targetAltitude); + * // Apply calculated options + * map.jumpTo(cameraOptions); + * ``` */ calculateCameraOptionsFromTo(from: LngLat, altitudeFrom: number, to: LngLat, altitudeTo: number = 0): CameraOptions { - const fromMerc = MercatorCoordinate.fromLngLat(from, altitudeFrom); - const toMerc = MercatorCoordinate.fromLngLat(to, altitudeTo); - const dx = toMerc.x - fromMerc.x; - const dy = toMerc.y - fromMerc.y; - const dz = toMerc.z - fromMerc.z; + const fromMercator = MercatorCoordinate.fromLngLat(from, altitudeFrom); + const toMercator = MercatorCoordinate.fromLngLat(to, altitudeTo); + const dx = toMercator.x - fromMercator.x; + const dy = toMercator.y - fromMercator.y; + const dz = toMercator.z - fromMercator.z; const distance3D = Math.hypot(dx, dy, dz); if (distance3D === 0) throw new Error('Can\'t calculate camera options with same From and To'); @@ -879,7 +988,8 @@ export abstract class Camera extends Evented { pitch = dz < 0 ? 90 - pitch : 90 + pitch; return { - center: toMerc.toLngLat(), + center: toMercator.toLngLat(), + elevation: altitudeTo, zoom, pitch, bearing @@ -887,7 +997,40 @@ export abstract class Camera extends Evented { } /** - * Changes any combination of `center`, `zoom`, `bearing`, `pitch`, and `padding` with an animated transition + * Given a camera position and rotation, calculates zoom and center point and returns them as {@link CameraOptions}. + * @param cameraLngLat - The lng, lat of the camera to look from + * @param cameraAlt - The altitude of the camera to look from, in meters above sea level + * @param bearing - Bearing of the camera, in degrees + * @param pitch - Pitch of the camera, in degrees + * @param roll - Roll of the camera, in degrees + * @returns the calculated camera options + * @example + * ```ts + * // Calculate options to look from camera position(1°, 0°, 1000m) with bearing = 90°, pitch = 30°, and roll = 45° + * const cameraLngLat = new LngLat(1, 0); + * const cameraAltitude = 1000; + * const bearing = 90; + * const pitch = 30; + * const roll = 45; + * const cameraOptions = map.calculateCameraOptionsFromCameraLngLatAltRotation(cameraLngLat, cameraAltitude, bearing, pitch, roll); + * // Apply calculated options + * map.jumpTo(cameraOptions); + * ``` + */ + calculateCameraOptionsFromCameraLngLatAltRotation(cameraLngLat: LngLat, cameraAlt: number, bearing: number, pitch: number, roll?: number): CameraOptions { + const centerInfo = this.transform.calculateCenterFromCameraLngLatAlt(cameraLngLat, cameraAlt, bearing, pitch); + return { + center: centerInfo.center, + elevation: centerInfo.elevation, + zoom: centerInfo.zoom, + bearing, + pitch, + roll + }; + } + + /** + * Changes any combination of `center`, `zoom`, `bearing`, `pitch`, `roll`, and `padding` with an animated transition * between old and new values. The map will retain its current values for any * details not specified in `options`. * @@ -896,7 +1039,7 @@ export abstract class Camera extends Evented { * unless `options` includes `essential: true`. * * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`, - * `pitch`, `pitchend`, and `rotate`. + * `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`. * * @param options - Options describing the destination and animation of the transition. * Accepts {@link CameraOptions} and {@link AnimationOptions}. @@ -922,8 +1065,10 @@ export abstract class Camera extends Evented { const tr = this._getTransformForUpdate(); const startBearing = this.getBearing(), startPitch = tr.pitch, + startRoll = tr.roll, bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing, pitch = 'pitch' in options ? +options.pitch : startPitch, + roll = 'roll' in options ? this._normalizeBearing(options.roll, startRoll) : startRoll, padding = ('padding' in options ? options.padding : tr.padding) as PaddingOptions; const offsetAsPoint = Point.convert(options.offset); @@ -938,12 +1083,14 @@ export abstract class Camera extends Evented { moving: this._moving, zooming: this._zooming, rotating: this._rotating, - pitching: this._pitching + pitching: this._pitching, + rolling: this._rolling }; const easeHandler = this.cameraHelper.handleEaseTo(tr, { bearing, pitch, + roll, padding, around, aroundPoint, @@ -955,6 +1102,7 @@ export abstract class Camera extends Evented { this._rotating = this._rotating || (startBearing !== bearing); this._pitching = this._pitching || (pitch !== startPitch); + this._rolling = this._rolling || (roll !== startRoll); this._padding = !tr.isPaddingEqual(padding as PaddingOptions); this._zooming = this._zooming || easeHandler.isZooming; this._easeId = options.easeId; @@ -993,6 +1141,9 @@ export abstract class Camera extends Evented { if (this._pitching && !currently.pitching) { this.fire(new Event('pitchstart', eventData)); } + if (this._rolling && !currently.rolling) { + this.fire(new Event('rollstart', eventData)); + } } _prepareElevation(center: LngLat) { @@ -1017,7 +1168,9 @@ export abstract class Camera extends Evented { _finalizeElevation() { this._elevationFreeze = false; - this.transform.recalculateZoom(this.terrain); + if (this.getCenterClampedToGround()) { + this.transform.recalculateZoomAndCenter(this.terrain); + } } /** @@ -1050,9 +1203,12 @@ export abstract class Camera extends Evented { * @param tr - The transform to check. */ _elevateCameraIfInsideTerrain(tr: ITransform) : { pitch?: number; zoom?: number } { - const cameraLngLat = tr.screenPointToLocation(tr.getCameraPoint()); + if (!this.terrain && tr.elevation >= 0 && tr.pitch <= 90) { + return {}; + } + const cameraLngLat = tr.getCameraLngLat(); const cameraAltitude = tr.getCameraAltitude(); - const minAltitude = this.terrain.getElevationForLngLatZoom(cameraLngLat, tr.zoom); + const minAltitude = this.terrain ? this.terrain.getElevationForLngLatZoom(cameraLngLat, tr.zoom) : 0; if (cameraAltitude < minAltitude) { const newCamera = this.calculateCameraOptionsFromTo( cameraLngLat, minAltitude, tr.center, tr.elevation); @@ -1073,9 +1229,7 @@ export abstract class Camera extends Evented { */ _applyUpdatedTransform(tr: ITransform) { const modifiers : ((tr: ITransform) => ReturnType)[] = []; - if (this.terrain) { - modifiers.push(tr => this._elevateCameraIfInsideTerrain(tr)); - } + modifiers.push(tr => this._elevateCameraIfInsideTerrain(tr)); if (this.transformCameraUpdate) { modifiers.push(tr => this.transformCameraUpdate(tr)); } @@ -1088,15 +1242,17 @@ export abstract class Camera extends Evented { const { center, zoom, + roll, pitch, bearing, elevation } = modifier(nextTransform); if (center) nextTransform.setCenter(center); + if (elevation !== undefined) nextTransform.setElevation(elevation); if (zoom !== undefined) nextTransform.setZoom(zoom); + if (roll !== undefined) nextTransform.setRoll(roll); if (pitch !== undefined) nextTransform.setPitch(pitch); if (bearing !== undefined) nextTransform.setBearing(bearing); - if (elevation !== undefined) nextTransform.setElevation(elevation); finalTransform.apply(nextTransform); } this.transform.apply(finalTransform); @@ -1113,6 +1269,9 @@ export abstract class Camera extends Evented { if (this._pitching) { this.fire(new Event('pitch', eventData)); } + if (this._rolling) { + this.fire(new Event('roll', eventData)); + } } _afterEase(eventData?: any, easeId?: string) { @@ -1126,10 +1285,12 @@ export abstract class Camera extends Evented { const wasZooming = this._zooming; const wasRotating = this._rotating; const wasPitching = this._pitching; + const wasRolling = this._rolling; this._moving = false; this._zooming = false; this._rotating = false; this._pitching = false; + this._rolling = false; this._padding = false; if (wasZooming) { @@ -1141,11 +1302,14 @@ export abstract class Camera extends Evented { if (wasPitching) { this.fire(new Event('pitchend', eventData)); } + if (wasRolling) { + this.fire(new Event('rollend', eventData)); + } this.fire(new Event('moveend', eventData)); } /** - * Changes any combination of center, zoom, bearing, and pitch, animating the transition along a curve that + * Changes any combination of center, zoom, bearing, pitch, and roll, animating the transition along a curve that * evokes flight. The animation seamlessly incorporates zooming and panning to help * the user maintain her bearings even after traversing a great distance. * @@ -1154,7 +1318,7 @@ export abstract class Camera extends Evented { * unless 'options' includes `essential: true`. * * Triggers the following events: `movestart`, `move`, `moveend`, `zoomstart`, `zoom`, `zoomend`, `pitchstart`, - * `pitch`, `pitchend`, and `rotate`. + * `pitch`, `pitchend`, `rollstart`, `roll`, `rollend`, and `rotate`. * * @param options - Options describing the destination and animation of the transition. * Accepts {@link CameraOptions}, {@link AnimationOptions}, @@ -1182,7 +1346,7 @@ export abstract class Camera extends Evented { flyTo(options: FlyToOptions, eventData?: any): this { // Fall through to jumpTo if user has set prefers-reduced-motion if (!options.essential && browser.prefersReducedMotion) { - const coercedOptions = pick(options, ['center', 'zoom', 'bearing', 'pitch']) as CameraOptions; + const coercedOptions = pick(options, ['center', 'zoom', 'bearing', 'pitch', 'roll', 'elevation']) as CameraOptions; return this.jumpTo(coercedOptions, eventData); } @@ -1206,10 +1370,12 @@ export abstract class Camera extends Evented { const tr = this._getTransformForUpdate(), startBearing = tr.bearing, startPitch = tr.pitch, + startRoll = tr.roll, startPadding = tr.padding; const bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing; const pitch = 'pitch' in options ? +options.pitch : startPitch; + const roll = 'roll' in options ? this._normalizeBearing(options.roll, startRoll) : startRoll; const padding = ('padding' in options ? options.padding : tr.padding) as PaddingOptions; const offsetAsPoint = Point.convert(options.offset); @@ -1219,6 +1385,7 @@ export abstract class Camera extends Evented { const flyToHandler = this.cameraHelper.handleFlyTo(tr, { bearing, pitch, + roll, padding, locationAtOffset, offsetAsPoint, @@ -1305,6 +1472,7 @@ export abstract class Camera extends Evented { this._zooming = true; this._rotating = (startBearing !== bearing); this._pitching = (pitch !== startPitch); + this._rolling = (roll !== startRoll); this._padding = !tr.isPaddingEqual(padding as PaddingOptions); this._prepareEase(eventData, false); @@ -1321,6 +1489,9 @@ export abstract class Camera extends Evented { if (this._pitching) { tr.setPitch(interpolates.number(startPitch, pitch, k)); } + if (this._rolling) { + tr.setRoll(interpolates.number(startRoll, roll, k)); + } if (this._padding) { tr.interpolatePadding(startPadding, padding as PaddingOptions, k); // When padding is being applied, Transform#centerPoint is changing continuously, diff --git a/src/ui/control/navigation_control.ts b/src/ui/control/navigation_control.ts index 25469488863..a3eaf195170 100644 --- a/src/ui/control/navigation_control.ts +++ b/src/ui/control/navigation_control.ts @@ -24,12 +24,17 @@ type NavigationControlOptions = { * If `true` the pitch is visualized by rotating X-axis of compass. */ visualizePitch?: boolean; + /** + * If `true` the roll is visualized by rotating the compass. + */ + visualizeRoll?: boolean; }; const defaultOptions: NavigationControlOptions = { showCompass: true, showZoom: true, - visualizePitch: false + visualizePitch: false, + visualizeRoll: true }; /** @@ -93,11 +98,19 @@ export class NavigationControl implements IControl { }; _rotateCompassArrow = () => { - const rotate = this.options.visualizePitch ? - `scale(${1 / Math.pow(Math.cos(this._map.transform.pitch * (Math.PI / 180)), 0.5)}) rotateX(${this._map.transform.pitch}deg) rotateZ(${this._map.transform.angle * (180 / Math.PI)}deg)` : - `rotate(${this._map.transform.angle * (180 / Math.PI)}deg)`; - - this._compassIcon.style.transform = rotate; + if (this.options.visualizePitch && this.options.visualizeRoll) { + this._compassIcon.style.transform = `scale(${1 / Math.pow(Math.cos(this._map.transform.pitchInRadians), 0.5)}) rotateZ(${-this._map.transform.roll}deg) rotateX(${this._map.transform.pitch}deg) rotateZ(${-this._map.transform.bearing}deg)`; + return; + } + if (this.options.visualizePitch) { + this._compassIcon.style.transform = `scale(${1 / Math.pow(Math.cos(this._map.transform.pitchInRadians), 0.5)}) rotateX(${this._map.transform.pitch}deg) rotateZ(${-this._map.transform.bearing}deg)`; + return; + } + if (this.options.visualizeRoll) { + this._compassIcon.style.transform = `rotate(${-this._map.transform.bearing - this._map.transform.roll}deg)`; + return; + } + this._compassIcon.style.transform = `rotate(${-this._map.transform.bearing}deg)`; }; /** {@inheritDoc IControl.onAdd} */ @@ -114,6 +127,9 @@ export class NavigationControl implements IControl { if (this.options.visualizePitch) { this._map.on('pitch', this._rotateCompassArrow); } + if (this.options.visualizeRoll) { + this._map.on('roll', this._rotateCompassArrow); + } this._map.on('rotate', this._rotateCompassArrow); this._rotateCompassArrow(); this._handler = new MouseRotateWrapper(this._map, this._compass, this.options.visualizePitch); @@ -131,6 +147,9 @@ export class NavigationControl implements IControl { if (this.options.visualizePitch) { this._map.off('pitch', this._rotateCompassArrow); } + if (this.options.visualizeRoll) { + this._map.off('roll', this._rotateCompassArrow); + } this._map.off('rotate', this._rotateCompassArrow); this._handler.off(); delete this._handler; diff --git a/src/ui/events.ts b/src/ui/events.ts index bc12096e00b..009422c0853 100644 --- a/src/ui/events.ts +++ b/src/ui/events.ts @@ -461,6 +461,7 @@ export type MapSourceDataEvent = MapLibreEvent & { source: SourceSpecification; sourceId: string; sourceDataType: MapSourceDataType; + sourceDataChanged?: boolean; /** * The tile being loaded or changed, if the event has a `dataType` of `source` and * the event is related to loading of a tile. diff --git a/src/ui/handler/drag_handler.ts b/src/ui/handler/drag_handler.ts index 7f08be11186..c440be1069d 100644 --- a/src/ui/handler/drag_handler.ts +++ b/src/ui/handler/drag_handler.ts @@ -6,6 +6,7 @@ import {Handler} from '../handler_manager'; interface DragMovementResult { bearingDelta?: number; pitchDelta?: number; + rollDelta?: number; around?: Point; panDelta?: Point; } @@ -23,6 +24,10 @@ export interface DragPitchResult extends DragMovementResult { pitchDelta: number; } +export interface DragRollResult extends DragMovementResult { + rollDelta: number; +} + type DragMoveFunction = (lastPoint: Point, point: Point) => T; export interface DragMoveHandler extends Handler { @@ -103,7 +108,7 @@ export class DragHandler implemen _move(...params: Parameters>) { const move = this._moveFunction(...params); - if (move.bearingDelta || move.pitchDelta || move.around || move.panDelta) { + if (move.bearingDelta || move.pitchDelta || move.rollDelta || move.around || move.panDelta) { this._active = true; return move; } diff --git a/src/ui/handler/drag_rotate.test.ts b/src/ui/handler/drag_rotate.test.ts index 6155d249841..a2203b41bff 100644 --- a/src/ui/handler/drag_rotate.test.ts +++ b/src/ui/handler/drag_rotate.test.ts @@ -74,6 +74,41 @@ describe('drag rotate', () => { map.remove(); }); + test('DragRotateHandler fires rollstart, roll, and rollend events at appropriate times in response to a Ctrl-right-click drag', () => { + const map = createMap({rollEnabled: true}); + + // Prevent inertial rotation. + jest.spyOn(browser, 'now').mockReturnValue(0); + + const rollstart = jest.fn(); + const roll = jest.fn(); + const rollend = jest.fn(); + + map.on('rollstart', rollstart); + map.on('roll', roll); + map.on('rollend', rollend); + + simulate.mousedown(map.getCanvas(), {buttons: 2, button: 2, ctrlKey: true}); + map._renderTaskQueue.run(); + expect(rollstart).toHaveBeenCalledTimes(0); + expect(roll).toHaveBeenCalledTimes(0); + expect(rollend).toHaveBeenCalledTimes(0); + + simulate.mousemove(map.getCanvas(), {buttons: 2, clientX: 10, clientY: 10}); + map._renderTaskQueue.run(); + expect(rollstart).toHaveBeenCalledTimes(1); + expect(roll).toHaveBeenCalledTimes(1); + expect(rollend).toHaveBeenCalledTimes(0); + + simulate.mouseup(map.getCanvas(), {buttons: 0, button: 2}); + map._renderTaskQueue.run(); + expect(rollstart).toHaveBeenCalledTimes(1); + expect(roll).toHaveBeenCalledTimes(1); + expect(rollend).toHaveBeenCalledTimes(1); + + map.remove(); + }); + test('DragRotateHandler stops firing events after mouseup', () => { const map = createMap(); diff --git a/src/ui/handler/mouse.ts b/src/ui/handler/mouse.ts index 3b327ef0a0d..0fa8431b5e4 100644 --- a/src/ui/handler/mouse.ts +++ b/src/ui/handler/mouse.ts @@ -1,7 +1,7 @@ import type Point from '@mapbox/point-geometry'; import {DOM} from '../../util/dom'; -import {DragMoveHandler, DragPanResult, DragRotateResult, DragPitchResult, DragHandler} from './drag_handler'; +import {DragMoveHandler, DragPanResult, DragRotateResult, DragPitchResult, DragHandler, DragRollResult} from './drag_handler'; import {MouseMoveStateManager} from './drag_move_state_manager'; /** @@ -16,6 +16,10 @@ export interface MouseRotateHandler extends DragMoveHandler {} +/** + * `MouseRollHandler` allows the user to roll the camera by holding `Ctrl`, right-clicking and dragging + */ +export interface MouseRollHandler extends DragMoveHandler {} const LEFT_BUTTON = 0; const RIGHT_BUTTON = 2; @@ -55,7 +59,7 @@ export const generateMouseRotationHandler = ({enable, clickTolerance, bearingDeg const mouseMoveStateManager = new MouseMoveStateManager({ checkCorrectEvent: (e: MouseEvent): boolean => (DOM.mouseButton(e) === LEFT_BUTTON && e.ctrlKey) || - (DOM.mouseButton(e) === RIGHT_BUTTON), + (DOM.mouseButton(e) === RIGHT_BUTTON && !e.ctrlKey), }); return new DragHandler({ clickTolerance, @@ -90,3 +94,24 @@ export const generateMousePitchHandler = ({enable, clickTolerance, pitchDegreesP assignEvents, }); }; + +export const generateMouseRollHandler = ({enable, clickTolerance, rollDegreesPerPixelMoved = 0.8}: { + clickTolerance: number; + rollDegreesPerPixelMoved?: number; + enable?: boolean; +}): MouseRollHandler => { + const mouseMoveStateManager = new MouseMoveStateManager({ + checkCorrectEvent: (e: MouseEvent): boolean => + (DOM.mouseButton(e) === RIGHT_BUTTON && e.ctrlKey), + }); + return new DragHandler({ + clickTolerance, + move: (lastPoint: Point, point: Point) => + ({rollDelta: (point.x - lastPoint.x) * rollDegreesPerPixelMoved}), + // prevent browser context menu when necessary; we don't allow it with roll + // because we can't discern roll gesture start from contextmenu on Mac + moveStateManager: mouseMoveStateManager, + enable, + assignEvents, + }); +}; diff --git a/src/ui/handler/mouse_handler_interface.test.ts b/src/ui/handler/mouse_handler_interface.test.ts index 6220cb6b56c..b9dedf250cc 100644 --- a/src/ui/handler/mouse_handler_interface.test.ts +++ b/src/ui/handler/mouse_handler_interface.test.ts @@ -1,6 +1,6 @@ import Point from '@mapbox/point-geometry'; -import {generateMousePanHandler, generateMousePitchHandler, generateMouseRotationHandler} from './mouse'; +import {generateMousePanHandler, generateMousePitchHandler, generateMouseRollHandler, generateMouseRotationHandler} from './mouse'; describe('mouse handler tests', () => { test('MouseRotateHandler', () => { @@ -108,4 +108,39 @@ describe('mouse handler tests', () => { expect(mousePan.dragMove(overToleranceMove, new Point(10, 10))).toBeUndefined(); expect(mousePan.isActive()).toBe(false); }); + + test('MouseRollHandler', () => { + const mouseRoll = generateMouseRollHandler({clickTolerance: 2}); + + expect(mouseRoll.isActive()).toBe(false); + expect(mouseRoll.isEnabled()).toBe(false); + mouseRoll.enable(); + expect(mouseRoll.isEnabled()).toBe(true); + + mouseRoll.dragStart(new MouseEvent('mousedown', {buttons: 2, button: 2, ctrlKey: true}), new Point(0, 0)); + expect(mouseRoll.isActive()).toBe(false); + + const underToleranceMove = new MouseEvent('mousemove', {buttons: 2, clientX: 1, clientY: 1}); + expect(mouseRoll.dragMove(underToleranceMove, new Point(1, 1))).toBeUndefined(); + expect(mouseRoll.isActive()).toBe(false); + + const overToleranceMove = new MouseEvent('mousemove', {buttons: 2, clientX: 10, clientY: 10}); + expect(mouseRoll.dragMove(overToleranceMove, new Point(10, 10))).toEqual({'rollDelta': 8}); + expect(mouseRoll.isActive()).toBe(true); + + mouseRoll.dragEnd(new MouseEvent('mouseup', {buttons: 0, button: 2})); + expect(mouseRoll.isActive()).toBe(false); + + mouseRoll.disable(); + expect(mouseRoll.isEnabled()).toBe(false); + + mouseRoll.dragStart(new MouseEvent('mousedown', {buttons: 2, button: 2}), new Point(0, 0)); + expect(mouseRoll.isActive()).toBe(false); + + expect(mouseRoll.dragMove(underToleranceMove, new Point(1, 1))).toBeUndefined(); + expect(mouseRoll.isActive()).toBe(false); + + expect(mouseRoll.dragMove(overToleranceMove, new Point(10, 10))).toBeUndefined(); + expect(mouseRoll.isActive()).toBe(false); + }); }); diff --git a/src/ui/handler/scroll_zoom.test.ts b/src/ui/handler/scroll_zoom.test.ts index 364b4264871..9155b6f0956 100644 --- a/src/ui/handler/scroll_zoom.test.ts +++ b/src/ui/handler/scroll_zoom.test.ts @@ -297,6 +297,58 @@ describe('ScrollZoomHandler', () => { }); + test('Zooms for single mouse wheel tick while in the center of the map, should zoom to center', () => { + const browserNow = jest.spyOn(browser, 'now'); + let now = 1555555555555; + browserNow.mockReturnValue(now); + + const map = createMap(); + map._renderTaskQueue.run(); + expect(map.getCenter().lat).toBeCloseTo(0, 10); + expect(map.getCenter().lng).toBeCloseTo(0, 10); + + // simulate a single 'wheel' event + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta, clientX: 200, clientY: 150}); + map._renderTaskQueue.run(); + + now += 400; + browserNow.mockReturnValue(now); + map._renderTaskQueue.run(); + + expect(map.getCenter().lat).toBeCloseTo(0, 10); + expect(map.getCenter().lng).toBeCloseTo(0, 10); + expect(map.getZoom()).toBeCloseTo(0.028567106927402726, 10); + + map.remove(); + }); + + test('Zooms for single mouse wheel tick while not in the center of the map, should zoom according to mouse position', () => { + const browserNow = jest.spyOn(browser, 'now'); + let now = 1555555555555; + browserNow.mockReturnValue(now); + + const map = createMap(); + map._elevateCameraIfInsideTerrain = (_tr : any) => ({}); + map._renderTaskQueue.run(); + map.terrain = { + pointCoordinate: () => null + } as any; + + // simulate a single 'wheel' event + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta, clientX: 1000, clientY: 1000}); + map._renderTaskQueue.run(); + + now += 400; + browserNow.mockReturnValue(now); + map._renderTaskQueue.run(); + + expect(map.getCenter().lat).toBeCloseTo(-11.6371, 3); + expect(map.getCenter().lng).toBeCloseTo(11.0286, 3); + expect(map.getZoom()).toBeCloseTo(0.028567106927402726, 10); + + map.remove(); + }); + test('Zooms for single mouse wheel tick while not in the center of the map and terrain is on, should zoom according to mouse position', () => { const browserNow = jest.spyOn(browser, 'now'); let now = 1555555555555; diff --git a/src/ui/handler/shim/drag_rotate.ts b/src/ui/handler/shim/drag_rotate.ts index ec3c1bf2482..f10b31a7626 100644 --- a/src/ui/handler/shim/drag_rotate.ts +++ b/src/ui/handler/shim/drag_rotate.ts @@ -1,4 +1,4 @@ -import type {MousePitchHandler, MouseRotateHandler} from '../mouse'; +import type {MousePitchHandler, MouseRollHandler, MouseRotateHandler} from '../mouse'; /** * Options object for `DragRotateHandler`. @@ -9,6 +9,11 @@ export type DragRotateHandlerOptions = { * @defaultValue true */ pitchWithRotate: boolean; + /** + * Control the map roll in addition to the bearing + * @defaultValue false + */ + rollEnabled: boolean; } /** @@ -21,13 +26,17 @@ export class DragRotateHandler { _mouseRotate: MouseRotateHandler; _mousePitch: MousePitchHandler; + _mouseRoll: MouseRollHandler; _pitchWithRotate: boolean; + _rollEnabled: boolean; /** @internal */ - constructor(options: DragRotateHandlerOptions, mouseRotate: MouseRotateHandler, mousePitch: MousePitchHandler) { + constructor(options: DragRotateHandlerOptions, mouseRotate: MouseRotateHandler, mousePitch: MousePitchHandler, mouseRoll: MouseRollHandler) { this._pitchWithRotate = options.pitchWithRotate; + this._rollEnabled = options.rollEnabled; this._mouseRotate = mouseRotate; this._mousePitch = mousePitch; + this._mouseRoll = mouseRoll; } /** @@ -41,6 +50,7 @@ export class DragRotateHandler { enable() { this._mouseRotate.enable(); if (this._pitchWithRotate) this._mousePitch.enable(); + if (this._rollEnabled) this._mouseRoll.enable(); } /** @@ -54,6 +64,7 @@ export class DragRotateHandler { disable() { this._mouseRotate.disable(); this._mousePitch.disable(); + this._mouseRoll.disable(); } /** @@ -62,7 +73,7 @@ export class DragRotateHandler { * @returns `true` if the "drag to rotate" interaction is enabled. */ isEnabled() { - return this._mouseRotate.isEnabled() && (!this._pitchWithRotate || this._mousePitch.isEnabled()); + return this._mouseRotate.isEnabled() && (!this._pitchWithRotate || this._mousePitch.isEnabled()) && (!this._rollEnabled || this._mouseRoll.isEnabled()); } /** @@ -71,6 +82,6 @@ export class DragRotateHandler { * @returns `true` if the "drag to rotate" interaction is active. */ isActive() { - return this._mouseRotate.isActive() || this._mousePitch.isActive(); + return this._mouseRotate.isActive() || this._mousePitch.isActive() || this._mouseRoll.isActive(); } } diff --git a/src/ui/handler_inertia.ts b/src/ui/handler_inertia.ts index 69f0b247675..6eba214b148 100644 --- a/src/ui/handler_inertia.ts +++ b/src/ui/handler_inertia.ts @@ -30,6 +30,11 @@ const defaultPitchInertiaOptions = extend({ maxSpeed: 90 }, defaultInertiaOptions); +const defaultRollInertiaOptions = extend({ + deceleration: 1000, + maxSpeed: 360 +}, defaultInertiaOptions); + export type InertiaOptions = { linearity: number; easing: (t: number) => number; @@ -77,6 +82,7 @@ export class HandlerInertia { zoom: 0, bearing: 0, pitch: 0, + roll: 0, pan: new Point(0, 0), pinchAround: undefined, around: undefined @@ -86,6 +92,7 @@ export class HandlerInertia { deltas.zoom += settings.zoomDelta || 0; deltas.bearing += settings.bearingDelta || 0; deltas.pitch += settings.pitchDelta || 0; + deltas.roll += settings.rollDelta || 0; if (settings.panDelta) deltas.pan._add(settings.panDelta); if (settings.around) deltas.around = settings.around; if (settings.pinchAround) deltas.pinchAround = settings.pinchAround; @@ -123,6 +130,12 @@ export class HandlerInertia { extendDuration(easeOptions, result); } + if (deltas.roll) { + const result = calculateEasing(deltas.roll, duration, defaultRollInertiaOptions); + easeOptions.roll = this._map.transform.roll + clamp(result.amount, -179, 179); + extendDuration(easeOptions, result); + } + if (easeOptions.zoom || easeOptions.bearing) { const last = deltas.pinchAround === undefined ? deltas.around : deltas.pinchAround; easeOptions.around = last ? this._map.unproject(last) : this._map.getCenter(); diff --git a/src/ui/handler_manager.ts b/src/ui/handler_manager.ts index 49c9a562ddb..feb806725f0 100644 --- a/src/ui/handler_manager.ts +++ b/src/ui/handler_manager.ts @@ -5,7 +5,7 @@ import {HandlerInertia} from './handler_inertia'; import {MapEventHandler, BlockableMapEventHandler} from './handler/map_event'; import {BoxZoomHandler} from './handler/box_zoom'; import {TapZoomHandler} from './handler/tap_zoom'; -import {generateMouseRotationHandler, generateMousePitchHandler, generateMousePanHandler} from './handler/mouse'; +import {generateMouseRotationHandler, generateMousePitchHandler, generateMousePanHandler, generateMouseRollHandler} from './handler/mouse'; import {TouchPanHandler} from './handler/touch_pan'; import {TwoFingersTouchZoomHandler, TwoFingersTouchRotateHandler, TwoFingersTouchPitchHandler} from './handler/two_fingers_touch'; import {KeyboardHandler} from './handler/keyboard'; @@ -22,7 +22,7 @@ import {browser} from '../util/browser'; import Point from '@mapbox/point-geometry'; import {MapControlsDeltas} from '../geo/projection/camera_helper'; -const isMoving = (p: EventsInProgress) => p.zoom || p.drag || p.pitch || p.rotate; +const isMoving = (p: EventsInProgress) => p.zoom || p.drag || p.roll || p.pitch || p.rotate; class RenderFrameEvent extends Event { type: 'renderFrame'; @@ -83,6 +83,7 @@ export type HandlerResult = { zoomDelta?: number; bearingDelta?: number; pitchDelta?: number; + rollDelta?: number; /** * the point to not move when changing the camera */ @@ -117,13 +118,14 @@ export type EventInProgress = { export type EventsInProgress = { zoom?: EventInProgress; + roll?: EventInProgress; pitch?: EventInProgress; rotate?: EventInProgress; drag?: EventInProgress; } function hasChange(result: HandlerResult) { - return (result.panDelta && result.panDelta.mag()) || result.zoomDelta || result.bearingDelta || result.pitchDelta; + return (result.panDelta && result.panDelta.mag()) || result.zoomDelta || result.bearingDelta || result.pitchDelta || result.rollDelta; } export class HandlerManager { @@ -254,9 +256,11 @@ export class HandlerManager { const mouseRotate = generateMouseRotationHandler(options); const mousePitch = generateMousePitchHandler(options); - map.dragRotate = new DragRotateHandler(options, mouseRotate, mousePitch); + const mouseRoll = generateMouseRollHandler(options); + map.dragRotate = new DragRotateHandler(options, mouseRotate, mousePitch, mouseRoll); this._add('mouseRotate', mouseRotate, ['mousePitch']); - this._add('mousePitch', mousePitch, ['mouseRotate']); + this._add('mousePitch', mousePitch, ['mouseRotate', 'mouseRoll']); + this._add('mouseRoll', mouseRoll, ['mousePitch']); if (options.interactive && options.dragRotate) { map.dragRotate.enable(); } @@ -448,6 +452,9 @@ export class HandlerManager { if (handlerResult.panDelta !== undefined) { eventsInProgress.drag = eventData; } + if (handlerResult.rollDelta !== undefined) { + eventsInProgress.roll = eventData; + } if (handlerResult.pitchDelta !== undefined) { eventsInProgress.pitch = eventData; } @@ -468,6 +475,7 @@ export class HandlerManager { if (change.zoomDelta) combined.zoomDelta = (combined.zoomDelta || 0) + change.zoomDelta; if (change.bearingDelta) combined.bearingDelta = (combined.bearingDelta || 0) + change.bearingDelta; if (change.pitchDelta) combined.pitchDelta = (combined.pitchDelta || 0) + change.pitchDelta; + if (change.rollDelta) combined.rollDelta = (combined.rollDelta || 0) + change.rollDelta; if (change.around !== undefined) combined.around = change.around; if (change.pinchAround !== undefined) combined.pinchAround = change.pinchAround; if (change.noInertia) combined.noInertia = change.noInertia; @@ -494,7 +502,7 @@ export class HandlerManager { // stop any ongoing camera animations (easeTo, flyTo) map._stop(true); - let {panDelta, zoomDelta, bearingDelta, pitchDelta, around, pinchAround} = combinedResult; + let {panDelta, zoomDelta, bearingDelta, pitchDelta, rollDelta, around, pinchAround} = combinedResult; if (pinchAround !== undefined) { around = pinchAround; @@ -509,6 +517,7 @@ export class HandlerManager { const deltasForHelper: MapControlsDeltas = { panDelta, zoomDelta, + rollDelta, pitchDelta, bearingDelta, around, @@ -518,16 +527,20 @@ export class HandlerManager { if (this._map.cameraHelper.useGlobeControls && !tr.isPointOnMapSurface(around)) { around = tr.centerPoint; } - const preZoomAroundLoc = tr.screenPointToLocation(panDelta ? around.sub(panDelta) : around); + // If we are rotating about the center point, avoid numerical issues near the horizon by using the transform's + // center directly, instead of computing it from the screen point + const preZoomAroundLoc = around.distSqr(tr.centerPoint) < 1.0e-2 ? + tr.center : + tr.screenPointToLocation(panDelta ? around.sub(panDelta) : around); if (!terrain) { - // Apply zoom, bearing, pitch - this._map.cameraHelper.handleMapControlsPitchBearingZoom(deltasForHelper, tr); + // Apply zoom, bearing, pitch, roll + this._map.cameraHelper.handleMapControlsRollPitchBearingZoom(deltasForHelper, tr); // Apply panning this._map.cameraHelper.handleMapControlsPan(deltasForHelper, tr, preZoomAroundLoc); } else { - // Apply zoom, bearing, pitch - this._map.cameraHelper.handleMapControlsPitchBearingZoom(deltasForHelper, tr); + // Apply zoom, bearing, pitch, roll + this._map.cameraHelper.handleMapControlsRollPitchBearingZoom(deltasForHelper, tr); // when 3d-terrain is enabled act a little different: // - dragging do not drag the picked point itself, instead it drags the map by pixel-delta. // With this approach it is no longer possible to pick a point from somewhere near @@ -610,7 +623,9 @@ export class HandlerManager { this._map._elevationFreeze = false; this._terrainMovement = false; const tr = this._map._getTransformForUpdate(); - tr.recalculateZoom(this._map.terrain); + if (this._map.getCenterClampedToGround()) { + tr.recalculateZoomAndCenter(this._map.terrain); + } this._map._applyUpdatedTransform(tr); } if (allowEndAnimation && finishedMoving) { diff --git a/src/ui/map.ts b/src/ui/map.ts index e1e50525bd6..1a8aab7783f 100644 --- a/src/ui/map.ts +++ b/src/ui/map.ts @@ -1,4 +1,4 @@ -import {extend, warnOnce, uniqueId, isImageBitmap, Complete} from '../util/util'; +import {extend, warnOnce, uniqueId, isImageBitmap, Complete, pick} from '../util/util'; import {browser} from '../util/browser'; import {DOM} from '../util/dom'; import packageJSON from '../../package.json' with {type: 'json'}; @@ -153,12 +153,12 @@ export type MapOptions = { */ maxZoom?: number | null; /** - * The minimum pitch of the map (0-85). Values greater than 60 degrees are experimental and may result in rendering issues. If you encounter any, please raise an issue with details in the MapLibre project. + * The minimum pitch of the map (0-180). * @defaultValue 0 */ minPitch?: number | null; /** - * The maximum pitch of the map (0-85). Values greater than 60 degrees are experimental and may result in rendering issues. If you encounter any, please raise an issue with details in the MapLibre project. + * The maximum pitch of the map (0-180). * @defaultValue 60 */ maxPitch?: number | null; @@ -212,6 +212,11 @@ export type MapOptions = { * @defaultValue [0, 0] */ center?: LngLatLike; + /** + * The elevation of the initial geographical centerpoint of the map, in meters above sea level. If `elevation` is not specified in the constructor options, it will default to `0`. + * @defaultValue 0 + */ + elevation?: number; /** * The initial zoom level of the map. If `zoom` is not specified in the constructor options, MapLibre 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`. * @defaultValue 0 @@ -227,6 +232,11 @@ export type MapOptions = { * @defaultValue 0 */ pitch?: number; + /** + * The initial roll angle of the map, measured in degrees counter-clockwise about the camera boresight. If `roll` is not specified in the constructor options, MapLibre 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`. + * @defaultValue 0 + */ + roll?: number; /** * If `true`, multiple copies of the world will be rendered side by side beyond -180 and 180 degrees longitude. If set to `false`: * @@ -313,6 +323,11 @@ export type MapOptions = { * @defaultValue true */ pitchWithRotate?: boolean; + /** + * If `false`, the map's roll control with "drag to rotate" interaction will be disabled. + * @defaultValue false + */ + rollEnabled?: boolean; /** * The pixel ratio. * The canvas' `width` attribute will be `container.clientWidth * pixelRatio` and its `height` attribute will be `container.clientHeight * pixelRatio`. Defaults to `devicePixelRatio` if not specified. @@ -336,6 +351,13 @@ export type MapOptions = { * @defaultValue true */ cancelPendingTileRequestsWhileZooming?: boolean; + /** + * If true, the elevation of the center point will automatically be set to the terrain elevation + * (or zero if terrain is not enabled). If false, the elevation of the center point will default + * to sea level and will not automatically update. Defaults to true. Needs to be set to false to + * keep the camera above ground when pitch \> 90 degrees. + */ + centerClampedToGround?: boolean; }; export type AddImageOptions = { @@ -361,7 +383,7 @@ const defaultMinPitch = 0; const defaultMaxPitch = 60; // use this variable to check maxPitch for validity -const maxPitchThreshold = 85; +const maxPitchThreshold = 180; const defaultOptions: Readonly> = { hash: false, @@ -391,9 +413,11 @@ const defaultOptions: Readonly> = { trackResize: true, center: [0, 0], + elevation: 0, zoom: 0, bearing: 0, pitch: 0, + roll: 0, renderWorldCopies: true, maxTileCacheSize: null, @@ -405,10 +429,12 @@ const defaultOptions: Readonly> = { clickTolerance: 3, localIdeographFontFamily: 'sans-serif', pitchWithRotate: true, + rollEnabled: false, validateStyle: true, /**Because GL MAX_TEXTURE_SIZE is usually at least 4096px. */ maxCanvasSize: [4096, 4096], - cancelPendingTileRequestsWhileZooming: true + cancelPendingTileRequestsWhileZooming: true, + centerClampedToGround: true }; /** @@ -616,6 +642,7 @@ export class Map extends Camera { this._antialias = resolvedOptions.antialias === true; this._trackResize = resolvedOptions.trackResize === true; this._bearingSnap = resolvedOptions.bearingSnap; + this._centerClampedToGround = resolvedOptions.centerClampedToGround; this._refreshExpiredTiles = resolvedOptions.refreshExpiredTiles === true; this._fadeDuration = resolvedOptions.fadeDuration; this._crossSourceCollisions = resolvedOptions.crossSourceCollisions === true; @@ -685,9 +712,11 @@ export class Map extends Camera { if (!this._hash || !this._hash._onHashChange()) { this.jumpTo({ center: resolvedOptions.center, + elevation: resolvedOptions.elevation, zoom: resolvedOptions.zoom, bearing: resolvedOptions.bearing, - pitch: resolvedOptions.pitch + pitch: resolvedOptions.pitch, + roll: resolvedOptions.roll }); if (resolvedOptions.bounds) { @@ -711,7 +740,8 @@ export class Map extends Camera { this.on('style.load', () => { if (this.transform.unmodified) { - this.jumpTo(this.style.stylesheet as any); + const coercedOptions = pick(this.style.stylesheet, ['center', 'zoom', 'bearing', 'pitch', 'roll']) as CameraOptions; + this.jumpTo(coercedOptions); } }); this.on('data', (event: MapDataEvent) => { @@ -1067,7 +1097,7 @@ export class Map extends Camera { * * A {@link ErrorEvent} event will be fired if minPitch is out of bounds. * - * @param minPitch - The minimum pitch to set (0-85). Values greater than 60 degrees are experimental and may result in rendering issues. If you encounter any, please raise an issue with details in the MapLibre project. + * @param minPitch - The minimum pitch to set (0-180). Values greater than 60 degrees are experimental and may result in rendering issues. If you encounter any, please raise an issue with details in the MapLibre project. * If `null` or `undefined` is provided, the function removes the current minimum pitch (i.e. sets it to 0). */ setMinPitch(minPitch?: number | null): Map { @@ -1103,7 +1133,7 @@ export class Map extends Camera { * * A {@link ErrorEvent} event will be fired if maxPitch is out of bounds. * - * @param maxPitch - The maximum pitch to set (0-85). Values greater than 60 degrees are experimental and may result in rendering issues. If you encounter any, please raise an issue with details in the MapLibre project. + * @param maxPitch - The maximum pitch to set (0-180). Values greater than 60 degrees are experimental and may result in rendering issues. If you encounter any, please raise an issue with details in the MapLibre project. * If `null` or `undefined` is provided, the function removes the current maximum pitch (sets it to 60). */ setMaxPitch(maxPitch?: number | null): Map { @@ -2010,7 +2040,9 @@ export class Map extends Camera { if (this.painter.renderToTexture) this.painter.renderToTexture.destruct(); this.painter.renderToTexture = null; this.transform.setMinElevationForCurrentTile(0); - this.transform.setElevation(0); + if (this._centerClampedToGround) { + this.transform.setElevation(0); + } } else { // add terrain const sourceCache = this.style.sourceCaches[options.source]; @@ -2034,7 +2066,9 @@ export class Map extends Camera { } else if (e.dataType === 'source' && e.tile) { if (e.sourceId === options.source && !this._elevationFreeze) { this.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this.transform.center, this.transform.tileZoom)); - this.transform.setElevation(this.terrain.getElevationForLngLatZoom(this.transform.center, this.transform.tileZoom)); + if (this._centerClampedToGround) { + this.transform.setElevation(this.terrain.getElevationForLngLatZoom(this.transform.center, this.transform.tileZoom)); + } } this.terrain.sourceCache.freeRtt(e.tile.tileID); } @@ -3180,12 +3214,14 @@ export class Map extends Camera { if (this.terrain) { this.terrain.sourceCache.update(this.transform, this.terrain); this.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this.transform.center, this.transform.tileZoom)); - if (!this._elevationFreeze) { + if (!this._elevationFreeze && this._centerClampedToGround) { this.transform.setElevation(this.terrain.getElevationForLngLatZoom(this.transform.center, this.transform.tileZoom)); } } else { this.transform.setMinElevationForCurrentTile(0); - this.transform.setElevation(0); + if (this._centerClampedToGround) { + this.transform.setElevation(0); + } } this._placementDirty = this.style && this.style._updatePlacement(this.transform, this.showCollisionBoxes, fadeDuration, this._crossSourceCollisions, transformUpdateResult.forcePlacementUpdate); diff --git a/src/ui/map_tests/map_bounds.test.ts b/src/ui/map_tests/map_bounds.test.ts index c1b09bb43dd..7c562d14a26 100644 --- a/src/ui/map_tests/map_bounds.test.ts +++ b/src/ui/map_tests/map_bounds.test.ts @@ -39,8 +39,8 @@ describe('#getBounds', () => { test('getBounds', () => { const map = createMap({zoom: 0}); - expect(parseFloat(map.getBounds().getCenter().lng.toFixed(10))).toBe(-0); - expect(parseFloat(map.getBounds().getCenter().lat.toFixed(10))).toBe(0); + expect(parseFloat(map.getBounds().getCenter().lng.toFixed(10))).toBeCloseTo(0, 10); + expect(parseFloat(map.getBounds().getCenter().lat.toFixed(10))).toBeCloseTo(0, 10); expect(toFixed(map.getBounds().toArray())).toEqual(toFixed([ [-70.31249999999976, -57.326521225216965], diff --git a/src/ui/map_tests/map_events.test.ts b/src/ui/map_tests/map_events.test.ts index 9ec2a260390..d4d90035d1d 100644 --- a/src/ui/map_tests/map_events.test.ts +++ b/src/ui/map_tests/map_events.test.ts @@ -975,6 +975,30 @@ describe('map events', () => { expect(actualZoom).toBe(map.getZoom()); }); + test('drag from center', () => { + const map = createMap({interactive: true, clickTolerance: 4}); + map.on('moveend', () => { + expect(map.getCenter().lng).toBeCloseTo(0, 10); + expect(map.getCenter().lat).toBeCloseTo(33.13755119234696, 10); + expect(map.getCenterElevation()).toBeCloseTo(0, 10); + }); + const canvas = map.getCanvas(); + simulate.dragWithMove(canvas, {x: 100, y: 100}, {x: 100, y: 150}); + map._renderTaskQueue.run(); + }); + + test('drag from off center', () => { + const map = createMap({interactive: true, clickTolerance: 4}); + map.on('moveend', () => { + expect(map.getCenter().lng).toBeCloseTo(0, 10); + expect(map.getCenter().lat).toBeCloseTo(33.13755119234696, 10); + expect(map.getCenterElevation()).toBeCloseTo(0, 10); + }); + const canvas = map.getCanvas(); + simulate.dragWithMove(canvas, {x: 50, y: 50}, {x: 50, y: 100}); + map._renderTaskQueue.run(); + }); + describe('error event', () => { test('logs errors to console when it has NO listeners', () => { // to avoid seeing error in the console in Jest diff --git a/src/ui/map_tests/map_pitch.test.ts b/src/ui/map_tests/map_pitch.test.ts index 9f7987bf189..35e4750bd3c 100644 --- a/src/ui/map_tests/map_pitch.test.ts +++ b/src/ui/map_tests/map_pitch.test.ts @@ -79,8 +79,8 @@ test('throw on maxPitch smaller than minPitch at init with falsey maxPitch', () test('throw on maxPitch greater than valid maxPitch at init', () => { expect(() => { - createMap({maxPitch: 90}); - }).toThrow(new Error('maxPitch must be less than or equal to 85')); + createMap({maxPitch: 190}); + }).toThrow(new Error('maxPitch must be less than or equal to 180')); }); test('throw on minPitch less than valid minPitch at init', () => { diff --git a/src/ui/map_tests/map_zoom.test.ts b/src/ui/map_tests/map_zoom.test.ts index 4fef8107475..f30cf051aaa 100644 --- a/src/ui/map_tests/map_zoom.test.ts +++ b/src/ui/map_tests/map_zoom.test.ts @@ -91,5 +91,5 @@ test('recalculate zoom is done on the camera update transform', () => { const canvas = map.getCanvas(); simulate.dragWithMove(canvas, {x: 100, y: 100}, {x: 100, y: 150}); map._renderTaskQueue.run(); - expect(map.getZoom()).toBe(0.20007702699728983); + expect(map.getZoom()).toBeCloseTo(0.20007702699730118, 10); }); diff --git a/src/util/geolocation_support.ts b/src/util/geolocation_support.ts index 30af11ff64a..98323d89811 100644 --- a/src/util/geolocation_support.ts +++ b/src/util/geolocation_support.ts @@ -14,10 +14,10 @@ export async function checkGeolocationSupport(forceRecalculation = false): Promi // insecure origin try { const permissions = await window.navigator.permissions.query({name: 'geolocation'}); - supportsGeolocation = permissions.state !== 'denied'; // eslint-disable-line require-atomic-updates + supportsGeolocation = permissions.state !== 'denied'; } catch { // Fix for iOS16 which rejects query but still supports geolocation - supportsGeolocation = !!window.navigator.geolocation; // eslint-disable-line require-atomic-updates + supportsGeolocation = !!window.navigator.geolocation; } return supportsGeolocation; } diff --git a/src/util/image_request.test.ts b/src/util/image_request.test.ts index 93ffca82520..ed9e67d9da3 100644 --- a/src/util/image_request.test.ts +++ b/src/util/image_request.test.ts @@ -225,7 +225,6 @@ describe('ImageRequest', () => { test('Cancel: getImage request cancelled for HTTPImageRequest', async () => { let imageUrl; const requestUrl = 'test'; - // eslint-disable-next-line accessor-pairs Object.defineProperty(global.Image.prototype, 'src', { set(url: string) { imageUrl = url; diff --git a/src/util/request_manager.ts b/src/util/request_manager.ts index bb98b284eac..f679dd0f856 100644 --- a/src/util/request_manager.ts +++ b/src/util/request_manager.ts @@ -7,8 +7,7 @@ export const enum ResourceType { Glyphs = 'Glyphs', Image = 'Image', Source = 'Source', - SpriteImage = 'SpriteImage', - SpriteJSON = 'SpriteJSON', + Sprite = 'Sprite', Style = 'Style', Tile = 'Tile', Unknown = 'Unknown', diff --git a/src/util/script_detection.ts b/src/util/script_detection.ts index 4043e4e043f..f31e6eb8738 100644 --- a/src/util/script_detection.ts +++ b/src/util/script_detection.ts @@ -1,5 +1,3 @@ -/* eslint-disable new-cap */ - import {unicodeBlockLookup as isChar} from './is_char_in_unicode_block'; export function allowsIdeographicBreaking(chars: string) { @@ -31,7 +29,7 @@ function sanitizedRegExpFromScriptCodes(scriptCodes: Array): RegExp { const supportedPropertyEscapes = scriptCodes.map(code => { try { return new RegExp(`\\p{sc=${code}}`, 'u').source; - } catch (e) { + } catch { return null; } }).filter(pe => pe); diff --git a/src/util/test/util.ts b/src/util/test/util.ts index 0bc96fe45ed..5be8154b4b3 100644 --- a/src/util/test/util.ts +++ b/src/util/test/util.ts @@ -153,7 +153,6 @@ export function stubAjaxGetImage(createImageBitmap) { global.URL.revokeObjectURL = () => {}; global.URL.createObjectURL = (_) => { return null; }; - // eslint-disable-next-line accessor-pairs Object.defineProperty(global.Image.prototype, 'src', { set(url: string) { if (url === 'error') { diff --git a/src/util/util.test.ts b/src/util/util.test.ts index 82a9e0de0c5..a57c29fcf19 100644 --- a/src/util/util.test.ts +++ b/src/util/util.test.ts @@ -1,5 +1,5 @@ import Point from '@mapbox/point-geometry'; -import {arraysIntersect, bezier, clamp, clone, deepEqual, easeCubicInOut, extend, filterObject, findLineIntersection, isCounterClockwise, isPowerOfTwo, keysDifference, mapObject, nextPowerOfTwo, parseCacheControl, pick, readImageDataUsingOffscreenCanvas, readImageUsingVideoFrame, uniqueId, wrap, mod, distanceOfAnglesRadians, distanceOfAnglesDegrees, differenceOfAnglesRadians, differenceOfAnglesDegrees, solveQuadratic, remapSaturate} from './util'; +import {arraysIntersect, bezier, clamp, clone, deepEqual, easeCubicInOut, extend, filterObject, findLineIntersection, isCounterClockwise, isPowerOfTwo, keysDifference, mapObject, nextPowerOfTwo, parseCacheControl, pick, readImageDataUsingOffscreenCanvas, readImageUsingVideoFrame, uniqueId, wrap, mod, distanceOfAnglesRadians, distanceOfAnglesDegrees, differenceOfAnglesRadians, differenceOfAnglesDegrees, solveQuadratic, remapSaturate, radiansToDegrees, degreesToRadians, rollPitchBearingToQuat, getRollPitchBearing} from './util'; import {Canvas} from 'canvas'; describe('util', () => { @@ -120,6 +120,14 @@ describe('util', () => { expect(mod(-1, 3)).toBe(2); }); + test('degreesToRadians', () => { + expect(degreesToRadians(1.0)).toBe(Math.PI / 180.0); + }); + + test('radiansToDegrees', () => { + expect(radiansToDegrees(1.0)).toBe(180.0 / Math.PI); + }); + test('distanceOfAnglesRadians', () => { const digits = 10; expect(distanceOfAnglesRadians(0, 1)).toBeCloseTo(1, digits); @@ -465,3 +473,30 @@ describe('util readImageDataUsingOffscreenCanvas', () => { ]); }); }); + +describe('util rotations', () => { + test('rollPitchBearingToQuat', () => { + const roll = 10; + const pitch = 20; + const bearing = 30; + + const rotation = rollPitchBearingToQuat(roll, pitch, bearing); + const angles = getRollPitchBearing(rotation); + + expect(angles.roll).toBeCloseTo(roll, 6); + expect(angles.pitch).toBeCloseTo(pitch, 6); + expect(angles.bearing).toBeCloseTo(bearing, 6); + }); + + test('rollPitchBearingToQuat sinuglarity', () => { + const roll = 10; + const pitch = 0; + const bearing = 30; + + const rotation = rollPitchBearingToQuat(roll, pitch, bearing); + const angles = getRollPitchBearing(rotation); + + expect(angles.pitch).toBeCloseTo(0, 5); + expect(wrap(angles.bearing + angles.roll, -180, 180)).toBeCloseTo(wrap(bearing + roll, -180, 180), 6); + }); +}); diff --git a/src/util/util.ts b/src/util/util.ts index ab0a4dc019e..9eb840907e7 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -3,7 +3,7 @@ import UnitBezier from '@mapbox/unitbezier'; import {isOffscreenCanvasDistorted} from './offscreen_canvas_distorted'; import type {Size} from './image'; import type {WorkerGlobalScopeInterface} from './web_worker'; -import {mat4, vec3, vec4} from 'gl-matrix'; +import {mat3, mat4, quat, vec3, vec4} from 'gl-matrix'; import {pixelsToTileUnits} from '../source/pixels_to_tile_units'; import {OverscaledTileID} from '../source/tile_id'; @@ -33,7 +33,7 @@ export function createIdentityMat4f64(): mat4 { * @param inViewportPixelUnitsUnits - True when the units accepted by the matrix are in viewport pixels instead of tile units. */ export function translatePosition( - transform: { angle: number; zoom: number }, + transform: { bearingInRadians: number; zoom: number }, tile: { tileID: OverscaledTileID; tileSize: number }, translate: [number, number], translateAnchor: 'map' | 'viewport', @@ -42,8 +42,8 @@ export function translatePosition( if (!translate[0] && !translate[1]) return [0, 0]; const angle = inViewportPixelUnitsUnits ? - (translateAnchor === 'map' ? transform.angle : 0) : - (translateAnchor === 'viewport' ? -transform.angle : 0); + (translateAnchor === 'map' ? -transform.bearingInRadians : 0) : + (translateAnchor === 'viewport' ? transform.bearingInRadians : 0); if (angle) { const sinA = Math.sin(angle); @@ -617,7 +617,7 @@ export function storageAvailable(type: string): boolean { storage.setItem('_mapbox_test_', 1); storage.removeItem('_mapbox_test_'); return true; - } catch (e) { + } catch { return false; } } @@ -838,7 +838,7 @@ export async function getImageData( if (isOffscreenCanvasDistorted()) { try { return await readImageUsingVideoFrame(image, x, y, width, height); - } catch (e) { + } catch { // fall back to OffscreenCanvas } } @@ -882,6 +882,58 @@ export function degreesToRadians(degrees: number): number { return degrees * Math.PI / 180; } +/** + * This method converts radians to degrees. + * The return value is the degrees value. + * @param degrees - The number of radians + * @returns degrees + */ +export function radiansToDegrees(degrees: number): number { + return degrees / Math.PI * 180; +} + +export type RollPitchBearing = { + roll: number; + pitch: number; + bearing: number; +}; + +/** + * This method converts a rotation quaternion to roll, pitch, and bearing angles in degrees. + * @param rotation - The rotation quaternion + * @returns roll, pitch, and bearing angles in degrees + */ +export function getRollPitchBearing(rotation: quat): RollPitchBearing { + const m: mat3 = new Float64Array(9) as any; + mat3.fromQuat(m, rotation); + + const xAngle = radiansToDegrees(-Math.asin(clamp(m[2], -1, 1))); + let roll: number; + let bearing: number; + if (Math.hypot(m[5], m[8]) < 1.0e-3) { + roll = 0.0; + bearing = -radiansToDegrees(Math.atan2(m[3], m[4])); + } else { + roll = radiansToDegrees((m[5] === 0.0 && m[8] === 0.0) ? 0.0 : Math.atan2(m[5], m[8])); + bearing = radiansToDegrees((m[1] === 0.0 && m[0] === 0.0) ? 0.0 : Math.atan2(m[1], m[0])); + } + + return {roll, pitch: xAngle + 90.0, bearing}; +} + +/** + * This method converts roll, pitch, and bearing angles in degrees to a rotation quaternion. + * @param roll - Roll angle in degrees + * @param pitch - Pitch angle in degrees + * @param bearing - Bearing angle in degrees + * @returns The rotation quaternion + */ +export function rollPitchBearingToQuat(roll: number, pitch: number, bearing: number): quat { + const rotation: quat = new Float64Array(4) as any; + quat.fromEuler(rotation, roll, pitch - 90.0, bearing); + return rotation; +} + /** * Makes optional keys required and add the the undefined type. * diff --git a/src/util/web_worker_transfer.ts b/src/util/web_worker_transfer.ts index a8f1cd4325b..d3d2df0788d 100644 --- a/src/util/web_worker_transfer.ts +++ b/src/util/web_worker_transfer.ts @@ -191,7 +191,7 @@ export function serialize(input: unknown, transferables?: Array | if (!klass.serialize) { for (const key in input) { - if (!input.hasOwnProperty(key)) continue; // eslint-disable-line no-prototype-builtins + if (!input.hasOwnProperty(key)) continue; if (registry[classRegistryKey].omit.indexOf(key) >= 0) continue; const property = input[key]; properties[key] = registry[classRegistryKey].shallow.indexOf(key) >= 0 ? diff --git a/src/util/webp_supported.ts b/src/util/webp_supported.ts index 7628b0aa9b4..056a09f2bc7 100755 --- a/src/util/webp_supported.ts +++ b/src/util/webp_supported.ts @@ -53,7 +53,7 @@ function testWebpTextureUpload(gl: WebGLRenderingContext|WebGL2RenderingContext) if (gl.isContextLost()) return; webpSupported.supported = true; - } catch (e) { + } catch { // Catch "Unspecified Error." in Edge 18. } diff --git a/test/bench/.eslintrc.json b/test/bench/.eslintrc.json deleted file mode 100644 index 769e1e778f9..00000000000 --- a/test/bench/.eslintrc.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "parserOptions": { - "ecmaFeatures": { - "jsx": true - } - }, - "plugins": [ - "react" - ], - "rules": { - "react/jsx-uses-vars": [2], - "no-restricted-properties": "off" - }, - "env": { - "browser": true - } -} diff --git a/test/bench/gl-stats.html b/test/bench/gl-stats.html index 8b459b459be..bc0c817a57e 100644 --- a/test/bench/gl-stats.html +++ b/test/bench/gl-stats.html @@ -8,7 +8,7 @@ diff --git a/test/bench/rollup_config_benchmarks.ts b/test/bench/rollup_config_benchmarks.ts index 27468da8f80..7cbeba406d6 100644 --- a/test/bench/rollup_config_benchmarks.ts +++ b/test/bench/rollup_config_benchmarks.ts @@ -41,7 +41,7 @@ const replaceConfig = { 'process.env.NODE_ENV': JSON.stringify('production') }; -const allPlugins = plugins(true, true).concat(replace(replaceConfig)); +const allPlugins = plugins(true).concat(replace(replaceConfig)); const intro = fs.readFileSync('build/rollup/bundle_prelude.js', 'utf8'); const splitConfig = (name: string): RollupOptions[] => [{ diff --git a/test/bench/versions/index.html b/test/bench/versions/index.html index d41b59004b8..edd4ad26839 100644 --- a/test/bench/versions/index.html +++ b/test/bench/versions/index.html @@ -55,7 +55,7 @@ } const results = await window.Benchmarks.run(benchmarks); - // eslint-disable-next-line require-atomic-updates + window.maplibreglBenchmarkResults = {}; for (const result of results) { window.maplibreglBenchmarkResults[result.name] = {}; @@ -63,7 +63,7 @@ window.maplibreglBenchmarkResults[result.name][version.name] = {name: version.name, summary: version.summary, regression: version.regression}; } } - // eslint-disable-next-line require-atomic-updates + window.maplibreglBenchmarkFinished = true; } catch (ex) { console.error(ex); diff --git a/test/build/min.test.ts b/test/build/min.test.ts index 6ab8fe9bdf2..4f8c99fac39 100644 --- a/test/build/min.test.ts +++ b/test/build/min.test.ts @@ -9,6 +9,7 @@ describe('test min build', () => { // confirm that the entire package.json isn't present by asserting // the absence of each of our script strings for (const name in packageJson.scripts) { + if (packageJson.scripts[name].length < 10) continue; // skip short names like "lint" expect(minBundle.includes(packageJson.scripts[name])).toBeFalsy(); } }); @@ -36,7 +37,7 @@ describe('test min build', () => { const decreaseQuota = 4096; // feel free to update this value after you've checked that it has changed on purpose :-) - const expectedBytes = 879615; + const expectedBytes = 889658; expect(actualBytes).toBeLessThan(expectedBytes + increaseQuota); expect(actualBytes).toBeGreaterThan(expectedBytes - decreaseQuota); diff --git a/test/examples/.eslintrc.json b/test/examples/.eslintrc.json deleted file mode 100644 index 80242dbdcec..00000000000 --- a/test/examples/.eslintrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "plugins": [ - "html" - ], - "rules": { - "no-restricted-properties": "off", - "new-cap": "off", - "@typescript-eslint/no-unused-vars": "off" - } -} diff --git a/test/examples/add-3d-model-threejs-with-shadow.html b/test/examples/add-3d-model-threejs-with-shadow.html new file mode 100644 index 00000000000..eb9f678da65 --- /dev/null +++ b/test/examples/add-3d-model-threejs-with-shadow.html @@ -0,0 +1,171 @@ + + + + + Add a 3D model with shadow using three.js + + + + + + + + + + +
+ + + + + diff --git a/test/examples/add-3d-model-with-terrain.html b/test/examples/add-3d-model-with-terrain.html index d7da02d79fc..08e63c892d6 100644 --- a/test/examples/add-3d-model-with-terrain.html +++ b/test/examples/add-3d-model-with-terrain.html @@ -2,7 +2,7 @@ - Adding 3D models with three.js on terrain + Adding 3D models using three.js on terrain @@ -20,13 +20,22 @@ height: 100%; } + - - - -
- - \ No newline at end of file + diff --git a/test/examples/add-3d-model.html b/test/examples/add-3d-model.html index 445c697a79a..727fc7625dd 100644 --- a/test/examples/add-3d-model.html +++ b/test/examples/add-3d-model.html @@ -1,7 +1,7 @@ - Add a 3D model with three.js + Add a 3D model using three.js @@ -13,11 +13,21 @@ - - +
- - \ No newline at end of file + diff --git a/test/examples/center-point.html b/test/examples/center-point.html new file mode 100644 index 00000000000..37c5258d036 --- /dev/null +++ b/test/examples/center-point.html @@ -0,0 +1,72 @@ + + + + Set center point above ground + + + + + + + + +
+ + + \ No newline at end of file diff --git a/test/examples/display-and-style-rich-text-labels.html b/test/examples/display-and-style-rich-text-labels.html index fefbf865116..f97653447d9 100644 --- a/test/examples/display-and-style-rich-text-labels.html +++ b/test/examples/display-and-style-rich-text-labels.html @@ -16,7 +16,7 @@
- + +

- - \ No newline at end of file + diff --git a/test/examples/globe-custom-tiles.html b/test/examples/globe-custom-tiles.html index 09ee1266a39..d87f8bdfd2c 100644 --- a/test/examples/globe-custom-tiles.html +++ b/test/examples/globe-custom-tiles.html @@ -303,7 +303,7 @@ // add the custom style layer to the map map.on('load', () => { - map.addLayer(highlightLayer); + map.addLayer(highlightLayer, 'countries-label'); }); diff --git a/test/examples/mapbox-gl-rtl-text.html b/test/examples/mapbox-gl-rtl-text.html index 896f8620221..1d68284afad 100644 --- a/test/examples/mapbox-gl-rtl-text.html +++ b/test/examples/mapbox-gl-rtl-text.html @@ -17,7 +17,7 @@ + - \ No newline at end of file + diff --git a/test/examples/navigation.html b/test/examples/navigation.html index 53c9f65ab60..2fd9116dcab 100644 --- a/test/examples/navigation.html +++ b/test/examples/navigation.html @@ -20,11 +20,17 @@ style: 'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL', center: [-74.5, 40], // starting position - zoom: 9 // starting zoom + zoom: 9, // starting zoom + rollEnabled: true // Enable mouse control of camera roll angle with `Ctrl` + right-click and drag }); // Add zoom and rotation controls to the map. - map.addControl(new maplibregl.NavigationControl()); + map.addControl(new maplibregl.NavigationControl({ + visualizePitch: true, + visualizeRoll: true, + showZoom: true, + showCompass: true + })); \ No newline at end of file diff --git a/test/examples/pmtiles.html b/test/examples/pmtiles.html index c64ceba632e..238ef17b870 100644 --- a/test/examples/pmtiles.html +++ b/test/examples/pmtiles.html @@ -7,7 +7,7 @@ - +