diff --git a/CHANGELOG.md b/CHANGELOG.md index f4ca8d322e..8d8401abdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,30 @@ ## main ### ✨ Features and improvements -- ⚠️ Changed `geometry-type` to identify "Multi-" features ([#4877](https://github.com/maplibre/maplibre-gl-js/pull/4877)) +- Catches network fetching errors such as CORS, DNS or malformed URL as actual `AJAXError` to expose HTTP request details to the `"error"` event (https://github.com/maplibre/maplibre-gl-js/pull/4822) +- Add setVerticalFieldOfView() to public API ([#4717](https://github.com/maplibre/maplibre-gl-js/issues/4717)) +- Disable sky when using globe and blend it in when changing to mercator ([#4853](https://github.com/maplibre/maplibre-gl-js/issues/4853)) - _...Add new stuff here..._ ### 🐞 Bug fixes -- ⚠️ Fix order of normalizeSpriteURL and transformRequest in loadSprite ([#3897](https://github.com/maplibre/maplibre-gl-js/issues/3897)) +- Fix line-placed map-pitch-aligned texts being too large when viewed from some latitudes on a globe ([#4786](https://github.com/maplibre/maplibre-gl-js/issues/4786)) - _...Add new stuff here..._ +## 5.0.0-pre.4 + +### ✨ 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)) + +### 🐞 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)) +- Fix 3D models in custom layers not being properly occluded by the globe ([#4817](https://github.com/maplibre/maplibre-gl-js/issues/4817)) +- Fix issue where raster tiles were not rendered correctly when using globe and terrain ([#4912](https://github.com/maplibre/maplibre-gl-js/pull/4912)) + ## v5.0.0-pre.3 ### ✨ Features and improvements @@ -17,7 +34,6 @@ ### 🐞 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)) - Fix loading of RTL plugin version 0.3.0 ([#4860](https://github.com/maplibre/maplibre-gl-js/pull/4860)) diff --git a/build/rollup_plugins.ts b/build/rollup_plugins.ts index 1016b4e866..d1b0a464eb 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,7 +31,7 @@ export const plugins = (production: boolean, minified: boolean): Plugin[] => [ sourceMap: true, functions: ['PerformanceUtils.*'] }), - minified && terser({ + terser({ compress: { 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 0000000000..f781f85e2a 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 0000000000..14a0c55b33 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 0000000000..1e05797d47 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 0000000000..f64e8de7c4 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 0000000000..0bad09c8ea --- /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/center-point.png b/docs/assets/examples/center-point.png new file mode 100644 index 0000000000..3a3b2b540b Binary files /dev/null and b/docs/assets/examples/center-point.png differ diff --git a/package-lock.json b/package-lock.json index 650c5c00a4..6c5eabfc91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "maplibre-gl", - "version": "5.0.0-pre.3", + "version": "5.0.0-pre.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "maplibre-gl", - "version": "5.0.0-pre.3", + "version": "5.0.0-pre.4", "license": "BSD-3-Clause", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", @@ -46,11 +46,11 @@ "@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", + "@stylistic/eslint-plugin-ts": "^2.10.0", "@types/benchmark": "^2.1.5", "@types/cssnano": "^5.1.3", "@types/d3": "^7.4.3", - "@types/diff": "^5.2.3", + "@types/diff": "^6.0.0", "@types/earcut": "^2.1.4", "@types/eslint": "^9.6.1", "@types/gl": "^6.0.5", @@ -60,7 +60,7 @@ "@types/minimist": "^1.2.5", "@types/murmurhash-js": "^1.0.6", "@types/nise": "^1.4.5", - "@types/node": "^22.7.9", + "@types/node": "^22.8.5", "@types/offscreencanvas": "^2019.7.3", "@types/pixelmatch": "^5.2.6", "@types/pngjs": "^6.0.5", @@ -69,17 +69,17 @@ "@types/request": "^2.48.12", "@types/shuffle-seed": "^1.1.3", "@types/window-or-global": "^1.0.6", - "@typescript-eslint/eslint-plugin": "^8.11.0", - "@typescript-eslint/parser": "^8.11.0", + "@typescript-eslint/eslint-plugin": "^8.12.2", + "@typescript-eslint/parser": "^8.12.2", "address": "^2.0.3", "autoprefixer": "^10.4.20", "benchmark": "^2.1.4", "canvas": "^2.11.2", - "cspell": "^8.15.4", + "cspell": "^8.15.5", "cssnano": "^7.0.6", "d3": "^7.9.0", "d3-queue": "^3.0.7", - "devtools-protocol": "^0.0.1373723", + "devtools-protocol": "^0.0.1376096", "diff": "^7.0.0", "dts-bundle-generator": "^9.5.1", "eslint": "^9.13.0", @@ -112,10 +112,10 @@ "postcss-cli": "^11.0.0", "postcss-inline-svg": "^6.0.0", "pretty-bytes": "^6.1.1", - "puppeteer": "^23.6.0", + "puppeteer": "^23.6.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "rollup": "^4.24.0", + "rollup": "^4.24.3", "rollup-plugin-sourcemaps2": "^0.4.2", "rw": "^1.3.3", "semver": "^7.6.3", @@ -731,16 +731,17 @@ "dev": true }, "node_modules/@cspell/cspell-bundled-dicts": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.15.4.tgz", - "integrity": "sha512-t5b2JwGeUmzmjl319mCuaeKGxTvmzLLRmrpdHr+ZZGRO4nf7L48Lbe9A6uwNUvsZe0cXohiNXsrrsuzRVXswVA==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.15.5.tgz", + "integrity": "sha512-Su1gnTBbE7ouMQvM4DISUmP6sZiFyQRE+ODvjBzW+c/x9ZLbVp+2hBEEmxFSn5fdZCJzPOMwzwsjlLYykb9iUg==", "dev": true, "dependencies": { "@cspell/dict-ada": "^4.0.5", + "@cspell/dict-al": "^1.0.3", "@cspell/dict-aws": "^4.0.7", "@cspell/dict-bash": "^4.1.8", "@cspell/dict-companies": "^3.1.7", - "@cspell/dict-cpp": "^5.1.22", + "@cspell/dict-cpp": "^5.1.23", "@cspell/dict-cryptocurrencies": "^5.0.3", "@cspell/dict-csharp": "^4.0.5", "@cspell/dict-css": "^4.0.16", @@ -752,7 +753,7 @@ "@cspell/dict-en_us": "^4.3.26", "@cspell/dict-en-common-misspellings": "^2.0.7", "@cspell/dict-en-gb": "1.1.33", - "@cspell/dict-filetypes": "^3.0.7", + "@cspell/dict-filetypes": "^3.0.8", "@cspell/dict-flutter": "^1.0.3", "@cspell/dict-fonts": "^4.0.3", "@cspell/dict-fsharp": "^1.0.4", @@ -762,7 +763,7 @@ "@cspell/dict-golang": "^6.0.16", "@cspell/dict-google": "^1.0.4", "@cspell/dict-haskell": "^4.0.4", - "@cspell/dict-html": "^4.0.9", + "@cspell/dict-html": "^4.0.10", "@cspell/dict-html-symbol-entities": "^4.0.3", "@cspell/dict-java": "^5.0.10", "@cspell/dict-julia": "^1.0.4", @@ -771,6 +772,7 @@ "@cspell/dict-lorem-ipsum": "^4.0.3", "@cspell/dict-lua": "^4.0.6", "@cspell/dict-makefile": "^1.0.3", + "@cspell/dict-markdown": "^2.0.7", "@cspell/dict-monkeyc": "^1.0.9", "@cspell/dict-node": "^5.0.4", "@cspell/dict-npm": "^5.1.8", @@ -782,12 +784,12 @@ "@cspell/dict-ruby": "^5.0.7", "@cspell/dict-rust": "^4.0.9", "@cspell/dict-scala": "^5.0.6", - "@cspell/dict-software-terms": "^4.1.11", + "@cspell/dict-software-terms": "^4.1.12", "@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.10", + "@cspell/dict-typescript": "^3.1.11", "@cspell/dict-vue": "^3.0.3" }, "engines": { @@ -795,30 +797,30 @@ } }, "node_modules/@cspell/cspell-json-reporter": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.15.4.tgz", - "integrity": "sha512-solraYhZG4l++NeVCOtpc8DMvwHc46TmJt8DQbgvKtk6wOjTEcFrwKfA6Ei9YKbvyebJlpWMenO3goOll0loYg==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.15.5.tgz", + "integrity": "sha512-yXd7KDBfUkA6y+MrOqK3q/UWorZgLIgyCZoFb0Pj67OU2ZMtgJ1VGFXAdzpKAEgEmdcblyoFzHkleYbg08qS6g==", "dev": true, "dependencies": { - "@cspell/cspell-types": "8.15.4" + "@cspell/cspell-types": "8.15.5" }, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-pipe": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.15.4.tgz", - "integrity": "sha512-WfCmZVFC6mX6vYlf02hWwelcSBTbDQgc5YqY+1miuMk+OHSUAHSACjZId6/a4IAID94xScvFfj7jgrdejUVvIQ==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.15.5.tgz", + "integrity": "sha512-X8QY73060hkR8040jabNJsvydeTG0owpqr9S0QJDdhG1z8uzenNcwR3hfwaIwQq5d6sIKcDFZY5qrO4x6eEAMw==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-resolver": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.15.4.tgz", - "integrity": "sha512-Zr428o+uUTqywrdKyjluJVnDPVAJEqZ1chQLKIrHggUah1cgs5aQ7rZ+0Rv5euYMlC2idZnP7IL6TDaIib80oA==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.15.5.tgz", + "integrity": "sha512-ejzUGLEwI8TQWXovQzzvAgSNToRrQe3h97YrH2XaB9rZDKkeA7dIBZDQ/OgOfidO+ZAsPIOxdHai3CBzEHYX3A==", "dev": true, "dependencies": { "global-directory": "^4.0.1" @@ -828,18 +830,18 @@ } }, "node_modules/@cspell/cspell-service-bus": { - "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==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.15.5.tgz", + "integrity": "sha512-zZJRRvNhvUJntnw8sX4J5gE4uIHpX2oe+Tqs3lu2vRwogadNEXE4QNJbEQyQqgMePgmqULtRdxSBzG4wy4HoQg==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-types": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.15.4.tgz", - "integrity": "sha512-1hDtgYDQVW11zgtrr12EmGW45Deoi7IjZOhzPFLb+3WkhZ46ggWdbrRalWwBolQPDDo6+B2Q6WXz5hdND+Tpwg==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.15.5.tgz", + "integrity": "sha512-bMRq9slD/D0vXckxe9vubG02HXrV4cASo6Ytkaw8rTfxMKpkBgxJWjFWphCFLOCICD71q45fUSg+W5vCp83f/Q==", "dev": true, "engines": { "node": ">=18" @@ -851,6 +853,12 @@ "integrity": "sha512-6/RtZ/a+lhFVmrx/B7bfP7rzC4yjEYe8o74EybXcvu4Oue6J4Ey2WSYj96iuodloj1LWrkNCQyX5h4Pmcj0Iag==", "dev": true }, + "node_modules/@cspell/dict-al": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@cspell/dict-al/-/dict-al-1.0.3.tgz", + "integrity": "sha512-V1HClwlfU/qwSq2Kt+MkqRAsonNu3mxjSCDyGRecdLGIHmh7yeEeaxqRiO/VZ4KP+eVSiSIlbwrb5YNFfxYZbw==", + "dev": true + }, "node_modules/@cspell/dict-aws": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/@cspell/dict-aws/-/dict-aws-4.0.7.tgz", @@ -870,9 +878,9 @@ "dev": true }, "node_modules/@cspell/dict-cpp": { - "version": "5.1.22", - "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-5.1.22.tgz", - "integrity": "sha512-g1/8P5/Q+xnIc8Js4UtBg3XOhcFrFlFbG3UWVtyEx49YTf0r9eyDtDt1qMMDBZT91pyCwLcAEbwS+4i5PIfNZw==", + "version": "5.1.23", + "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-5.1.23.tgz", + "integrity": "sha512-59VUam6bYWzn50j8FASWWLww0rBPA0PZfjMZBvvt0aqMpkvXzoJPnAAI4eDDSibPWVHKutjpqLmast+uMLHVsQ==", "dev": true }, "node_modules/@cspell/dict-cryptocurrencies": { @@ -948,9 +956,9 @@ "dev": true }, "node_modules/@cspell/dict-filetypes": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.7.tgz", - "integrity": "sha512-/DN0Ujp9/EXvpTcgih9JmBaE8n+G0wtsspyNdvHT5luRfpfol1xm/CIQb6xloCXCiLkWX+EMPeLSiVIZq+24dA==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.8.tgz", + "integrity": "sha512-D3N8sm/iptzfVwsib/jvpX+K/++rM8SRpLDFUaM4jxm8EyGmSIYRbKZvdIv5BkAWmMlTWoRqlLn7Yb1b11jKJg==", "dev": true }, "node_modules/@cspell/dict-flutter": { @@ -1008,9 +1016,9 @@ "dev": true }, "node_modules/@cspell/dict-html": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.9.tgz", - "integrity": "sha512-BNp7w3m910K4qIVyOBOZxHuFNbVojUY6ES8Y8r7YjYgJkm2lCuQoVwwhPjurnomJ7BPmZTb+3LLJ58XIkgF7JQ==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.10.tgz", + "integrity": "sha512-I9uRAcdtHbh0wEtYZlgF0TTcgH0xaw1B54G2CW+tx4vHUwlde/+JBOfIzird4+WcMv4smZOfw+qHf7puFUbI5g==", "dev": true }, "node_modules/@cspell/dict-html-symbol-entities": { @@ -1061,6 +1069,18 @@ "integrity": "sha512-R3U0DSpvTs6qdqfyBATnePj9Q/pypkje0Nj26mQJ8TOBQutCRAJbr2ZFAeDjgRx5EAJU/+8txiyVF97fbVRViw==", "dev": true }, + "node_modules/@cspell/dict-markdown": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@cspell/dict-markdown/-/dict-markdown-2.0.7.tgz", + "integrity": "sha512-F9SGsSOokFn976DV4u/1eL4FtKQDSgJHSZ3+haPRU5ki6OEqojxKa8hhj4AUrtNFpmBaJx/WJ4YaEzWqG7hgqg==", + "dev": true, + "peerDependencies": { + "@cspell/dict-css": "^4.0.16", + "@cspell/dict-html": "^4.0.10", + "@cspell/dict-html-symbol-entities": "^4.0.3", + "@cspell/dict-typescript": "^3.1.11" + } + }, "node_modules/@cspell/dict-monkeyc": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@cspell/dict-monkeyc/-/dict-monkeyc-1.0.9.tgz", @@ -1131,9 +1151,9 @@ "dev": true }, "node_modules/@cspell/dict-software-terms": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-4.1.11.tgz", - "integrity": "sha512-77CTHxWFTVw6tVoMN8WBMrlNW2F2FbgATwD/6vcOuiyrJUmh8klN5ZK3m+yyK3ZzsnaW2Bduoc0fw2Ckcm/riQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-4.1.12.tgz", + "integrity": "sha512-MHDAK/WlEdMJiDQ6lJ3SF7VogdfJcRXGYWfO4v90rxW8HDVfKDXVk3zpJhjoZGq56ujR1qmeYkQsM6MlB69uJA==", "dev": true }, "node_modules/@cspell/dict-sql": { @@ -1161,9 +1181,9 @@ "dev": true }, "node_modules/@cspell/dict-typescript": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.1.10.tgz", - "integrity": "sha512-7Zek3w4Rh3ZYyhihJ34FdnUBwP3OmRldnEq3hZ+FgQ0PyYZjXv5ztEViRBBxXjiFx1nHozr6pLi74TxToD8xsg==", + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.1.11.tgz", + "integrity": "sha512-FwvK5sKbwrVpdw0e9+1lVTl8FPoHYvfHRuQRQz2Ql5XkC0gwPPkpoyD1zYImjIyZRoYXk3yp9j8ss4iz7A7zoQ==", "dev": true }, "node_modules/@cspell/dict-vue": { @@ -1173,9 +1193,9 @@ "dev": true }, "node_modules/@cspell/dynamic-import": { - "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==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.15.5.tgz", + "integrity": "sha512-xfLRVi8zHKCGK1fg1ixXQ0bAlIU9sGm7xfbTmGG8TQt+iaKHVMIlt+XeCAo0eE7aKjIaIfqcC/PCIdUJiODuGA==", "dev": true, "dependencies": { "import-meta-resolve": "^4.1.0" @@ -1185,27 +1205,27 @@ } }, "node_modules/@cspell/filetypes": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/@cspell/filetypes/-/filetypes-8.15.4.tgz", - "integrity": "sha512-sNl6jr3ym/4151EY76qlI/00HHsiLZBqW7Vb1tqCzsgSg3EpL30ddjr74So6Sg2PN26Yf09hvxGTJzXn1R4aYw==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@cspell/filetypes/-/filetypes-8.15.5.tgz", + "integrity": "sha512-ljEFUp61mw5RWZ3S6ke6rvGKy8m4lZZjRd5KO07RYyGwSeLa4PX9AyTgSzuqXiN9y1BwogD3xolCMfPsMrtZIQ==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/strong-weak-map": { - "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==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.15.5.tgz", + "integrity": "sha512-7VzDAXsJPDXllTIi9mvQwd7PR43TPk1Ix3ocLTZDVNssf1cQbmLiQX6YWk0k8FWGfIPoIMlByw4tTSizRJcTcw==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/url": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/@cspell/url/-/url-8.15.4.tgz", - "integrity": "sha512-K2oZu/oLQPs5suRpLS8uu04O3YMUioSlEU1D66fRoOxzI5NzLt7i7yMg3HQHjChGa09N5bzqmrVdhmQrRZXwGg==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@cspell/url/-/url-8.15.5.tgz", + "integrity": "sha512-z8q7LUppFiNvytX2qrKDkXcsmOrwjqFf/5RkcpNppDezLrFejaMZu4BEVNcPrFCeS2J04K+uksNL1LYSob8jCg==", "dev": true, "engines": { "node": ">=18.0" @@ -2428,9 +2448,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.3.tgz", + "integrity": "sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==", "cpu": [ "arm" ], @@ -2441,9 +2461,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.3.tgz", + "integrity": "sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==", "cpu": [ "arm64" ], @@ -2454,9 +2474,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.3.tgz", + "integrity": "sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==", "cpu": [ "arm64" ], @@ -2467,9 +2487,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.3.tgz", + "integrity": "sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==", "cpu": [ "x64" ], @@ -2479,10 +2499,36 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.3.tgz", + "integrity": "sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.3.tgz", + "integrity": "sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==", + "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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.3.tgz", + "integrity": "sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==", "cpu": [ "arm" ], @@ -2493,9 +2539,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.3.tgz", + "integrity": "sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==", "cpu": [ "arm" ], @@ -2506,9 +2552,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.3.tgz", + "integrity": "sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==", "cpu": [ "arm64" ], @@ -2519,9 +2565,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.3.tgz", + "integrity": "sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==", "cpu": [ "arm64" ], @@ -2532,9 +2578,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.3.tgz", + "integrity": "sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==", "cpu": [ "ppc64" ], @@ -2545,9 +2591,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.3.tgz", + "integrity": "sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==", "cpu": [ "riscv64" ], @@ -2558,9 +2604,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.3.tgz", + "integrity": "sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==", "cpu": [ "s390x" ], @@ -2571,9 +2617,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.3.tgz", + "integrity": "sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==", "cpu": [ "x64" ], @@ -2584,9 +2630,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.3.tgz", + "integrity": "sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==", "cpu": [ "x64" ], @@ -2597,9 +2643,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.3.tgz", + "integrity": "sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==", "cpu": [ "arm64" ], @@ -2610,9 +2656,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.3.tgz", + "integrity": "sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==", "cpu": [ "ia32" ], @@ -2623,9 +2669,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.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.3.tgz", + "integrity": "sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==", "cpu": [ "x64" ], @@ -2735,14 +2781,14 @@ "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==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.10.0.tgz", + "integrity": "sha512-/pydlXkvbvn0MUrXzxfLElne/wr5Mb5fC+inncpBUB2nvSUdSjiNDBJC0ehbg0Z6U3FD2XNotOIQV9srHbP3nQ==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "^8.8.0", - "eslint-visitor-keys": "^4.1.0", - "espree": "^10.2.0" + "@typescript-eslint/utils": "^8.12.2", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2752,9 +2798,9 @@ } }, "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==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3092,9 +3138,9 @@ } }, "node_modules/@types/diff": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz", - "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-6.0.0.tgz", + "integrity": "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==", "dev": true }, "node_modules/@types/earcut": { @@ -3269,12 +3315,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.7.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", - "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "version": "22.8.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz", + "integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==", "dev": true, "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.19.8" } }, "node_modules/@types/offscreencanvas": { @@ -3410,16 +3456,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", - "integrity": "sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.12.2.tgz", + "integrity": "sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.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", + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/type-utils": "8.12.2", + "@typescript-eslint/utils": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -3443,15 +3489,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.11.0.tgz", - "integrity": "sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.12.2.tgz", + "integrity": "sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==", "dev": true, "dependencies": { - "@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", + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/typescript-estree": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", "debug": "^4.3.4" }, "engines": { @@ -3471,13 +3517,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz", - "integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.12.2.tgz", + "integrity": "sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.11.0", - "@typescript-eslint/visitor-keys": "8.11.0" + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3488,13 +3534,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "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==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.12.2.tgz", + "integrity": "sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.11.0", - "@typescript-eslint/utils": "8.11.0", + "@typescript-eslint/typescript-estree": "8.12.2", + "@typescript-eslint/utils": "8.12.2", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3512,9 +3558,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz", - "integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.12.2.tgz", + "integrity": "sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3525,13 +3571,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz", - "integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.12.2.tgz", + "integrity": "sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.11.0", - "@typescript-eslint/visitor-keys": "8.11.0", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3577,15 +3623,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz", - "integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.12.2.tgz", + "integrity": "sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.11.0", - "@typescript-eslint/types": "8.11.0", - "@typescript-eslint/typescript-estree": "8.11.0" + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/typescript-estree": "8.12.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3599,12 +3645,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "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==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.12.2.tgz", + "integrity": "sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/types": "8.12.2", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3632,9 +3678,9 @@ "license": "ISC" }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -4227,13 +4273,12 @@ } }, "node_modules/bare-stream": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.0.tgz", - "integrity": "sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.2.tgz", + "integrity": "sha512-EFZHSIBkDgSHIwj2l2QZfP4U5OcD4xFAOwhSb/vlr9PIqyGJGvB/nfClJbcnh3EY4jtPE4zsb5ztae96bVF79A==", "dev": true, "optional": true, "dependencies": { - "b4a": "^1.6.6", "streamx": "^2.20.0" } }, @@ -4983,29 +5028,29 @@ } }, "node_modules/cspell": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.15.4.tgz", - "integrity": "sha512-hUOxcwmNWuHzVeGHyN5v/T9MkyCE5gi0mvatxsM794B2wOuR1ZORgjZH62P2HY1uBkXe/x5C6ITWrSyh0WgAcg==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.15.5.tgz", + "integrity": "sha512-Vp1WI6axghVvenZS7GUlsZf6JFF7jDXdV5f4nXWjrZLbTrH+wbnFEO2mg+QJWa4IN35igjNYeu9TbA9/EGJzog==", "dev": true, "dependencies": { - "@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", + "@cspell/cspell-json-reporter": "8.15.5", + "@cspell/cspell-pipe": "8.15.5", + "@cspell/cspell-types": "8.15.5", + "@cspell/dynamic-import": "8.15.5", + "@cspell/url": "8.15.5", "chalk": "^5.3.0", "chalk-template": "^1.1.0", "commander": "^12.1.0", - "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", + "cspell-dictionary": "8.15.5", + "cspell-gitignore": "8.15.5", + "cspell-glob": "8.15.5", + "cspell-io": "8.15.5", + "cspell-lib": "8.15.5", "fast-json-stable-stringify": "^2.1.0", "file-entry-cache": "^9.1.0", "get-stdin": "^9.0.0", "semver": "^7.6.3", - "tinyglobby": "^0.2.9" + "tinyglobby": "^0.2.10" }, "bin": { "cspell": "bin.mjs", @@ -5019,12 +5064,12 @@ } }, "node_modules/cspell-config-lib": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.15.4.tgz", - "integrity": "sha512-vUgikQTRkRMTdkZqSs7F2cTdPpX61cTjr/9L/VCkXkbW38ObCr4650ioiF1Wq3zDF3Gy2bc4ECTpD2PZUXX5SA==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.15.5.tgz", + "integrity": "sha512-16XBjAlUWO46uEuUKHQvSeiU7hQzG9Pqg6lwKQOyZ/rVLZyihk7JGtnWuG83BbW0RFokB/BcgT1q6OegWJiEZw==", "dev": true, "dependencies": { - "@cspell/cspell-types": "8.15.4", + "@cspell/cspell-types": "8.15.5", "comment-json": "^4.2.5", "yaml": "^2.6.0" }, @@ -5033,14 +5078,14 @@ } }, "node_modules/cspell-dictionary": { - "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==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.15.5.tgz", + "integrity": "sha512-L+4MD3KItFGsxR8eY2ed6InsD7hZU1TIAeV2V4sG0wIbUXJXbPFxBTNZJrPLxTzAeCutHmkZwAl4ZCGu18bgtw==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.15.4", - "@cspell/cspell-types": "8.15.4", - "cspell-trie-lib": "8.15.4", + "@cspell/cspell-pipe": "8.15.5", + "@cspell/cspell-types": "8.15.5", + "cspell-trie-lib": "8.15.5", "fast-equals": "^5.0.1" }, "engines": { @@ -5048,14 +5093,14 @@ } }, "node_modules/cspell-gitignore": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.15.4.tgz", - "integrity": "sha512-9n5PpQ8gEf8YcvEtoZGZ2Ma6wnqSFkD2GrmyjISy39DfIX/jNLN7GX2wJm6OD2P4FjXer95ypmIb/JWTlfmbTw==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.15.5.tgz", + "integrity": "sha512-z5T0Xswfiu2NbkoVdf6uwEWzOgxCBb3L8kwB6lxzK5iyQDW2Bqlk+5b6KQaY38OtjTjJ9zzIJfFN3MfFlMFd3A==", "dev": true, "dependencies": { - "@cspell/url": "8.15.4", - "cspell-glob": "8.15.4", - "cspell-io": "8.15.4", + "@cspell/url": "8.15.5", + "cspell-glob": "8.15.5", + "cspell-io": "8.15.5", "find-up-simple": "^1.0.0" }, "bin": { @@ -5066,12 +5111,12 @@ } }, "node_modules/cspell-glob": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.15.4.tgz", - "integrity": "sha512-TTfRRHRAN+PN9drIz4MAEgKKYnPThBOlPMdFddyuisvU33Do1sPAnqkkOjTEFdi3jAA5KwnSva68SVH6IzzMBQ==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.15.5.tgz", + "integrity": "sha512-VpfP16bRbkHEyGGjf6/EifFxETfS7lpcHbYt1tRi6VhCv1FTMqbB7H7Aw+DQkDezOUN8xdw0gYe/fk5AJGOBDg==", "dev": true, "dependencies": { - "@cspell/url": "8.15.4", + "@cspell/url": "8.15.5", "micromatch": "^4.0.8" }, "engines": { @@ -5079,13 +5124,13 @@ } }, "node_modules/cspell-grammar": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.15.4.tgz", - "integrity": "sha512-MKiKyYi05mRtXOxPoTv3Ksi0GwYLiK84Uq0C+5PaMrnIjXeed0bsddSFXCT+7ywFJc7PdjhTtz0M/9WWK3UgbA==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.15.5.tgz", + "integrity": "sha512-2YnlSATtWHNL6cgx1qmTsY5ZO0zu8VdEmfcLQKgHr67T7atLRUnWAlmh06WMLd5qqp8PpWNPaOJF2prEYAXsUA==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.15.4", - "@cspell/cspell-types": "8.15.4" + "@cspell/cspell-pipe": "8.15.5", + "@cspell/cspell-types": "8.15.5" }, "bin": { "cspell-grammar": "bin.mjs" @@ -5095,40 +5140,40 @@ } }, "node_modules/cspell-io": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.15.4.tgz", - "integrity": "sha512-rXIEREPTFV9dwwg4EKfvzqlCNOvT6910AYED5YrSt8Y68usRJ9lbqdx0BrDndVCd33bp1o+9JBfHuRiFIQC81g==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.15.5.tgz", + "integrity": "sha512-6kBK+EGTG9hiUDfB55r3xbhc7YUA5vJTXoc65pe9PXd4vgXXfrPRuy+5VRtvgSMoQj59oWOQw3ZqTAR95gbGnw==", "dev": true, "dependencies": { - "@cspell/cspell-service-bus": "8.15.4", - "@cspell/url": "8.15.4" + "@cspell/cspell-service-bus": "8.15.5", + "@cspell/url": "8.15.5" }, "engines": { "node": ">=18" } }, "node_modules/cspell-lib": { - "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", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.15.5.tgz", + "integrity": "sha512-DGieMWc82ouHb6Rq2LRKAlG4ExeQL1D5uvemgaouVHMZq4GvPtVaTwA6qHhw772/5z665oOVsRCicYbDtP4V3w==", + "dev": true, + "dependencies": { + "@cspell/cspell-bundled-dicts": "8.15.5", + "@cspell/cspell-pipe": "8.15.5", + "@cspell/cspell-resolver": "8.15.5", + "@cspell/cspell-types": "8.15.5", + "@cspell/dynamic-import": "8.15.5", + "@cspell/filetypes": "8.15.5", + "@cspell/strong-weak-map": "8.15.5", + "@cspell/url": "8.15.5", "clear-module": "^4.1.2", "comment-json": "^4.2.5", - "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", + "cspell-config-lib": "8.15.5", + "cspell-dictionary": "8.15.5", + "cspell-glob": "8.15.5", + "cspell-grammar": "8.15.5", + "cspell-io": "8.15.5", + "cspell-trie-lib": "8.15.5", "env-paths": "^3.0.0", "fast-equals": "^5.0.1", "gensequence": "^7.0.0", @@ -5155,13 +5200,13 @@ } }, "node_modules/cspell-trie-lib": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.15.4.tgz", - "integrity": "sha512-sg9klsNHyrfos0Boiio+qy5d6fI9cCNjBqFYrNxvpKpwZ4gEzDzjgEKdZY1C76RD2KoBQ8I1NF5YcGc0+hhhCw==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.15.5.tgz", + "integrity": "sha512-DAEkp51aFgpp9DFuJkNki0kVm2SVR1Hp0hD3Pnta7S4X2h5424TpTVVPltAIWtcdxRLGbX6N2x26lTI4K/YfpQ==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.15.4", - "@cspell/cspell-types": "8.15.4", + "@cspell/cspell-pipe": "8.15.5", + "@cspell/cspell-types": "8.15.5", "gensequence": "^7.0.0" }, "engines": { @@ -6056,9 +6101,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1373723", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1373723.tgz", - "integrity": "sha512-MvgeYHFSB3/x8AcH1ibnp4XVfxef3RYrsKJGtA/h4cwIAiNEi1wj3+5Z88W7PE1tbwXrYdPFwcOJhs3a8IglKg==", + "version": "0.0.1376096", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1376096.tgz", + "integrity": "sha512-asCCFvz+8QkGghdWdt4AhUUsxlJC8mJRn4c5+713mg+2K0ZdNOHRRdKPaMRXAcbGiaXMc0FLALKD+f58zI3lsw==", "dev": true }, "node_modules/diff": { @@ -6927,14 +6972,14 @@ } }, "node_modules/espree": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", - "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.1.0" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6944,9 +6989,9 @@ } }, "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==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7226,9 +7271,9 @@ } }, "node_modules/fdir": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", - "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", "dev": true, "peerDependencies": { "picomatch": "^3 || ^4" @@ -12388,9 +12433,9 @@ } }, "node_modules/puppeteer": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.6.0.tgz", - "integrity": "sha512-l+Fgo8SVFSd51STtq2crz8t1Y3VXowsuR4zfR64qDOn6oggz7YIZauWiNR4IJjczQ6nvFs3S4cgng55/nesxTQ==", + "version": "23.6.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.6.1.tgz", + "integrity": "sha512-8+ALGQgwXd3P/tGcuSsxTPGDaOQIjcDIm04I5hpWZv/PiN5q8bQNHRUyfYrifT+flnM9aTWCP7tLEzuB6SlIgA==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -12398,7 +12443,7 @@ "chromium-bidi": "0.8.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1354347", - "puppeteer-core": "23.6.0", + "puppeteer-core": "23.6.1", "typed-query-selector": "^2.12.0" }, "bin": { @@ -12409,9 +12454,9 @@ } }, "node_modules/puppeteer-core": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.6.0.tgz", - "integrity": "sha512-se1bhgUpR9C529SgHGr/eyT92mYyQPAhA2S9pGtGrVG2xob9qE6Pbp7TlqiSPlnnY1lINqhn6/67EwkdzOmKqQ==", + "version": "23.6.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.6.1.tgz", + "integrity": "sha512-DoNLAzQfGklPauEn33N4h9cM9GubJSINEn+AUMwAXwW159Y9JLk5y34Jsbv4c7kG8P0puOYWV9leu2siMZ/QpQ==", "dev": true, "dependencies": { "@puppeteer/browsers": "2.4.0", @@ -12789,9 +12834,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.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.3.tgz", + "integrity": "sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==", "dev": true, "dependencies": { "@types/estree": "1.0.6" @@ -12804,22 +12849,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.3", + "@rollup/rollup-android-arm64": "4.24.3", + "@rollup/rollup-darwin-arm64": "4.24.3", + "@rollup/rollup-darwin-x64": "4.24.3", + "@rollup/rollup-freebsd-arm64": "4.24.3", + "@rollup/rollup-freebsd-x64": "4.24.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.3", + "@rollup/rollup-linux-arm-musleabihf": "4.24.3", + "@rollup/rollup-linux-arm64-gnu": "4.24.3", + "@rollup/rollup-linux-arm64-musl": "4.24.3", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.3", + "@rollup/rollup-linux-riscv64-gnu": "4.24.3", + "@rollup/rollup-linux-s390x-gnu": "4.24.3", + "@rollup/rollup-linux-x64-gnu": "4.24.3", + "@rollup/rollup-linux-x64-musl": "4.24.3", + "@rollup/rollup-win32-arm64-msvc": "4.24.3", + "@rollup/rollup-win32-ia32-msvc": "4.24.3", + "@rollup/rollup-win32-x64-msvc": "4.24.3", "fsevents": "~2.3.2" } }, @@ -14289,13 +14336,10 @@ } }, "node_modules/text-decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.0.tgz", - "integrity": "sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", + "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", + "dev": true }, "node_modules/text-table": { "version": "0.2.0", @@ -14309,12 +14353,12 @@ "dev": true }, "node_modules/tinyglobby": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.9.tgz", - "integrity": "sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", + "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", "dev": true, "dependencies": { - "fdir": "^6.4.0", + "fdir": "^6.4.2", "picomatch": "^4.0.2" }, "engines": { @@ -14766,9 +14810,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": { diff --git a/package.json b/package.json index 136b7ec197..417d9744ad 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.3", + "version": "5.0.0-pre.4", "main": "dist/maplibre-gl.js", "style": "dist/maplibre-gl.css", "license": "BSD-3-Clause", @@ -54,11 +54,11 @@ "@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", + "@stylistic/eslint-plugin-ts": "^2.10.0", "@types/benchmark": "^2.1.5", "@types/cssnano": "^5.1.3", "@types/d3": "^7.4.3", - "@types/diff": "^5.2.3", + "@types/diff": "^6.0.0", "@types/earcut": "^2.1.4", "@types/eslint": "^9.6.1", "@types/gl": "^6.0.5", @@ -68,7 +68,7 @@ "@types/minimist": "^1.2.5", "@types/murmurhash-js": "^1.0.6", "@types/nise": "^1.4.5", - "@types/node": "^22.7.9", + "@types/node": "^22.8.5", "@types/offscreencanvas": "^2019.7.3", "@types/pixelmatch": "^5.2.6", "@types/pngjs": "^6.0.5", @@ -77,17 +77,17 @@ "@types/request": "^2.48.12", "@types/shuffle-seed": "^1.1.3", "@types/window-or-global": "^1.0.6", - "@typescript-eslint/eslint-plugin": "^8.11.0", - "@typescript-eslint/parser": "^8.11.0", + "@typescript-eslint/eslint-plugin": "^8.12.2", + "@typescript-eslint/parser": "^8.12.2", "address": "^2.0.3", "autoprefixer": "^10.4.20", "benchmark": "^2.1.4", "canvas": "^2.11.2", - "cspell": "^8.15.4", + "cspell": "^8.15.5", "cssnano": "^7.0.6", "d3": "^7.9.0", "d3-queue": "^3.0.7", - "devtools-protocol": "^0.0.1373723", + "devtools-protocol": "^0.0.1376096", "diff": "^7.0.0", "dts-bundle-generator": "^9.5.1", "eslint": "^9.13.0", @@ -120,10 +120,10 @@ "postcss-cli": "^11.0.0", "postcss-inline-svg": "^6.0.0", "pretty-bytes": "^6.1.1", - "puppeteer": "^23.6.0", + "puppeteer": "^23.6.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "rollup": "^4.24.0", + "rollup": "^4.24.3", "rollup-plugin-sourcemaps2": "^0.4.2", "rw": "^1.3.3", "semver": "^7.6.3", @@ -148,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", diff --git a/rollup.config.csp.ts b/rollup.config.csp.ts index ada6543350..a63c13841e 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 e09e175473..b13fa799b1 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/projection/globe.ts b/src/geo/projection/globe.ts index 4f4e28bece..58f3fcd889 100644 --- a/src/geo/projection/globe.ts +++ b/src/geo/projection/globe.ts @@ -12,10 +12,6 @@ import {ProjectionErrorMeasurement} from './globe_projection_error_measurement'; import {createTileMeshWithBuffers, CreateTileMeshOptions} from '../../util/create_tile_mesh'; export const globeConstants = { - /** - * The size of border region for stencil masks, in internal tile coordinates. - * Used for globe rendering. - */ globeTransitionTimeSeconds: 0.5, maxGlobeZoom: 12.0, errorTransitionTimeSeconds: 0.5 diff --git a/src/geo/projection/globe_transform.test.ts b/src/geo/projection/globe_transform.test.ts index 151057ac8e..18738697a7 100644 --- a/src/geo/projection/globe_transform.test.ts +++ b/src/geo/projection/globe_transform.test.ts @@ -41,16 +41,26 @@ describe('GlobeTransform', () => { describe('getProjectionData', () => { const globeTransform = createGlobeTransform(globeProjectionMock); test('mercator tile extents are set', () => { - const projectionData = globeTransform.getProjectionData(new OverscaledTileID(1, 0, 1, 1, 0)); + const projectionData = globeTransform.getProjectionData({overscaledTileID: new OverscaledTileID(1, 0, 1, 1, 0)}); expectToBeCloseToArray(projectionData.tileMercatorCoords, [0.5, 0, 0.5 / EXTENT, 0.5 / EXTENT]); }); + + test('Globe transition is not 0 when not ignoring the globe matrix', () => { + const projectionData = globeTransform.getProjectionData({overscaledTileID: new OverscaledTileID(1, 0, 1, 1, 0)}); + expect(projectionData.projectionTransition).not.toBe(0); + }); + + test('Ignoring the globe matrix sets transition to 0', () => { + const projectionData = globeTransform.getProjectionData({overscaledTileID: new OverscaledTileID(1, 0, 1, 1, 0), ignoreGlobeMatrix: true}); + expect(projectionData.projectionTransition).toBe(0); + }); }); describe('clipping plane', () => { const globeTransform = createGlobeTransform(globeProjectionMock); describe('general plane properties', () => { - const projectionData = globeTransform.getProjectionData(new OverscaledTileID(0, 0, 0, 0, 0)); + const projectionData = globeTransform.getProjectionData({overscaledTileID: new OverscaledTileID(0, 0, 0, 0, 0)}); test('plane vector length', () => { const len = Math.sqrt( diff --git a/src/geo/projection/globe_transform.ts b/src/geo/projection/globe_transform.ts index 659a23d184..41ff63f429 100644 --- a/src/geo/projection/globe_transform.ts +++ b/src/geo/projection/globe_transform.ts @@ -16,7 +16,7 @@ import {PaddingOptions} from '../edge_insets'; import {tileCoordinatesToMercatorCoordinates} from './mercator_utils'; import {angularCoordinatesToSurfaceVector, getGlobeRadiusPixels, getZoomAdjustment, mercatorCoordinatesToAngularCoordinatesRadians, projectTileCoordinatesToSphere, sphereSurfacePointToCoordinates} from './globe_utils'; import {EXTENT} from '../../data/extent'; -import type {ProjectionData} from './projection_data'; +import type {ProjectionData, ProjectionDataParams} from './projection_data'; import {globeCoveringTiles} from './globe_covering_tiles'; import {Frustum} from '../../util/primitives'; @@ -448,8 +448,9 @@ export class GlobeTransform implements ITransform { return (this._lastUpdateTimeSeconds - this._lastGlobeChangeTimeSeconds) < globeConstants.globeTransitionTimeSeconds; } - getProjectionData(overscaledTileID: OverscaledTileID, aligned?: boolean, ignoreTerrainMatrix?: boolean): ProjectionData { - const data = this._mercatorTransform.getProjectionData(overscaledTileID, aligned, ignoreTerrainMatrix); + getProjectionData(params: ProjectionDataParams): ProjectionData { + const {overscaledTileID, aligned, ignoreTerrainMatrix, ignoreGlobeMatrix} = params; + const data = this._mercatorTransform.getProjectionData({overscaledTileID, aligned, ignoreTerrainMatrix}); // Set 'projectionMatrix' to actual globe transform if (this.isGlobeRendering) { @@ -457,7 +458,7 @@ export class GlobeTransform implements ITransform { } data.clippingPlane = this._cachedClippingPlane as [number, number, number, number]; - data.projectionTransition = this._globeness; + data.projectionTransition = ignoreGlobeMatrix ? 0 : this._globeness; return data; } @@ -701,8 +702,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); } @@ -719,6 +720,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); @@ -827,6 +832,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). @@ -1173,7 +1182,7 @@ export class GlobeTransform implements ITransform { } getProjectionDataForCustomLayer(): ProjectionData { - const projectionData = this.getProjectionData(new OverscaledTileID(0, 0, 0, 0, 0)); + const projectionData = this.getProjectionData({overscaledTileID: new OverscaledTileID(0, 0, 0, 0, 0)}); projectionData.tileMercatorCoords = [0, 0, 1, 1]; // Even though we requested projection data for the mercator base tile which covers the entire mercator range, diff --git a/src/geo/projection/mercator_camera_helper.ts b/src/geo/projection/mercator_camera_helper.ts index a7603f349c..91be1b7db2 100644 --- a/src/geo/projection/mercator_camera_helper.ts +++ b/src/geo/projection/mercator_camera_helper.ts @@ -35,6 +35,12 @@ export class MercatorCameraHelper implements ICameraHelper { } 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); } diff --git a/src/geo/projection/mercator_transform.test.ts b/src/geo/projection/mercator_transform.test.ts index 865e2f26a5..10487f4c41 100644 --- a/src/geo/projection/mercator_transform.test.ts +++ b/src/geo/projection/mercator_transform.test.ts @@ -53,6 +53,7 @@ describe('transform', () => { 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}); }); @@ -436,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)); @@ -449,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.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); - expect(transform.zoom).toBeCloseTo(14.1845318986, 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.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.6895075574, 10); + expect(transform.zoom).toBeCloseTo(13.836362951286565, 10); }); test('pointCoordinate with terrain when returning null should fall back to 2D', () => { @@ -528,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 23c347b374..b2dfd81a98 100644 --- a/src/geo/projection/mercator_transform.ts +++ b/src/geo/projection/mercator_transform.ts @@ -1,5 +1,5 @@ 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, degreesToRadians} from '../../util/util'; import {mat2, mat4, vec3, vec4} from 'gl-matrix'; @@ -10,9 +10,9 @@ 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 type {ProjectionData, ProjectionDataParams} from './projection_data'; import {scaleZoom, TransformHelper, zoomScale} from '../transform_helper'; import {mercatorCoveringTiles} from './mercator_covering_tiles'; @@ -273,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.pitchInRadians) * 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._pitchInRadians) * this.cameraToCenterDistance / pixelPerMeter: - const requiredPixelPerMeter = Math.cos(this.pitchInRadians) * 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), @@ -331,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 @@ -357,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); } /** @@ -523,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._fovInRadians / 2; + const halfFov = this.fovInRadians / 2; const offset = this.centerOffset; const point = projectToWorldCoordinates(this.worldSize, this.center); const x = point.x, y = point.y; @@ -534,10 +584,12 @@ 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.pitchInRadians); + 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.pitchInRadians); + 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 @@ -552,13 +604,14 @@ export class MercatorTransform implements ITransform { // 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.pitchInRadians) * 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) @@ -678,6 +731,14 @@ export class MercatorTransform implements ITransform { 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; @@ -689,7 +750,8 @@ export class MercatorTransform implements ITransform { return false; } - getProjectionData(overscaledTileID: OverscaledTileID, aligned?: boolean, ignoreTerrainMatrix?: boolean): ProjectionData { + getProjectionData(params: ProjectionDataParams): ProjectionData { + const {overscaledTileID, aligned, ignoreTerrainMatrix} = params; const matrix = overscaledTileID ? this.calculatePosMatrix(overscaledTileID, aligned) : null; return getBasicProjectionData(overscaledTileID, matrix, ignoreTerrainMatrix); } @@ -765,7 +827,7 @@ export class MercatorTransform implements ITransform { getProjectionDataForCustomLayer(): ProjectionData { const tileID = new OverscaledTileID(0, 0, 0, 0, 0); - const projectionData = this.getProjectionData(tileID, false, true); + const projectionData = this.getProjectionData({overscaledTileID: tileID, ignoreTerrainMatrix: true}); const tileMatrix = calculateTileMatrix(tileID, this.worldSize); mat4.multiply(tileMatrix, this._viewProjMatrix, tileMatrix); diff --git a/src/geo/projection/mercator_utils.test.ts b/src/geo/projection/mercator_utils.test.ts index b5853d167a..44b302e3ce 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 dee0e77feb..de2a6ab878 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/projection/projection_data.ts b/src/geo/projection/projection_data.ts index f2c41c9828..0f880c745c 100644 --- a/src/geo/projection/projection_data.ts +++ b/src/geo/projection/projection_data.ts @@ -1,4 +1,5 @@ import type {mat4} from 'gl-matrix'; +import type {OverscaledTileID} from '../../source/tile_id'; /** * This type contains all data necessary to project a tile to screen in MapLibre's shader system. @@ -44,3 +45,22 @@ export type ProjectionData = { */ fallbackMatrix: mat4; } + +export type ProjectionDataParams = { + /** + * The ID of the current tile + */ + overscaledTileID: OverscaledTileID | null; + /** + * Set to true if a pixel-aligned matrix should be used, if possible (mostly used for raster tiles under mercator projection) + */ + aligned?: boolean; + /** + * Set to true if the terrain matrix should be ignored + */ + ignoreTerrainMatrix?: boolean; + /** + * Set to true if the globe matrix should be ignored (i.e. when rendering to texture for terrain) + */ + ignoreGlobeMatrix?: boolean; +} diff --git a/src/geo/transform_helper.ts b/src/geo/transform_helper.ts index bf5d0df754..f7b9418787 100644 --- a/src/geo/transform_helper.ts +++ b/src/geo/transform_helper.ts @@ -1,7 +1,7 @@ import {LngLat} from './lng_lat'; import {LngLatBounds} from './lng_lat_bounds'; import Point from '@mapbox/point-geometry'; -import {wrap, clamp} from '../util/util'; +import {wrap, clamp, degreesToRadians, radiansToDegrees} from '../util/util'; import {mat4, mat2} from 'gl-matrix'; import {EdgeInsets} from './edge_insets'; import type {PaddingOptions} from './edge_insets'; @@ -317,13 +317,13 @@ export class TransformHelper implements ITransformGetters { return this._fovInRadians; } get fov(): number { - return this._fovInRadians / Math.PI * 180; + return radiansToDegrees(this._fovInRadians); } setFov(fov: number) { - fov = Math.max(0.01, Math.min(60, fov)); - if (this._fovInRadians === fov) return; + fov = clamp(fov, 0.1, 150); + if (this.fov === fov) return; this._unmodified = false; - this._fovInRadians = fov / 180 * Math.PI; + this._fovInRadians = degreesToRadians(fov); this._calcMatrices(); } diff --git a/src/geo/transform_interface.ts b/src/geo/transform_interface.ts index f26135e8dd..e316bcd0fa 100644 --- a/src/geo/transform_interface.ts +++ b/src/geo/transform_interface.ts @@ -8,7 +8,7 @@ import type {PaddingOptions} from './edge_insets'; import {Terrain} from '../render/terrain'; import {PointProjection} from '../symbol/projection'; import {MapProjectionEvent} from '../ui/events'; -import type {ProjectionData} from './projection/projection_data'; +import type {ProjectionData, ProjectionDataParams} from './projection/projection_data'; export type CoveringZoomOptions = { /** @@ -195,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. @@ -377,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; /** @@ -423,10 +437,9 @@ export interface IReadonlyTransform extends ITransformGetters { /** * @internal * Generates a `ProjectionData` instance to be used while rendering the supplied tile. - * @param overscaledTileID - The ID of the current tile. - * @param aligned - Set to true if a pixel-aligned matrix should be used, if possible (mostly used for raster tiles under mercator projection). + * @param params - Parameters for the projection data generation. */ - getProjectionData(overscaledTileID: OverscaledTileID, aligned?: boolean, ignoreTerrainMatrix?: boolean): ProjectionData; + getProjectionData(params: ProjectionDataParams): ProjectionData; /** * @internal diff --git a/src/render/draw_background.ts b/src/render/draw_background.ts index 1db26599eb..bd09a2ce78 100644 --- a/src/render/draw_background.ts +++ b/src/render/draw_background.ts @@ -42,7 +42,7 @@ export function drawBackground(painter: Painter, sourceCache: SourceCache, layer const crossfade = layer.getCrossfadeParameters(); for (const tileID of tileIDs) { - const projectionData = transform.getProjectionData(tileID); + const projectionData = transform.getProjectionData({overscaledTileID: tileID}); const uniformValues = image ? backgroundPatternUniformValues(opacity, painter, image, {tileID, tileSize}, crossfade) : diff --git a/src/render/draw_circle.ts b/src/render/draw_circle.ts index 7960ff99f7..63fe26c6e7 100644 --- a/src/render/draw_circle.ts +++ b/src/render/draw_circle.ts @@ -80,7 +80,7 @@ export function drawCircles(painter: Painter, sourceCache: SourceCache, layer: C const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord); const uniformValues = circleUniformValues(painter, tile, layer, translateForUniforms, radiusCorrectionFactor); - const projectionData = transform.getProjectionData(coord); + const projectionData = transform.getProjectionData({overscaledTileID: coord}); const state: TileRenderState = { programConfiguration, diff --git a/src/render/draw_collision_debug.ts b/src/render/draw_collision_debug.ts index 99f362792b..9da839118c 100644 --- a/src/render/draw_collision_debug.ts +++ b/src/render/draw_collision_debug.ts @@ -62,7 +62,7 @@ export function drawCollisionDebug(painter: Painter, sourceCache: SourceCache, l CullFaceMode.disabled, collisionUniformValues(painter.transform), painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord), - transform.getProjectionData(coord), + transform.getProjectionData({overscaledTileID: coord}), layer.id, buffers.layoutVertexBuffer, buffers.indexBuffer, buffers.segments, null, painter.transform.zoom, null, null, buffers.collisionVertexBuffer); diff --git a/src/render/draw_custom.test.ts b/src/render/draw_custom.test.ts index 9e5868ca4c..47f7217e70 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_debug.ts b/src/render/draw_debug.ts index 80551e241d..8974d8f9e9 100644 --- a/src/render/draw_debug.ts +++ b/src/render/draw_debug.ts @@ -91,7 +91,7 @@ function drawDebugTile(painter: Painter, sourceCache: SourceCache, coord: Oversc const tileLabel = `${tileIdText} ${tileSizeKb}kB`; drawTextToOverlay(painter, tileLabel); - const projectionData = painter.transform.getProjectionData(coord); + const projectionData = painter.transform.getProjectionData({overscaledTileID: coord}); program.draw(context, gl.TRIANGLES, depthMode, stencilMode, ColorMode.alphaBlended, CullFaceMode.disabled, debugUniformValues(Color.transparent, scaleRatio), null, projectionData, id, diff --git a/src/render/draw_fill.ts b/src/render/draw_fill.ts index 0b23b5b572..fb2ebba7ce 100644 --- a/src/render/draw_fill.ts +++ b/src/render/draw_fill.ts @@ -107,7 +107,7 @@ function drawFillTiles( updatePatternPositionsInProgram(programConfiguration, fillPropertyName, constantPattern, tile, layer); - const projectionData = transform.getProjectionData(coord); + const projectionData = transform.getProjectionData({overscaledTileID: coord}); const translateForUniforms = translatePosition(transform, tile, propertyFillTranslate, propertyFillTranslateAnchor); diff --git a/src/render/draw_fill_extrusion.ts b/src/render/draw_fill_extrusion.ts index 02c2d8f457..021778b29b 100644 --- a/src/render/draw_fill_extrusion.ts +++ b/src/render/draw_fill_extrusion.ts @@ -79,7 +79,7 @@ function drawExtrusionTiles( programConfiguration.updatePaintBuffers(crossfade); } - const projectionData = transform.getProjectionData(coord); + const projectionData = transform.getProjectionData({overscaledTileID: coord}); updatePatternPositionsInProgram(programConfiguration, fillPropertyName, constantPattern, tile, layer); const translate = translatePosition( diff --git a/src/render/draw_heatmap.ts b/src/render/draw_heatmap.ts index c28b103a90..4bea505487 100644 --- a/src/render/draw_heatmap.ts +++ b/src/render/draw_heatmap.ts @@ -79,7 +79,7 @@ function prepareHeatmapFlat(painter: Painter, sourceCache: SourceCache, layer: H const programConfiguration = bucket.programConfigurations.get(layer.id); const program = painter.useProgram('heatmap', programConfiguration); - const projectionData = transform.getProjectionData(coord); + const projectionData = transform.getProjectionData({overscaledTileID: coord}); const radiusCorrectionFactor = transform.getCircleRadiusCorrection(); @@ -145,7 +145,7 @@ function prepareHeatmapTerrain(painter: Painter, tile: Tile, layer: HeatmapStyle const programConfiguration = bucket.programConfigurations.get(layer.id); const program = painter.useProgram('heatmap', programConfiguration, true); - const projectionData = painter.transform.getProjectionData(tile.tileID); + const projectionData = painter.transform.getProjectionData({overscaledTileID: tile.tileID}); const terrainData = painter.style.map.terrain.getTerrainData(coord); program.draw(context, gl.TRIANGLES, DepthMode.disabled, stencilMode, colorMode, CullFaceMode.disabled, @@ -177,7 +177,7 @@ function renderHeatmapTerrain(painter: Painter, layer: HeatmapStyleLayer, coord: context.activeTexture.set(gl.TEXTURE1); colorRampTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); - const projectionData = transform.getProjectionData(coord, false, true); + const projectionData = transform.getProjectionData({overscaledTileID: coord, ignoreTerrainMatrix: true}); painter.useProgram('heatmapTexture').draw(context, gl.TRIANGLES, DepthMode.disabled, StencilMode.disabled, painter.colorModeForRenderPass(), CullFaceMode.disabled, diff --git a/src/render/draw_hillshade.ts b/src/render/draw_hillshade.ts index 6469ea153c..b6f1d8e5e1 100644 --- a/src/render/draw_hillshade.ts +++ b/src/render/draw_hillshade.ts @@ -73,7 +73,7 @@ function renderHillshade( context.activeTexture.set(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, fbo.colorAttachment.get()); - const projectionData = transform.getProjectionData(coord, align); + const projectionData = transform.getProjectionData({overscaledTileID: coord, aligned: align}); program.draw(context, gl.TRIANGLES, depthMode, stencilModes[coord.overscaledZ], colorMode, CullFaceMode.backCCW, hillshadeUniformValues(painter, tile, layer), terrainData, projectionData, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); diff --git a/src/render/draw_line.ts b/src/render/draw_line.ts index 0a8b0b1d9d..38c4cbf5a8 100644 --- a/src/render/draw_line.ts +++ b/src/render/draw_line.ts @@ -67,7 +67,7 @@ export function drawLine(painter: Painter, sourceCache: SourceCache, layer: Line if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom); } - const projectionData = transform.getProjectionData(coord); + const projectionData = transform.getProjectionData({overscaledTileID: coord}); const pixelRatio = transform.getPixelScale(); const uniformValues = image ? linePatternUniformValues(painter, tile, layer, pixelRatio, crossfade) : diff --git a/src/render/draw_raster.ts b/src/render/draw_raster.ts index ccff8b98ff..2abb08eddb 100644 --- a/src/render/draw_raster.ts +++ b/src/render/draw_raster.ts @@ -21,7 +21,7 @@ const cornerCoords = [ new Point(0, EXTENT), ]; -export function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterStyleLayer, tileIDs: Array) { +export function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterStyleLayer, tileIDs: Array, isRenderingToTexture: boolean = false) { if (painter.renderPass !== 'translucent') return; if (layer.paint.get('raster-opacity') === 0) return; if (!tileIDs.length) return; @@ -43,16 +43,16 @@ export function drawRaster(painter: Painter, sourceCache: SourceCache, layer: Ra // Stencil mask and two-pass is not used for ImageSource sources regardless of projection. if (source instanceof ImageSource) { // Image source - no stencil is used - drawTiles(painter, sourceCache, layer, tileIDs, null, false, false, source.tileCoords, source.flippedWindingOrder); + drawTiles(painter, sourceCache, layer, tileIDs, null, false, false, source.tileCoords, source.flippedWindingOrder, isRenderingToTexture); } else if (useSubdivision) { // Two-pass rendering const [stencilBorderless, stencilBorders, coords] = painter.stencilConfigForOverlapTwoPass(tileIDs); - drawTiles(painter, sourceCache, layer, coords, stencilBorderless, false, true, cornerCoords); // draw without borders - drawTiles(painter, sourceCache, layer, coords, stencilBorders, true, true, cornerCoords); // draw with borders + drawTiles(painter, sourceCache, layer, coords, stencilBorderless, false, true, cornerCoords, false, isRenderingToTexture); // draw without borders + drawTiles(painter, sourceCache, layer, coords, stencilBorders, true, true, cornerCoords, false, isRenderingToTexture); // draw with borders } else { // Simple rendering const [stencil, coords] = painter.stencilConfigForOverlap(tileIDs); - drawTiles(painter, sourceCache, layer, coords, stencil, false, true, cornerCoords); + drawTiles(painter, sourceCache, layer, coords, stencil, false, true, cornerCoords, false, isRenderingToTexture); } } @@ -65,7 +65,8 @@ function drawTiles( useBorder: boolean, allowPoles: boolean, corners: Array, - flipCullfaceMode: boolean = false) { + flipCullfaceMode: boolean = false, + isRenderingToTexture: boolean = false) { const minTileZ = coords[coords.length - 1].overscaledZ; const context = painter.context; @@ -120,7 +121,7 @@ function drawTiles( } const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord); - const projectionData = transform.getProjectionData(coord, align); + const projectionData = transform.getProjectionData({overscaledTileID: coord, aligned: align, ignoreGlobeMatrix: isRenderingToTexture}); const uniformValues = rasterUniformValues(parentTL || [0, 0], parentScaleBy || 1, fade, layer, corners); const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, allowPoles, 'raster'); diff --git a/src/render/draw_sky.ts b/src/render/draw_sky.ts index 27b4d0c357..83b28b5b5f 100644 --- a/src/render/draw_sky.ts +++ b/src/render/draw_sky.ts @@ -85,7 +85,7 @@ export function drawAtmosphere(painter: Painter, sky: Sky, light: Light) { const sunPos = getSunPos(light, painter.transform); - const projectionData = transform.getProjectionData(null); + const projectionData = transform.getProjectionData({overscaledTileID: null}); const atmosphereBlend = sky.properties.get('atmosphere-blend') * projectionData.projectionTransition; if (atmosphereBlend === 0) { diff --git a/src/render/draw_symbol.ts b/src/render/draw_symbol.ts index 550ec1b77f..bef88de127 100644 --- a/src/render/draw_symbol.ts +++ b/src/render/draw_symbol.ts @@ -379,7 +379,7 @@ function drawLayerSymbols( const glCoordMatrixForShader = getGlCoordMatrix(pitchWithMap, rotateWithMap, painter.transform, s); const translation = translatePosition(transform, tile, translate, translateAnchor); - const projectionData = transform.getProjectionData(coord); + const projectionData = transform.getProjectionData({overscaledTileID: coord}); const hasVariableAnchors = hasVariablePlacement && bucket.hasTextData(); const updateTextFitIcon = layer.layout.get('icon-text-fit') !== 'none' && diff --git a/src/render/draw_terrain.ts b/src/render/draw_terrain.ts index ce5bcd5e02..68c24b9d1f 100644 --- a/src/render/draw_terrain.ts +++ b/src/render/draw_terrain.ts @@ -27,7 +27,7 @@ function drawDepth(painter: Painter, terrain: Terrain) { context.clear({color: Color.transparent, depth: 1}); for (const tile of tiles) { const terrainData = terrain.getTerrainData(tile.tileID); - const projectionData = tr.getProjectionData(tile.tileID, false, true); + const projectionData = tr.getProjectionData({overscaledTileID: tile.tileID, ignoreTerrainMatrix: true}); const uniformValues = terrainDepthUniformValues(terrain.getMeshFrameDelta(tr.zoom)); program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, 'terrain', mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); } @@ -61,7 +61,7 @@ function drawCoords(painter: Painter, terrain: Terrain) { context.activeTexture.set(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, coords.texture); const uniformValues = terrainCoordsUniformValues(255 - terrain.coordsIndex.length, terrain.getMeshFrameDelta(tr.zoom)); - const projectionData = tr.getProjectionData(tile.tileID, false, true); + const projectionData = tr.getProjectionData({overscaledTileID: tile.tileID, ignoreTerrainMatrix: true}); program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, 'terrain', mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); terrain.coordsIndex.push(tile.tileID.key); } @@ -89,7 +89,7 @@ function drawTerrain(painter: Painter, terrain: Terrain, tiles: Array) { const eleDelta = terrain.getMeshFrameDelta(tr.zoom); const fogMatrix = tr.calculateFogMatrix(tile.tileID.toUnwrapped()); const uniformValues = terrainUniformValues(eleDelta, fogMatrix, painter.style.sky, tr.pitch); - const projectionData = tr.getProjectionData(tile.tileID, false, true); + const projectionData = tr.getProjectionData({overscaledTileID: tile.tileID, ignoreTerrainMatrix: true}); program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.backCCW, uniformValues, terrainData, projectionData, 'terrain', mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); } } diff --git a/src/render/painter.ts b/src/render/painter.ts index 741fb535f1..7d4b102750 100644 --- a/src/render/painter.ts +++ b/src/render/painter.ts @@ -297,7 +297,7 @@ export class Painter { const mesh = projection.getMeshFromTileID(this.context, tileID.canonical, useBorders, true, 'stencil'); - const projectionData = transform.getProjectionData(tileID); + const projectionData = transform.getProjectionData({overscaledTileID: tileID}); program.draw(context, gl.TRIANGLES, DepthMode.disabled, // Tests will always pass, and ref value will be written to stencil buffer. @@ -327,7 +327,7 @@ export class Painter { 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); + const projectionData = transform.getProjectionData({overscaledTileID: tileID}); program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, ColorMode.disabled, CullFaceMode.backCCW, null, @@ -632,7 +632,7 @@ export class Painter { drawCoords(this, this.style.map.terrain); } - renderLayer(painter: Painter, sourceCache: SourceCache, layer: StyleLayer, coords: Array) { + renderLayer(painter: Painter, sourceCache: SourceCache, layer: StyleLayer, coords: Array, isRenderingToTexture: boolean = false) { if (layer.isHidden(this.transform.zoom)) return; if (layer.type !== 'background' && layer.type !== 'custom' && !(coords || []).length) return; this.id = layer.id; @@ -660,7 +660,7 @@ export class Painter { drawHillshade(painter, sourceCache, layer as any, coords); break; case 'raster': - drawRaster(painter, sourceCache, layer as any, coords); + drawRaster(painter, sourceCache, layer as any, coords, isRenderingToTexture); break; case 'background': drawBackground(painter, sourceCache, layer as any, coords); diff --git a/src/render/program/sky_program.ts b/src/render/program/sky_program.ts index ce5a4d8014..ccf77d2c00 100644 --- a/src/render/program/sky_program.ts +++ b/src/render/program/sky_program.ts @@ -11,6 +11,7 @@ export type SkyUniformsType = { 'u_horizon': Uniform2f; 'u_horizon_normal': Uniform2f; 'u_sky_horizon_blend': Uniform1f; + 'u_sky_blend': Uniform1f; }; const skyUniforms = (context: Context, locations: UniformLocations): SkyUniformsType => ({ @@ -19,12 +20,15 @@ const skyUniforms = (context: Context, locations: UniformLocations): SkyUniforms '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), + 'u_sky_blend': new Uniform1f(context, locations.u_sky_blend), }); 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); + const projectionData = transform.getProjectionData({overscaledTileID: null}); + const skyBlend = projectionData.projectionTransition; return { 'u_sky_color': sky.properties.get('sky-color'), 'u_horizon_color': sky.properties.get('horizon-color'), @@ -32,6 +36,7 @@ const skyUniformValues = (sky: Sky, transform: IReadonlyTransform, pixelRatio: n (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, + 'u_sky_blend': skyBlend, }; }; diff --git a/src/render/render_to_texture.test.ts b/src/render/render_to_texture.test.ts index e8e00421f4..0f807ce680 100644 --- a/src/render/render_to_texture.test.ts +++ b/src/render/render_to_texture.test.ts @@ -108,7 +108,7 @@ describe('render to texture', () => { test('check state', () => { expect(rtt._renderableTiles.map(t => t.tileID.key)).toStrictEqual(['923']); - expect(rtt._coordsDescendingInv).toEqual({ + expect(rtt._coordsAscending).toEqual({ 'maine': { '923': [ { @@ -126,7 +126,7 @@ describe('render to texture', () => { ] } }); - expect(rtt._coordsDescendingInvStr).toStrictEqual({maine: {'923': '923'}}); + expect(rtt._coordsAscendingStr).toStrictEqual({maine: {'923': '923'}}); }); test('should render text after a line by not adding the text to the stack', () => { diff --git a/src/render/render_to_texture.ts b/src/render/render_to_texture.ts index 16ec56d04b..8c4fd96b30 100644 --- a/src/render/render_to_texture.ts +++ b/src/render/render_to_texture.ts @@ -29,15 +29,15 @@ export class RenderToTexture { terrain: Terrain; pool: RenderPool; /** - * coordsDescendingInv contains a list of all tiles which should be rendered for one render-to-texture tile + * coordsAscending contains a list of all tiles which should be rendered for one render-to-texture tile * e.g. render 4 raster-tiles with size 256px to the 512px render-to-texture tile */ - _coordsDescendingInv: {[_: string]: {[_:string]: Array}}; + _coordsAscending: {[_: string]: {[_:string]: Array}}; /** * create a string representation of all to tiles rendered to render-to-texture tiles * this string representation is used to check if tile should be re-rendered. */ - _coordsDescendingInvStr: {[_: string]: {[_:string]: string}}; + _coordsAscendingStr: {[_: string]: {[_:string]: string}}; /** * store for render-stacks * a render stack is a set of layers which should be rendered into one texture @@ -83,36 +83,36 @@ export class RenderToTexture { this._renderableTiles = this.terrain.sourceCache.getRenderableTiles(); this._renderableLayerIds = style._order.filter(id => !style._layers[id].isHidden(zoom)); - this._coordsDescendingInv = {}; + this._coordsAscending = {}; for (const id in style.sourceCaches) { - this._coordsDescendingInv[id] = {}; + this._coordsAscending[id] = {}; const tileIDs = style.sourceCaches[id].getVisibleCoordinates(); for (const tileID of tileIDs) { const keys = this.terrain.sourceCache.getTerrainCoords(tileID); for (const key in keys) { - if (!this._coordsDescendingInv[id][key]) this._coordsDescendingInv[id][key] = []; - this._coordsDescendingInv[id][key].push(keys[key]); + if (!this._coordsAscending[id][key]) this._coordsAscending[id][key] = []; + this._coordsAscending[id][key].push(keys[key]); } } } - this._coordsDescendingInvStr = {}; + this._coordsAscendingStr = {}; for (const id of style._order) { const layer = style._layers[id], source = layer.source; if (LAYERS[layer.type]) { - if (!this._coordsDescendingInvStr[source]) { - this._coordsDescendingInvStr[source] = {}; - for (const key in this._coordsDescendingInv[source]) - this._coordsDescendingInvStr[source][key] = this._coordsDescendingInv[source][key].map(c => c.key).sort().join(); + if (!this._coordsAscendingStr[source]) { + this._coordsAscendingStr[source] = {}; + for (const key in this._coordsAscending[source]) + this._coordsAscendingStr[source][key] = this._coordsAscending[source][key].map(c => c.key).sort().join(); } } } // check tiles to render for (const tile of this._renderableTiles) { - for (const source in this._coordsDescendingInvStr) { + for (const source in this._coordsAscendingStr) { // rerender if there are more coords to render than in the last rendering - const coords = this._coordsDescendingInvStr[source][tile.tileID.key]; + const coords = this._coordsAscendingStr[source][tile.tileID.key]; if (coords && coords !== tile.rttCoords[source]) tile.rtt = []; } } @@ -177,11 +177,11 @@ export class RenderToTexture { painter.currentStencilSource = undefined; for (let l = 0; l < layers.length; l++) { const layer = painter.style._layers[layers[l]]; - const coords = layer.source ? this._coordsDescendingInv[layer.source][tile.tileID.key] : [tile.tileID]; + const coords = layer.source ? this._coordsAscending[layer.source][tile.tileID.key] : [tile.tileID]; painter.context.viewport.set([0, 0, obj.fbo.width, obj.fbo.height]); painter._renderTileClippingMasks(layer, coords, true); - painter.renderLayer(painter, painter.style.sourceCaches[layer.source], layer, coords); - if (layer.source) tile.rttCoords[layer.source] = this._coordsDescendingInvStr[layer.source][tile.tileID.key]; + painter.renderLayer(painter, painter.style.sourceCaches[layer.source], layer, coords, true); + if (layer.source) tile.rttCoords[layer.source] = this._coordsAscendingStr[layer.source][tile.tileID.key]; } } drawTerrain(this.painter, this.terrain, this._rttTiles); diff --git a/src/shaders/sky.fragment.glsl b/src/shaders/sky.fragment.glsl index 3317a86028..aa46f660b8 100644 --- a/src/shaders/sky.fragment.glsl +++ b/src/shaders/sky.fragment.glsl @@ -4,6 +4,7 @@ uniform vec4 u_horizon_color; uniform vec2 u_horizon; uniform vec2 u_horizon_normal; uniform float u_sky_horizon_blend; +uniform float u_sky_blend; void main() { float x = gl_FragCoord.x; @@ -16,4 +17,5 @@ void main() { gl_FragColor = u_sky_color; } } + gl_FragColor = mix(gl_FragColor, vec4(vec3(0.0), 0.0), u_sky_blend); } \ No newline at end of file diff --git a/src/shaders/symbol_sdf.vertex.glsl b/src/shaders/symbol_sdf.vertex.glsl index f72fe62f8f..552bf62368 100644 --- a/src/shaders/symbol_sdf.vertex.glsl +++ b/src/shaders/symbol_sdf.vertex.glsl @@ -120,8 +120,7 @@ void main() { float projectionScaling = 1.0; #ifdef GLOBE - if(u_pitch_with_map && !u_is_along_line) { - // Lines would behave in very weird ways if this adjustment was used for them. + if(u_pitch_with_map) { float anchor_pos_tile_y = (u_coord_matrix * vec4(projected_pos.xy / projected_pos.w, z, 1.0)).y; projectionScaling = mix(projectionScaling, 1.0 / circumferenceRatioAtTileY(anchor_pos_tile_y) * u_pitched_scale, u_projection_transition); } diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index 7ad46cb0bd..7e4193e4d5 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/source_cache.test.ts b/src/source/source_cache.test.ts index 02ba196d8d..2810efbd69 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 20b5a07518..edeb458021 100644 --- a/src/source/source_cache.ts +++ b/src/source/source_cache.ts @@ -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/style/style.ts b/src/style/style.ts index 9fd2f7ad48..5514da3630 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/custom_style_layer.ts b/src/style/style_layer/custom_style_layer.ts index a1823e15d2..95c41410c8 100644 --- a/src/style/style_layer/custom_style_layer.ts +++ b/src/style/style_layer/custom_style_layer.ts @@ -86,7 +86,7 @@ export type CustomRenderMethodInput = { * For more details of this object's internals, see its doc comments in `src/geo/projection/projection_data.ts`. * * These uniforms are set so that `projectTile` in shader accepts a vec2 in range 0..1 in web mercator coordinates. - * Use `map.transform.getProjectionData(tileID)` to get uniforms for a given tile and pass vec2 in tile-local range 0..EXTENT instead. + * Use `map.transform.getProjectionData({overscaledTileID: tileID})` to get uniforms for a given tile and pass vec2 in tile-local range 0..EXTENT instead. * * For projection 3D features, use `projectTileFor3D` in the shader. * diff --git a/src/symbol/collision_index.ts b/src/symbol/collision_index.ts index 072c795b03..bf80189cd8 100644 --- a/src/symbol/collision_index.ts +++ b/src/symbol/collision_index.ts @@ -199,7 +199,8 @@ export class CollisionIndex { const tileUnitAnchorPoint = new Point(symbol.anchorX, symbol.anchorY); const perspectiveRatio = this.getPerspectiveRatio(tileUnitAnchorPoint.x, tileUnitAnchorPoint.y, unwrappedTileID, getElevation); - const labelPlaneFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio; + + const labelPlaneFontSize = pitchWithMap ? (fontSize * this.transform.getPitchedTextCorrection(symbol.anchorX, symbol.anchorY, unwrappedTileID) / perspectiveRatio) : fontSize * perspectiveRatio; const labelPlaneFontScale = labelPlaneFontSize / ONE_EM; const projectionCache = {projections: {}, offsets: {}, cachedAnchorPoint: undefined, anyProjectionOccluded: false}; diff --git a/src/symbol/projection.ts b/src/symbol/projection.ts index bc843ef785..c9ea57257a 100644 --- a/src/symbol/projection.ts +++ b/src/symbol/projection.ts @@ -279,7 +279,7 @@ export function updateLineLabels(bucket: SymbolBucket, const perspectiveRatio = getPerspectiveRatio(painter.transform.cameraToCenterDistance, cameraToAnchorDistance); const fontSize = symbolSize.evaluateSizeForFeature(sizeData, partiallyEvaluatedSize, symbol); - const pitchScaledFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio; + const pitchScaledFontSize = pitchWithMap ? (fontSize * painter.transform.getPitchedTextCorrection(symbol.anchorX, symbol.anchorY, unwrappedTileID) / perspectiveRatio) : fontSize * perspectiveRatio; const placeUnflipped = placeGlyphsAlongLine({ projectionContext, diff --git a/src/ui/camera.test.ts b/src/ui/camera.test.ts index 9e6cfdeed2..3ae21ad980 100644 --- a/src/ui/camera.test.ts +++ b/src/ui/camera.test.ts @@ -155,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}); @@ -216,6 +275,11 @@ describe('#jumpTo', () => { expect(camera.getRoll()).toBe(45); }); + test('sets field of view', () => { + camera.setVerticalFieldOfView(29); + expect(camera.getVerticalFieldOfView()).toBeCloseTo(29, 10); + }); + test('sets multiple properties', () => { camera.jumpTo({ center: [10, 20], @@ -259,6 +323,21 @@ describe('#jumpTo', () => { expect(ended).toBe('ok'); }); + test('emits move events when FOV changes, preserving eventData', () => { + let started, moved, ended; + const eventData = {data: 'ok'}; + + camera + .on('movestart', (d) => { started = d.data; }) + .on('move', (d) => { moved = d.data; }) + .on('moveend', (d) => { ended = d.data; }); + + camera.setVerticalFieldOfView(44, eventData); + expect(started).toBe('ok'); + expect(moved).toBe('ok'); + expect(ended).toBe('ok'); + }); + test('emits zoom events, preserving eventData', () => { let started, zoomed, ended; const eventData = {data: 'ok'}; @@ -1949,7 +2028,7 @@ describe('#flyTo', () => { }; camera.transform = { elevation: 0, - recalculateZoom: () => true, + recalculateZoomAndCenter: () => true, setMinElevationForCurrentTile: (_a) => true, setElevation: (e) => { camera.transform.elevation = e; } }; diff --git a/src/ui/camera.ts b/src/ui/camera.ts index 2a8600141f..844bfac81c 100644 --- a/src/ui/camera.ts +++ b/src/ui/camera.ts @@ -69,6 +69,10 @@ export type CameraOptions = CenterZoomBearing & { * 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; }; /** @@ -308,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; @@ -368,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. * @@ -495,6 +550,42 @@ export abstract class Camera extends Evented { return this; } + /** + * Returns the map's current vertical field of view, in degrees. + * + * @returns The map's current vertical field of view. + * @defaultValue 36.87 + * @example + * ```ts + * const verticalFieldOfView = map.getVerticalFieldOfView(); + * ``` + */ + getVerticalFieldOfView(): number { return this.transform.fov; } + + /** + * Sets the map's vertical field of view, in degrees. + * + * Triggers the following events: `movestart`, `move`, and `moveend`. + * + * @param fov - The vertical field of view to set, in degrees (0-180). + * @param eventData - Additional properties to be added to event objects of events triggered by this method. + * @defaultValue 36.87 + * @example + * Change vertical field of view to 30 degrees + * ```ts + * map.setVerticalFieldOfView(30); + * ``` + */ + setVerticalFieldOfView(fov: number, eventData?: any): this { + if (fov != this.transform.fov) { + this.transform.setFov(fov); + this.fire(new Event('movestart', eventData)) + .fire(new Event('move', eventData)) + .fire(new Event('moveend', eventData)); + } + return this; + } + /** * Returns the map's current bearing. The bearing is the compass direction that is "up"; for example, a bearing * of 90° orients the map so that east is up. @@ -842,6 +933,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); @@ -893,20 +988,30 @@ export abstract class Camera extends Evented { } /** - * 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'); @@ -919,13 +1024,47 @@ 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 }; } + /** + * 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 @@ -1065,7 +1204,9 @@ export abstract class Camera extends Evented { _finalizeElevation() { this._elevationFreeze = false; - this.transform.recalculateZoom(this.terrain); + if (this.getCenterClampedToGround()) { + this.transform.recalculateZoomAndCenter(this.terrain); + } } /** @@ -1098,9 +1239,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); @@ -1121,9 +1265,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)); } @@ -1142,11 +1284,11 @@ export abstract class Camera extends Evented { 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); @@ -1240,7 +1382,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', 'roll']) as CameraOptions; + const coercedOptions = pick(options, ['center', 'zoom', 'bearing', 'pitch', 'roll', 'elevation']) as CameraOptions; return this.jumpTo(coercedOptions, eventData); } diff --git a/src/ui/events.ts b/src/ui/events.ts index bc12096e00..009422c085 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/scroll_zoom.test.ts b/src/ui/handler/scroll_zoom.test.ts index 364b426487..9155b6f095 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_manager.ts b/src/ui/handler_manager.ts index aac62cc90b..feb806725f 100644 --- a/src/ui/handler_manager.ts +++ b/src/ui/handler_manager.ts @@ -527,7 +527,11 @@ 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, roll @@ -619,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 ef1bcc087f..1a8aab7783 100644 --- a/src/ui/map.ts +++ b/src/ui/map.ts @@ -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 @@ -346,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 = { @@ -371,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, @@ -401,6 +413,7 @@ const defaultOptions: Readonly> = { trackResize: true, center: [0, 0], + elevation: 0, zoom: 0, bearing: 0, pitch: 0, @@ -420,7 +433,8 @@ const defaultOptions: Readonly> = { validateStyle: true, /**Because GL MAX_TEXTURE_SIZE is usually at least 4096px. */ maxCanvasSize: [4096, 4096], - cancelPendingTileRequestsWhileZooming: true + cancelPendingTileRequestsWhileZooming: true, + centerClampedToGround: true }; /** @@ -628,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; @@ -697,6 +712,7 @@ 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, @@ -1081,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 { @@ -1117,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 { @@ -2024,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]; @@ -2048,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); } @@ -3194,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 c1b09bb43d..7c562d14a2 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 9ec2a26039..d4d90035d1 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 9f7987bf18..35e4750bd3 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 4fef810747..f30cf051aa 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/ajax.ts b/src/util/ajax.ts index 3ac99989db..730dc63385 100644 --- a/src/util/ajax.ts +++ b/src/util/ajax.ts @@ -155,7 +155,16 @@ async function makeFetchRequest(requestParameters: RequestParameters, abortContr request.headers.set('Accept', 'application/json'); } - const response = await fetch(request); + let response: Response; + try { + response = await fetch(request); + } catch (e) { + // When the error is due to CORS policy, DNS issue or malformed URL, the fetch call does not resolve but throws a generic TypeError instead. + // It is preferable to throw an AJAXError so that the Map event "error" can catch it and still have + // access to the faulty url. In such case, we provide the arbitrary HTTP error code of `0`. + throw new AJAXError(0, e.message, requestParameters.url, new Blob()); + } + if (!response.ok) { const body = await response.blob(); throw new AJAXError(response.status, response.statusText, requestParameters.url, body); diff --git a/src/util/create_tile_mesh.ts b/src/util/create_tile_mesh.ts index 98b94b6351..94a18d16f0 100644 --- a/src/util/create_tile_mesh.ts +++ b/src/util/create_tile_mesh.ts @@ -6,6 +6,10 @@ import {NORTH_POLE_Y, SOUTH_POLE_Y} from '../render/subdivision'; import {EXTENT} from '../data/extent'; import posAttributes from '../data/pos_attributes'; +/** + * The size of border region for stencil masks, in internal tile coordinates. + * Used for globe rendering. + */ const EXTENT_STENCIL_BORDER = EXTENT / 128; /** diff --git a/test/bench/rollup_config_benchmarks.ts b/test/bench/rollup_config_benchmarks.ts index 27468da8f8..7cbeba406d 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/build/min.test.ts b/test/build/min.test.ts index c9594b1f4a..ee5bbc5544 100644 --- a/test/build/min.test.ts +++ b/test/build/min.test.ts @@ -37,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 = 886416; + const expectedBytes = 890732; expect(actualBytes).toBeLessThan(expectedBytes + increaseQuota); expect(actualBytes).toBeGreaterThan(expectedBytes - decreaseQuota); diff --git a/test/examples/center-point.html b/test/examples/center-point.html new file mode 100644 index 0000000000..37c5258d03 --- /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/globe-custom-tiles.html b/test/examples/globe-custom-tiles.html index d87f8bdfd2..4a0f928143 100644 --- a/test/examples/globe-custom-tiles.html +++ b/test/examples/globe-custom-tiles.html @@ -262,7 +262,7 @@ } }; - const projectionData = map.transform.getProjectionData(tileID); + const projectionData = map.transform.getProjectionData({overscaledTileID: tileID}); gl.uniform4f( locations['u_projection_clipping_plane'], diff --git a/test/examples/measure.html b/test/examples/measure.html index d558c87470..78968a9c8b 100644 --- a/test/examples/measure.html +++ b/test/examples/measure.html @@ -36,7 +36,7 @@
- + - \ No newline at end of file + diff --git a/test/integration/render/tests/field-of-view/default/expected.png b/test/integration/render/tests/field-of-view/default/expected.png new file mode 100644 index 0000000000..3a6e114a2b Binary files /dev/null and b/test/integration/render/tests/field-of-view/default/expected.png differ diff --git a/test/integration/render/tests/field-of-view/default/style.json b/test/integration/render/tests/field-of-view/default/style.json new file mode 100644 index 0000000000..97f91a0b6c --- /dev/null +++ b/test/integration/render/tests/field-of-view/default/style.json @@ -0,0 +1,25 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512 + } + }, + "center": [35.372566, 31.556437], + "zoom": 16.25, + "pitch": 30, + "bearing": 22.5, + "sources": { + "repeat": { + "type": "raster", + "tiles": ["local://tiles/white-with-x.png"], + "minzoom": 0, + "maxzoom": 22, + "tileSize": 256 + } + }, + "layers": [ + {"id": "repeat", "type": "raster", "source": "repeat"} + ] +} \ No newline at end of file diff --git a/test/integration/render/tests/field-of-view/field-of-view-10/expected.png b/test/integration/render/tests/field-of-view/field-of-view-10/expected.png new file mode 100644 index 0000000000..8503a8dccb Binary files /dev/null and b/test/integration/render/tests/field-of-view/field-of-view-10/expected.png differ diff --git a/test/integration/render/tests/field-of-view/field-of-view-10/style.json b/test/integration/render/tests/field-of-view/field-of-view-10/style.json new file mode 100644 index 0000000000..00463ae38f --- /dev/null +++ b/test/integration/render/tests/field-of-view/field-of-view-10/style.json @@ -0,0 +1,29 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512, + "operations": [ + ["setVerticalFieldOfView", 10], + ["wait"] + ] + } + }, + "center": [35.372566, 31.556437], + "zoom": 16.25, + "pitch": 30, + "bearing": 22.5, + "sources": { + "repeat": { + "type": "raster", + "tiles": ["local://tiles/white-with-x.png"], + "minzoom": 0, + "maxzoom": 22, + "tileSize": 256 + } + }, + "layers": [ + {"id": "repeat", "type": "raster", "source": "repeat"} + ] +} \ No newline at end of file diff --git a/test/integration/render/tests/field-of-view/field-of-view-150/expected.png b/test/integration/render/tests/field-of-view/field-of-view-150/expected.png new file mode 100644 index 0000000000..88d56a6d9a Binary files /dev/null and b/test/integration/render/tests/field-of-view/field-of-view-150/expected.png differ diff --git a/test/integration/render/tests/field-of-view/field-of-view-150/style.json b/test/integration/render/tests/field-of-view/field-of-view-150/style.json new file mode 100644 index 0000000000..071b994bae --- /dev/null +++ b/test/integration/render/tests/field-of-view/field-of-view-150/style.json @@ -0,0 +1,29 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512, + "operations": [ + ["setVerticalFieldOfView", 150], + ["wait"] + ] + } + }, + "center": [35.372566, 31.556437], + "zoom": 16.25, + "pitch": 30, + "bearing": 22.5, + "sources": { + "repeat": { + "type": "raster", + "tiles": ["local://tiles/white-with-x.png"], + "minzoom": 0, + "maxzoom": 22, + "tileSize": 256 + } + }, + "layers": [ + {"id": "repeat", "type": "raster", "source": "repeat"} + ] +} \ No newline at end of file diff --git a/test/integration/render/tests/field-of-view/field-of-view-90/expected.png b/test/integration/render/tests/field-of-view/field-of-view-90/expected.png new file mode 100644 index 0000000000..97bd4204f0 Binary files /dev/null and b/test/integration/render/tests/field-of-view/field-of-view-90/expected.png differ diff --git a/test/integration/render/tests/field-of-view/field-of-view-90/style.json b/test/integration/render/tests/field-of-view/field-of-view-90/style.json new file mode 100644 index 0000000000..45e7590abf --- /dev/null +++ b/test/integration/render/tests/field-of-view/field-of-view-90/style.json @@ -0,0 +1,29 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512, + "operations": [ + ["setVerticalFieldOfView", 90], + ["wait"] + ] + } + }, + "center": [35.372566, 31.556437], + "zoom": 16.25, + "pitch": 30, + "bearing": 22.5, + "sources": { + "repeat": { + "type": "raster", + "tiles": ["local://tiles/white-with-x.png"], + "minzoom": 0, + "maxzoom": 22, + "tileSize": 256 + } + }, + "layers": [ + {"id": "repeat", "type": "raster", "source": "repeat"} + ] +} \ No newline at end of file diff --git a/test/integration/render/tests/high-pitch/pitch95-roll135/expected.png b/test/integration/render/tests/high-pitch/pitch95-roll135/expected.png new file mode 100644 index 0000000000..d0d61e0edf Binary files /dev/null and b/test/integration/render/tests/high-pitch/pitch95-roll135/expected.png differ diff --git a/test/integration/render/tests/high-pitch/pitch95-roll135/style.json b/test/integration/render/tests/high-pitch/pitch95-roll135/style.json new file mode 100644 index 0000000000..d496e338ba --- /dev/null +++ b/test/integration/render/tests/high-pitch/pitch95-roll135/style.json @@ -0,0 +1,41 @@ +{ + "version": 8, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "timeout": 60000, + "metadata": { + "test": { + "height": 512, + "width": 512, + "maxPitch": 95, + "operations": [ + ["setCenterClampedToGround", false], + ["setCenterElevation", 10000], + ["setPitch", 95], + ["wait"] + ] + } + }, + "center": [ + 35.38, + 31.55 + ], + "roll": 135, + "zoom": 10, + "sources": { }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#ccaa83" + } + } + ], + "sky": { + "sky-color": "#199EF3", + "horizon-color": "#daeff0", + "sky-horizon-blend": 1, + "fog-ground-blend": 1 + } +} diff --git a/test/integration/render/tests/high-pitch/pitch95/expected.png b/test/integration/render/tests/high-pitch/pitch95/expected.png new file mode 100644 index 0000000000..92b3fead29 Binary files /dev/null and b/test/integration/render/tests/high-pitch/pitch95/expected.png differ diff --git a/test/integration/render/tests/high-pitch/pitch95/style.json b/test/integration/render/tests/high-pitch/pitch95/style.json new file mode 100644 index 0000000000..e0c3f9cf0c --- /dev/null +++ b/test/integration/render/tests/high-pitch/pitch95/style.json @@ -0,0 +1,40 @@ +{ + "version": 8, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "timeout": 60000, + "metadata": { + "test": { + "height": 512, + "width": 512, + "maxPitch": 95, + "operations": [ + ["setCenterClampedToGround", false], + ["setCenterElevation", 10000], + ["setPitch", 95], + ["wait"] + ] + } + }, + "center": [ + 35.38, + 31.55 + ], + "zoom": 10, + "sources": { }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#ccaa83" + } + } + ], + "sky": { + "sky-color": "#199EF3", + "horizon-color": "#daeff0", + "sky-horizon-blend": 1, + "fog-ground-blend": 1 + } +} diff --git a/test/integration/render/tests/high-pitch/terrain-pitch95/expected.png b/test/integration/render/tests/high-pitch/terrain-pitch95/expected.png new file mode 100644 index 0000000000..77ce8a5bf6 Binary files /dev/null and b/test/integration/render/tests/high-pitch/terrain-pitch95/expected.png differ diff --git a/test/integration/render/tests/high-pitch/terrain-pitch95/style.json b/test/integration/render/tests/high-pitch/terrain-pitch95/style.json new file mode 100644 index 0000000000..34c9de3660 --- /dev/null +++ b/test/integration/render/tests/high-pitch/terrain-pitch95/style.json @@ -0,0 +1,53 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512, + "maxPitch": 180, + "operations": [ + ["setCenterClampedToGround", false], + ["setCenterElevation", 750], + ["wait"] + ] + } + }, + "center": [-113.33496, 35.96022], + "zoom": 13, + "pitch": 95, + "sources": { + "terrain": { + "type": "raster-dem", + "tiles": ["local://tiles/{z}-{x}-{y}.terrain.png"], + "maxzoom": 15, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": ["local://tiles/{z}-{x}-{y}.satellite.png"], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-opacity": 1.0 + } + } + ], + "terrain": { + "source": "terrain", + "exaggeration": 1 + } +} diff --git a/test/integration/render/tests/projection/globe/collision-text-line-equator/expected.png b/test/integration/render/tests/projection/globe/collision-text-line-equator/expected.png new file mode 100644 index 0000000000..638c2581c1 Binary files /dev/null and b/test/integration/render/tests/projection/globe/collision-text-line-equator/expected.png differ diff --git a/test/integration/render/tests/projection/globe/text-line-pole-to-pole/style.json b/test/integration/render/tests/projection/globe/collision-text-line-equator/style.json similarity index 81% rename from test/integration/render/tests/projection/globe/text-line-pole-to-pole/style.json rename to test/integration/render/tests/projection/globe/collision-text-line-equator/style.json index 4546aded70..b8419379dc 100644 --- a/test/integration/render/tests/projection/globe/text-line-pole-to-pole/style.json +++ b/test/integration/render/tests/projection/globe/collision-text-line-equator/style.json @@ -2,6 +2,7 @@ "version": 8, "metadata": { "test": { + "collisionDebug": true, "height": 256, "width": 256 } @@ -9,9 +10,15 @@ "sky": { "atmosphere-blend": 0.0 }, - "zoom": 1, + "zoom": 0, + "center": [ + 0, + 45 + ], "glyphs": "local://glyphs/{fontstack}/{range}.pbf", - "projection": { "type": "globe" }, + "projection": { + "type": "globe" + }, "sources": { "geojson": { "type": "geojson", @@ -21,12 +28,12 @@ "type": "LineString", "coordinates": [ [ - 0, - -85 + -45, + 0 ], [ - 0, - 85 + 45, + 0 ] ] } @@ -38,7 +45,7 @@ "id": "background", "type": "background", "paint": { - "background-color": "white" + "background-color": "grey" } }, { @@ -60,7 +67,7 @@ "source": "geojson", "layout": { "symbol-placement": "line", - "text-field": "AAAA AAAAAA A AAAAA AAAA BB", + "text-field": "AAAAAAAAAAA", "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" diff --git a/test/integration/render/tests/projection/globe/collision-text-line-pole-to-pole/expected.png b/test/integration/render/tests/projection/globe/collision-text-line-pole-to-pole/expected.png index af1bc7d712..c08c44cfd0 100644 Binary files a/test/integration/render/tests/projection/globe/collision-text-line-pole-to-pole/expected.png and b/test/integration/render/tests/projection/globe/collision-text-line-pole-to-pole/expected.png differ diff --git a/test/integration/render/tests/projection/globe/collision-text-line-pole-to-pole/style.json b/test/integration/render/tests/projection/globe/collision-text-line-pole-to-pole/style.json index 73b29cea23..a6b1297200 100644 --- a/test/integration/render/tests/projection/globe/collision-text-line-pole-to-pole/style.json +++ b/test/integration/render/tests/projection/globe/collision-text-line-pole-to-pole/style.json @@ -12,7 +12,9 @@ }, "zoom": 1, "glyphs": "local://glyphs/{fontstack}/{range}.pbf", - "projection": { "type": "globe" }, + "projection": { + "type": "globe" + }, "sources": { "geojson": { "type": "geojson", @@ -39,7 +41,7 @@ "id": "background", "type": "background", "paint": { - "background-color": "white" + "background-color": "grey" } }, { @@ -61,7 +63,7 @@ "source": "geojson", "layout": { "symbol-placement": "line", - "text-field": "AAAA AAAAAA A AAAAA AAAA BB", + "text-field": "AAAAAAAAAA", "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" diff --git a/test/integration/render/tests/projection/globe/sky/expected.png b/test/integration/render/tests/projection/globe/sky/expected.png new file mode 100644 index 0000000000..d357cc0750 Binary files /dev/null and b/test/integration/render/tests/projection/globe/sky/expected.png differ diff --git a/test/integration/render/tests/projection/globe/sky/style.json b/test/integration/render/tests/projection/globe/sky/style.json new file mode 100644 index 0000000000..9a5416ba1b --- /dev/null +++ b/test/integration/render/tests/projection/globe/sky/style.json @@ -0,0 +1,45 @@ +{ + "version": 8, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "timeout": 60000, + "metadata": { + "test": { + "height": 512, + "width": 512, + "operations": [ + [ + "wait", + 1000 + ] + ] + } + }, + "center": [ + 0, + 0 + ], + "zoom": 1, + "pitch": 80, + "maxPitch": 85, + "sources": { }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#000000" + } + } + ], + "sky": { + "sky-color": "#199EF3", + "horizon-color": "#daeff0", + "sky-horizon-blend": 1, + "fog-ground-blend": 1, + "atmosphere-blend": 0 + }, + "projection": { + "type": "globe" + } +} diff --git a/test/integration/render/tests/projection/globe/terrain/expected-ubuntu.png b/test/integration/render/tests/projection/globe/terrain/expected-ubuntu.png new file mode 100644 index 0000000000..d09dd71614 Binary files /dev/null and b/test/integration/render/tests/projection/globe/terrain/expected-ubuntu.png differ diff --git a/test/integration/render/tests/projection/globe/terrain/expected-windows.png b/test/integration/render/tests/projection/globe/terrain/expected-windows.png new file mode 100644 index 0000000000..b55c3f215a Binary files /dev/null and b/test/integration/render/tests/projection/globe/terrain/expected-windows.png differ diff --git a/test/integration/render/tests/projection/globe/terrain/expected.png b/test/integration/render/tests/projection/globe/terrain/expected.png new file mode 100644 index 0000000000..e81af98352 Binary files /dev/null and b/test/integration/render/tests/projection/globe/terrain/expected.png differ diff --git a/test/integration/render/tests/projection/globe/terrain/style.json b/test/integration/render/tests/projection/globe/terrain/style.json new file mode 100644 index 0000000000..f77251e933 --- /dev/null +++ b/test/integration/render/tests/projection/globe/terrain/style.json @@ -0,0 +1,53 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 0, + "pitch": 0, + "sources": { + "terrain": { + "type": "raster-dem", + "tiles": ["local://tiles/{z}-{x}-{y}.terrain.png"], + "maxzoom": 15, + "tileSize": 256 + }, + "satellite": { + "type": "raster", + "tiles": ["local://tiles/{z}-{x}-{y}.satellite.png"], + "maxzoom": 17, + "tileSize": 256 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "raster", + "type": "raster", + "source": "satellite", + "paint": { + "raster-opacity": 1.0 + } + } + ], + "terrain": { + "source": "terrain", + "exaggeration": 2 + }, + "projection": { + "type": "globe" + } +} diff --git a/test/integration/render/tests/projection/globe/text-line-pole-to-pole/expected.png b/test/integration/render/tests/projection/globe/text-line-pole-to-pole/expected.png deleted file mode 100644 index 82dc82b80f..0000000000 Binary files a/test/integration/render/tests/projection/globe/text-line-pole-to-pole/expected.png and /dev/null differ diff --git a/test/integration/render/tests/symbol-visibility/visible/expected-flaky.png b/test/integration/render/tests/symbol-visibility/visible/expected-flaky.png new file mode 100644 index 0000000000..61a08a23e4 Binary files /dev/null and b/test/integration/render/tests/symbol-visibility/visible/expected-flaky.png differ